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:
- se crea una conexión única para cada uno de los clients
- 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:
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. | |
identificador de la sesión actual | |
fecha de creación de la sesión actual (número de milisegundos transcurridos desde el 1 de enero de 1970, 0:00). | |
fecha del último acceso del cliente a la sesión | |
tiempo máximo en segundos de inactividad de una sesión. Transcurrido este tiempo, la sesión queda invalidada. | |
Establece en segundos la duración máxima de inactividad de una sesión. Transcurrido este tiempo, la sesión se invalida. | |
verdadero si la sesión acaba de crearse | |
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. | |
elimina datos de la sesión. | |
valor asociado al parámetro paramètre de la sesión. Devuelve null si este último no existe. | |
lista en forma de enumeración de todos los atributos de la sesión actual | |
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:
En la carpeta docBase anterior, se encuentran los siguientes elementos:

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:

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:

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:
url de la página que muestra las características de la sesión actual | |
url de la página que se muestra tras la invalidación de la sesión actual | |
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:
servlet principal: analiza la solicitud del cliente;
| |
| |
se muestra cuando el usuario ha invalidado la sesión actual. A continuación, propone crear una nueva. | |
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
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:
. 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:

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

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

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:

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:

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

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

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

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:

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

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

Se rellena el campo del nombre:

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

Rellenamos el campo de la edad:

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

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:

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

La aplicación se compone de un servlet y cuatro páginas JSP:
muestra la página0 | |
muestra la página 1 | |
muestra la página 2 | |
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:
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.
