martes, 26 de junio de 2012

Struts 2 - Parte 6: Interceptores

Al desarrollar una aplicación, es necesario desarrollar algunas funcionalidades de utilidad que nos hagan más fácil el desarrollo de los procesos de negocio en los cuales estamos interesados. Algunos ejemplos de estas funcionalidades pueden ser el verificar que el usuario haya iniciado una sesión en el sistema, o que tenga permisos para ejecutar la operación que está solicitando, convertir los parámetros de un tipo a otro (de String a Date, o a int), verificar que se hayan proporcionado los datos que son obligatorios, agregar alguna información a la sesión del usuario, etc.

La mayoría de estas funcionalidades pueden ser creadas de una forma genérica y usadas en varias partes de nuestra aplicación o en varias aplicaciones.

Struts 2 integra un mecanismo que nos permite abstraer estas funcionalidades de una manera sencilla, aplicarlas de forma transparente a las acciones que la necesiten (que pueden ser desde una hasta todas las acciones de la aplicación) y configurarlas a través del uso de parámetros.

En este tutorial aprenderemos cómo configurar los interceptores que vienen integrados con Struts 2, además de crear nuestros propios interceptores, agregarlos a un Action, y agregarlos a la pila que se aplica por default a los Actions de nuestra aplicación.

Recordando un poco de la teoría que vimos en el primer tutorial: los interceptores son clases que siguen el patrón interceptor. Estos permiten que se implementen funcionalidades "cruzadas" o comunes para todos los Actions, pero que se ejecuten fuera del Action (por ejemplo validaciones de datos, conversiones de tipos, población de datos, etc.).

Los interceptores realizan tareas antes y después de la ejecución de un Action y también pueden evitar que un Action se ejecute (por ejemplo si estamos haciendo alguna validación que no se ha cumplido). Sirven para ejecutar algún proceso particular que se quiere aplicar a un conjunto de Actions. De hecho muchas de las características con que cuenta Struts 2 son proporcionadas por los interceptores.

Si alguna funcionalidad que necesitamos no se encuentra en los interceptores de Struts 2, podemos crear nuestro propio interceptor y agregarlo a la cadena o pila que se ejecuta por default.

De la misma forma, podemos modificar la pila de interceptores de Struts 2, por ejemplo para quitar un interceptor o modificar su orden de ejecución.

Cada interceptor proporciona una característica distinta al Action. Para sacar la mayor ventaja posible de los interceptores, un Action permite que se aplique más de un interceptor. Para lograr esto Struts 2 permite crear pilas o stacks de interceptores y aplicarlas a los Actions. Cada interceptor es aplicado en el orden en el que aparece en el stack. También podemos formar pilas de interceptores en base a otras pilas. Una de estas pilas de interceptores es aplicada por default a todos los Actions de la aplicación (aunque si queremos también podemos aplicar una pila particular a un Action).

En el primer tutorial pueden ver una tabla con algunos de los interceptores más usados en Struts 2, y de las pilas de interceptores que vienen pre-configuradas.

Para decirlo de otra forma, los interceptores en Struts 2 son los equivalentes a los filtros en la especificación de Servlets.

Comencemos a ver cómo funcionan los interceptores en Struts 2 de la forma en la que nos gusta a los programadores, con código ^^.

Creamos un nuevo proyecto web en NetBeans, vamos al Menú "File->New Project...". En la ventana que se abre seleccionamos la categoría "Java Web" y en el tipo de proyecto "Web Application". Le damos una ubicación y un nombre al proyecto, en mi caso será "Struts2Interceptores". Presionamos el botón "Next". En la siguiente pantalla deberemos configurar el servidor de aplicaciones que usaremos. Yo usaré la versión 7 de Tomcat. Como versión de Java EE usaré la 5 (pueden usar también la 6 si quieren, solo que en ese caso NetBeans no genera el archivo "web.xml" por default, y tendrán que crearlo a mano o usando el wizard correspondiente). De la misma forma, si quieren pueden modificar el context path de la aplicación. Presionamos el botón "Finish", con lo que veremos aparecer la página "index.jsp" en nuestro editor:



Ya que nuestro proyecto está creado, agregamos la biblioteca "Struts2" que creamos en el primer tutorial de la serie. Para esto hacemos clic derecho en el nodo "Libraries" del panel de proyectos. En el menú que aparece seleccionamos la opción "Add Library...". En la ventana que aparece seleccionamos la biblioteca "Struts2" y presionamos "Add Library". Con esto ya tendremos los jars de Struts 2 en nuestro proyecto.

A continuación configuramos el filtro "struts2" en el deployment descriptor. Abrimos el archivo "web.xml" y colocamos el siguiente contenido, como se explicó en el primer tutorial de la serie:


<filter>
    <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>struts2</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>


Ahora crearemos un nuevo archivo llamado "struts.xml" en el paquete default de código fuente de nuestra aplicación:



Este archivo contendrá la configuración de Struts 2 de nuestra aplicación. Colocaremos la configuración inicial, con un par de constantes para indicar que nos encontramos en modo de desarrollo y que cualquier cambio que hagamos en el archivo de configuración debe recargarse de inmediato, y un paquete inicial:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
    "http://struts.apache.org/dtds/struts-2.0.dtd">

<struts>
    <constant name="struts.devMode" value="true" />
    <constant name="struts.configuration.xml.reload" value="true" />
  
    <package name="struts-interceptores" extends="struts-default">
    </package>
</struts>


Creamos un nuevo paquete en el nodo "Source Packages" llamado "com.javatutoriales.interceptores.actions" que será donde colocaremos las clases Action de nuestra aplicación.

Antes de comenzar con los ejemplos, dejaré la lista de los interceptores de dos de las pilas de interceptores que vienen configuradas en Struts 2. La primera es la pila básica ("basicStack"), que contiene, como pueden imaginar, los interceptores mínimos necesarios para que una aplicación pueda proporcionar las funcionalidades básicas. Los interceptores de esta pila son:

  • exception
  • servletConfig
  • prepare
  • checkbox
  • params
  • conversionError



La segunda pila importante es la pila que se aplica por default a todos los Actions se nuestra aplicación ("defaultStack"). Esta pila aplicará todos los interceptores, en el orden en que se encuentran:

  • exception
  • alias
  • servletConfig
  • prepare
  • i18n
  • chain
  • debugging
  • profiling
  • scopedModelDriven
  • modelDriven
  • fileUpload
  • checkbox
  • staticParams
  • conversionError
  • validation
  • workflow


Es importante tener en cuenta cuáles y en qué orden se ejecutan estos interceptores para comprender algunos de los ejemplos que haremos.

Ahora sí, ya con esta información comenzaremos con el primer ejemplo del tutorial:


Configurar parámetros de Interceptores

Iniciaremos modificando el comportamiento de uno de los interceptores que ya vienen en la pila por default. Ya hemos tratado un poco con este interceptor en el cuarto tutorial de la serie: el interceptor "fileUpload" (que si revisan la lista anterior se aplica sólo en la "defaultStack").

Este interceptor permite que se envíen a través de un formulario, además de la información en texto plano normal, archivos de cualquier tipo.

Veamos cómo funciona por default este interceptor. Crearemos un nuevo directorio llamado "parametros" dentro de las páginas web:



Dentro de este directorio creamos dos JSPs, la primera se llamará "index.jsp" y la segunda "resultado.jsp". "index.jsp" contendrá un formulario con un único campo de tipo "file" para subir un archivo. Este formulario será procesado por una Action llamado "carga-archivo".

Recuerden que cuando se realizará la carga de un archivo en una página web, el formulario debe estar codificado con el tipo "multipart/form-data".

También recuerden que para hacer uso de las etiquetas de Struts 2 es necesario indicar en la página que se usará el siguiente taglib:


<%@taglib prefix="s" uri="/struts-tags" %>


La declaración del formulario queda de la siguiente forma:


<s:form action="carga-archivo" enctype="multipart/form-data">
    <s:file name="archivo" label="Archivo" />
    <s:submit />
</s:form>


Ya teniendo listo el formulario vamos a la página "resultado.jsp". En esta página sólo pondremos un mensaje que indicará cuál es el content-type del archivo que hemos cargado. Para esto usamos la etiqueta "<s:property>" de la siguiente forma:


El archivo es de tipo <s:property value="archivoContentType" />


Ahora crearemos el Action que procesará este formulario. Dentro del paquete "com.javatutoriales.interceptores.actions" creamos una nueva clase llamada "CargaArchivo", esta clase debe extender de "ActionSupport":


public class CargaArchivo extends ActionSupport
{
}


Esta será una clase sencilla. Sólo contendrá dos atributos, uno de tipo "File" para almacenar el archivo que sea cargado desde la página, y otro de tipo "String" para mantener el content-type del archivo que subamos (para más detalles pueden revisar el cuarto tutorial de la serie):


private File archivo;
private String archivoContentType;


Para el ejemplo regresaremos el valor del content-type, por lo tanto colocaremos su getter, y como que el framework establezca los valores de los atributos anteriores necesitamos sus setters, colocamos los siguientes métodos en nuestra clase:


public void setArchivo(File archivo)
{
    this.archivo = archivo;
}

public String getArchivoContentType()
{
    return archivoContentType;
}

public void setArchivoContentType(String archivoContentType)
{
    this.archivoContentType = archivoContentType;
}


Ahora sobre-escribimos el método "execute" de la clase. En este caso lo único que hará será regresar el valor "SUCCESS" para que nos envíe a la página del resultado "success":


@Override
public String execute() throws Exception
{
    return SUCCESS;
}


La clase "CargaArchivo" completa queda de la siguiente forma:


public class CargaArchivo extends ActionSupport
{
    private File archivo;
    private String archivoContentType;

    @Override
    public String execute() throws Exception
    {
        return SUCCESS;
    }

    public void setArchivo(File archivo)
    {
        this.archivo = archivo;
    }

    public String getArchivoContentType()
    {
        return archivoContentType;
    }

    public void setArchivoContentType(String archivoContentType)
    {
        this.archivoContentType = archivoContentType;
    }
}


Eso es todo, como pueden ver es una clase muy sencilla que lo único que hace es mantener el archivo recibido a través del formulario, y regresar el content-type del archivo.

Ahora haremos la declaración de este. Vamos al archivo "struts.xml" y dentro del paquete "struts-interceptores" que creamos el inicio del tutorial declaramos un nuevo "action" llamado "carga-archivo" y que será implementado por la clase "com.javatutoriales.interceptores.actions.CargaArchivo":


<package name="struts-interceptores" extends="struts-default">
    <action name="carga-archivo" class="com.javatutoriales.interceptores.actions.CargaArchivo">
    </action>
</package>


Este "action" tendrá dos results, el primero, "success" enviará a la página "resultado.jsp", y el segundo será un resultado de tipo "input" que nos regresará nuevamente a nuestro formulario:


<action name="carga-archivo" class="com.javatutoriales.interceptores.actions.CargaArchivo">
    <result>/parametros/resultado.jsp</result>
    <result name="input">/parametros/index.jsp</result>
</action>


Ya está lista la primer parte del ejemplo. Ahora ejecutamos nuestra aplicación y entramos a la siguiente dirección:


http://localhost:8080/interceptores/parametros/index.jsp


Con lo que deberemos ver nuestro formulario:



Seleccionamos un archivo de cualquier tipo, en este caso yo seleccionaré un archivo .pdf, y presionamos el botón para enviar el formulario. Al hacer esto debemos ver la siguiente salida:



Como podemos ver, el content-type del archivo es "application/pdf". Si ahora repetimos la operación seleccionando una imagen de tipo jpg, obtendremos la siguiente salida:



Podemos ver que ahora el archivo es de tipo "image/jpeg".

Podríamos estar todo el día subiendo archivos de distintos tipos para ver qué aparece en pantalla... pero eso evitaría que viéramos la parte del tutorial en la que tenemos que colocar los parámetros para configurar el interceptor ^^.

Struts 2 nos permite indicar de forma explícita qué interceptores utilizará cada Action de nuestra aplicación. Para esto podemos utilizar el elemento "<interceptor-ref>" que se coloca dentro de "<action>".

"<interceptor-ref>" tiene solamente un parámetro, "name", en el que se indica el nombre del interceptor que queremos configurar. En este caso, si miran la segunda tabla, podrán ver que el nombre del interceptor para la carga de archivos es "fileUpload":


<action name="carga-archivo" class="com.javatutoriales.interceptores.actions.CargaArchivo">

    <interceptor-ref name="fileUpload">
    </interceptor-ref>

    <result>/parametros/resultado.jsp</result>
    <result name="input">/parametros/index.jsp</result>
</action>


Ahora que hemos indicado cuál interceptor vamos a parametrizar, el siguiente paso es realizar dicha parametrización. Los parámetros los colocamos como pares nombre-valor utilizando el elemento "<param>", dentro del interceptor que queremos parametrizar. El nombre lo indicamos con el atributo "name" del elemento anterior, y el valor lo indicamos como valor del elemento (o sea entre las dos etiquetas). Por ejemplo, para establecer el valor de un parámetro llamado "nombre" con el valor de "Alex", lo haríamos de la siguiente forma:


<param name="allowedTypes">image/png</param>


Si vamos a la documentación de la clase "org.apache.struts2.interceptor.FileUploadInterceptor", que es la que implementa el interceptor "fileUpload", podremos ver que puede recibir dos parámetros: "maximumSize" y "allowedTypes". El primero ya lo vimos en el cuarto tutorial de la serie, así que ahora trabajaremos con el segundo.

Con las pruebas que hicimos anteriormente pudimos notar que podemos subir cualquier tipo de archivo al servidor, pero ¿qué pasaría si solo quisiéramos subir archivos de imágenes de tipo "png" y "jpg"? Para estos casos usamos el segundo parámetro, en el que debemos indicar los content-types de los archivos que serán aceptados por este Action. En este caso los content-types son: "image/png" y "image/jpg".

Cuando tenemos más de un valor para este parámetro, los separamos usando una coma, por lo que el valor del parámetro queda de la siguiente forma:


<param name="allowedTypes">image/png,image/jpeg</param>


Y la configuración completa del Action así:


<action name="carga-archivo" class="com.javatutoriales.interceptores.actions.CargaArchivo">
    <interceptor-ref name="fileUpload">
        <param name="allowedTypes">image/png,image/jpeg</param>
    </interceptor-ref>
    <result>/parametros/resultado.jsp</result>
    <result name="input">/parametros/index.jsp</result>
</action>


Si ahora ejecutamos nuestra aplicación y entramos a la siguiente dirección:


http://localhost:8080/interceptores/parametros/index.jsp


Veremos nuevamente nuestro formulario. Seleccionamos cualquier archivo que no sea una imagen "png" o "jpg", en mi caso será nuevamente un archivo PDF, y presionamos el botón enviar. Es ese momento veremos... nada :|

Así es, no hay mensaje de error, ni ha aparecido el content-type del archivo, nada ¿Por qué ha pasado esto?...me alegra que pregunten ^_^.

En realidad si ha aparecido un mensaje de error, aunque un poco más discreto. Su vamos a la consola del Tomcat veremos un mensaje de error que dice algo así:


ADVERTENCIA: Content-Type not allowed: archivo "Ing de Sw Java tutoriales.pdf" "upload_5010ddf2_137aed166ed__8000_00000002.tmp" application/pdf


Anteriormente dije que "podemos configurar los interceptores que se aplicarán a un Action, directamente dentro del elemento <action>". Esto quiere sólo se aplicarán los interceptores que indiquemos. En el ejemplo anterior, lo que ocurre es que sólo se aplica el interceptor "fileUpload" al Action, y no el resto de la cadena que teníamos por default. O sea, no se aplican los interceptores que se encargan de enviar los parámetros al Action, ni los que se encargan de validar los datos, ni los que se encargan de mostrar los errores; sólo "fileUpload".

Entonces ¿cómo hacemos para que el resto de los interceptores normales también se apliquen? Simplemente indicando que, además del interceptor "fileUpload", se deben aplicar el resto de los interceptores del "defaultStack" y, esto es importante, se deben aplicar después de "fileUpload". De esta forma Struts 2 marcará que este interceptor ya se ha aplicado y no volverá a hacerlo. Si colocan la parametrización del interceptor "fileUpload" después del "defaultStack", no verán ningún cambio con el comportamiento normal, así que lo repetiré, coloquen siempre el "defaultStack" al final de los interceptores que hayan paremetrizado o agregado.

Por lo que la declaración, completa y correcta, del Action queda de la siguiente forma:


<action name="carga-archivo" class="com.javatutoriales.interceptores.actions.CargaArchivo">
    <interceptor-ref name="fileUpload">
        <param name="allowedTypes">image/png,image/jpg</param>
    </interceptor-ref>
    <interceptor-ref name="defaultStack"/>
    <result>/parametros/resultado.jsp</result>
    <result name="input">/parametros/index.jsp</result>
</action>


Si volvemos a ejecutar nuestra aplicación y entramos al formulario, al seleccionar un archivo que no sea una imagen "jpg" o "png" obtendremos el siguiente error:



Si seleccionamos una imagen debemos obtener el siguiente resultado:



Podemos ver que los parámetros se han aplicado correctamente al interceptor.

Existe una segunda forma de establecer los parámetros de los interceptores, la cual es útil cuando vamos a configurar muchos interceptores, y esta es usando como nombre del parámetro el nombre del interceptor, seguido de un punto y el nombre del parámetro que queremos configurar. Esto se hace dentro de la declaración del stack, por lo que la declaración anterior también puede quedar de la siguiente forma:


<interceptor-ref name="defaultStack">
    <param name="fileUpload.allowedTypes">image/png,image/jpg</param>
</interceptor-ref>


Las dos dan los mismos resultados. Ahora que ya sabemos cómo configurar nuestros interceptores, veremos cómo agregar un interceptor que no se encuentra en la pila por default de Struts 2.


Uso de Interceptores que no están en el defaultStack

Struts 2 permite agregar a la ejecución de un Action interceptores que no estén en declarados en ninguna pila (y de igual forma configurarlos).En este ejemplo veremos cómo agregar dos interceptores muy útiles, logger y timer. Para no agregar un nuevo <action>, agregaremos este par de interceptores al que ya tenemos declarado.

Como su nombre lo indica, logger registra el inicio y el final de la ejecución de un Action. Agregarlo a su ejecución consiste solamente en dos cosas:

  1. 1. Conocer el nombre del interceptor (en este caso "logger")
  2. 2. Indicar en qué punto se debe comenzar a registrar la información.


El primer punto es sencillo, ya sabemos que el nombre del interceptor es "logger". El segundo punto necesita una pequeña explicación. El lugar en el que declaremos el interceptor es importante, ya que su funcionamiento comenzará en el punto en el que lo coloquemos. Esto quiere decir que si colocamos el interceptor antes del Action (o, como veremos, antes de los results), este registrará sólo la ejecución del Action. Si lo colocamos antes de la pila de interceptores, se registrará la ejecución de cada uno de los interceptores que conforman la pila además del Action. Por lo tanto, si colocamos la declaración de esta forma:


<action name="carga-archivo" class="com.javatutoriales.interceptores.actions.CargaArchivo">
    <interceptor-ref name="fileUpload">
        <param name="allowedTypes">image/png,image/jpeg</param>
    </interceptor-ref>
    <interceptor-ref name="defaultStack"/>
    
    <interceptor-ref name="logger" />
    
    <result>/parametros/resultado.jsp</result>
    <result name="input">/parametros/index.jsp</result>
</action>


Se registrará sólo la ejecución del Action. Probémoslo ejecutando la aplicación. Al subir un archivo deberán de ver una salida parecida a esta en su consola:


4/06/2012 07:47:26 PM com.opensymphony.xwork2.util.logging.jdk.JdkLogger info
INFO: Starting execution stack for action carga-archivo
4/06/2012 07:47:26 PM com.opensymphony.xwork2.util.logging.jdk.JdkLogger info
INFO: Finishing execution stack for action carga-archivo


La salida deberá cambiar dependiendo de si el archivo que suben es válido o no, pero noten que prácticamente los únicos mensajes que aparecen son los de la ejecución del Action "carga-archivo".

Si por el contrario, declaramos el interceptor de la siguiente forma:


<action name="carga-archivo" class="com.javatutoriales.interceptores.actions.CargaArchivo">

    <interceptor-ref name="logger" />

    <interceptor-ref name="fileUpload">
        <param name="allowedTypes">image/png,image/jpeg</param>
    </interceptor-ref>
    <interceptor-ref name="defaultStack"/>
    <result>/parametros/resultado.jsp</result>
    <result name="input">/parametros/index.jsp</result>
</action>


Obtendremos la siguiente salida:


4/06/2012 07:58:11 PM com.opensymphony.xwork2.util.logging.jdk.JdkLogger info
INFO: Starting execution stack for action carga-archivo
4/06/2012 07:58:11 PM com.opensymphony.xwork2.util.logging.jdk.JdkLogger info
INFO: Finishing execution stack for action carga-archivo


(Si, se que se ve igual, pero eso es por el Action tan simple que estamos ejecutando)

Ahora agregaremos el interceptor "timer". Este interceptor se encarga de tomar el tiempo de ejecución de un Action y mostrarlo en la consola. Agregar el interceptor "timer" a nuestra aplicación es igual de sencillo.

Nota: Comentaré el interceptor "logger" para que la salida de la consola quede más limpia.

Podemos aplicar la misma lógica para este interceptor "timer": si lo colocamos antes del Action (de los results) sólo tomará el tiempo de ejecución del Action, si lo colocamos antes de los interceptores tomará el tiempo de ejecución del Action y del resto de los interceptores.

Por lo tanto, si declaramos el interceptor de esta forma:


<action name="carga-archivo" class="com.javatutoriales.interceptores.actions.CargaArchivo">
    <interceptor-ref name="fileUpload">
        <param name="allowedTypes">image/png,image/jpeg</param>
    </interceptor-ref>
    <interceptor-ref name="defaultStack"/>
            
    <interceptor-ref name="timer" />
            
    <result>/parametros/resultado.jsp</result>
    <result name="input">/parametros/index.jsp</result>
</action>


Veremos más o menos la siguiente salida:


INFO: Executed action [carga-archivo!execute] took 16 ms.


Y si lo declaramos de esta forma:


<action name="carga-archivo" class="com.javatutoriales.interceptores.actions.CargaArchivo">
        
    <interceptor-ref name="timer" />
            
    <interceptor-ref name="fileUpload">
        <param name="allowedTypes">image/png,image/jpeg</param>
    </interceptor-ref>
    <interceptor-ref name="defaultStack"/>
    <result>/parametros/resultado.jsp</result>
    <result name="input">/parametros/index.jsp</result>
</action>


Veremos más o menos la siguiente salida:


INFO: Executed action [carga-archivo!execute] took 24 ms.


Como podemos ver, agregar un Action que no esté en la pila por default es, en realidad, muy sencillo.

Ahora, imaginen esta situación, ¿qué pasaría si quisiéramos aplicar uno de estos interceptores, digamos "timer" a todos los Actions de nuestra aplicación? Con lo que hemos visto hasta ahora, tendríamos que agregar la declaración del interceptor "timer" (y la correspondiente declaración del "defaultStack" en cada uno de los Actions de la aplicación. Esto, como podrán imaginarse, no es muy práctico, así que ahora veremos cómo anexar interceptores a la pila por default.


Agregando Interceptores a un Stack

Para evitar el tener que repetir declaraciones de interceptores que usaremos de manera frecuente en nuestros Actions, Struts 2 permite que agreguemos los interceptores que queramos en una pila de interceptores, que puede estar formado por otras pilas, y luego declarar esta pila como la pila por default para los Actions de un paquete (un package de struts, no de Java) y de todos los paquetes que hereden de este.

Para todo trabajo relacionado con interceptores el archivo "struts.xml" tiene una sección especial dentro del elemento "<interceptors>". Dentro de este elemento podemos declarar interceptores nuevos (lo cual veremos cómo hacer en la siguiente parte del tutorial), o crear nuevas pilas. Para esto último usamos los elementos "<interceptor-stack>" y "<interceptor-ref>", que usamos hace un momento.

Para declarar una nueva pila le asignamos un nombre, que debe ser único dentro del paquete, usando el atributo "name" del elemento "<interceptor-stack>". El nombre que le daré a la nueva pila será "defaultTimerStack":


<interceptors>
    <interceptor-stack name="defaultTimerStack">
                
    </interceptor-stack>
</interceptors>


El siguiente paso es indicar qué interceptores conformarán esta pila. Para esto podemos indicar interceptores individuales (como "timer" o "logger") o indicar otras pilas de interceptores (como "basicStack" o "defaultStack"). Para ambos casos se utiliza el elemento "<interceptor-ref>", y se indica en su elemento "name" el nombre del interceptor o de la pila del interceptores al que hacemos referencia. Por ejemplo, para que la nueva pila sea una copia de la pila "defaultStack", colocamos la configuración de la siguiente forma:


<interceptors>
    <interceptor-stack name="defaultTimerStack">
        <interceptor-ref name="defaultStack" />
    </interceptor-stack>
</interceptors>


Así, si aplicamos esta pila a un Action particular, como el Action "carga-archivo" que tenemos, debemos agregarla como en el ejemplo de la sección anterior, usando el elemento "<interceptor-ref>" dentro del elemento "<action>":


<action name="carga-archivo" class="com.javatutoriales.interceptores.actions.CargaArchivo">
                    
    <interceptor-ref name="defaultTimerStack" />
            
    <result>/parametros/resultado.jsp</result>
    <result name="input">/parametros/index.jsplt;/result>
</action>


De esta forma se aplicará la pila por default a nuestro Action. Si ejecutan la aplicación, esta deberá comportarse de forma normal (subiendo cualquier tipo de archivo y sin mostrar ninguna salida en la consola.

El hacer esto nos permite que modifiquemos los interceptores de la pila (junto con sus atributos) y que se refleje el cambio en todos los Actions que utilicen esta pila. Por ejemplo, agreguemos a esta pila la parametrización del interceptor "fileUpload" para que sólo acepte imágenes de tipo "png":


<interceptors>
    <interceptor-stack name="defaultTimerStack">
        <interceptor-ref name="fileUpload">
            <param name="allowedTypes">image/png</param>
        </interceptor-ref>
        <interceptor-ref name="defaultStack" />
    </interceptor-stack>
>/interceptors>


La definición de nuestro "<action>" permanece sin cambios.

Si ejecutamos nuevamente la aplicación, esta deberá aceptar sólo la carga de archivos cuyo content-type sea "image/png".

Podemos agregar también los dos interceptores anteriores, "logger" y "timer", recordando que las reglas anteriores (los interceptores se ejecutan en el orden en el que están declarados) continúan aplicando para las pilas propias:


<interceptors>
    <interceptor-stack name="defaultTimerStack">
        <interceptor-ref name="fileUpload">
            <param name="allowedTypes">image/png</param>
        </interceptor-ref>
        <interceptor-ref name="defaultStack" />
        <interceptor-ref name="timer" />
        <interceptor-ref name="logger" />
    </interceptor-stack>
</interceptors>


Si ahora ejecutamos nuestra aplicación, y cargamos una imagen válida, debemos ver la siguiente salida en la consola:


9/06/2012 07:40:37 PM com.opensymphony.xwork2.util.logging.jdk.JdkLogger info
INFO: Starting execution stack for action carga-archivo
9/06/2012 07:40:37 PM com.opensymphony.xwork2.util.logging.jdk.JdkLogger info
INFO: Finishing execution stack for action carga-archivo
9/06/2012 07:40:37 PM com.opensymphony.xwork2.util.logging.jdk.JdkLogger info
INFO: Executed action [carga-archivo!execute] took 71 ms.


Podemos ver que la nueva pila de interceptores se aplica con la configuración adecuada a nuestro action.

Esto funciona bien si tenemos unos cuantos Actions en nuestra aplicación, o si la pila debe aplicarse sólo a unos cuantos Actions. Pero si esta debe aplicarse a todos o la mayor parte de los Actions, puede ser muy tedioso el tener que declarar dentro de cada uno que se desea aplicar ese stack. Para evitar esto, Struts 2 proporciona una forma sencilla de decir que queremos que una pila sea aplicada por default a todos los Actions de nuestra aplicación.


Asignando una Pila de Interceptores por default para Nuestra Aplicación

Esta parte será realmente corta. Una vez que ya hemos creado y configurado una pila propia podemos indicar cuál de las pilas de nuestro paquete será la pila que debe usarse por default usando el elemento "<default-interceptor-ref>", en cuyo atributo "name" indicaremos cuál pila de interceptores deberá aplicarse por default a todos los Actions de la aplicación. En nuestro caso queremos que por default se aplique nuestra nueva pila de interceptores "defaultTimerStack":


<default-interceptor-ref name="defaultTimerStack" />


Y... eso es todo. Como nuestra pila se aplica ahora por default, podemos dejar la declaración de nuestro action libre de esta información:


<action name="carga-archivo" class="com.javatutoriales.interceptores.actions.CargaArchivo">
    <result>/parametros/resultado.jsp</result>
    <result name="input">/parametros/index.jsp</result>
</action>


Si volvemos a ejecutar nuestra aplicación, al subir una imagen válida debemos ver la siguiente salida en consola:


9/06/2012 08:22:11 PM com.opensymphony.xwork2.util.logging.jdk.JdkLogger info
INFO: Starting execution stack for action carga-archivo
9/06/2012 08:22:11 PM com.opensymphony.xwork2.util.logging.jdk.JdkLogger info
INFO: Finishing execution stack for action carga-archivo
9/06/2012 08:22:11 PM com.opensymphony.xwork2.util.logging.jdk.JdkLogger info
INFO: Executed action [carga-archivo!execute] took 70 ms.


Podemos ver, la pila se aplicó correctamente al único Action de la aplicación.

Ahora que conocemos los detalles del aspecto de configuración de los interceptores, veremos la última parte del tutorial, y la que seguramente la mayoría están esperando:


Creación de interceptores propios

Un interceptor es, como habíamos dicho, un componente que permite pre-procesar una petición y post-procesar una respuesta. O sea que podemos manipular una petición antes de que esta llegue al Action, y procesar una respuesta una vez que la ejecución del Action ha terminado pero antes de que sea regresada al usuario.

Tener la posibilidad de crear interceptores propios nos permite agregar bloques de lógica a nuestra aplicación de una forma más ordenada y reutilizable.

Para crear un interceptor debemos, como casi siempre en Java, implementar una interface. En este caso la interface a implementar es "com.opensymphony.xwork2.interceptor.Interceptor". Esta interface tiene tres métodos, y se ve de la siguiente forma:


public interface Interceptor
{
  void destroy();
  void init();
  String intercept(ActionInvocation invocation);
}


De estos tres métodos el que realmente nos interesa es "intercept", ya que es aquí en donde se implementa la lógica del interceptor. "init" se utiliza para inicializar los valores (conexiones a base de datos, valores iniciales, etc.) que usará el interceptor. "destroy" es su contraparte y se usa para liberar los recursos que hayan sido apartados en "init".

El argumento que recibe "intercept" , un objeto de tipo "ActionInvocation", representa el estado de ejecución del Action y nos permitirá, entre otras cosas, obtener una referencia al Action que será ejecutado y modificar el resultado que será mostrado al usuario. Es gracias a este objeto que podemos agregar valores al Action, evitar que este se invoque, o permitir que continúe el flujo normal de la aplicación.

Para los casos en los que no usaremos los métodos "init" y "destroy", y no queramos proporcionar implementaciones vacías, también podemos extender de la clase "com.opensymphony.xwork2.interceptor.AbstractInterceptor"

Hagamos un primer ejemplo sencillo para asimilar estos conceptos. Nuestro primer interceptor indicará qué Action se está invocando junto con la fecha y la hora de dicha invocación; de paso también haremos que este nos salude ^_^.

Lo primero que haremos es crear un nuevo paquete llamado "interceptores" ( o sea que le nombre completo del paquete será "com.javatutoriales.interceptores.interceptores"). Dentro de este paquete creamos una clase llamada "InterceptorSaludo" que implemente la interface "com.opensymphony.xwork2.interceptor.Interceptor":


public class InterceptorSaludo implements Interceptor
{

}


Debemos implementar los tres métodos (vistos anteriormente) de esta interface, por el momento permanecerán vacios:


public class InterceptorSaludo implements Interceptor
{
    @Override
    public void destroy()
    {
    }

    @Override
    public void init()
    {
    }

    @Override
    public String intercept(ActionInvocation actionInvocation) throws Exception
    {
    }    
}


En este caso no haremos nada en los métodos "init" y "destroy", sólo implementaremos el método "intercept". Iniciemos con la parte fácil, el saludo, para lo que usaremos el tradicional "System.out.println":


@Override
public String intercept(ActionInvocation actionInvocation) throws Exception
{
    System.out.println("Hola desarrollador");
}


Ahora, para obtener el nombre del Action que se está ejecutando usamos el objeto "ActionContext" que hemos usado en los tutoriales anteriores. Este objeto tiene un método "get" que permite obtener algunos datos referentes al Action en ejecución; debemos indicar qué valor queremos obtener usando algunas de las constantes que tiene el objeto "ActionContext", en particular a nosotros nos interesa "ACTION_NAME". Recordemos que este objeto es un "Singleton" por lo que debemos obtener esta instancia usando su método "getContext":


String actionName = (String)ActionContext.getContext().get(ActionContext.ACTION_NAME);


Debemos castear el valor anterior a un String porque "get" regresa un "Object".
Para obtener la fecha y hora actual usamos un objeto tipo "Date" y para que se vea más presentable usaremos un objeto de tipo "DateFormat". Como no es el tema del tutorial, no explicaré lo que se está haciendo y sólo pondré el código:


String tiempoActual = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(new Date());


Finalmente, imprimimos la información anterior en consola:


System.out.println("Ejecutando " + actionName + " a las " + tiempoActual);


Ya que hemos obtenido e impreso la información que nos interesaba, el siguiente paso es indicar que queremos que se continúe con la ejecución de la pila de interceptores (o dicho de otra forma, que no queremos que se interrumpa). Para esto, es necesario explicar dos cosas.

Lo primero que hay que saber es que el método "intercept" regresa un "String", este "String" es el nombre del result que será mostrado al usuario.

Lo segundo que hay que saber es que no podemos regresar cualquier valor que queramos (bueno, en realidad sí, pero debemos hacerlo por una razón muy especial, veremos esto un poco más adelante) sino que debemos regresar el mismo valor que el Action dentro de su ejecución. ¿Y cómo obtenemos este valor?

Para obtener el nombre del result que regresa el método "execute" del Action debemos indicar que el interceptor se ha procesado correctamente y que se debe continuar con la ejecución normal de la aplicación (ya sea ejecutar el siguiente interceptor en la pila o ejecutar el Action en caso de que este sea el último interceptor), para lo cual debemos invocar el método "invoque" del "ActionInvocation" que el método recibe como parámetro. Esté método regresa un String que es (así es, adivinaron) el nombre del result, por lo que lo único que debemos hacer es almacenar el valor que regresa este método para posteriormente volverlo a regresar o, como en mi caso, regresar directamente este valor:


return actionInvocation.invoke();
Por lo que nuestro método "intecept" completo queda de la siguiente forma:


@Override
public String intercept(ActionInvocation actionInvocation) throws Exception
{
    System.out.println("Hola desarrollador");
    String actionName = (String)ActionContext.getContext().get(ActionContext.ACTION_NAME);
    String tiempoActual = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(new Date());
        
    System.out.println("Ejecutando " + actionName + " a las " + tiempoActual);
        
    return actionInvocation.invoke();
}


Si queremos realizar un proceso después de que el Action se haya terminado de ejecutar, debemos almacenar el valor regresado por la llamada a "invoke", y realizar el post-procesamiento después de este punto. Por ejemplo, si queremos enviar un mensaje cuando el Action ha terminado de ejecutarse debemos dejar el método "intercept" de la siguiente forma:


@Override
public String intercept(ActionInvocation actionInvocation) throws Exception
{
    System.out.println("Hola desarrollador");
    String actionName = (String)ActionContext.getContext().get(ActionContext.ACTION_NAME);
    String tiempoActual = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(new Date());
        
    System.out.println("Ejecutando " + actionName + " a las " + tiempoActual);
        
    String resultado = actionInvocation.invoke();
        
    System.out.println("Gracias, regresa pronto :)");
        
    return resultado;
}


Y el interceptor completo de esta otra:


public class InterceptorSaludo implements Interceptor
{
    @Override
    public void destroy()
    {
    }

    @Override
    public void init()
    {
    }

    @Override
    public String intercept(ActionInvocation actionInvocation) throws Exception
    {
        System.out.println("Hola desarrollador");
        String actionName = (String)ActionContext.getContext().get(ActionContext.ACTION_NAME);
        String tiempoActual = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(new Date());
        
        System.out.println("Ejecutando " + actionName + " a las " + tiempoActual);
        
        String resultado = actionInvocation.invoke();
        
        System.out.println("Gracias, regresa pronto :)");
        
        return resultado;
    }    
}


Ahora que ya tenemos lista la clase que implementa el interceptor, debemos configurarlo. Para esto vamos nuevamente a nuestro archivo "struts.xml". Si recuerdan, anteriormente dijimos que todo lo que tiene que ver con configuración de los interceptores se hace dentro de la sección "<interceptors>" de este archivo.

Para declarar un nuevo interceptor usamos el elemento..."<interceptor>". Este elemento tiene dos atributos: "name", en el que se indica cuál será el nombre que se use para hacer referencia a este interceptor, y "class", en el que se indica cuál clase implementa la funcionalidad de este interceptor.

En nuestro ejemplo, llamaremos al interceptor "saludo", y la clase que lo implementa es "com.javatutoriales.interceptores.interceptores.InterceptorSaludo". La declaración completa del interceptor, dentro de la sección "<interceptors>", queda de la siguiente forma:


<interceptor name="saludo" class="com.javatutoriales.interceptores.interceptores.InterceptorSaludo" />


Ahora lo único que debemos hacer es agregar este interceptor a la pila "defaultTimerStack" que creamos anteriormente. Recuerden que el interceptor se ejecutará en la posición en la que lo coloquen dentro de la pila; para este ejemplo pueden colocarlo en cualquier posición que deseen, en mí caso será el segundo interceptor que se ejecute:


<interceptor-stack name="defaultTimerStack">
    <interceptor-ref name="fileUpload">
        <param name="allowedTypes">image/png</param>
    </interceptor-ref>
    
    <interceptor-ref name="saludo" />
    
    <interceptor-ref name="defaultStack" />
    <interceptor-ref name="timer" />
    <interceptor-ref name="logger" />
</interceptor-stack>


Ahora ejecutamos nuestra aplicación, y al ejecutar nuestro Action debemos ver la siguiente salida en la consola:


Hola desarrollador
Ejecutando carga-archivo a las 23/06/2012 11:46:33 PM
Gracias, regresa pronto :)


Esto quiere decir que el interceptor se ejecutó de forma correcta ^_^

Para terminar con este tutorial, veremos un pequeño ejemplo que combina todos los elementos que hemos visto:


Creación de un Interceptor de Verificación de Sesión Válida

Una de las cosas más comunes y útiles que podemos, y queremos, hacer con interceptores propios es verificar que un usuario pueda ejecutar los Actions de nuestra aplicación, pero solamente si es un usuario válido, o sea, si existe una sesión creada para este usuario dentro del sistema.

Para hacer esto debemos pensar básicamente en dos cosas:

  • ¿Cuáles Actions dentro de la aplicación deben estar protegidos?
  • ¿Cuáles Actions deben ser públicos?
  • ¿A dónde debe dirigirse al usuario en caso de que este no se haya logueado dentro del sistema aún?
Para responder a estas preguntas diremos que normalmente queremos que casi todos los Actions de la aplicación estén protegidos....¿casi? Si, casi todos, debemos dejar al menos un Action sin proteger (público): el Action para el login de la aplicación. Claro, podemos dejar públicos tantos Actions como queramos, pero necesitamos al menos un punto de entrada a la aplicación para que el usuario pueda loguearse.

Si el usuario intenta ejecutar un Action que esté protegido, y aún no se ha logueado, podemos enviarlo a una página de error o directamente al formulario de logueo. Nosotros haremos esto último.

Ahora vamos al código del ejemplo. Lo primero que haremos es crear cuatro páginas: "menu.jsp", "login.jsp" , "publica.jsp", y "protegida.jsp", en la raíz de las páginas web:



"publica.jsp" y "protegida.jsp" serán el resultado de la ejecución de un par de Actions, "login.jsp" será un formulario para que el usuario pueda ingresar su usuario y contraseña de acceso al sitio; finalmente "menú.jsp" será una página que mantendrá dos ligas, cada una a uno de los Actions de nuestro interés.

Las dos primeras páginas serán muy sencillas y sólo contendrán un mensaje indicando si es un recuso protegido o público. El contenido de "publica.jsp" es el siguiente:


<h1>Esta página puede ser vista por cualquier persona</h1>


El contenido de "protegida.jsp" es el siguiente:


<h1>Esta página sólo puede ser vista por los usuarios autorizados</h1>


En "menú.jsp" tendremos las ligas a dos Actions que crearemos en un momento, cada a una a uno de los recursos que queremos proteger o mantener públicos:


<ul>
    <li><s:a action="protegido">Action Protegido</s:a></li>
    <li><s:a action="publico">Action Público</s:a></li>
</ul>


No olviden agregar el taglib de Struts 2 a la página anterior, ya que estamos haciendo uso de sus etiquetas.

El contenido de "login.jsp" es un poco más interesante, un formulario que permite ingresar dos campos, un nombre de usuario y una contraseña; además del botón que permite enviar estos datos. También agregaremos un elemento que nos mostrará los mensajes de error en caso de que algo malo ocurra:


<s:actionerror />
<s:form action="login">
    <s:textfield name="username" label="Username" />
    <s:password name="password" label="Password" />
    <s:submit value="Ingresar" />
</s:form>
El siguiente paso es crear el Action para que el usuario pueda iniciar una sesión en el sistema. Para indicar que el usuario se ha logueado en el sistema, colocaremos un atributo preestablecido en la sesión, usando una de las técnicas que aprendimos en la cuarta parte de la serie.

*Nota: Normalmente crearíamos un objeto "Usuario" con algunas propiedades del cliente que va a loguearse, y haríamos todo el manejo de los datos de la sesión utilizando este objeto. Además haríamos una verificación de los permisos del usuario para saber a qué recursos tiene acceso (autorización). En este tutorial sólo agregaremos una cadena como indicador se la sesión para poder centrarnos en el objetivo del tutorial (los interceptores).

Creamos una nueva clase llamada "LoginAction" en el paquete "actions". Esta clase debe extender de "ActionSupport":


public class LoginAction extends ActionSupport
{
}


Colocaremos un par de atributos, con sus correspondientes setters, para almacenar el nombre del usuario y su contraseña:


public class LoginAction extends ActionSupport
{
    private String username;
    private String password;

    public void setPassword(String password)
    {
        this.password = password;
    }

    public void setUsername(String username)
    {
        this.username = username;
    }
}


Para terminar con este Action, sobre-escribiremos su método "execute" para agregar al usuario a la sesión (en un sistema real debemos verificar a este usuario contra algún almacén de datos como una base de datos o un directorio ldap, pero aquí no lo haremos por... por lo mismo que dice la nota anterior ^_^):


@Override
public String execute() throws Exception
{
    HttpServletRequest request = ServletActionContext.getRequest();
    HttpSession sesion = request.getSession();
        
    sesion.setAttribute("usuario", username);
        
    return SUCCESS;
}


Nuestra clase "LoginAction" completa queda de la siguiente forma:


public class LoginAction extends ActionSupport
{
    private String username;
    private String password;

    @Override
    public String execute() throws Exception
    {

        HttpServletRequest request = ServletActionContext.getRequest();
        HttpSession sesion = request.getSession();
        
        sesion.setAttribute("usuario", username);
        
        return SUCCESS;
    }

    public void setPassword(String password)
    {
        this.password = password;
    }

    public void setUsername(String username)
    {
        this.username = username;
    }
}


Ahora necesitamos configurar estos elementos en el archivo "struts.xml". Lo primero que haremos es configurar los dos Actions con sus results correspondientes:


<action name="publico">
    <result>/publica.jsp</result>
</action>

<action name="protegido">
    <result>/protegida.jsp</result>
</action>

<action name="login" class="com.javatutoriales.interceptores.actions.LoginAction">
    <result>/menu.jsp</result>
    <result name="input">/login.jsp</result>
</action>


Podemos ver que en el caso de "LoginAction", cuando el usuario ingrese este será enviado de nuevo al menú, y si algo sale mal será enviado nuevamente el formulario de login.

Como el result para el caso del login será usado para todos los Actions de la aplicación, debemos colocarlo de forma global (recuerden que este elemento se configura después de los interceptores pero antes de los Actions, dentro del archivo de configuración):


<global-results>
    <result name="login">/login.jsp</result>
</global-results>


Si ahora ejecutamos la aplicación, y entramos a la siguiente dirección:


http://localhost:8080/interceptores/menu.jsp


Debemos ver nuestro menú:



Cuando entremos a cada una de las ligas, debemos ver las páginas correspondientes:



Podemos ver que hasta aquí la aplicación sigue funcionando de la misma forma (no se ha restringido el acceso a nada). Ahora crearemos el interceptor que nos permitirá proteger nuestras páginas.

Primero debemos crear una clase llama "InterceptorAcceso" en el paquete "interceptores" de la aplicación. En esta ocasión, esta clase extenderá de "AbstractInterceptor" y sólo sobre-escribirá su método "intercept":


public class InterceptorAcceso extends AbstractInterceptor 
{
    @Override
    public String intercept(ActionInvocation ai) throws Exception
    {
    }
}


Por ahora sólo usaremos el método "intercept", en el cual verificaremos que exista la propiedad llamada "usuario" en la sesión, la cual se establece en el Action que se encarga de realizar el logueo del usuario. Si no encuentra este objeto entonces supondremos que el usuario aún no se ha logueado y lo enviaremos al formulario de ingreso al sistema:


@Override
public String intercept(ActionInvocation actionInvocation) throws Exception
{
    String result = Action.LOGIN;

    if (actionInvocation.getInvocationContext().getSession().containsKey("usuario"))
    {
        result = actionInvocation.invoke();
    }

    return result;
}


Primero asumimos que el usuario no se ha logueado, así que usamos la constante "LOGIN" de la interface "Action" como valor inicial de la variable que regresaremos en el interceptor. Verificamos, usando el mapa de sesión que obtenemos a través del objeto "ActionInvovation", si el existe un usuario en sesión. Si el usuario existe, entonces ejecutamos el Action y regresamos el valor que obtengamos de la ejecución de su método "execute", en caso contrario este será enviado al formulario de login.

Ya teniendo nuestro interceptor podemos configurarlo en el archivo "struts.xml" de la forma en la que lo hicimos hace un momento. El nombre del interceptor será "sesionValida":


<interceptor name="sesionValida" class="com.javatutoriales.interceptores.interceptores.InterceptorAcceso" />


Y colocamos el interceptor en la pila que se ejecuta por default (junto con el resto de los interceptores que hemos configurado en el tutorial) para que sea aplicado a todos los Actions de la aplicación. Podemos colocar el interceptor a cualquier altura que queramos de la pila:


<interceptor-stack name="defaultTimerStack">
    
    <interceptor-ref name="sesionValida" />
    
    <interceptor-ref name="fileUpload">
        <param name="allowedTypes">image/png</param>
    </interceptor-ref>
    <interceptor-ref name="saludo" />
    <interceptor-ref name="defaultStack" />
    <interceptor-ref name="timer" />
    <interceptor-ref name="logger" />
</interceptor-stack>


Ahora podemos ejecutar nuestra aplicación y entrar a la siguiente dirección:


http://localhost:8080/interceptores/menu.jsp


Con lo que veremos el menú de hace un momento. Cuando demos clic en el enlace para ir al Action protegido, la petición pasará primero por el interceptor "sesionValida", este interceptor verá que el usuario aún no ha iniciado sesión y lo enviará el formulario de acceso:



Con lo que podemos comprobar que la verificación está funcionando correctamente.

Si ahora regresamos y hacemos clic en el enlace para ir al Action público podremos comprobar que...



También nos envía al formulario de acceso °_°. Bueno, parece que algo está pasando. No importa. Si ahora ingresamos nuestro nombre de usuario y contraseña, y hacemos clic en el botón "Ingresar" (no importa los datos que coloquen, recuerden que no se están validando), deberemos ser enviados a...



Bueno, una vez más estamos siendo enviados al formulario de acceso, aun cuando ya hemos ingresado nuestros datos para entrar al sistema. ¿Qué es lo que está pasando?... me alegra que pregunten ^_^.

Lo que ocurre es que, como debe de ser, todas las peticiones (incluyendo la petición de acceso al sitio) están pasando por el interceptor "sesionValida", el cual verifica que la sesión exista. Como en realidad la petición nunca llega al Action "LoginAction", nunca iniciamos una sesión en el sistema (el filtro no tiene forma de saber que esta es la finalidad de este Action), y por lo tanto nunca podemos pasar a través del interceptor (de hecho si revisan el Action de la carga de imágenes, este también ha dejado de funcionar ^_^).

¿Qué podemos para que esto no ocurra? Bueno, un paso muy importante, como acabamos de ver, es indicarle a nuestro interceptor cuáles Actions no debe filtrar, o dicho de otra forma: cuáles Actions deben poder ser accesibles aún sin una sesión de usuario válida. Para hacer eso le pasaremos a nuestro interceptor, como parámetros, la lista de los Actions que no debe filtrar.

Regresamos a nuestra clase "InterceptorAcceso" y agregamos un atributo, de tipo "String", llamado "actionsPublicos", con su correspondiente setter:


private String actionsPublicos;

public void setActionsPublicos(String actionsPublicos)
{
    this.actionsPublicos = actionsPublicos;
}


Este atributo recibirá una lista separada por comas con los nombres de los Actions que no serán filtrados. Como este parámetro es una cadena, la transformaremos en una lista para usar los métodos de la interface "List" para saber si el Action que se esté ejecutando es protegido o no. Esta transformación la haremos en el método "init", por lo que lo sobre-escribimos de la siguiente forma:


private List<String> actionsSinFiltrar = new ArrayList<String>();
    
@Override
public void init()
{
    actionsSinFiltrar = Arrays.asList(actionsPublicos.split(","));
}


Finalmente, modificamos la condición del método "execute", para verificar si el nombre del Action que se está ejecutando actualmente está en la lista de los Actions que no deben ser filtrados:


String actionActual = (String)ActionContext.getContext().get(ActionContext.ACTION_NAME);
     
if (actionInvocation.getInvocationContext().getSession().containsKey("usuario") || actionsSinFiltrar.contains(actionActual))
{
    result = actionInvocation.invoke();
}


Nuestra clase "InterceptorAcceso" completa queda de la siguiente forma:


public class InterceptorAcceso extends AbstractInterceptor
{
    private String actionsPublicos;
    private List<String> actionsSinFiltrar = new ArrayList<String>();
    
    @Override
    public void init()
    {
        actionsSinFiltrar = Arrays.asList(actionsPublicos.split(","));
    }

    @Override
    public String intercept(ActionInvocation actionInvocation) throws Exception
    {
        String result = Action.LOGIN;

        String actionActual = (String)ActionContext.getContext().get(ActionContext.ACTION_NAME);
     
        if (actionInvocation.getInvocationContext().getSession().containsKey("usuario") || actionsSinFiltrar.contains(actionActual))
        {
            result = actionInvocation.invoke();
        }

        return result;
    }

    public void setActionsPublicos(String actionsPublicos)
    {
        this.actionsPublicos = actionsPublicos;
    }
}


Ahora, para terminar, regresamos al archivo "struts.xml" e indicamos cuáles son los actions que queremos dejar públicos, pasándolos como el parámetro "actionsPublicos" del interceptor "sesionValida":


<interceptor-ref name="sesionValida">
    <param name="actionsPublicos">login,publico</param>
</interceptor-ref>  


Y esto es todo (lo prometo ^_^). Ahora, volvemos a ejecutar nuestra aplicación. Ingresamos a la siguiente dirección:


http://localhost:8080/interceptores/menu.jsp


Cuando ingresemos en la liga "Action Público" debemos ver el siguiente mensaje:



Hasta ahora vamos bien. Ahora, cuando entremos a la liga "Action Protegido" debemos ver el formulario de login:



Cuando ingresemos un usuario y una contraseña debemos ser reenviados al menú:



Van dos de dos ^_^!. Si ahora volvemos a entrar a la liga "Action Protegido" debemos ver finalmente el mensaje que hemos estado esperando:



Con esto comprobamos que el interceptor está funcionando de forma adecuada (también el Action para la carga de las imágenes está funcionando correctamente bajo este esquema) ^_^.

Quiero que noten una cosa: con esto hemos protegido los Actions, sin embargo las páginas JSPs aún siguen siendo accesibles de forma libre. Para evitar esto existen varias alternativas, de hecho algunos frameworks para plantillas como tiles o sitemesh son muy útiles para estos casos. Pero si no usamos un framework de plantillas aún podemos proteger nuestras JSPs de varias formas, sólo las listaré y no entraré en detalles ^_^:

  • Hacer que el filtro de Struts 2 también procese las peticiones que lleguen con extensión ".jsp" (recuerden que por default procesa las peticiones que llegan con ".action" o que llegan sin extensión) e indicar en el filtro, además de los Actions públicos, las páginas que también serán accesibles libremente.
  • Colocar los JSPs dentro del directorio WEB-INF, el cual no es regresado por el servidor. De esta forma ninguna página puede ser accedida por el navegador, esto significa que para ver las JSPs que deben ser públicas deberemos colocar "<actions>" vacios (sin atributo "class") que en su result "success" regresen esta página. También se pueden dejar las JSPs públicas fuera del directorio WEB-INF y las protegidas dentro.
  • Colocar una validación en cada una de las JSPs (por medio de tags) para que ellas deciden si deben mostrarse o no. También pueden ser colocadas en una página que sirva como encabezado a todas las páginas del sitio, e incluirlas usando la directiva apropiada, así sólo debemos colocar en un lugar este código.
Cada una de estas alternativas tiene ventajas y desventajas, así que elijan la que más se ajuste a sus necesidades.

Con esto hemos terminado con este tutorial, ahora saben todo lo referente a Interceptores en Struts 2 :). Este iba a ser el último tutorial de la serie, sin embargo algunas personas han pedido ampliarlo para tocar un tema más... y como me gusta mucho hacer estos tutoriales, hablaremos de un segundo tema, así que en el siguiente tutorial aprenderemos cómo usar todas (si, leyeron bien: todas) las etiquetas de Struts 2.

No olviden dejar sus dudas comentarios y sugerencias en la sección de comentarios, o enviar un correo a: programadorjavablog@gmail.com (pueden agregarme al chat de gmail). También pueden seguir JavaTutoriales en las siguientes redes sociales:



Descarga los archivos de este tutorial desde aquí:



Entradas Relacionadas:

11 comentarios:

  1. Muy bueno!!!!!!!!! tenes que hacer mas !!!

    ResponderEliminar
  2. excelente amigo, te pasaste. ahora aprenderé spring

    ResponderEliminar
  3. Hola intento poner a funcionar tu ejemplo con algunas modificaciones, pero cuando paso parámetros al archivo de login aparecen como nullos parece que al pasar por el interceptor no me los manda.¿Puedes ayudarme o alguien más que me ayude?

    Por lo demás han sido de gran ayuda tus tutoriales.
    Marisol

    ResponderEliminar
  4. PBX is ideal for small business and entrepreneurs. It gives a professional image and make sure that you don't miss a single call. Few months back, I decided to buy PBX from Telcan. You can get more information here: PBX

    ResponderEliminar
  5. Hola ahora con todo este conocimiento quiera hacer un sitio Web como podria integrar CSS3 en el mismo para darle formato o estilo al sitio?? alguien me podria ayudar quiero desarrollar un sitio completo con un menu imagenes transferencia de archivos, login, todo, AYUDA!! jaja por favor

    ResponderEliminar
  6. Hola muy buen contenido, tengo una pregunta. Cuál es la diferencia en usar las validaciones en los formularios (ya sea por anotaciones o por archivo xml) y usar los interceptores.

    Gracias

    ResponderEliminar
  7. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  8. Muchas gracias de nuevo por tus tutoriales, Álex. Lástima que no continuases con el tutorial de las etiquetas de Struts 2 que pensabas hacer.

    ResponderEliminar
    Respuestas
    1. Hola, si esta el tutorial de etiquetas de Struts2. Ahi va el link http://www.javatutoriales.com/2013/11/struts-2-parte-7-tags.html. Saludos!

      Eliminar
  9. Buenos dias, me sirve de mucho (el profesor que tengo es malisimo de echo me duerme el jodio) jajaja si el tio es un maquina en struts 2 pero no sabe enseñar y con este tutorial me estoy enterando de todo y empieza a coger sentido el disparo de palabras que hace, solo una cosa te pido permiso para pasar esto a documentos de google docs (mas que nada porque no seria la primera vez que pierdo mis boockmarks por una reinstalacion o algo) y porque me gusta tenerlo) pero sobre todo para poder pasarselo a mis compañeros que estan igual de perdidos a como estaba yo eso si poniendo en el pie de pagina la url original tambien mas que nada porque en mi caso dispongo de una conexion bastante inestable (falla la central desde hace cosa de 1 año y no parece que vayan a repararlo supongo que no lo repararan porque conlleva un gasto de dinero muy alto cuando tienen pensamiento de desplegar fibra optica)

    ResponderEliminar