lunes, 27 de enero de 2014

Un proyecto en NetBeans de principio a fin (VII). Gestión de sesiones

Todas las aplicaciones de comercio electrónico que ofrecen algún tipo de funcionalidad del estilo del carrito de la compra, necesitan ser capaces de recordar los datos específicos de un usuario y sus acciones en el sitio web. Desafortunadamente, el protocolo HTTP -base de las comunicaciones en Internet- es un protocolo stateless. Esto quiere decir que cada petición recibida por el servidor, es un fragmento independiente de información, que no tiene relación con peticiones recibidas previamente. Por lo tanto, si un cliente pulsa un botón y añade algún artículo a su carrito, nuestra aplicación debe tomar las medidas necesarias para asegurar que el estado del carrito del usuario se actualiza, y además, esa acción no debe afectar al carrito de otro usuario que eventualmente pueda estar navegando por el website al mismo tiempo.

A fin de poder resolver el escenario descrito, necesitamos implementar una funcionalidad que permita que una sesión pueda ser creada y mantenida durante la duración de la visita del usuario a nuestra web. La tecnología de servlet, que es fundamento de todas las aplicaciones web basadas en Java, proporciona para esta tarea su interfaz HttpSession. Además necesitaremos definir varias clases (que llamaremos ShoppingCart y ShoppingCartItem) que permitirán que la aplicación almacene temporalmente datos de usuario mientras sea mantenida la sesión.

El enfoque de esta parte del tutorial será diferente con respecto a las otras. En lugar de crear archivos de proyecto y proporcionar los pasos con fragmentos de código -para copiar y pegar en nuestro proyecto-, vamos a abrir una versión completa del proyecto para esta unidad (se puede descargar en este enlace, y vamos a examinar el código usando el depurador del IDE y otras herramientas. Aprenderemos cómo aplicar un objeto HttpSession a nuestro código, de forma que cada visita a la web de lugar a una sesión dedicada. Veremos las variables de ámbito, y cómo se usan en clases de Java y páginas JSP. Discutiremos cuales son los mecanismos por defecto de HttpSession para mantener las sesiones (por ejemplo las cookies) y veremos que tendríamos que hacer en caso de encontrarnos las cookies desactivadas en el navegador del usuario. Finalmente veremos los time-outs, y demostraremos como se manejan usando filtros simples que interceptan las peticiones para comprobar si existe o no una sesión. 

MANEJANDO DATOS DE SESIÓN

Las aplicaciones pueden gestionar sesiones de usuario con el objeto HttpSession. Se puede enlazar un datos específico de usuario con el objeto HttpSession, y acceder a este dato posteriormente. Ambas acciones, enlazado y acceso, pueden ser hechas desde clases Java, así como desde variables de ámbito de sesión en expresiones EL. 

Trabajando con un objeto HttpSession

La aplicación AffableBean usa el objeto HttpSession para identificar usuarios a través de múltiples peticiones. Un objeto HttpSession se obtiene usando getSession() en una petición dada: 

HttpSession session = request.getSession()

Si todavía no existe un objeto de sesión para la petición, este método lo crea y lo devuelve.

El objeto de sesión puede usarse como vehículo para pasar datos entre peticiones. El método setAttribute se usa para enlazar objetos con la sesión. Del mismo modo, el método getAttribute se utiliza para recuperar objetos de la sesión. En la aplicación AffableBean por ejemplo, el carrito del usuario se crea y enlaza con la sesión del usuario de la siguiente forma: 

ShoppingCart cart = new ShoppingCart(); 
session.setAttribute(“cart”, cart);

A la hora de recuperar el carrito de la sesión, aplicamos el método getAttribute: 

cart = (shoppingCart) session.getAttribute(“cart”);

En las páginas JSP, podemos acceder a los objetos enlazados con la sesión usando expresiones EL. Por ejemplo, si un objeto ShoppingCart llamado "cart" está enlazado con la sesión podemos acceder a él usando sencillamente la expresión ${cart}

Lo cierto es que acceder al objeto ShoppingCart por sí solo tiene más bien poco interés. Lo interesante en poder acceder a los valores almacenados en el objeto. Si exploramos la clase ShoppingCart en la demo del proyecto, notaremos que contiene una serie de propiedades: 

· double total
· int numberOfItems
· List<String, ShoppingCartItem> items


Puesto que las propiedades llevan métodos asociados, es posible acceder a los valores de propiedades particulares usando simple notación de punto en una expresión EL. En la página cart.jsp podemos ver cómo es accedido el valor de la propiedad numberOfItems: 

<p>Your shopping cart contains ${cart.numberOfItems} items.</p>

Y a la hora de extraer datos de propiedades que contienen múltiples valores -como la lista de ítems del carrito-, la página cart.jsp usa un bucle:

<c:forEach var=”cartItem” items=”${cart.items}” varStatus=”iter”>
   <c:set var=”product” value”${cartItem.product}”/>
      <tr class=”${((iter.index % 2) == 0) ? 'lightBlue' : 'white'}”>
    <td>
       <img src=”${initParam.productImagePath}${product.name}.png” alt=”${product.name}”>
    </td>    
    <td>
       &euro; ${cartItem.total}
       <br>
       <span class=”smallText”>( &euro; ${product.price} / unit )</span>
    </td>
    ...
      </tr>
</c:forEach>

La propiedad product de ShoppingCartItem identifica el tipo de producto para un artículo del carrito. El bucle anterior se beneficia de esto configurando en primer lugar una variable product en la expresión ${cartItem.product}. Luego usa esa variable para obtener información sobre el precio del producto. 

Trabajando con variable de ámbito en aplicaciones web

Cuando trabajamos con tecnología JSP/Servlet, hay cuatro objetos de ámbito disponibles en la esfera de las aplicaciones. JSP implementa objetos implícitos que permiten acceder a las clases definidas por la API de Servlet.

Ámbito Definición Clase Servlet Objeto Implícito JSP
Aplicación Memoria global para una aplicación web javax.servlet.ServletContext applicationScope
Sesión Datos específicos de una sesión de usuario javax.servlet.http.HttpSession sessionScope
Request Datos específicos de una petición individual del servidor javax.servlet.HttpServletRequest requestScope
Página Datos que sólo son válidos en el contexto de una página (sólo JSPs) [n/a] pageScope

Si abrimos el archivo category.jsp en el editor, veremos que las expresiones EL incluyen varias variables de ámbito: ${categories}, ${selectedCategory} y ${categoryProducts}. La variable ${categories} es del ámbito de la aplicación, y está definida en el método init del servlet del controlador:  

//store category list in servlet context 
getServletContext().setAttribute("categories", categoryFacade.findAll());

 Las otras dos, ${selectedCategory} y ${categoryProducts}, son ubicadas en el ámbito de sesión de la aplicación por el ControllerServlet de la siguiente forma: 

//place selected category in session scope 
session.setAttribute("selectedCategory", selectedCategory); 

Comparando el ControllerServlet.java de la demo, que estamos viendo en esta parte del tutorial, con el mismo archivo de nuestro proyecto, tal cual quedó al final de la última lección, los lectores más avezados se habrán dado cuenta de que las expresiones ${selectedCategory} y ${categoryProducts} estaban originalmente emplazadas en el ámbito de la petición (request). En la unidad anterior esto estaba bien, pero consideremos ahora que pasa si un usuario pulsa el botón "add to cart" en una página de categoría. El servidor responde a una petición addToCart devolviendo la página de la categoría que se está viendo. Ésta por tanto necesita conocer la categoría seleccionada (selectedCategory) y los productos de dicha categoría (categoryProduct). Mejor que establecer esta información para cada petición, la podemos tener en el ámbito de sesión de una petición de categoría, de forma que se mantendrá a través de múltiples peticiones, y podrá ser accedida cuando sea necesario. Además, si examinamos la funcionalidad prevista para la página del carrito de compra, el botón de "continue shopping" devuelve al usuario a la categoría vista previamente. Una vez más, la variable de la categoría seleccionada y los productos de la categoría son requeridas.

Cuando nos referimos a variables con ámbito en una expresión EL, no necesitamos especificar el ámbito de la variable (siempre y cuando no tengamos dos variables con el mismo nombre en distintos ámbitos). El motor de JSP examina los cuatro ámbitos posibles y devuelve la primera variable coincidente que encuentra. Por ejemplo, en category.jsp se puede usar la siguiente expresión:  

${categoryProducts}

lo que equivale a: 

${sessionScope.categoryProducts}

EXAMINANDO DATOS DE SESIÓN CON EL DEPURADOR DE JAVA

En este punto vamos a explorar cómo se comporta la aplicación en tiempo de ejecución. Usaremos el depurador del IDE para recorrer el código paso a paso y examinaremos cómo se crea el HttpSession, y cómo otros objetos se pueden definir en el ámbito de la sesión para su posterior recuperación.

Hay diferencias sustanciales entre la demo del proyecto que vamos a usar en este apartado y el proyecto que venimos desarrollando desde las primeras entregas de este tutorial. Aunque podemos ejecutar esta versión desde el IDE, como el objetivo es, a fin de cuentas, enredar todo lo que se pueda, yo he preferido incorporar las modificaciones a mi proyecto. Hay que hacer cambios en un buen número de archivos: el web.xml, el fichero de estilos affablebean.css, el servlet del controlador ControllerServlet.java, los ficheros header.jspf y footer.jspf, y las vistas cart.jsp, category.jsp y checkout.jsp... Además hay clases nuevas: ShoppingCart.java y ShoppingCartItem.java... Pero así es mucho más divertido. ¿De qué otra manera podrías pasar unas horitas buscando por qué demonios la demo funciona perfectamente y tu proyecto no?...

Tanto si hemos utilizado la versión del proyecto sugerida en este capítulo, como si hemos mejorado nuestro propio proyecto (lo que en la práctica equivale a haberse pateado todos los archivos buscando las diferencias para incluirlas en nuestra solución), encontraremos que la aplicación ha mejorado en los siguientes aspectos: 

Página de Categorías

Cuando hacemos click en el botón "add to cart" por primera vez, se habilitan los widgets del carrito de la compra y el proceso de pago.

Usar el botón "add to cart" implica una actualización en el número de elementos del carrito, visible en el widget de la cabecera.

Al pulsar en "view cart" se muestra la página del carrito.

Al pulsar en "proceed to checkout" se muestra la página del proceso de pago. 

Página del carrito

Cuando pulsamos "clear cart" se eliminan los elementos del carrito.

Cuando se pulsa "continue shopping" se vuelve a la página previa de categorías que estuviéramos visitando.

Al pulsar en "proceed to checkout" se muestra la página del proceso de pago.

Si ponemos un 0 en la cantidad de algún artículo del carrito, dicho artículo se elimina de la lista de la compra. 

Página de pago

Al pulsar en "view cart" se muestra la página del carrito.

Al pulsar "submit purchase" se muestra la página de confirmación (todavía sin datos específicos de usuario)

Veamos ahora cómo va lo de los puntos de interrupción. Abrimos el ControllerServlet.java y añadimos un punto de interrupción en el método doPost, en la línea donde se crea el objeto HttpSession: 

HttpSession session = request.getSession()

Al ejecutar el depurador (botón Debug Proyect en la barra de herramientas) el servidor GlassFish se inicia (o se reinicia si ya estaba en ejecución) y abre una conexión en el puerto de depuración (este puerto puede verse y modificarse en la ventana de servidores, "tools>servers"). La página de bienvenida se abre en el navegador.

Si pinchamos en cualquier imagen de categoría vamos a la página de Categorías, una vez ahí, pulsando el botón "add to cart" se envía una petición addToCart al servidor: 

<form action="addToCart" method="post">

(!)Conviene recordar aquí que el método doPost del ControllerServlet manejaba peticiones para el patrón URL /addToCart, por lo que cabe esperar que cuando un usuario toque el mentado botón el método doPost sea llamado (habría que echarle un vistazo a la cuarta entrega aquí).

Si seleccionamos cualquier producto para añadirlo al carrito y volvemos al IDE, veremos que el depurador se detiene en el punto de interrupción. Posicionando el cursor sobre la llamada a getSession() podremos ver la documentación de Javadoc pulsando Ctrl+Space. Se puede comprobar que getSession() devuelve el HttpSession actualmente asociado con la petición, y si no existe el método crea un nuevo objeto session.

En el momento en que tenemos detenido el debugger la variable session todavía no tiene valor y no se reconoce como tal en el contexto actual. Un paso más adelante en la depuración y podremos ver que la variable session ha sido dotada de valor.

Y así pasito a pasito (vamos, F8 a F8) llegaremos hasta la sentencia if donde la expresión userPath.equals("/addToCart") debería ser evaluada a TRUE. Un poco más adelante podemos ver cómo el objeto shoppingCart para el usuario de la sesión se crea, por primera y única vez, cuando se añade un primer ítem al carrito. Puesto que es la primera vez que se recibe una petición de tipo addToCart cabe esperar que el objeto cart sea NULL.

Una vez que se ha creado el objeto ShoppingCart podemos continuar y saltar dentro de su método constructor. Si volvemos a la pestaña del ControllerServlet, un símbolo nos indica que el depurador está actualmente suspendido en un método que está por encima en la pila de llamadas.

Si continuamos con F8, el depurador termina con el constructor de ShoppingCart y vuelve al servlet del controlador. Durante la ejecución del depurador podemos activar la ventana de Variables (en "Window>Debugging>Variables"). En esta ventana podemos observar en cada momento las variables que se están usando durante el proceso de depuración. Concretamente, si desplegamos el nodo "session>session>attributes", podemos examinar los objetos que están enlazados a la sesión. Desde aquí podemos crear también inspecciones (Watch) sobre la variable de sesión que pasarán a estar disponibles en la ventana de inspecciones (Watches). Ahí podremos ver los valores contenidos en la sesión durante todos los pasos de la depuración (también es posible añadir watch expressions pulsando botón derecho del ratón sobre las variables en el editor o tecleando directamente en la ventana de Watches)...

En fin, el depurador del IDE es, sin duda, un mundo maravilloso, pero por el momento voy a cambiar de registro, ya que a estas alturas es posible que cualquiera que lea esta entrada esté haciendo una lectura diagonal más o menos comprensiva... Eso los que sigan leyendo a estas alturas claro... 

EXAMINANDO LAS OPCIONES DE RASTREO (TRACKING) DE SESIÓN 

Existen tres formas convencionales de rastrear sesiones entre cliente y servidor. De lejos la más común son las cookies. Si las cookies no son soportadas o están desactivadas usamos reescritura de URL. Finalmente los campos ocultos en formularios pueden ser utilizados como medio para mantener en estado sobre múltiples peticiones, aunque este último caso estaría limitado al uso de formularios.

El proyecto AffableBean incluye sendos ejemplos del método de campos ocultos en las páginas de categorías y carrito. Si abrimos, por ejemplo, la página cart.jsp en el editor podemos ver el siguiente código:

<form action="updateCart" method="post">
    <input type="hidden"
           name="productId"
           value="${product.id}">
    ...
</form>

De esta manera el ID del producto es enviado como parámetro de la petición y es usado por el servidor para identificar el ítem del carrito cuya cantidad necesita ser modificada.

El Servlet API proporciona un mecanismo de alto nivel para el manejo de sesiones. Básicamente crea y pasa cookies entre el cliente y el servidor en cada ciclo de peticiones y respuestas. Si el navegador del cliente no acepta cookies entonces el motor del servlet se encarga, automáticamente, de cambiar a reescritura de URL.

Por defecto, el motor del servlet utiliza cookies para mantener e identificar sesiones entre peticiones. Una secuencia alfanumérica se genera de forma aleatoria para cada objeto de sesión y sirve como identificador único. El identificador se pasa al cliente como una cookie "JSESSIONID". Cuando el cliente hace una petición, el motor del servlet lee el valor de la cookie para determinar a que sesión pertenece.

Cuando el motor del servlet detecta que el browser del cliente no soporta las cookies, cambia a la reescritura de URL como medio para mantener las sesiones. Todo esto ocurre de forma transparente para el cliente. Sin embargo para el desarrollador el proceso no es del todo transparente.

Como desarrolladores debemos asegurarnos de que la aplicación es capaz de reescribir URLs cuando las cookies están desactivadas. Esto se consigue llamando al método encodeURL de la respuesta en todas las direcciones URL devueltas por los servlets de la aplicación. Haciéndolo así se permite que el identificador de la sesión se añada a las URLs. Por ejemplo, si el navegador envía una petición para la categoría de panadería (Bakery, Category?3), el servidor responderá con el identificador de la sesión incluido en la URL de forma parecida a esta:

//AffableBean/Category;jsessionid:364b636d75d90a6e4d0085119990?3

Como sabemos, todas las URLs devueltas por los servlets de la aplicación deben ser codificadas. No hay que perder de vista que las páginas JSP son compiladas dentro de los servlets. Las etiquetas JSTL <c:url> sirven a este propósito.

Si desactiváramos temporalmente las cookies del navegador y ejecutáramos la aplicación AffableBean -en el estado actual-, nos daríamos cuenta de que al intentar añadir productos al carrito desde alguna de las categorías, la funcionalidad de la aplicación se vería seriamente comprometida. Básicamente el servidor generaría una sesión y enlazaría objetos con ella. Sin embargo fallaría estrepitosamente al intentar configurar la cookie JSESSIONID. Así que cuando el cliente hiciera peticiones, el servidor no tendría forma de identificar la sesión a la que pertenecen dichas peticiones; es más, ni siquiera sería capaz de localizar ningún atributo previamente definido en la sesión, como la categoría seleccionada o los productos de la categoría... Vamos, que en términos técnicos se haría la picha un lío...

En cualquier caso, la reescritura de URL sólo debe ser utilizada en los casos en los que las cookies no se admitan como método de tracking. La reecritura de URL se considera una solución poco óptima, porque expone IDs de sesión en los logs, así como marcadores, referencias a cabeceras y HTML en la barra de direcciones del browser. Además es una técnica que requiere más recursos en el lado del servidor, así como mecanismos adicionales para gestionar y comparar los IDs de sesión de cada petición entrante con una sesión existente. 

MANEJANDO LOS TIME-OUTS DE SESIÓN 

Configuración de intervalos de tiempo de sesión

Decidir cual es el tiempo que nuestro servidor debe mantener abierta una sesión puede dar lugar a un verdadero quebradero de cabeza. Mantener un número alto de sesiones abierto durante mucho tiempo puede suponer un gasto excesivo en la memoria de nuestro servidor. Por otro lado, acortar demasiado los tiempos de sesión puede lastrar la usabilidad de nuestra aplicación, perjudicando al negocio: nadie quiere perder todos los datos de un formulario y el contenido del carrito de la compra en los dos minutos que emplea en localizar la tarjeta de crédito...

En cualquier caso nuestro framework nos permite configurar el tiempo de sesión mediante unos ligeros cambios en el archivo web.xml. La plantilla que proporciona NetBeans por defecto para el archivo web.xml incluye una configuración de 30 minutos para la sesión.
<session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>

Basta modificar este valor al que más se ajuste a nuestras necesidades y listo. Adicionalmente nos podemos cargar la etiqueta de <session-timeout> y editar las propiedades de sesión del descriptor de despliegue del servidor GlassFish (archivo sun-web.xml). Esto configurará un time-out global para todas las aplicaciones en el módulo web del servidor.

Manejo de time-outs mediante programación

Si nuestra aplicación depende de las sesiones debemos ser capaces de resolver convenientemente aquellas situaciones en las que nos llegue una petición para una sesión que haya caducado o que no pueda ser identificada. Esto es posible mediante la creación de filtros. En nuestro caso implementaremos un filtro que intercepte las peticiones dirigidas al ControllerServlet. El filtro comprobará si la sesión existe, y de no ser así nos redirigirá a la página de bienvenida. De esta forma evitaremos desagradables pantallazos de errores en el servidor por punteros a null descontrolados.

Para crear un filtro, crearemos un nuevo archivo de tipo Filter en la categoría Web. Lo vamos a llamar "SessionTimeoutFilter", y en el campo Package tecleremos "filter" para encapsularlo en un nuevo paquete. Una plantilla para SessionTimeoutFilter se genera y se muestra en el editor.

En el editor modificaremos la anotación @WebFilter de la siguiente forma:

@WebFilter(servletNames = {"ControllerServlet"}) 
public class SessionTimeoutFilter implements Filter { 

Esto configura el filtro para interceptar cualquier petición que sea manejada por el ControllerServlet
La plantilla para filtros que proporciona el IDE genera un montón de código interesante, sin embargo la mayoría no lo necesitaremos para el propósito de este tutorial. Baste decir que una clase filter debe implementar la interfaz Filter en la que tienen que estar definidos tres métodos: init, que puede realizar cualquier acción antes de iniciar el filtro; destroy, que quita el filtro del servicio; y doFilter, que realiza operaciones en cada petición que el filtro intercepta.

Nosotros por ahora vamos a pasar de todo el código generado y vamos a sustituirlo por lo siguiente:
 
@WebFilter(servletNames = {"Controller"})
public class SessionTimeoutFilter implements Filter {

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;

        HttpSession session = req.getSession(false);

        // if session doesn't exist, forward user to welcome page
        if (session == null) {
            try {
                req.getRequestDispatcher("/index.jsp").forward(request, response);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            return;
        }

        chain.doFilter(request, response);
    }

    public void init(FilterConfig filterConfig) throws ServletException {}

    public void destroy() {}

} 
 
Ya sólo restaría importar las clases necesarias (mediante Ctrl+Shift+I) para quitar los errores que aparecen al pegar este código (Hacen falta imports para HttpServletRequest y HttpSession). También podemos añadir la anotación @Override a los métodos init, destroy y doFilter.
Mediante el depurador y los puntos de interrupción apropiados en el método doFilter podemos comprobar el funcionamiento del filtro a la hora de determinar el comportamiento de las peticiones y las sesiones activas.
… 
En el desarrollo de esta entrada he tenido que optar por recortar bastante del contenido original, el cual podemos encontrar íntegramente aquí. Esto es lo que yo estoy siguiendo en realidad para desarrollar el ejemplo (y es lo que recomiendo a poco que nos aclaremos con el inglés), estas entradas no son más que una referencia (vale, un poco extensa, pero referencia a fin de cuentas) a los contenidos originales, cuyos autores, por cierto, se merecen el mérito y los lectores... De acuerdo, peloteos a parte, también me da una pereza mayúscula transcribir absolutamente todo y mostrar todas las capturas de pantalla... ¬_¬U. La falta de tiempo, la densidad de los contenidos y el esfuerzo de traducción han hecho que esto se prolongue en exceso. Supongo que cualquiera que no esté interesado en ahondar en los detalles hará, en el mejor de los casos, una lectura diagonal de éste y los otros posts relacionados. Lo entiendo, yo haría lo mismo. Sin embargo para mí está sirviendo a un propósito formativo, así que seguiremos con ello (al ritmo que los avatares diarios lo permitan)... Próximamente: Un proyecto en NetBeans de principio a fin (VIII). Integración transaccional de la lógica de negocio.
 

No hay comentarios:

Publicar un comentario