Skip to content

4. Seguimiento de la sesión

4.1. El problema

Una aplicación web puede consistir en varios intercambios de formularios entre el servidor y el cliente. En ese caso, el funcionamiento es el siguiente:

  • paso 1
    • el cliente C1 abre una conexión con el servidor y realiza su solicitud inicial.
    • El servidor envía el formulario F1 al cliente C1 y cierra la conexión abierta en el paso 1.
  • paso 2
    • El cliente C1 lo rellena y lo reenvía al servidor. Para ello, el navegador abre una nueva conexión con el servidor.
    • Este procesa los datos del formulario 1, calcula la información I1 a partir de ellos, envía un formulario F2 al cliente C1 y cierra la conexión abierta en el paso 3.
  • paso 3
    • El ciclo de los pasos 3 y 4 se repite en los pasos 5 y 6. Al finalizar el paso 6, el servidor habrá recibido dos formularios F1 y F2 y, a partir de ellos, habrá calculado la información I1 y I2.

El problema que se plantea es: ¿cómo hace el servidor para conservar la información I1 y I2 relacionada con el cliente C1? A este problema se le denomina seguimiento de la sesión del cliente C1. Para comprender su origen, examinemos el esquema de una aplicación de servidor TCP-IP que atiende simultáneamente a varios clients:

En una aplicación cliente-servidor TCP-IP clásica:

  • el cliente establece una conexión con el servidor
  • intercambia datos con el servidor a través de ella
  • La conexión la cierra uno de los dos interlocutores

Los dos puntos importantes de este mecanismo son:

  1. se crea una conexión única para cada uno de los clients
  2. esta conexión se utiliza durante toda la duración del diálogo del servidor con su cliente

Lo que permite al servidor saber en un momento dado con qué cliente está trabajando es la conexión o, dicho de otro modo, el «canal» que lo conecta con su cliente. Al estar este canal dedicado a un cliente concreto, todo lo que llega por este canal procede de ese cliente y todo lo que se envía por este canal llega al cliente.

El mecanismo cliente-servidor HTTP sigue fielmente el esquema anterior, con la particularidad de que el diálogo cliente-servidor se limita a un único intercambio entre el cliente y el servidor:

  • el cliente abre una conexión con el servidor y realiza su solicitud
  • el servidor responde y cierra la conexión

Si en el momento T1, un cliente C realiza una solicitud al servidor, obtiene una conexión C1 que servirá para el intercambio único de solicitud-respuesta. Si en el momento T2, ese mismo cliente realiza una segunda solicitud al servidor, obtendrá una conexión C2 diferente de la conexión C1. Para el servidor, no hay entonces ninguna diferencia entre esta segunda solicitud del usuario C y su solicitud inicial: en ambos casos, el servidor considera al cliente como un nuevo cliente. Para que exista un vínculo entre las diferentes conexiones del cliente C al servidor, es necesario que el servidor «reconozca» al cliente C como un «usuario habitual» y que el servidor recupere la información que tiene sobre dicho usuario habitual.

Imaginemos una administración que funcionara de la siguiente manera:

  • Hay una única cola
  • Hay varias ventanillas. Por lo tanto, se puede atender a varios clients simultáneamente. Cuando se libera una ventanilla, un cliente sale de la cola para ser atendido en esa ventanilla
  • Si es la primera vez que el cliente acude, la persona de la ventanilla le entrega un ticket con un número. El cliente solo puede formular una pregunta. Cuando obtiene su respuesta, debe abandonar la ventanilla y pasar al final de la cola. El empleado de la ventanilla anota los datos de este cliente en un expediente con su número.
  • Cuando vuelve a ser su turno, el cliente puede ser atendido por un empleado de ventanilla diferente al de la vez anterior. Este le pide su ficha y recupera el expediente con el número de la ficha. De nuevo, el cliente realiza una solicitud, obtiene una respuesta y se añade información a su expediente.
  • Y así sucesivamente... Con el tiempo, el cliente obtendrá respuesta a todas sus solicitudes. El seguimiento entre las diferentes solicitudes se realiza gracias al ticket y al expediente asociado a este.

El mecanismo de seguimiento de sesión en una aplicación web cliente-servidor es análogo al funcionamiento anterior:

  • en su primera solicitud, el servidor web le asigna un token al cliente
  • presentará este token en cada una de sus solicitudes posteriores para identificarse

El token puede adoptar diferentes formas:

  • el de un campo oculto en un formulario
    • el cliente realiza su primera solicitud (el servidor lo reconoce por el hecho de que el cliente no tiene token)
    • el servidor envía su respuesta (un formulario) e incluye el token en un campo oculto del mismo. En ese momento, se cierra la conexión (el cliente abandona la página con su token). El servidor se ha encargado, si procede, de asociar información a este token.
    • el cliente realiza su segunda solicitud reenviando el formulario. El servidor recupera el token de este. A continuación, puede procesar la segunda solicitud del cliente al tener acceso, gracias al token, a la información calculada durante la primera solicitud. Se añade nueva información al archivo vinculado al token, se envía una segunda respuesta al cliente y se cierra la conexión por segunda vez. El token se ha vuelto a incluir en el formulario de la respuesta para que el usuario pueda presentarlo en su siguiente solicitud.
    • Y así sucesivamente...

El principal inconveniente de esta técnica es que el token debe incluirse en un formulario. Si la respuesta del servidor no es un formulario, el método del campo oculto ya no es utilizable.

  • El de la cookie
    • el cliente realiza su primera solicitud (el servidor lo reconoce por el hecho de que el cliente no tiene token)
    • el servidor responde añadiendo una cookie en los encabezados HTTP de la misma. Esto se hace mediante el comando HTTP Set-Cookie:

Set-Cookie: param1=valor1;param2=valor2;....

donde parami son los nombres de los parámetros y valeursi sus valores. Entre los parámetros se encontrará el token. A menudo, solo hay un token en la cookie, ya que el servidor almacena el resto de la información en la carpeta vinculada al token. El navegador que recibe la cookie la almacenará en un archivo en el disco. Tras la respuesta del servidor, se cierra la conexión (el cliente abandona la ventanilla con su token).

  • (continuación)
    • el cliente realiza su segunda solicitud al servidor. Cada vez que se realiza una solicitud a un servidor, el navegador busca entre todas las cookies que tiene para ver si hay alguna procedente del servidor solicitado. Si es así, la envía al servidor siempre en forma de comando HTTP, el comando Cookie que tiene una sintaxis análoga a la del comando Set-Cookie utilizado por el servidor:

Cookie: param1=valor1;param2=valor2;....

Entre los parámetros enviados por el navegador, el servidor encontrará el token que le permite reconocer al cliente y recuperar la información relacionada con él.

Es la forma de token más utilizada. Presenta un inconveniente: un usuario puede configurar su navegador para que no acepte cookies. Este tipo de usuario no tendrá entonces acceso a las aplicaciones web que utilizan cookies.

  • Reescritura de URL
    • el cliente realiza su primera solicitud (el servidor lo reconoce por el hecho de que el cliente no tiene token)
    • El servidor envía su respuesta. Esta contiene enlaces que el usuario debe utilizar para continuar con la aplicación. En el URL de cada uno de estos enlaces, el servidor añade el token en el formato URL;token=valor.
    • Cuando el usuario hace clic en uno de los enlaces para continuar con la aplicación, el navegador realiza su solicitud al servidor web enviándole en los encabezados HTTP el URL URL;token=valor solicitado. El servidor es entonces capaz de recuperar el token.

4.2. Java para el seguimiento de sesiones

A continuación, presentamos los principales métodos útiles para el seguimiento de la sesión:

HttpSession [HttpServletRequest].getSession()
obtiene el objeto Session al que pertenece la solicitud en curso. Si esta aún no formaba parte de una sesión, se crea dicha sesión.
String [HttpSession].getId()
identificador de la sesión actual
long [HttpSession].getCreationTime()
fecha de creación de la sesión actual (número de milisegundos transcurridos desde el 1 de enero de 1970, 0:00).
long [HttpSession].getLastAccessedTime()
fecha del último acceso del cliente a la sesión
long [HttpSession].getMaxInactiveInterval()
tiempo máximo en segundos de inactividad de una sesión. Transcurrido este tiempo, la sesión queda invalidada.
[HttpSession].setMaxInactiveInterval(int durée)
Establece en segundos la duración máxima de inactividad de una sesión. Transcurrido este tiempo, la sesión se invalida.
boolean [HttpSession].isNew()
verdadero si la sesión acaba de crearse
[HttpSession].setAttribute(String paramètre, Object valeur)
asigna un valor a un parámetro en una sesión determinada. Este mecanismo permite almacenar información que permanecerá disponible durante toda la sesión.
[HttpSession].removeAttribute(String paramètre)
elimina datos de la sesión.
Object [HttpSession].getAttribute(String paramètre)
valor asociado al parámetro paramètre de la sesión. Devuelve null si este último no existe.
Enumeration [HttpSession].getAttributeNames()
lista en forma de enumeración de todos los atributos de la sesión actual
[HttpSession].invalidate()
Cierra la sesión actual. Se elimina toda la información asociada a ella.

4.3. Ejemplo 1

Presentamos un ejemplo extraído del excelente libro «Programación con J2EE», publicado por Wrox y distribuido por Eyrolles. Este libro es una mina de información de alto nivel para los desarrolladores de soluciones web en Java. La aplicación presentada en este libro en forma de un único servlet Java se ha retomado aquí en forma de un servlet principal que recurre a páginas JSP para mostrar las diversas respuestas posibles al cliente.

La aplicación se llama «sessions» y está configurada de la siguiente manera en el archivo <tomcat>\conf\server.xml:

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

En la carpeta docBase anterior, se encuentran los siguientes elementos:

Image

Los archivos erreur.jsp, invalide.jsp y valide.jsp están todos asociados a la aplicación sessions. En la carpeta WEB-INF anterior se encuentran:

Image

Arriba se muestra el archivo web.xml de configuración de la aplicación sessions. En la carpeta classes se encuentra el archivo de clase del servlet:

Image

El archivo web.xml de la aplicación es el siguiente:

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

El servlet principal se llama cycledevie (servlet-name) y está asociado al archivo de clase cycledevie.class (servlet-class). Tiene un alias /cycledevie (servlet-mapping) que permite llamarla a través de URL http://localhost:8080/sessions/cycledevie. Tiene tres parámetros de inicialización:

urlSessionValide
url de la página que muestra las características de la sesión actual
urlSessionInvalide
url de la página que se muestra tras la invalidación de la sesión actual
urlErreur
url de la página que se muestra en caso de error de inicialización del servlet principal cycledevie

Los componentes de la aplicación de sesiones son los siguientes:

cycledevie
servlet principal: analiza la solicitud del cliente;
  • si esta forma parte de una sesión, pasa el control a la página valide.jsp, que mostrará las características de dicha sesión. Desde esta página, el usuario puede:
    • recargarla
    • invalídarla
  • si la solicitud pide invalidar la sesión actual, el servlet pasa el control a la página invalide.jsp, que propondrá al usuario volver a crear una nueva sesión
  • si durante su inicialización, el servlet encuentra errores, pasa el control a la página erreur.jsp, que mostrará un mensaje de error.
valide.jsp
  • muestra las características de la sesión actual y ofrece dos enlaces:
    • uno para recargar la página y ver así cómo evoluciona el parámetro del último acceso a la sesión actual
    • y otro para invalidar la sesión actual
invalide.jsp
se muestra cuando el usuario ha invalidado la sesión actual. A continuación, propone crear una nueva.
erreur.jsp
se muestra cuando el servlet principal encuentra errores durante su inicialización.

El servlet principal cycledevie es el siguiente:

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

public class cycledevie extends HttpServlet{

    // variables de instancia
    String msgErreur=null;
    String urlSessionInvalide=null;
    String urlSessionValide=null;
    String urlErreur=null;

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

        // ¿Se ha realizado correctamente la inicialización?
        if(msgErreur!=null){
             // se pasa el control a la página de error
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // se recupera la sesión actual
        HttpSession session=request.getSession();

         // se analiza la acción a realizar
        String action=request.getParameter("action");
        // invalidar la sesión actual
        if(action!=null && action.equals("invalider")){
            // se invalida la sesión actual
            session.invalidate();
             // se pasa el control a url urlSessionInvalide
            getServletContext().getRequestDispatcher(urlSessionInvalide).forward(request,response);
        }
         // otros casos
         // se pasa el control a 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(){
         // se recuperan los parámetros de inicialización
        ServletConfig config=getServletConfig();
        urlSessionInvalide=config.getInitParameter("urlSessionInvalide");
        urlSessionValide=config.getInitParameter("urlSessionValide");
        urlErreur=config.getInitParameter("urlErreur");

        // ¿parámetros ok?
        if(urlSessionValide==null || urlSessionInvalide==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

Cabe destacar los siguientes puntos:

  • en su método de inicialización, el servlet recupera sus tres parámetros
  • en el procesamiento (doGet) de una solicitud, el servlet:
    • En primer lugar, comprueba que no se haya producido ningún error durante la inicialización. Si se ha producido algún error, pasa el control a la página erreur.jsp.
    • Comprueba el valor del parámetro action. Si este tiene el valor «invalider», el servlet pasa a la página invalide.jsp; de lo contrario, pasa a la página valide.jsp.

La página JSP valide.jsp de visualización de las características de la sesión actual:

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

<%
     // jspService
   // aquí nos encontramos en el caso en el que hay que describir la sesión en curso
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
%>
<!-- Inicio de la página 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>

Cabe señalar que en la línea

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

se utiliza un objeto de sesión que surge de la nada. De hecho, este objeto forma parte de los objetos implícitos puestos a disposición de las páginas JSP, al igual que los objetos request, response, out, config (ServletConfig), context (ServletContext), que ya hemos visto. Los dos enlaces de la página hacen referencia al servlet cycledevie presentado anteriormente:

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

El enlace para invalidar la sesión incluye el parámetro action=invalider, que permitirá al servlet cycledevie reconocer que el usuario desea invalidar la sesión actual. El otro enlace permite recargar la página. Para que el navegador no la recupere de la caché, la directiva HTML:

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

. Indica al navegador que no utilice la caché para la página que recibe.

La página invalide.jsp es la siguiente:

<!-- Inicio de la página 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>

Ofrece un enlace que apunta al servlet cycledevie sin el parámetro action. Este enlace hará que el servlet cycledevie cree una nueva sesión.

La página erreur.jsp es la siguiente:

<%
     // jspService
   // aquí nos encontramos en el caso en el que hay que describir la sesión en curso
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée)";
%>
<!-- Inicio de la página 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>

Su función es mostrar el mensaje de error que le ha transmitido el servlet cycledevie. Veamos ahora algunos ejemplos de ejecución. Se solicita el servlet por primera vez:

Image

La página anterior indica que estamos en una nueva sesión. Utilizamos el enlace «Recargar la página»:

Image

El resultado anterior indica que seguimos en la misma sesión que en la página anterior (el mismo ID). Observaremos que ha cambiado la hora del último acceso a esta sesión. Ahora utilicemos el enlace «Invalidar la sesión»:

Image

Obsérvese el URL de esta nueva página con el parámetro action=invalider. Utilicemos el enlace «Crear una nueva sesión» para crear una nueva sesión:

Image

Se observa que se ha iniciado una nueva sesión. En los ejemplos anteriores, la sesión se basa en el mecanismo de las cookies. Desactivemos ahora el uso de cookies en nuestro navegador y repitamos las pruebas. Los siguientes ejemplos se han realizado con Netscape Communicator. Por alguna razón inexplicable, las pruebas realizadas con IE6 daban resultados inesperados, como si IE6 siguiera utilizando cookies a pesar de que estas se habían desactivado. Se solicita el servlet cycledevie por primera vez:

Image

Ahora utilizamos el enlace «Recargar la página»:

Image

Se pueden observar dos cosas:

  • el ID de la sesión ha cambiado
  • el servlet detecta la sesión como una nueva sesión

El servidor Tomcat ofrece una solución al problema de los usuarios que desactivan el uso de cookies en su navegador. Utiliza dos mecanismos para implementar el token del que hablamos al principio de este párrafo: las cookies y la reescritura de URL. Si la cookie de sesión no está disponible, intentará obtener el token a partir del URL solicitado por el cliente. Para ello, es necesario que esta contenga el token. En general, todos los enlaces generados en un documento HTML hacia la aplicación web deben contener el token de esta. Esto se puede hacer con el método encodeURL:

String [HttpResponse].encodeURL(String URL)
añade el token de la sesión actual al URL pasado como parámetro en la forma URL;jsessionid=xxxx

Modificamos nuestra aplicación de la siguiente manera:

  • en el servlet cycledevie.java, los URL se codifican:
             // se pasa el control a la página de error
            getServletContext().getRequestDispatcher(response.encodeURL(urlErreur)).forward(request,response);
....
             // se pasa el control a url urlSessionInvalide
            getServletContext().getRequestDispatcher(response.encodeURL(urlSessionInvalide)).forward(request,response);
....
         // se pasa el control a url urlSessionInvalide
        getServletContext().getRequestDispatcher(response.encodeURL(urlSessionValide)).forward(request,response);
  • en la página valide.jsp, los URL están codificados:
<%
     // jspService
   // aquí nos encontramos en el caso en el que hay que describir la sesión en curso
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
   // codificación URL ciclo de vida
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");  
%>
............
      <br><a href="<%= URLcycledevie %>?action=invalider">Invalider la session</a>
      <br><a href="<%= URLcycledevie %>">Recharger la page</a>
  • en la página invalide.jsp, los URL están codificados:
<%
     // jspservice: invalidamos la sesión actual
  session.invalidate();
   // codificación URL ciclo de vida
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");
%>  
..........
    <a href="<%= URLcycledevie %>">Créer une nouvelle session</a>

Ahora estamos listos para las pruebas. Utilizamos Netscape 4.5 y las cookies han sido desactivadas. Solicitamos por primera vez el servlet cycledevie:

Image

y recargamos la página con el enlace «Recargar la página»:

Image

Podemos ver que:

  • la sesión no ha cambiado (el mismo ID)
  • el URL del servlet cycledevie contiene efectivamente el token, tal y como muestra el campo Adresse anterior
  • por lo tanto, el servidor Tomcat recupera el token de sesión en el URL solicitado (si el desarrollador se ha encargado de codificarlo).

4.4. Ejemplo 2

A continuación, presentamos un ejemplo que muestra cómo almacenar información en la sesión de un cliente. En este caso, la única información será un contador que se incrementará cada vez que el usuario llame a la función URL del servlet. Cuando se llama a esta función por primera vez, aparece la siguiente página:

Image

Si hacemos clic en el enlace «Recargar la página» de arriba, obtenemos la siguiente página nueva:

Image

La aplicación tiene tres componentes:

  • un servlet que procesa la solicitud del cliente
  • una página JSP que muestra el valor del contador
  • una página JSP que muestra cualquier error

Estos tres componentes se instalan en la aplicación web sessions ya utilizada. El archivo web.xml de esta se ha modificado para configurar los nuevos servlets:

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

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

<web-app>
...
  <servlet>
      <servlet-name>compteur</servlet-name>
    <servlet-class>compteur</servlet-class>
    <init-param>
          <param-name>urlAffichageCompteur</param-name>
        <param-value>/compteur.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlErreur</param-name>
        <param-value>/erreurcompteur.jsp</param-value>
    </init-param>
  </servlet>
...
  <servlet-mapping>
      <servlet-name>compteur</servlet-name>
    <url-pattern>/compteur</url-pattern>
  </servlet-mapping>
</web-app>
  • El servlet se llama contador (servlet-name) y está vinculado al archivo de clase compteur.class (servlet-class)
  • tiene dos parámetros de inicialización:
    • urlAffichageCompteur: URL de la página JSP de visualización del contador
    • urlErreur: URL de la página JSP de visualización de un posible error
  • y un alias /contador que hace que se llame a través de URL http://localhost:8080/sessions/contador

El servlet compteur.java es el siguiente:

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

public class compteur extends HttpServlet{

     // variables de instancia
    String msgErreur=null;
    String urlAffichageCompteur=null;
    String urlErreur=null;

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

        // ¿Se ha realizado correctamente la inicialización?
        if(msgErreur!=null){
             // se pasa el control a la página de error
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // se recupera la sesión actual
        HttpSession session=request.getSession();
         // y el contador
        String compteur=(String)session.getAttribute("compteur");
        if(compteur==null) compteur="0";
         // incremento del contador
        try{
            compteur=""+(Integer.parseInt(compteur)+1);
        }catch(Exception ex){}
         // almacenamiento del contador en la sesión
        session.setAttribute("compteur",compteur);
         // y en la consulta
        request.setAttribute("compteur",compteur);

         // se pasa el control al url para mostrar el contador
        getServletContext().getRequestDispatcher(urlAffichageCompteur).forward(request,response);
    }

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

     //-------- INIT
    public void init(){
         // se recuperan los parámetros de inicialización
        ServletConfig config=getServletConfig();
        urlAffichageCompteur=config.getInitParameter("urlAffichageCompteur");
        urlErreur=config.getInitParameter("urlErreur");

         // ¿parámetros ok?
        if(urlAffichageCompteur==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

Este servlet tiene la misma estructura que los servlets que ya hemos visto. Solo cabe destacar la gestión del contador:

  • la sesión se recupera mediante request.getSession()
  • el contador se recupera en esta sesión mediante session.getAttribute("contador")
  • si se recupera un valor null, significa que la sesión acaba de comenzar. El contador se pone entonces a 0.
  • el contador se incrementa, se vuelve a introducir en la sesión (session.setAttribute("contador",contador)) y se coloca en la solicitud que se va a pasar al servlet de visualización (request.setAttribute("contador",contador)).

La página de visualización compteur.jsp es la siguiente:

<%
     // jspService
   // se recupera el contador
  String compteur= (String) request.getAttribute("compteur");
  if(compteur==null) compteur="inconnu";
%>
<!-- Inicio de la página 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 página anterior se limita a recuperar el atributo compteur (request.getAttribute("contador")) que le ha pasado el servlet principal y lo muestra.

La página de error erreurcompteur.jsp es la siguiente:

<%
     // jspService
   // se ha producido un error
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- Inicio de la página 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. Ejemplo 3

Nos proponemos escribir una aplicación Java que actúe como cliente de la aplicación compteur anterior. La llamaría N veces seguidas, donde N se pasaría como parámetro. Nuestro objetivo es mostrar un cliente web programado y cómo gestionar las cookies. Nuestro punto de partida será un cliente web genérico presentado en el folleto de Java del mismo autor. Se invoca de la siguiente manera:

clientweb URL GET/HEAD

  • URL: url solicitada
  • GET/HEAD: GET para solicitar el código HTML de la página, HEAD para limitarse únicamente a los encabezados HTTP

He aquí un ejemplo con el URL http://localhost:8080/sessions/compteur:


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


<!-- Inicio de la página 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>

El programa clientweb muestra todo lo que recibe del servidor. Arriba se ve el comando HTTP Set-cookie con el que el servidor envía una cookie a su cliente. En este caso, la cookie contiene dos datos:

  • JSESSIONID, que es el token de la sesión
  • Path, que define la URL a la que pertenece la cookie. Path=/sessions indica al navegador que deberá reenviar la cookie al servidor cada vez que solicite una URL que comience por /sessions. En la aplicación sessions, hemos utilizado diferentes servlets, entre ellos los servlets /sessions/cycledevie y /sessions/compteur. Si llamamos al servlet /sessions/cycledevie, el navegador recibirá un token J. Si con ese mismo navegador se llama a continuación al servlet /sessions/compteur, el navegador devolverá al servidor el token J, ya que este es válido para todos los URL que comienzan por /sessions. En nuestro ejemplo, los servlets cycledevie y compteur no tienen por qué compartir el mismo token de sesión. Por lo tanto, no deberían haberse incluido en la misma aplicación web. Es un punto a tener en cuenta: todos los servlets de una misma aplicación comparten el mismo token de sesión.
  • Una cookie también puede definir un periodo de validez. En este caso, esta información no está presente. Por lo tanto, la cookie se eliminará al cerrar el navegador. Una cookie puede tener un periodo de validez de N días, por ejemplo. Mientras sea válida, el navegador la reenviará cada vez que se consulte una de las URL de su dominio (Path). Tomemos como ejemplo una tienda online de CD. Esta puede seguir el recorrido de su cliente por su catálogo y determinar poco a poco sus preferencias: la música clásica, por ejemplo. Estas preferencias pueden almacenarse en una cookie con una duración de 3 meses. Si ese mismo cliente vuelve al sitio web al cabo de un mes, el navegador reenviará la cookie a la aplicación del servidor. Esta, basándose en la información contenida en la cookie, podrá entonces adaptar las páginas generadas a las preferencias de su cliente.

A continuación se muestra el código del cliente web. Más adelante servirá de punto de partida para otro cliente.

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

public class clientweb{

    // solicita un URL
     // muestra el contenido de esta en pantalla

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

        // número de argumentos
        if(args.length != 2)
            erreur(syntaxe,1);

         // se anota el URI solicitado
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // verificación de la validez del URI
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorrecto
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//captura
         // verificación del pedido
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // pedido incorrecto
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // se extrae la información útil de 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();

         // se puede trabajar
        Socket  client=null;                        // el cliente
        BufferedReader IN=null;                    // el flujo de lectura del cliente
        PrintWriter OUT=null;                        // el flujo de escritura del cliente
        String réponse=null;                        // respuesta del servidor
        try{
             // se conecta al servidor
            client=new Socket(host,port);

            // se crean los flujos de entrada-salida del cliente TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // se solicita el URL - envío de los encabezados HTTP
            OUT.println(commande + " " + path + query + " HTTP/1.1");   
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // se lee la respuesta
            while((réponse=IN.readLine())!=null){
                 // se procesa la respuesta
                System.out.println(réponse);
            }//mientras
             // se ha terminado
            client.close();
        } catch(Exception e){
            // se gestiona la excepción
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // visualización de errores
    public static void erreur(String msg, int exitCode){
         // visualización del error
        System.err.println(msg);
         // parada con error
        System.exit(exitCode);
    }//error
}//clase

Ahora creamos el programa clientCompteur, que se llama de la siguiente manera:

clientCompteur URL N [JSESSIONID]

  • URL: url del servlet contador
  • N: número de llamadas que se deben realizar a este servlet
  • JSESSIONID: parámetro opcional - token de sesión

El objetivo del programa es llamar N veces al servlet contador gestionando la cookie de sesión y mostrando cada vez el valor del contador devuelto por el servidor. Al final de las N llamadas, el valor de este debe ser N. A continuación se muestra un primer ejemplo de ejecución:


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

El programa muestra:

  • los encabezados HTTP que envía al servidor en forma de -->
  • los encabezados HTTP que recibe
  • el valor del contador tras cada llamada

Se observa que en la primera llamada:

  • el cliente no envía ninguna cookie
  • el servidor envía una

En las siguientes solicitudes:

  • el cliente reenvía sistemáticamente la cookie que recibió del servidor en la primera llamada. Esto es lo que permitirá al servidor reconocerlo e incrementar su contador.
  • El servidor, por su parte, ya no le devuelve ninguna cookie

Volvemos a ejecutar el programa anterior pasando el token anterior como tercer parámetro:


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

Aquí vemos que, desde la primera llamada del cliente, el servidor recibe una cookie de sesión válida. Hay que tener en cuenta que, para Tomcat, el tiempo máximo de inactividad de una sesión es, por defecto, de 20 minutos (aunque, de hecho, es configurable). Si la segunda llamada del programa envía con suficiente rapidez la cookie recibida en la primera llamada, para el servidor se trata de la misma sesión. Aquí se señala un posible agujero de seguridad. Si soy capaz de interceptar en la red un token de sesión, entonces puedo hacerme pasar por quien la inició. En nuestro ejemplo, la primera llamada representa a quien inicia la sesión (quizás con un nombre de usuario y una contraseña que le dan derecho a obtener un token) y la segunda llamada representa a quien ha «hackeado» el token de sesión de la primera llamada. Si la operación en curso es una operación bancaria, esto puede resultar muy problemático...

El código del cliente es el siguiente:

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

public class clientCompteur{

     // solicita un URL
     // muestra el contenido de esta en pantalla

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

         // número de argumentos
        if(args.length !=2 && args.length != 3)
            erreur(syntaxe,1);

         // se anota el URL solicitado
        String URLString=args[0];

        // verificación de la validez del URL
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorrecto
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//captura
         // verificación del número de llamadas N
        int N=0;
        try{
            N=Integer.parseInt(args[1]);
            if(N<=0) throw new Exception();
        }catch(Exception ex){
             // argumento N incorrecto
            erreur("Le nombre d'appels N doit être un entier >0",3);
        }
         // ¿Se ha pasado el token JSESSIONID como parámetro?
        String JSESSIONID="";
        if (args.length==3) JSESSIONID=args[2];

        // se extrae la información útil de 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();

         // se puede trabajar
        Socket  client=null;                        // el cliente
        BufferedReader IN=null;                    // el flujo de lectura del cliente
        PrintWriter OUT=null;                        // el flujo de escritura del cliente
        String réponse=null;                        // respuesta del servidor
         // el modelo buscado en los encabezados HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
        // el patrón buscado en el código HTML
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
         // el resultado de la comparación con el patrón
        Matcher résultat=null;
         // un valor booleano que indica el resultado de la búsqueda del contador
        boolean compteurTrouvé;

        try{
             // se realizan las N llamadas al servidor
            for(int i=0;i<N;i++){
                // se conecta al servidor
                client=new Socket(host,port);

                // se crean los flujos de entrada-salida del cliente TCP
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // se solicita el URL - envío de los encabezados 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 lee la respuesta hasta el final de los encabezados buscando la posible cookie
                while((réponse=IN.readLine())!=null){
                     // seguimiento de la respuesta
                    System.out.println(réponse);
                     // ¿línea vacía?
                    if(réponse.equals("")) break;
                     // línea HTTP no vacía
                     // si no se tiene el token de la sesión, se busca
                    if (JSESSIONID.equals("")){
                        // comparamos la línea HTTP con el patrón de la cookie
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // se ha encontrado la cookie
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while

                 // se han terminado los encabezados HTTP: pasamos al código HTML
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // ¿contiene la línea actual el contador?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // se ha encontrado el contador; se muestra
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while
                 // se ha terminado
                client.close();
            }//for
        } catch(Exception e){
            // se gestiona la excepción
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // visualización de errores
    public static void erreur(String msg, int exitCode){
         // visualización del error
        System.err.println(msg);
         // parada con error
        System.exit(exitCode);
    }//error

     // seguimiento de intercambios cliente-servidor
    public static void envoie(PrintWriter OUT,String msg){
        // envía mensaje al servidor
        OUT.println(msg);
         // seguimiento de pantalla
        System.out.println("--> "+msg);
    }//error
}//clase

Analicemos los puntos importantes de este programa:

  • hay que realizar N intercambios cliente-servidor. Por eso se encuentran en un bucle
            for(int i=0;i<N;i++){
  • En cada intercambio, el cliente establece una conexión TCP-IP con el servidor. Una vez establecida, envía al servidor los encabezados HTTP de su solicitud:
                 // se solicita el URL - envío de los encabezados 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 el token JSESSIONID está disponible, se envía en forma de cookie; de lo contrario, no se envía.

  • Una vez enviada su solicitud, el cliente espera la respuesta del servidor. Empieza por analizar los encabezados HTTP de esta respuesta en busca de una posible cookie. Para encontrarla, compara las líneas que recibe con la expresión regular de la cookie:
         // el patrón buscado en los encabezados HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
                 // se lee la respuesta hasta el final de los encabezados buscando la posible cookie
                while((réponse=IN.readLine())!=null){
                     // seguimiento de la respuesta
                    System.out.println(réponse);
                     // ¿línea vacía?
                    if(réponse.equals("")) break;
                     // línea HTTP no vacía
                     // si no se tiene el token de la sesión, se busca
                    if (JSESSIONID.equals("")){
                        // comparamos la línea HTTP con el patrón de la cookie
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // se ha encontrado la cookie
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while
  • una vez que se haya encontrado el token por primera vez, ya no se buscará en las siguientes llamadas al servidor. Cuando se hayan procesado los encabezados HTTP de la respuesta, se pasa al código HTML de esa misma respuesta. En este, se busca la línea que proporciona el valor del contador. Esta búsqueda también se realiza con una expresión regular:
         // el patrón del contador buscado en el código HTML
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
..................................
                 // se han terminado los encabezados HTTP - pasamos al código HTML
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // ¿contiene la línea actual el contador?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // se ha encontrado el contador; se muestra
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while

4.6. Ejemplo 4

En el ejemplo anterior, el cliente web devuelve el token en forma de cookie. Hemos visto que también puede devolverlo dentro de la propia solicitud URL con el formato URL;jsessionid=xxx. Comprobémoslo. El programa clientCompteur.java se transforma en clientCompteur2.java y se modifica de la siguiente manera:

....
                 // se solicita el URL - envío de los encabezados 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,"");
....

Por lo tanto, el cliente solicita el URL del contador mediante GET URL;jsessionid=xx HTTP/1.1 y ya no envía ninguna cookie. Este es el único cambio. Estos son los resultados de una primera llamada:


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

En la primera llamada, el cliente solicita el URL sin token de sesión. El servidor le responde enviándole el token. A continuación, el cliente vuelve a consultar el mismo URL añadiendo el token recibido. Se observa que el contador se ha incrementado, lo que demuestra que el servidor ha reconocido correctamente que se trata de la misma sesión.

4.7. Ejemplo 5

Este ejemplo muestra una aplicación compuesta por tres páginas que llamaremos page0, page1 y page2. El usuario debe acceder a ellas en este orden:

  • página0 es un formulario que solicita una información: un nombre
  • La página 1 es un formulario obtenido como respuesta al envío del formulario de la página 0. Solicita una segunda información: una edad
  • página2 es un documento HTML que muestra el nombre obtenido en página0 y la edad obtenida en página1.

Aquí hay tres intercambios cliente-servidor:

  • en el primer intercambio, el cliente solicita el formulario de la página 0 y el servidor lo envía
  • en el segundo intercambio, el cliente solicita el formulario de la página 1 y el servidor lo envía. El cliente envía el nombre al servidor.
  • En el tercer intercambio, el cliente solicita el documento page3 y el servidor lo envía. El cliente envía la edad al servidor. El documento page3 debe mostrar el nombre y la edad. El servidor obtuvo el nombre en el segundo intercambio y lo ha «olvidado» desde entonces. Se utiliza una sesión para guardar el nombre en el intercambio 2, de modo que esté disponible en el intercambio 3.

La página page0 obtenida en el primer intercambio es la siguiente:

Image

Se rellena el campo del nombre:

Image

Se utiliza el botón Suite y se obtiene entonces la página page1 siguiente:

Image

Rellenamos el campo de la edad:

Image

Se utiliza el botón Suite y se obtiene la siguiente página page2:

Image

Cuando se envía la página page0 al servidor, este puede devolverla con un código de error si el nombre está vacío:

Image

Al enviar la página page1 al servidor, este puede devolverla con un código de error si la edad no es válida:

Image

La aplicación se compone de un servlet y cuatro páginas JSP:

page0.jsp
muestra la página0
page1.jsp
muestra la página 1
page2.jsp
muestra la página 2
erreur.jsp
muestra una página de error

La aplicación web se llama suitedepages y está configurada de la siguiente manera en el archivo server.xml de Tomcat:

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

El archivo de configuración web.xml de la aplicación suitedepages es el siguiente:

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

El servlet principal se llama «main» y, gracias a su alias (servlet-mapping), se puede acceder a él a través de URL http://localhost:8080/suitedepages/main. Tiene cuatro parámetros de inicialización que son los URL de las cuatro páginas JSP utilizadas para las diferentes visualizaciones. El código del servlet main es el siguiente:

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

public class main extends HttpServlet{

    // variables de instancia
    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{

        // ¿Se ha realizado correctamente la inicialización?
        if(msgErreur!=null){
             // se pasa el control a la página de error
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }
         // se recupera el parámetro de etapa
        String étape=request.getParameter("etape");
         // se recupera la sesión actual
        HttpSession session=request.getSession();
         // se procesa la etapa actual
        if(étape==null) étape0(request,response,session);
        if(étape.equals("1")) étape1(request,response,session);
        if(étape.equals("2")) étape2(request,response,session);
         // los demás casos son inválidos
        étape0(request,response,session);
    }

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

     //-------- INIT
    public void init(){
         // se recuperan los parámetros de inicialización
        ServletConfig config=getServletConfig();
        urlPage0=config.getInitParameter("urlPage0");
        urlPage1=config.getInitParameter("urlPage1");
        urlPage2=config.getInitParameter("urlPage2");
        urlErreur=config.getInitParameter("urlErreur");

         // ¿parámetros ok?
        if(urlPage0==null || urlPage1==null || urlPage2==null){
            msgErreur="Configuration incorrecte";
        }
    }

     //-------- paso 0
    public void étape0(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
        // se establecen algunos atributos
        request.setAttribute("nom","");
         // se muestra la página 0
        request.getRequestDispatcher(urlPage0).forward(request,response);
    }

     //-------- paso 1
    public void étape1(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // se recupera el nombre de la consulta
        String nom=request.getParameter("nom");
        // ¿nombre establecido?
        if(nom==null) étape0(request,response,session);
         // se eliminan los posibles espacios del nombre
        nom=nom.trim();
         // se coloca en un atributo de la consulta
        request.setAttribute("nom",nom);
         // ¿nombre vacío?
        if(nom.equals("")){
             // es un error
            ArrayList erreurs=new ArrayList();
            erreurs.add("Nous n'avez pas indiqué de nom");
             // se incluyen los errores en la consulta
            request.setAttribute("erreurs",erreurs);
             // volver a la página 0
            étape0(request,response,session);
        }
         // nombre válido: se guarda en la sesión actual
        session.setAttribute("nom",nom);
         // se establece el atributo «edad» en la consulta
        request.setAttribute("age","");
         // se muestra la página 1
        request.getRequestDispatcher(urlPage1).forward(request,response);
    }

     //-------- paso 2
    public void étape2(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // se recupera el nombre de la sesión
        String nom=(String)session.getAttribute("nom");
         // ¿nombre establecido?
        if(nom==null) étape0(request,response,session);
         // se coloca en un atributo de la consulta
        request.setAttribute("nom",nom);
         // se recupera la edad en la consulta
        String age=request.getParameter("age");
        // ¿Se ha establecido la edad?
        if(age==null){
            // volver a la página 1
            request.setAttribute("age","");
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // se almacena la edad en la consulta
        age=age.trim();
        request.setAttribute("age",age);
        // ¿Edad válida?
        if(! Pattern.matches("^\\s*\\d+\\s*$",age)){
            // es un error
            ArrayList erreurs=new ArrayList();
            erreurs.add("Age invalide");
            // se incluyen los errores en la consulta
            request.setAttribute("erreurs",erreurs);
             // volver a la página 1
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // edad válida - se muestra la página 2
        request.getRequestDispatcher(urlPage2).forward(request,response);
    }
}
  • El método init recupera los cuatro parámetros de inicialización y genera un mensaje de error si falta alguno de ellos
  • Hemos visto que la consulta incluye tres intercambios. Para saber en qué punto nos encontramos en ellos, los formularios page0 y page1 tienen una variable oculta etape que tiene el valor 1 (page0) o 2 (page1). Aquí podríamos considerar este número como el número de la página siguiente que se va a mostrar. En el método doGet, este parámetro se recupera de la solicitud y, según su valor, el procesamiento se delega a otros tres métodos:
    • étape0 procesa la solicitud inicial y envía page0
    • étape1 procesa el formulario de page0 y envía page1 o, de nuevo, page0 si se ha producido un error
    • El paso 2 procesa el formulario de page1 y envía page2 o, de nuevo, page1 si se ha producido un error
  • paso 0
    • muestra page0 con un nombre vacío
  • paso 1
    • Recupera el parámetro nom del formulario page0.
    • comprueba que el nombre existe (no es nulo). Si no es así, vuelve a mostrar page0 como si fuera la primera llamada.
    • Comprueba que el nombre no esté vacío. Si no es así, se vuelve a mostrar page0 con un mensaje de error.
    • almacena el nombre en la sesión actual y muestra page1 si el nombre es válido.
  • paso 2
    • Recupera el parámetro nom de la sesión actual.
    • Comprueba que el nombre existe (no es nulo). Si no es así, se vuelve a mostrar page0 como si fuera la primera llamada.
    • Recupera el parámetro age de la solicitud actual enviada por page1.
    • Comprueba que la edad sea válida. Si no es así, se vuelve a mostrar page1 con un mensaje de error.
    • Almacena el nombre y la edad como atributos de la solicitud y muestra page2 si el nombre y la edad son válidos.

La página page0.jsp es la siguiente:

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

<% // page0.jsp
     // se recuperan los atributos de la consulta
  String nom=(String)request.getAttribute("nom");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
   // ¿Atributos válidos?
  if(nom==null){
       // vuelta al servlet principal
    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>
    <% // ¿Hay errores?
      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 página page0.jsp puede ser llamada por el servlet principal en dos casos:
    • durante la solicitud inicial
    • tras el procesamiento del formulario de page0 cuando se produce un error
  • el parámetro nom que se debe mostrar le es proporcionado por el servlet principal, así como la posible lista de errores. Por lo tanto, el servlet page0.jsp comienza por recuperar esta información.
  • El formulario se «envía» al servlet principal con el campo oculto (hidden) etape, que indica en qué etapa de la aplicación nos encontramos.

La página page1.jsp es la siguiente:

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

<% // page1.jsp
     // se recuperan los atributos de la solicitud
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
  // ¿atributos válidos?
  if(nom==null || age==null){
      // vuelta al servlet principal
    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>
    <% // ¿Hay errores?
      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 página page1.jsp tiene una estructura similar a la de la página page0.jsp, con la diferencia de que ahora recibe dos atributos del servlet principal: nom y age. Por último, la página page2.jsp es la siguiente:

<% 
     // page2.jsp
     // se recuperan los atributos de la solicitud
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  // ¿atributos válidos?
  if(nom==null || age==null){
      // vuelta al servlet principal
    request.getRequestDispatcher("/main").forward(request,response);
  }
%>  


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

La página page2.jsp también recibe los atributos nom y age del servlet principal. Se limita a mostrarlos. Por último, la página erreur.jsp, encargada de mostrar un error en caso de inicialización incorrecta del servlet, es la siguiente:

<%
     // jspService
   // se ha producido un error
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- Inicio de la página HTML -->
<html>
  <head>
      <title>Suite de pages</title>
  </head>
  <body>
      <h3>Suite de pages</h3>
      <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

Muestra el atributo msgErreur que le ha pasado el servlet principal.

En conclusión, se puede observar que, a lo largo de las tres etapas de la aplicación, es siempre el servlet principal el que el navegador consulta en primer lugar. Pero no es él quien genera la respuesta que se va a mostrar, sino una de las cuatro páginas JSP. El usuario no percibe este detalle, ya que el navegador sigue mostrando en su campo «Dirección» la URL solicitada inicialmente, es decir, la del servlet principal.