22 de octubre de 2011

Struts 2 - Parte 3: Trabajo con Formularios

En el desarrollo de aplicaciones web, una de las partes más importantes que existen (sino es que la más importante) es el manejo de datos que son recibidos del usuario a través de los formularios de nuestra aplicación.

Aunque es algo que usamos (y hacemos) todos los días, el manejo de los datos de los formularios puede ser un poco engañoso, por no decir complicado, cuando comenzamos a trabajar con la recepción de múltiples valores para un mismo parámetro, o cuando de antemano no conocemos los nombres de los parámetros que recibiéremos; esto sin mencionar las validaciones para los distintos tipos de datos, la carga y descarga de archivos, etc.

En este tutorial aprenderemos la forma en la que se trabaja con formularios en Struts 2, y cómo manejar todas las situaciones mencionadas anteriormente. Concretamente aprenderemos cómo hacer 7 cosas: recepción de parámetros simples, cómo hacer que el framework llene de forma automática los atributos de un objeto si es que todos los datos del formulario pertenecen a ese objeto, a recibir múltiples valores para un mismo parámetro, cómo recibir parámetros cuando no conocemos el nombre de los mismos, a realizar validaciones de datos de varias maneras, cómo subir archivos al servidor, y cómo enviar archivos desde el servidor hacia nuestros clientes.

En el primer tutorial de la serie vimos los pasos básicos para enviar datos a nuestros Actions a través de un formulario, sin embargo en esta ocasión veremos algunos conceptos un poco más avanzados que nos harán la vida más fácil cuando trabajemos con formularios.

Lo primero que haremos es crear un nuevo proyecto en NetBeans. Vamos al menú "File -> New Project...". En la ventana que aparece seleccionamos la categoría "Java Web" y en el tipo de proyecto "Web Application". Presionamos el botón "Next >" y le damos un nombre y una ubicación a nuestro proyecto; presionamos nuevamente el botón "Next >" y en este punto se nos preguntará el servidor que queremos usar. En nuestro caso usaremos el servidor "Tomcat 7.0", con la versión 5 de JEE y presionamos el botón "Finish".

Una vez que tengamos nuestro proyecto debemos recordar agregar la biblioteca "Struts2" (o "Struts2Anotaciones" si van a hacer uso de anotaciones, como es mi caso ^_^), que creamos en el primer tutorial de la serie. Hacemos clic derecho sobre el nodo "Libraries" del proyecto. En el menú que aparece seleccionamos la opción "Add Library...". En la ventana que aparece seleccionamos la biblioteca "Struts2" o "Struts2Anotaciones" y presionamos "Add Library". Con esto ya tendremos los jars de Struts 2 en nuestro proyecto.

Ahora 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 procedemos a crear una clase que nos servirá como modelo de datos. Esta será una clase "Usuario" la cual tendrá atributos de varios tipos para que veamos como Struts 2 realiza su "magia".

Creamos un paquete para nuestro modelo de datos. En mi caso el primer paquete se llamará "com.javatutoriales.struts2.formularios.modelo". Hacemos clic derecho sobre el nodo "Source packages" del proyecto, en el menú que aparece seleccionamos la opción "new -> package". En la ventana que aparece le damos el nombre correspondiente a nuestro paquete.

Ahora, dentro de este paquete creamos la clase "Usuario": sobre el paquete que acabamos de crear hacemos clic derecho y en el menú contextual que aparece seleccionaos la opción "new -> Java Class". El la ventana que aparece le damos el nombre "Usuario" y hacemos clic en el botón "Finish". Con esto aparecerá en nuestro editor la clase "Usuario" de la siguiente forma:


public class Usuario
{
}


Antes que nada hacemos que esta clase implemente la interface "java.io.Serializable" como deben hacerlo todas las clases de transporte de datos:


public class Usuario implements Serializable
{
}


Ahora agregaremos unos cuantos atributos a esta clase, de distintos tipos, representado algunas de las características de nuestro usuario. Dentro de estos atributos incluiremos su nombre, edad, y fecha de nacimiento:


public class Usuario implements Serializable
{
    private String nombre;
    private String username;
    private String password;
    private int edad;
    private Date fechaNacimiento;
}


Recuerden que el "Date" de "fechaNacimiento" debe ser de tipo "java.util.Date".

Para terminar con la clase Usuario agregaremos los getters y los setters de estos atributos, y adicionalmente dos constructores, uno que reciba todos los atributos, y uno vacio:


public class Usuario implements Serializable
{
    private String nombre;
    private String username;
    private String password;
    private int edad;
    private Date fechaNacimiento;

    public Usuario()
    {
    }

    public Usuario(String nombre, String username, String password, int edad, Date fechaNacimiento)
    {
        this.nombre = nombre;
        this.username = username;
        this.password = password;
        this.edad = edad;
        this.fechaNacimiento = fechaNacimiento;
    }

    public int getEdad()
    {
        return edad;
    }

    public void setEdad(int edad)
    {
        this.edad = edad;
    }

    public Date getFechaNacimiento()
    {
        return fechaNacimiento;
    }

    public void setFechaNacimiento(Date fechaNacimiento)
    {
        this.fechaNacimiento = fechaNacimiento;
    }

    public String getNombre()
    {
        return nombre;
    }

    public void setNombre(String nombre)
    {
        this.nombre = nombre;
    }

    public String getPassword()
    {
        return password;
    }

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

    public String getUsername()
    {
        return username;
    }

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


Lo primero que haremos es recordar cómo podemos obtener parámetros planos o simples desde nuestros formularios:


1. Recepción de parámetros simples

En el primer tutorial de la serie de Struts 2 vimos de una forma muy rápida cómo obtener parámetros de un formulario. Lo único que hay que hacer es:

  • Colocar un formulario en nuestra JSP usando la etiqueta <s:form>
  • Colocar los campos del formulario usando las etiquetas correspondientes de Struts
  • Crear un Action y colocar setters para cada uno de los elementos que recibiremos a través del formulario
  • Procesar los datos del formulario en el método "execute"
Hagamos un pequeño ejemplo para refrescar la memoria.

En este primer ejemplo obtendremos los datos para crear un nuevo objeto "Usuario", el tipo que definimos anteriormente.

Lo primero que hacemos es crear una nueva página JSP, en la raíz de las páginas web, llamada "nuevo-usuario.jsp". Hacemos clic derecho en el nodo "Web Pages". En el menú que se abre seleccionamos la opción "New –> JSP...".



Colocamos "nuevo-usuario" como nombre de la página y presionamos el botón "Finish".

En esta página indicamos que se usará la biblioteca de etiquetas de Struts 2:


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


Usamos la etiqueta "<s:form>" para colocar un formulario en nuestra página. En su atributo "action" colocamos el nombre del Action que se encargará de procesar los datos de este formulario:


<s:form action="datosUsuario">            
</s:form>


Dentro de este formulario colocaremos un campo para cada uno de los atributos que puede recibir un Usuario:


<s:form action="datosUsuario">
    <s:textfield name="nombre" label="Nombre" />
    <s:textfield name="username" label="Username" />
    <s:password name="password" label="Password" />
    <s:textfield name="edad" label="Edad" />
    <s:textfield name="fechaNacimiento" label="Fecha de Nacimiento" />
    <s:submit value="Enviar" />
</s:form>


Ya que nuestro formulario está listo, crearemos el Action que se encargará de procesar los datos del mismo.

Creamos un nuevo paquete, llamado "actions", a la misma altura que el paquete "modelo". Dentro de este paquete creamos una nueva clase Java llamada "UsuarioAction". Haremos que esta clase extienda de "ActionSupport":


public class UsuarioAction extends ActionSupport
{    
}


Recuerden que para recibir los datos del formulario debemos colocar los atributos en los que se almacenarán estos datos, y setters para que los interceptores correspondientes puedan inyectar los valores dentro del Action:


public class UsuarioAction extends ActionSupport
{
    private String nombre;
    private String username;
    private String password;
    private int edad;
    private Date fechaNacimiento;

    public void setEdad(int edad)
    {
        this.edad = edad;
    }

    public void setFechaNacimiento(Date fechaNacimiento)
    {
        this.fechaNacimiento = fechaNacimiento;
    }

    public void setNombre(String nombre)
    {
        this.nombre = nombre;
    }

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

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


Cuando esté Action termine su ejecución (la cual aún no hemos implementado), queremos poder crear un Usuario y mostrar sus datos en otra página. Para hacer eso debemos colocar un atributo que almacene una referencia al Usuario, y un getter para poder obtener esta referencia:


private Usuario usuario;

public Usuario getUsuario()
{
    return usuario;
}


Ahora sí, sobre-escribimos el método "execute" para crear un nuevo objeto de tipo Usuario y establecer sus datos usando los valores que recibimos del formulario:


@Override
public String execute() throws Exception
{
    usuario = new Usuario();
    usuario.setNombre(nombre);
    usuario.setUsername(username);
    usuario.setPassword(password);
    usuario.setEdad(edad);
    usuario.setFechaNacimiento(fechaNacimiento);

    return SUCCESS;
}


Y eso es todo, ya hemos creado un nuevo Usuario con los datos que recibimos a través del formulario de captura.

Para que este ejemplo funcione aún tenemos que hacer un par de cosas. Primero marcaremos nuestra clase con la anotación "@Action" para indicar que esta clase debe ser tratada con un Action de Struts 2, como lo vimos en el primer tutorial de la serie:


@Namespace(value="/")
@Action(value="datosUsuario", results={@Result(name="success", location="/datos-usuario.jsp")})
public class UsuarioAction extends ActionSupport
{
}


Como podemos ver, el nombre de nuestro Action es "datosUsuario" (que es el mismo que colocamos en el atributo "action" del formulario que creamos hace un momento). Si todo el proceso de nuestro Action sale bien seremos enviados a la página "datosUsuario.jsp". Esta página solamente mostrará los datos del usuario que se acaba de crear. Antes de ver esta página veamos cómo queda la clase "UsuarioAction":


@Namespace(value="/")
@Action(value="datosUsuario", results={@Result(name="success", location="/datos-usuario.jsp")})
public class UsuarioAction extends ActionSupport
{
    private String nombre;
    private String username;
    private String password;
    private int edad;
    private Date fechaNacimiento;

    private Usuario usuario;

    @Override
    public String execute() throws Exception
    {
        usuario = new Usuario();
        usuario.setNombre(nombre);
        usuario.setUsername(username);
        usuario.setPassword(password);
        usuario.setEdad(edad);
        usuario.setFechaNacimiento(fechaNacimiento);

        return SUCCESS;
    }


    public Usuario getUsuario()
    {
        return usuario;
    }

    public void setEdad(int edad)
    {
        this.edad = edad;
    }

    public void setFechaNacimiento(Date fechaNacimiento)
    {
        this.fechaNacimiento = fechaNacimiento;
    }

    public void setNombre(String nombre)
    {
        this.nombre = nombre;
    }

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

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


Ahora sí, creamos una nueva página JSP; el nombre de esta página será "datos-usuario". En esta página haremos uso de algunas etiquetas de Struts, por lo que deberemos indicarlo usando la directiva "taglib" correspondiente:


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


En esta página solamente mostraremos los datos del usuario que acabamos de crear. Recordemos que para esto debemos usar la etiqueta "<s:property>". Mostramos cada uno de los datos anteriores del usuario:


Nombre: <strong><s:property value="usuario.nombre" /></strong> <br />
Username: <strong><s:property value="usuario.username" /></strong> <br />
Password: <strong><s:property value="usuario.password" /></strong> <br />
Edad: <strong><s:property value="usuario.edad" /></strong> <br />
Fecha de Nacimiento: <strong><s:property value="usuario.fechaNacimiento" /></strong>


Cuando entremos a la siguiente página:


http://localhost:8080/formularios/nuevo-usuario.jsp


Veremos el formulario para capturar los datos del usuario. En mi caso, con los siguientes datos:



Obtengo la siguiente salida:



En el ejemplo anterior podemos ver que aunque hemos pasado pocos datos al formulario, todos los hemos establecido en el objeto "Usuario", que se creó dentro método "execute", y los pasamos "como van", es decir sin realizar ninguna transformación o procesamiento sobre ellos. Cuando tenemos 5 o 6 atributos esto puede no ser algo muy pesado. Pero ¿qué pasa si tenemos que llenar un objeto con 30 o 40 propiedades? Nuestro Action sería muy grande por todos los atributos y los setters y getters de las propiedades; el trabajo dentro del método "execute" también sería bastante cansado llamar a los setters de nuestro modelos para pasarle los parámetros.

Para esos casos Struts 2 proporciona una forma de simplificarnos estas situaciones usando un mecanismo conocido como "Model Driven" el cual veremos a continuación.


2. Model Driven

Model Driven es una forma que Struts 2 proporciona para poder establecer todos los parámetros que se reciben de un formulario directamente dentro de un objeto. Este objeto es conocido como el modelo.

Usando este modelo nos evitamos estar llamando nosotros mismos a los setters de este objeto modelo.

Esto también permite que si realizamos validaciones de datos del formulario (lo cual veremos cómo hacer un poco más adelante) estas se realizarán sobre este objeto modelo.

Un interceptor especial, llamado model driven interceptor, se encarga de manejar todo esto de forma automática ^_^.

Extenderemos nuestro ejemplo para usar model driven.

Creamos una nueva clase Java, en el paquete "actions", llamada "UsuarioActionMD". Esta clase extenderá de "ActionSupport":


public class UsuarioActionMD extends ActionSupport
{    
}


Para indicarle a Struts 2 que este Action será Model Driven, la clase "UsuarioActionMD" debe implementar la interface "ModelDriven", indicando de qué tipo de objeto será usado como modelo:

public class UsuarioActionMD extends ActionSupport implements ModelDriven<Usuario>
{
}


Ahora pondremos un objeto Usuario dentro de nuestra clase, con una variable de instancia, que será el objeto que usaremos como modelo:


public class UsuarioActionMD extends ActionSupport implements ModelDriven<Usuario>
{
    private Usuario usuario = new Usuario();    
}


Es importante tener creado este objeto antes de que nuestro Action reciba alguna petición, por lo que podemos inicializarlo en la misma declaración o en el constructor del Action, en caso de tener alguno.

La interface "ModelDriven" tiene tan solo un método: "getModel". Esté método no recibe ningún parámetro, y regresa el objeto que estamos usando como modelo:


public Usuario getModel()
{
    return usuario;
}


Lo único que queda es realizar algún proceso en el método "execute", que podría ser almacenar al Usuario en alguna base de datos, enviarlo por algún stream, etc. Como en este caso no haremos nada con el Usuario más que regresarlo para poder mostrar sus datos en una página, nuestro método "execute" solo regresará un valor de "SUCCESS".


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


Noten que en esta ocasión no es necesario tener un getter para poder obtener la instancia del "Usuario" desde nuestra JSP.

Finalmente, anotamos nuestra clase para que Struts 2 lo reconozca como un Action. El nombre del Action será "datosUsuario". Para diferenciar este Action del anterior (que tiene el mismo nombre) lo colocaremos en el namespace "modeldriven".

El Action al terminar de ejecutarse nos enviará a la página "/modeldriven/datos-usuario.jsp":


@Namespace(value = "/modeldriven")
@Action(value="datosUsuario", results={@Result(location="/modeldriven/datos-usuario.jsp")})
public class UsuarioActionMD extends ActionSupport implements ModelDriven<Usuario>
{
}


La clase "UsuarioActionMD" completa (omitiendo los imports) queda de la siguiente forma:


@Namespace(value = "/modeldriven")
@Action(value="datosUsuario", results={@Result(location="/modeldriven/datos-usuario.jsp")})
public class UsuarioActionMD extends ActionSupport implements ModelDriven<Usuario>
{
    private Usuario usuario = new Usuario();

    public Usuario getModel()
    {
        return usuario;
    }

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


Podemos ver que el código de este Action es mucho más pequeño que el que hicimos para el ejemplo anterior.

Ahora antes de crear la página que se encargará de mostrar el resutado, creamos un nuevo directorio para nuestras páginas web. Para eso hacemos clic derecho sobre el nodo "WebPages" de nuestro proyecto. En el menú que aparece seleccionamos la opción "New -> Folder":



Damos "modeldriven" como nombre del directorio y presionamos el botón "Finish".

Dentro de este directorio crearemos dos páginas. Primero crearemos la página que contendrá el formulario que nos permitirá capturar los datos del usuario. Hacemos clic derecho en el directorio "modeldriven" del nodo "Web Pages". En el menú que se abre seleccionamos la opción "New –> JSP...". Damos "nuevo-usuario" como nombre de la página y presionamos el botón "Finish".

El formulario de la página será idéntico al que creamos en la primer parte del tutorial, con la única diferencia de que enviará los datos al Action "datosUsuarioMD":


<s:form action="datosUsuario">
    <s:textfield name="nombre" label="Nombre" />
    <s:textfield name="username" label="Username" />
    <s:password name="password" label="Password" />
    <s:textfield name="edad" label="Edad" />
    <s:textfield name="fechaNacimiento" label="Fecha de Nacimiento" />
    <s:submit value="Enviar" />
</s:form>


La segunda página, que se llamará "datos-usuario.jsp" y que también estará en el directorio "modeldriven", mostrará los valores de los atributos del objeto "usuario" que se usa como modelo. Esta página también será muy parecida a la que creamos anteriormente, solo que en esta ocasión no será necesario indicar que los atributos que estamos usando corresponden al objeto "usuario":


Nombre: <strong><s:property value="nombre" /></strong> <br />
Username: <strong><s:property value="username" /></strong> <br />
Password: <strong><s:property value="password" /></strong> <br />
Edad: <strong><s:property value="edad" /></strong> <br />
Fecha de Nacimiento: <strong><s:property value="fechaNacimiento" /></strong>


Al ejecutar la aplicación y entrar en la siguiente dirección:


http://localhost:8080/formularios/modeldriven/nuevo-usuario.jsp


Veremos un formulario muy parecido al anterior:



Cuando llenemos los datos y enviemos el formulario, deberemos ver una salida también muy parecida a la anterior:



Pero ¿qué ocurre si queremos recibir un parámetro que no sea un atributo del "usuario"? En ese caso, solo tenemos que agregar un nuevo atributo, con su correspondiente setter para establecer su valor (y su getter si es que pensamos recuperarlo posteriormente) dentro de nuestro Action.

Modifiquemos un poco el ejemplo anterior para ver esto. Agregaremos al formulario un campo de texto para que el usuario proporcione un número de confirmación:


<s:form action="datosUsuario">
    <s:textfield name="nombre" label="Nombre" />
    <s:textfield name="username" label="Username" />
    <s:password name="password" label="Password" />
    <s:textfield name="edad" label="Edad" />
    <s:textfield name="fechaNacimiento" label="Fecha de Nacimiento" />
    <s:textfield name="numero" label="Número de Confirmación" />
            
    <s:submit value="Enviar" />
</s:form>


También cambiaremos nuestro Action. Colocamos un atributo de tipo entero para contener este número, junto con sus correspondientes setter (para establecer el valor desde el formulario) y getter (para obtener el valor desde la JSP):


private int numero;

public void setNumero(int numero)
{
    this.numero = numero;
}

public int getNumero()
{
    return numero;
}


De igual forma modificaremos la página "datos-usuarios.jsp" del directorio "modeldriven" para poder mostrar el valor del número de confirmación:


Nombre: <strong><s:property value="nombre" /></strong> <br />
Username: <strong><s:property value="username" /></strong> <br />
Password: <strong><s:property value="password" /></strong> <br />
Edad: <strong><s:property value="edad" /></strong> <br />
Fecha de Nacimiento: <strong><s:property value="fechaNacimiento" /></strong><br />
Número de confirmación: <strong><s:property value="numero" /></strong>


Al ejecutar la aplicación y entrar al formulario, veremos el campo que hemos agregado:



Al enviar los datos del formulario obtendremos la siguiente pantalla:



Ahora veremos cómo podemos hacer para recibir un conjunto de valores en un solo atributo de nuestro Action.


3. Recepción de múltiples parámetros

En los ejemplos anteriores cada uno de nuestros atributos recibe solo un valor de algún tipo. Sin embargo algunas veces nos será conveniente recibir un conjunto de parámetros, ya sea como un arreglo de valores, o como una lista de valores para poder procesar cada uno de sus elementos.

Esto es útil para cuando, por ejemplo, tenemos campos de formulario que se van generando dinámicamente, o que tienen el mismo significado en datos (por ejemplo cuando tenemos varios campos para cargar archivos adjuntos en correos electrónicos, o para poder subir varias imágenes de una vez a un sitio):



Poder obtener estos parámetros es sencillo gracias a los interceptores de Struts 2.

Veamos un ejemplo parecido a la pantalla anterior. Tendremos un formulario que nos permitirá recibir un conjunto de correos electrónicos de personas para ser procesados en el servidor. Colocaremos de forma estática 5 campos (claro que estos podrían ir aumentando de forma dinámica usando JavaScript).

Aunque el formulario tendrá 5 campos para correos, el usuario podría no llenarlos todos, por lo que no sabremos cuántos elementos estaremos recibiendo.

El formulario tendrá además un campo que permitirá que el usuario coloque su correo nombre, para saber quién está enviando los correos.

Comencemos primero con el formulario que mostrará los campos para los correos. Crearemos un nuevo directorio, en la raíz de las páginas web del proyecto, llamado "multidatos" (lo sé, no es un nombre muy original, pero refleja el cometido del ejemplo):



En este directorio crearemos una nueva JSP llamada "datos.jsp". Esta página contendrá el formulario con los 6 campos mencionados antes: un campo para que el usuario coloque su nombre, y 5 para colocar una dirección de correo electrónico:


<s:form action="envioCorreo">
    <s:textfield name="nombre" label="Nombre" />
    
    <s:textfield name="correo" label="Correo" />
    <s:textfield name="correo" label="Correo" />
    <s:textfield name="correo" label="Correo" />
    <s:textfield name="correo" label="Correo" />
    <s:textfield name="correo" label="Correo" />
            
    <s:submit value="Enviar" />
</s:form>


No olviden indicar que usaremos las etiquetas de Struts 2, con la directiva taglib correspondiente:


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


Podemos ver en el formulario que los 5 campos para colocar los correos electrónicos son exactamente iguales, todos tienen por nombre "correo". Esto es necesario para podamos recibir los valores como un solo conjunto de datos.

Veamos el Action que recibirá estos valores. Primero crearemos una nueva clase java, llamada "MultiplesDatosAction", en el paquete "Actions". Esta clase extenderá de ActionSupport:


public class MultiplesDatosAction extends ActionSupport
{
}


Ahora colocaremos el atributo que nos permitirá recibir los múltiples parámetros. Para esto debemos colocar un arreglo del tipo de datos que estemos esperando. En este caso será un arreglo de Strings:


private String[] correos;


En realidad, ese es todo el secreto para poder recibir estos múltiples parámetros ^_^. Uno de los interceptores de Struts 2 se encargará de recibir los parámetros del formulario que tienen el mismo nombre; este interceptor colocará los parámetros en un arreglo o una colección, dependiendo de qué es lo que estemos esperando, y lo colocará en nuestro Action usando el setter apropiado.

Según la explicación anterior, debemos colocar el setter del atributo llamado "correos" para poder establecer el arreglo de correos, y su getter para poder obtenerlo posteriormente:


public String[] getCorreos()
{
    return correos;
}

public void setCorreos(String[] correos)
{
    this.correos = correos;
}


Ahora que tenemos el arreglo de valores colocaremos un atributo para mantener el nombre del usuario, con su correspondiente getter y setter:


private String nombre;

public String getNombre()
{
    return nombre;
}

public void setNombre(String nombre)
{
    this.nombre = nombre;
}


Como en esta ocasión no se realizará ningún proceso sobre los datos, por lo que nuestro método "execute" solo regresará un valor de "SUCCESS":


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


Para terminar anotaremos esta clase. Primero indicamos que este Action estará en el namespace "multidatos". Además responderá al nombre de "envioCorreo" (el mismo que pusimos en el formulario). Al terminar el proceso, el Action nos enviará a una página llamada "datos-enviados.jsp" del directorio "multidatos":


@Namespace(value="/multidatos")
@Action(value="envioCorreo", results={@Result(location="/multidatos/datos-enviados.jsp")})
public class MultiplesDatosAction extends ActionSupport
{
}


El código completo de la clase "MultiplesDatosAction" queda de la siguiente forma:


@Namespace(value="/multidatos")
@Action(value="envioCorreo", results={@Result(location="/multidatos/datos-enviados.jsp")})
public class MultiplesDatosAction extends ActionSupport
{
    private String[] correos;
    private String nombre;

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

    public String getNombre()
    {
        return nombre;
    }

    public void setNombre(String nombre)
    {
        this.nombre = nombre;
    }

    public String[] getCorreos()
    {
        return correos;
    }

    public void setCorreos(String[] correos)
    {
        this.correos = correos;
    }
}


Ahora solo falta crear la página a la que regresaremos al terminar el Action.

Creamos, en el directorio "multidatos" de las páginas web, una nueva JSP llamada "datos-enviados.jsp":



En esta página lo que haremos será mostrar los mismos datos que recibimos del formulario.

Recuerden que primero debemos indicar que usaremos las etiquetas de Struts 2, usando el taglib correspondiente:


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


Primero mostraremos el valor del nombre del usuario, de la misma manera en la que hemos venido haciéndolo a lo largo de los ejemplos del tutorial:


<s:property value="nombre" />


Ahora mostraremos los valores de nuestro arreglo de cadenas, para eso Struts 2 proporciona una etiqueta especial que nos permite iterar a través de todos los valores de un arreglo o colección, la etiqueta "iterator".

La etiqueta "iterator" recibirá el arreglo o colección a través del cual ciclaremos para obtener sus valores. Indicamos este arreglo o colección usando el atributo "value". En este caso indicaremos que el valor a través del cual queremos iterar es arreglo llamado "correos":


<s:iterator value="correos">
</s:iterator>


Ya podemos ciclar a través de todos los elementos del arreglo, ahora necesitamos una manera de obtener el elemento actual dentro del ciclo. Para esto usamos el atributo "var", en este indicaremos el nombre de la variable que mantendrá el valor del elemento actual. No debemos de preocuparnos por crear esta variable, ya que Struts 2 la creara automáticamente y la pondrá a nuestra disposición. Esta variable será del tipo de elementos que sea mantenido por nuestro arreglo o colección:


<s:iterator value="correos" var="correo">
</s:iterator>


Algunas veces querremos poder obtener alguna información relativa a la iteración, como el índice del elemento actual, el número total de elementos que tiene el arreglo o colección, si el elemento actual es par o impar, etc. Para obtener esta información podemos indicarle a la etiqueta "iterator" que la coloque, como una instancia de la clase "IteratorStatus" y que la ponga a nuestra disposición en una variable. Para esto, indicamos el nombre de la variable en el atributo "status":


<s:iterator value="correos" var="correo" status="estatus">
</s:iterator>


El estatus es una variable que no será colocada dentro en la raíz del stack de Struts 2 (el cual explicamos en el segundo tutorial de la serie), por lo que es necesario hacer referencia a el usando el operador "#".

Ahora que podemos iterar a través de cada uno de los elementos de nuestro arreglo, solo hace falta mostrar el valor de cada uno de estos elementos, adicionalmente también mostraremos su índice. Colocaremos estos valores en una lista para que se vea más presentable:


<ul>
    <s:iterator value="correos" var="correo" status="estatus">
        <li><s:property value="#estatus.index" /> - <s:property value="correo" /></li>
    </s:iterator>
</ul>


Ahora, cuando coloquemos los siguientes datos en el formulario:



Obtendremos la siguiente salida:



Podemos ver que de una forma sencilla colocamos el conjunto de valores dentro del arreglo. Pero qué pasa si por alguna razón no podemos o no queremos usar un arreglo, y preferimos trabajar con una colección, como un Set o un List, lo único que tenemos que hacer es cambiar el tipo del atributo "correos" (junto con sus correspondientes getters y setters). Usemos un "Set", el cual es una colección que no permite elementos duplicados.

Modifiquemos el atributo "correos" para que quede de la siguiente forma:


private Set<String> correos;

public Set<String> getCorreos()
{
    return correos;
}

public void setCorreos(Set<String> correos)
{
    this.correos = correos;
}


Al introducir los siguientes datos en el formulario:



Obtendremos la siguiente salida:



Podemos ver que en realidad no fue necesario realizar grandes cambios para modificar la forma en la que recibimos los datos. Si quisiéramos que "correos" fuera una Lista, lo único que debemos hacer es, nuevamente, cambiar solo el tipo de dato.

El arreglo o la colección que usemos para mantener los valores, pueden ser de cualquier tipo de dato (cadenas, fechas, wrappers, archivos, etc).

Los ejemplos anteriores funcionan bien cuándo sabemos cuántos y cuáles serán los parámetros que estamos esperando recibir. Pero ¿y si no supiéramos de antemano los parámetros que recibiremos?

Como pueden imaginarse, Struts 2 proporciona una manera elegante de manejar estas situaciones.


4. Recepción de Parámetros Desconocidos

Algunas veces estamos esperando recibir, dentro de nuestro Action, un conjunto de parámetros de los cuales no conocemos ni su cantidad ni sus nombres, como por ejemplo cuando estamos haciendo un filtrado de datos y no sabes qué filtros recibiremos y cuáles serán los valores de estos filtros, o cuando la generación de componentes se realiza de forma dinámica.

Para estos casos Struts 2 proporciona una manera de indicar que deberá entregarnos todos los parámetros en un objeto tipo "java.util.Map". Las llaves de este mapa representarán los nombres de los parámetros, y sus valores representarán un arreglo de Strings con los valores correspondientes de cada parámetro. ¿Por qué un arreglo de Strings? Porque como acabamos de ver, algunos de los parámetros que recibimos pueden tener más de un valor.

Para lograr que Struts 2 nos proporcione estos parámetros, nuestro Action debe implementar la interface "ParameterAware". Esta interface se ve de la siguiente forma:


public interface ParameterAware
{
    void setParameters(Map<String, String[]> parameters);
}


O sea, que esta interface solo tiene un método llamado "setParameters", que tiene como argumento el mapa de parámetros recibidos en la petición.

Veamos un ejemplo para entender cómo funciona esta interface.

Primero crearemos un nuevo directorio, dentro de nuestras páginas web, llamado "multiparametros". Dentro de este directorio crearemos dos nuevas JSPs. La primera, llamada "datos.jsp" contendrá un pequeño formulario que nos permitirá enviar los parámetros dinámicos a nuestro Action. La segunda, llamada "parametros.jsp", mostrara el nombre y el valor de cada uno de los parámetros que hemos colocado en el formulario. Comencemos creando la pagina "datos.jsp".

Primero, indicaremos que haremos uso de las etiquetas de Struts 2 usando el taglib correspondiente:


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


Ahora crearemos un formulario, de una forma un poco distinta a como lo hemos estado haciendo hasta ahora. Primero indicaremos, con la etiqueta "<s:form>" que haremos uso de un formulario, los datos del mismo serán procesados por un Action cuyo nombre será "multiparametros":


<s:form action="multiparametros">
</s:form>


Para que sea más claro entender un paso que haremos un poco más adelante, en vez de dejar que Struts 2 sea quien defina la estructura de nuestro formulario, lo haremos nosotros mismos. Para eso debemos indicar, en el atributo "theme" del formulario, el valor "simple":


<s:form action="multiparametros" theme="simple">
</s:form>


Ahora colocaremos un elemento en el formulario, un campo de entrada de texto usando el la etiqueta "<s:textfield>":


<s:form action="multiparametros" id="formulario" theme="simple">
    <s:textfield id="valor1" name="valor1" /> 
</s:form>


Como en esta ocasión no se colocará una etiqueta de forma automática en el campo de texto, deberemos ponerla nosotros mismos, usando la etiqueta "<s:label>", de la siguiente manera:


<s:form action="multiparametros" theme="simple">
    <s:label for="valor1" value="Valor 1: " />
    <s:textfield id="valor1" name="valor1" />
</s:form>


Como pueden ver hemos tenido que colocar un valor en el atributo "id" del textfield, y poner este mismo valor en el atributo "for" de la etiqueta "<s:label>".

Como estamos haciendo nosotros mismos el acomodo de los componentes, también debemos colocar un salto de línea entre los elementos del formulario; de lo contrario aparecerían todos en la misma línea. Para eso usamos la etiqueta "<br>":


<s:form action="multiparametros" theme="simple">
    <s:label for="valor1" value="Valor 1: " />
    <s:textfield id="valor1" name="valor1" /> <br /> 
</s:form>


El siguiente paso consiste en colocar el botón de envió del formulario, usando la etiqueta "<s:submit>":


<s:form action="multiparametros" theme="simple">
    <s:label for="valor1" value="Valor 1: " />
    <s:textfield id="valor1" name="valor1" /> <br /> 
    <s:submit value="Enviar" />    
</s:form>


Hasta ahora tenemos un formulario que se ve de la siguiente forma:



Podemos ver que tenemos un solo parámetro, sin embargo para el ejemplo necesitamos varios campos de texto. Haremos que estos se agreguen de forma dinámica usando una biblioteca de JavaScript: jQuery.

Colocaremos un botón que al presionarlo agregará un nuevo campo para introducir texto, junto con su etiqueta y salto de línea correspondiente.

El HTML del botón es el siguiente:


<button id="btnAgregar">Agregar Elemento</button>


Y el código de jQuery para agregar los elementos al formulario es el siguiente:


$(document).ready(function () 
{
    $("#btnAgregar").click(function() 
    {
        var num = $("input[type=text]").length;
        var numeroSiguiente = num + 1;
        var elementoNuevo = $("#valor" + num).clone().attr('id', 'valor'+numeroSiguiente).attr("name", "valor"+numeroSiguiente);
        var etiquetaNueva = $("label[for=valor"+num+"]").clone().attr("for", "valor"+numeroSiguiente).text("Valor " + numeroSiguiente + ": ");
                           
        $("#valor" + num).after(elementoNuevo);
        elementoNuevo.before(etiquetaNueva);
        etiquetaNueva.before("<br />");
    });
});


Como este no es un tutorial de jQuery no explicaré el código anterior ^_^. Pero el resultado es el siguiente:



Cada vez que presionemos el botón "Agregar Elemento" se agregará un nuevo campo de entrada, de la siguiente forma:



Ahora que tenemos un formulario pasemos a crear nuestro Action. Creamos una nueva clase Java, en el paquete "Actions". El nombre de esta clase será "MultiplesParametrosAction", extenderá de "ActionSupport" e implementará la interface "ParameterAware":


public class MultiplesParametrosAction extends ActionSupport implements ParameterAware
{
}


Debemos colocar una variable que nos permita almacenar la referencia al mapa que los interceptores inyectarán con la lista de parámetros recibidos:


private Map<String, String[]> parametros;


Además debemos implementar el método "setParameters" de la interface "ParameterAware":


public void setParameters(Map<String, String[]> parametros)
{
    this.parametros = parametros;
}


Adicionalmente pondremos un getter para el mapa de parámetros, para poder recuperarlo desde la JSP correspondiente:


public Map<String, String[]> getParametros()
{
    return parametros;
}


Nuestro método "execute" nuevamente será muy simple y solo regresara el valor "SUCESS":


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


El siguiente paso es anotar nuestra clase para convertirla en un Action. Las anotaciones (y la explicación) serán las mismas que hemos venido usando hasta ahora:


@Namespace(value="/multiparametros")
@Action(value="multiparametros", results={@Result(location="/multiparametros/parametros.jsp")})
public class MultiplesParametrosAction extends ActionSupport implements ParameterAware
{
}


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


@Namespace(value="/multiparametros")
@Action(value="multiparametros", results={@Result(location="/multiparametros/parametros.jsp")})
public class MultiplesParametrosAction extends ActionSupport implements ParameterAware
{
    private Map<String, String[]> parametros;
    
    public void setParameters(Map<String, String[]> parametros)
    {
        this.parametros = parametros;
    }

    public Map<String, String[]> getParametros()
    {
        return parametros;
    }
    
    @Override
    public String execute() throws Exception
    {   
        return SUCCESS;
    }
}


Finalmente debemos crear el contenido de la pagina "parametros.jsp". Nuevamente usaremos una etiqueta "<s:iterator>". En este caso en vez de pasarle una lista, le pasaremos el mapa con los parámetros que recibimos desde el Action, y nuevamente colocaremos una variable en donde se colocará el valor del elemento actual del ciclo:


<s:iterator value="parametros" var="parametro">
</s:iterator>


Como un Map tiene dos elementos importantes, la llave y el valor, debemos obtener cada uno de estos valores para mostrarlos en nuestra página. Para tener acceso a la llave usamos la propiedad "key" del elemento actual, y para obtener el valor, usamos la propiedad "value" la cual nos regresará el arreglo de cadenas asociado con el parámetro. Para obtener el primer valor de este parámetro debemos accederlo usando la sintaxis de los arreglos, como vimos en el segundo tutorial de la serie:


<s:iterator value="parametros" var="parametro">
    <s:property value="#parametro.key" />: <s:property value="#parametro.value[0]" />
</s:iterator>


Para que todo se vea más ordenado, colocaremos los resultados en una lista:


<ul>
    <s:iterator value="parametros" var="parametro">
        <li><s:property value="#parametro.key" />: <s:property value="#parametro.value[0]" /></li>
    </s:iterator>
</ul>


Ahora que ya tenemos el código listo ejecutemos la aplicación; cuando entremos a la dirección:


http://localhost:8080/formularios/multiparametros/datos.jsp


Veremos el formulario que creamos:



Si ingresamos los siguientes datos, y enviamos el formulario:



Veremos la siguiente salida:



Como podemos ver, aunque no tenemos ni idea del nombre o la cantidad de parámetros que recibiremos en la petición, Struts 2 aún nos proporciona una forma sencilla y elegante de manejarlos.

Hasta ahora hemos visto varias maneras de recibir parámetros desde nuestros formularios. Sin embargo otra parte muy importante del trabajo con estos últimos es el poder realizar algunas validaciones sobre los datos que recibimos. En la siguiente sección veremos cómo usar validaciones en nuestros formularios.


5. Validaciones

Una parte importante del trabajo con formularios es que podamos asegurarnos que la información de los mismos cumpla con ciertos requisitos para poder procesarlos. Por ejemplo que las direcciones de correo electrónico cumplan con el formato establecido, o que ciertos valores estén dentro de un rango valido, o simplemente que se estén proporcionando los campos obligatorios del formulario.

Hacer esto con Struts 2 es muy simple, ya que proporciona varias maneras de realizar validaciones de forma automática para los datos de nuestros formularios.

Las validaciones son realizadas por dos interceptores: "validation" y "workfllow". El primero realiza las validaciones y crea una lista de errores específicos para cada uno de los campos que no pase la validación. Posteriormente el interceptor "workflow" verifica si hay errores de validación; si encuentra algún error regresa un resultado "input", por lo que es necesario que proporcionemos un result con este nombre.

Struts 2 tiene básicamente tres formas de realizar validaciones:

  • Validaciones mediante un archivo XML
  • Validaciones mediante anotaciones
  • Validaciones manuales


Primero comencemos creando la estructura con los elementos que usaremos en las validaciones:

Lo primero que haremos será crear un nuevo directorio en las páginas web, llamado "validaciones". Dentro de este directorio crearemos una nueva JSP llamada "formulario.jsp":



Dentro de esta página crearemos un formulario sencillo que solicitará al usuario algunos datos básicos como nombre, correo electrónico, teléfono, etc. No olviden colocar en la JSP la directiva taglib correspondiente para indicar que esta hará uso de las bibliotecas de etiquetas de Struts. El formulario quedará, por el momento, de la siguiente forma:


<s:form action="validacionDatos">
    <s:textfield name="nombre" label="Nombe" />
    <s:textfield name="username" label="Username" />
    <s:password  name="password" label="Password" />
    <s:textfield name="email" label="Email" />
    <s:textfield name="edad" label="Edad" />
    <s:textfield name="telefono" label="Telefono" />
    <s:submit value="Enviar" />
</s:form>


Otra de las pequeñas ayudas que nos proporcionan las etiquetas de Struts 2 es que nos permiten indicar si cuáles campos son obligatorios, con lo que se le colocará una pequeña marca en la etiqueta para que el usuario sepa cuáles campos son los que está obligado a llenar. Esta marca es solo una ayuda visual, no realizará ningún proceso de validación del campo.

Para agregar dicha marca e indicar que el campo será obligatorio, debemos colocar el valor de "true" en el atributo "required" de las etiquetas del formulario. En este caso todos los campos, con excepción del teléfono, serán requeridos:


<s:form action="validacionDatos">
    <s:textfield name="nombre" label="Nombe" required="true" />
    <s:textfield name="username" label="Username" required="true" />
    <s:password  name="password" label="Password" required="true" />
    <s:textfield name="email" label="Email" required="true" />
    <s:textfield name="edad" label="Edad" required="true" />
    <s:textfield name="telefono" label="Telefono" />
    <s:submit value="Enviar" />
</s:form>


Con el código anterior obtenemos el siguiente formulario:



Debido al tema ("theme") que se usa por default para construir el formulario, los errores serán mostrados directamente sobre los campos que no pasen la validación. Sin embargo, si quisiéramos mostrar, de manera adicional, una lista con todos los errores del formulario, debemos agregar en nuestra JSP la etiqueta "<s:fielderror />". Solo la colocamos sobre el formulario, de la siguiente forma:


<s:fielderror />
        
<s:form Action="validacionDatos">
    <s:textfield name="nombre" label="Nombre" required="true" />
    <s:textfield name="username" label="Username" required="true" />
    <s:password  name="password" label="Password" required="true" />
    <s:textfield name="email" label="Email" required="true" />
    <s:textfield name="edad" label="Edad" required="true" />
    <s:textfield name="telefono" label="Telefono" />
    <s:submit value="Enviar" />
</s:form>


Ahora creamos el Action que se encargará de procesar los datos anteriores. Para eso creamos una nueva clase llamada "ValidacionDatos" en el paquete "Actions". Esta clase extenderá de "ActionSupport" y tendrá las anotaciones que para este momento ya deben sernos más que familiares:


@Namespace(value="/validaciones")
@Action(value="validacionDatos", results={@Result(location="/validaciones/alta-exitosa.jsp")})
public class ValidacionDatos extends ActionSupport
{
    
}


La clase "ValidacionDatos" tendrá setters y getters para cada uno de los valores que recibiremos del formulario. En este caso los getters son importantes por dos razones, la primera es que queremos que el formulario sea repoblado con los valores que el usuario ya habia introducido (los cuales solo pueden obtenerse a partir de los getters); la segunda es que, por alguna razón que no alcanzo a entender, las validaciones se realizan sobre los getters de las propiedades. La case "ValidacionDatos" hasta ahora se ve de la siguiente forma:


public class ValidacionDatos extends ActionSupport
{
    private String nombre;
    private String username;
    private String password;
    private String email;
    private int edad;
    private String telefono;

    public void setEdad(int edad)
    {
        this.edad = edad;
    }

    public void setEmail(String email)
    {
        this.email = email;
    }

    public void setNombre(String nombre)
    {
        this.nombre = nombre;
    }

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

    public void setTelefono(String telefono)
    {
        this.telefono = telefono;
    }

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

    public int getEdad()
    {
        return edad;
    }

    public String getEmail()
    {
        return email;
    }

    public String getNombre()
    {
        return nombre;
    }

    public String getPassword()
    {
        return password;
    }

    public String getTelefono()
    {
        return telefono;
    }

    public String getUsername()
    {
        return username;
    }
}


En su método "execute" esta clase solo se encargará de imprimir los valores que se han recibido a través del formulario:


@Override
public String execute() throws Exception
{
    System.out.println("nombre: " + nombre);
    System.out.println("username: " + username);
    System.out.println("password: " + password);
    System.out.println("email: " + email);
    System.out.println("edad: " + edad);
    System.out.println("telefono: " + telefono);
        
    return SUCCESS;
}


Ahora crearemos la página que se encargará de mostrar el resultado del alta de usuario. Para esto crearemos una JSP llamada "alta-exitosa.jsp" en el directorio "validaciones". Esta página será muy sencilla y solo mostrará el mensaje "Usuario dado de alta exitosamente."

Finalmente, recordemos que si existe un error de validación el interceptor correspondiente regresará un resultado con valor "input". Este resultado debe enviarnos a una página en la que se mostrará la lista de errores de datos de entrada. Normalmente esta página es el mismo formulario de entrada para que el usuario pueda corregir los datos que introdujo de forma incorrecta. Asi que modificaremos la anotación "@Action" de la clase "ValidacionDatos" para aregar un segundo resultado, que en caso de un error de entrada ("input") nos regrese al formulario:


@Action(value = "validacionDatos", results =
{
    @Result(location = "/validaciones/alta-exitosa.jsp"),
    @Result(name="input", location="/validaciones/formulario.jsp")
})


Como podemos ver, en este caso si es necesario indicar de forma explícita el nombre del result usando el atributo "name".

Al ejecutar nuestra aplicación y entrar en la siguiente dirección:


http://localhost:8080/formularios/validaciones/formulario.jsp


Deberemos ver el formulario que creamos hace un momento. Llenemos un par de campos para ver el resultado obtenido:



Al enviar los datos del formulario deberemos ver la siguiente salida:



Si analizamos la salida anterior veremos que esta no es correcta, ya que el usuario no ha proporcionado todos los datos requeridos, por lo que debería ver un mensaje de error. Solucionaremos esto agregando validaciones en los datos. Primero veremos cómo hacer validaciones usando archivos XML.


5.1 Validaciones usando archivos XML

Para realizar validaciones usando archivos en XML debemos crear un archivo por cada una de las clases que será validada. El nombre de este archivo debe seguir una convención muy sencilla:


<NombreDeNuestroAction>-validation.xml


O sea, que debe tener el mismo nombre del Action + la cadena "-validation" y la extensión ".xml". El archivo debe estar colocado en el mismo directorio que la clase Action que validará.

Dentro de este archivo se indicará cuáles campos serán validados y qué validaciones se le aplicarán. Cada una de estas validaciones tiene un parámetro obligatorio, que es el nombre del campo que validará. Adicionalmente pueden o no tener más parámetros.

Struts 2 ya viene con algunas validaciones predeterminadas que podemos usar, aunque también tenemos la posibilidad de crear nuestras propias validaciones en caso de ser necesario. Las validaciones incuidas con Struts 2 se muestran en la siguiente tabla:

ValidadorDescripción
requiredVerifica que el campo especificado no sea nulo.
requiredstringVerifica que un campo de tipo String no sea nulo y que tenga un longitud mayor a 0.
stringlengthVerifica que una cadena tenga una cierta longitud.
int, long, y shortVerifica que el int, long, o short especificado estén dentro de un rango determinado.
doubleVerifica que el valor double especificado esté dentro de cierto rango.
dateVerifica que la fecha proporcionada esté dentro del rango proporcionado. Por default se usa el formato Date.SHORT para indicar las fechas.
emailVerifica que el campo cumpla con el formato de una dirección de email válida
urlVerifica que el campo cumpla con el formato de una URL válida.
visitorPermite enviar la validación a las propiedades del objeto del Action usando los propios de archivos de validación del objeto. Esto permite usar ModelDriven y administrar las validaciones de los objetos de modelo en un solo lugar.
conversionVerifica si ocurre un error de conversión en el campo.
expressionRealiza una validación basada en una expresión regular en OGNL.
fieldexpressionValida un campo usando una expresión OGNL.
regexValida un campo usando una expresión regular.


Sabiendo qué validadores podemos aplicar a los campos del formulario, pasemos a crear nuestro archivo de validación.

Creamos en el paquete "actions" un nuevo archivo XML llamado "ValidacionDatos-validation.xml":



Todas las validaciones deben estar contenidas dentro del elemento "<validators>":


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
<validators>

</validators>


Lo siguiente es indicar cada campo que será validado usando el elemento "<field>", e indicamos el nombre del campo que queremos validar en su atributo "name":


<field name="nombre">
</field>


Posteriormente indicamos cada una de las validaciones que se aplicarán a ese campo, usando el elemento "<field-validator>" y su atributo "type":


<field name="nombre">
    <field-validator type="requiredstring">
            
    </field-validator>
</field>


Para agregar un parámetro a la validación usamos el elemento "<param>" y colocamos el nombre del parámetro en su atributo "name". En este caso usaremos el parámetro "trim" del validador "requiredstring" para indicar que deseamos que se eliminen los espacios en blanco del inicio y fin de la cadena (en caso de existir) antes de que la validación sea aplicada:


<field-validator type="requiredstring">
    <param name="trim">true</param>
</field-validator>


Para terminar, debemos agregar un mensaje que será mostrado en caso de que la validación no sea exitosa. Para esto usamos el elemento "<message>":


<field-validator type="requiredstring">
    <param name="trim">true</param>
    <message>El nombre del usuario es un campo obligatorio.</message>
</field-validator>


Y esto es todo, con esto el campo "nombre" del formulario ha quedado validado.

Si necesitáramos agregar más validaciones a este campo, solo bastaría con agregar un segundo elemento "<field-validator>". Agregamos un validador de tipo "stringlength" para asegurarnos que el nombre que introduzca el usuario tenga una longitud mínima de 4 caracteres, y una longitud máxima de 12. El proceso es básicamente el mismo por lo que no lo explicaré, pero al final la validación queda de la siguiente forma:


<field-validator type="stringlength">
    <param name="trim">true</param>
    <param name="minLength">4</param>
    <param name="maxLength">12</param>
    <message>El nombre del usuario debe tener entre 4 y 12 caracteres</message>
</field-validator>


Hasta ahora nuestro archivo de validaciones se ve de la siguiente forma:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
<validators>
    <field name="nombre">
        <field-validator type="requiredstring">
            <param name="trim">true</param>
            <message>El nombre del usuario es un campo obligatorio.</message>
        </field-validator>
        
        <field-validator type="stringlength">
            <param name="trim">true</param>
            <param name="minLength">4</param>
            <param name="maxLength">12</param>
            <message>El nombre del usuario debe tener entre 4 y 12 caracteres</message>
        </field-validator>
    </field>
</validators>


El proceso es básicamente el mismo para todos los campos del formulario que vamos a validar. No explicaré cada una de las validaciones de los campos porque creo que la idea se entiende bastante en este momento, pero el archivo final de validaciones queda de la siguiente forma:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
<validators>
    <field name="nombre">
        <field-validator type="requiredstring">
            <param name="trim">true</param>
            <message>El nombre del usuario es un campo obligatorio.</message>
        </field-validator>
        
        <field-validator type="stringlength">
            <param name="trim">true</param>
            <param name="minLength">4</param>
            <param name="maxLength">12</param>
            <message>El nombre del usuario debe tener entre 4 y 12 caracteres</message>
        </field-validator>
    </field>
    
    <field name="username">
        <field-validator type="requiredstring">
            <param name="trim">true</param>
            <message>El username es un campo obligatorio.</message>
        </field-validator>
        
        <field-validator type="stringlength">
            <param name="trim">true</param>
            <param name="minLength">6</param>
            <message>El username debe tener al menos 6 caracteres</message>
        </field-validator>
    </field>
    
    <field name="password">
        <field-validator type="requiredstring">
            <param name="trim">true</param>
            <message>La contraseña es un campo obligatorio.</message>
        </field-validator>
        
        <field-validator type="stringlength">
            <param name="trim">true</param>
            <param name="minLength">6</param>
            <param name="maxLength">8</param>
            <message>La contraseña debe tener entre 6  y 8 caracteres</message>
        </field-validator>
        
        <field-validator type="regex">
            <param name="expression">^\w*(?=\w*\d)(?=\w*[a-z])(?=\w*[A-Z])\w*$</param>
            <message>La contraseña debe ser alfanumérica, debe tener al menos una letra mayúscula, una letra minúscula, y al menos un número.</message>
        </field-validator>
    </field>
    
    <field name="email">
        <field-validator type="requiredstring">
            <param name="trim">true</param>
            <message>El correo electrónico es un campo obligatorio.</message>
        </field-validator>
        
        <field-validator type="email">
            <message>El correo electrónico está en un formato inválido.</message>
        </field-validator>
    </field>
    
    <field name="edad">
        <field-validator type="required">
            <message>La edad es un campo obligatorio.</message>
        </field-validator>

        <field-validator type="conversion">
            <message>La edad debe contener solo números enteros.</message>
        </field-validator>
        
        <field-validator type="int">
            <param name="min">0</param>
            <param name="max">200</param>
            <message>La edad proporcionada no está dentro del rango permitido.</message>
        </field-validator>
    </field>
    
</validators>


Ahora que todo está listo podemos ejecutar nuestra aplicación y entrar a la siguiente dirección:


http://localhost:8080/formularios/validaciones/formulario.jsp


Nuevamente veremos nuestro formulario anterior. Si no colocamos ningún valor en ningún campo y simplemente presionamos el botón "Enviar" veremos la siguiente lista de errores:



Podemos ver que estamos obteniendo los errores que definimos en el archivo de validaciones, con los mensajes correspondientes. Si observamos el código fuente de la pagina generada veremos que cada uno de los errores marcados está colocado en un elemento que tiene como clase CSS "errorMessage", por lo que podemos personalizar la forma en la que se muestran los errores de validación de la aplicación a través de una hoja de estilos.

Si ahora colocamos algunos valores incorrectos en los campos del formulario:



Obtendremos una lista distinta de errores:



Si ahora colocamos datos correctos:



Veremos la página correspondiente:



Por lo que los datos han sido validados correctamente.

A pesar de que la mayoría de las veces realizaremos validaciones como las anteriores, o sea validaciones sobre los campos de un formulario, algunas veces será necesario realizar validaciones que no son sobre los campos (llamadas validaciones planes).

Las validaciones planas son un tipo especial de validación que no está atada a un campo específico, como la validación de expresiones. Por ejemplo, agreguemos una validación extra que verifique que el "nombre" y el "username" no sean iguales.

Para agregar una validación plana en vez de usar el elemento "<field>" usamos el elemento "<validator>", que es muy similar a "<field-validator>", indicamos usando su atributo "type" el tipo de validación que se realizará; si la validación necesita algún parámetro lo agregamos con el elemento "<param>"; el mensaje que se mostrará en caso de que la validación no pase lo colocamos con el elemento "<message>".

La validación para verificar que el nombre del usuario y el username no sean iguales queda de la siguiente forma (pueden colocarla después de todas las validaciones "field"):


<validator type="expression">
    <param name="expression">!nombre.equals(username)</param>
    <message>El nombre de usuario y el username no deben ser iguales.</message>
</validator>


Como estas validaciones no son realizadas sobre ningún campo, sus mensajes de error no se mostrarán usando la etiqueta "<s:fielderror />", si queremos verlos en la JSP debemos usar la etiqueta "<s:actionerror />":


<s:fielderror />
<s:actionerror />


Ahora, si volvemos a ejecutar la aplicación e introducimos el mismo nombre de usuario y contraseña:



Y presionamos el botón enviar obtendremos el siguiente resultado:



Con lo que podemos comprobar que estas validaciones también funcionan correctamente.

Ahora que vimos cómo realizar validaciones usando archivos XML veamos cómo realizar esto mismo usando anotaciones.


5.2 Validaciones usando anotaciones

Realizar validaciones de los datos de un formulario usando anotaciones es un proceso muy simple. Modificaremos un poco la clase "ValidacionDatos" que creamos hace un momento (de hecho yo crearé una clase aparte para no modificar el código que ya teníamos).

Las anotaciones que podemos usar para realizar las validaciones se encuentran en el paquete "com.opensymphony.xwork2.validator.annotations", y tenemos las siguientes validaciones disponibles:

  • @ConversionErrorFieldValidator
  • @DateRangeFieldValidator
  • @DoubleRangeFieldValidator
  • @EmailValidator
  • @ExpressionValidator
  • @FieldExpressionValidator
  • @IntRangeFieldValidator
  • @RegexFieldValidator
  • @RequiredFieldValidator
  • @RequiredStringValidator
  • @StringLengthFieldValidator
  • @UrlValidator
  • @VisitorFieldValidator


Como podemos ver, existe una anotación por cada una de las validaciones que se tienen cuando trabajamos con los archivos XML; la explicación para cada una de las validaciones también es la misma que para el caso anterior.

Esta anotación puede ser colocada tanto en el setter como en el getter de la propiedad que queremos validar. También podemos usar varias validaciones para una misma propiedad.

Veamos un primer ejemplo validando el campo "nombre". En la clase "ValidacionDatos" primero verificaremos que el nombre del usuario no esté vacio cuando se reciba del fomulario, para esto usaremos la anotación "@RequiredStringValidator", yo la pondré en el setter de la propiedad:


@RequiredStringValidator    
public void setNombre(String nombre)
{
    this.nombre = nombre;
}


Todas las anotaciones de validaciones proporcionan una serie de atributos que nos permiten configurar el comportamiento de la validación. Casi en todos los casos lo único que tendremos que configurar es el mensaje que se mostrará en caso de que la validación no sea correcta. Para eso usamos el atributo "message":


@RequiredStringValidator(message = "El nombre de usuario es un campo obligatorio.")
public void setNombre(String nombre)
{
    this.nombre = nombre;
}


También validaremos que el nombre tenga una cierta longitud, o sea que sea mayor a cierto número de caracteres y menor a otro cierto número. Para eso usamos la anotación la anotación "@StringLengthFieldValidator" que colocamos justo debajo de la anotación anterior:


@RequiredStringValidator(message = "El nombre de usuario es un campo obligatorio.")
@StringLengthFieldValidator(minLength="4", maxLength="12", message="El nombre del usuario debe tener entre 4 y 12 caracteres")
public void setNombre(String nombre)
{
    this.nombre = nombre;
}


Haremos lo mismo para cada uno de los campos que necesitemos validar. Nuevamente, no explicaré cada una de las anotaciones ya que son bastante intuitivas, pero al final quedan de la siguiente forma:


@RequiredFieldValidator(message="La edad es un campo obligatorio.")
@ConversionErrorFieldValidator(message="La edad debe contener solo números enteros.")
@IntRangeFieldValidator(min="0", max="200", message="La edad proporcionada no está dentro del rango permitido.")
public void setEdad(int edad)
{
    this.edad = edad;
}

@RequiredStringValidator(message="El correo electrónico es un campo obligatorio.")
@EmailValidator(message="El correo electrónico está en un formato inválido.")
public void setEmail(String email)
{
    this.email = email;
}

@RequiredStringValidator(message = "El nombre de usuario es un campo obligatorio.")
@StringLengthFieldValidator(minLength = "4", maxLength = "12", message = "El nombre del usuario debe tener entre 4 y 12 caracteres")
public void setNombre(String nombre)
{
    this.nombre = nombre;
}

@RequiredStringValidator(message="La contraseña es un campo obligatorio.")
@StringLengthFieldValidator(minLength="6", maxLength="8", message="La contraseña debe tener entre 6  y 8 caracteres")
@RegexFieldValidator(expression="^\\w*(?=\\w*\\d)(?=\\w*[a-z])(?=\\w*[A-Z])\\w*$", message="La contraseña debe ser alfanumérica, debe tener al menos una letra mayúscula, una letra minúscula, y al menos un número.")
public void setPassword(String password)
{
    this.password = password;
}

public void setTelefono(String telefono)
{
    this.telefono = telefono;
}

@RequiredStringValidator(message="El username es un campo obligatorio.")
@StringLengthFieldValidator(minLength="6", message="El username debe tener al menos 6 caracteres")
public void setUsername(String username)
{
    this.username = username;
}


Probemos nuevamente que todo funcione correctamente. Al ejecutar la aplicación y entrar en nuestro formulario, en la siguiente dirección:


http://localhost:8080/formularios/validaciones/formulario.jsp


(Bueno, de hecho mi dirección es un poco distinto porque cree el contenido en una página alterna ^^) Debemos ver el formulario que ya teníamos anteriormente:



Al hacer clic en el botón "Enviar", con los campos vacios, obtendremos la siguiente salida:



Podemos ver que, hasta el momento, las validaciones han funcionado correctamente. Si colocamos algunos datos de forma equivocada:



Obtendremos la siguiente salida:



Si ahora, colocamos los datos de forma correcta:



Obtendremos la salida que estamos esperando:



Como podrán imaginar, todas las validaciones anteriores son validaciones de campos. Usando anotaciones también podemos realizar validaciones planas (las validaciones que no están relacionadas con algún campo del formulario). Solo que en este caso las validaciones planas se colocan directamente en el método "execute" del Action, usando una anotación especial: "@Validations". En los atributos de esta anotación podemos indicar cada una de las validaciones que se necesiten en la aplicación (campos requeridos, emails, urls, longitud de las cadenas, rango de enteros, fechas, etc.). En nuestro caso lo que nos interesa es realizar una validación sobre una expresión. Para esto usamos el atributo "expressions" de la anotación "@Validations":


@Override
@Validations(expressions={})
public String execute() throws Exception


En este atributo colocamos, usando la anotación "@ExpressionValidator", la expresión que será validada, en este caso queremos asegurarnos de que el nombre del usuario y su username no son iguales, por lo que la validación queda de la siguiente forma:


@ExpressionValidator(expression="!nombre.equals(username)", message="El nombre de usuario y el username no deben ser iguales.")


La anotación completa queda de la siguiente forma:


@Override
@Validations(expressions =
{
    @ExpressionValidator(expression = "!nombre.equals(username)", message = "El nombre de usuario y el username no deben ser iguales.")
})
public String execute() throws Exception


Cuando ejecutemos nuevamente la aplicación, poniendo el mismo nombre de usuario y contraseña:



Obtendremos el mensaje correspondiente de error:



Cuando coloquemos los datos correctos deberíamos dejar de ver mensajes de error.

Los dos tipos de validaciones que hemos visto nos permiten verificar que los datos que proporcionemos sean correctos, siempre y cuando conozcamos de antemano los valores, los rangos, o las reglas que los campos del formulario pueden tener pero ¿qué pasa si no conocemos estos valores? por ejemplo, en el caso de que debamos comparar un valor contra un registro de la base de datos o contra algún web service o alguna cosa más sofisticada, las validaciones anteriores nos servirán de poco.

Afortunadamente, para esos casos, Struts 2 nos permite realizar un tercer tipo de validación: las validaciones manuales.


5.3 Validaciones Manuales

Ya sé qué es lo que deben estar pensando: que las validaciones manuales son las que estamos acostumbrados a realizar, que no es un mecanismo automático que nos ayude a simplificar nuestro trabajo de validación de datos. Si es así, no están tan equivocados ^_^; bueno, más o menos.

Las validaciones manuales son aquellas que son tan particulares a nuestro proceso de negocio que estas no se pueden tomar de alguna plantilla, como los ejemplos que mencioné anteriormente.

Este tipo de validación, aunque las tenemos que realizar nosotros mismos, sigue siendo administrado por el framework. Para ello nos proporciona una interface llamada "Validateable", que proporciona únicamente un método:


public void validate();


Afortunadamente para nosotros, la clase "ActionSupport" implementa esta interface, por lo que lo unico que tenemos que hacer es sobre-escribirlo en nuestros Actions y, ayudados de algunos métodos auxiliares de la clase "ActionSupport", enviar los errores correspondientes en caso de que estos existan.

Para poder indicar si existe algún error en las validaciones nos ayudamos del método "addFieldError", en caso de que exista un error de la validación de un campo, y de "addActionError", en caso de existir un error que no esté relacionado con un campo.

"addFieldError" recibe dos parámetros, el primero es el nombre del campo que generó el error y el segundo es la descripción del error. "addActionError" recibe un solo parámetro que es la descripción del error.

Como se podrán imaginar, si existe algún error en un campo este se mostrará en el campo correspondiente del formulario y en la etiqueta "<s:fielderror />". Si existe un error de otro tipo este se mostrará en la etiqueta "<s:actionerror />".

Ahora que conocemos la teoría sobre las validaciones manuales pasemos verlas en la práctica. Para esto modificaremos las validaciones que ya tenemos en nuestra clase.

Lo primero que debemos hacer es sobre-escribir el método "validate":


@Override
public void validate()
{        
}


Las validaciones que realizaremos serán muy simples: si el username es "programadorjava" (supongamos que este fue un nombre que se trajo de la base de datos, para comprobar que no haya nombres repetidos) o el correo es "programadorjavablog@gmail.com" entonces no se le permitirá al usuario completar el registro:


@Override
public void validate()
{
    if("programadorjava".equals(username))
    {
        addFieldError("username", "El username seleccionado ya se encuentra ocupado, por favor seleccione otro.");
    }
    if("programadorjavablog@gmail.com".equals(email))
    {
        addFieldError("email", "El correo electrónico proporcionado ya ha sido registrado anteriormente.");
    }
}


El framework detectará de forma automática si hemos agregado algo a los campos de error, si es así regresará esta lista de errores a nuestro resultado "input", de lo contrario continuará con la ejecución del método "execute".

Si ejecutamos la aplicación y entramos a nuestro formulario, proporcionando alguno de los datos reservados y que son validados dentro del método "validate":



Veremos los mensajes de error que hemos establecido:



Podemos comprobar nuevamente que las validaciones han funcionado correctamente ^_^.

Como vemos, realizar validaciones usando Struts 2 es muy sencillo y puede ahorrarnos bastante tiempo que podemos dedicar a mejorar otros aspectos de nuestra aplicación.

Ahora veremos cómo realizar otro de los trabajos que normalmente hacemos al hacer uso de formularios, la carga de archivos desde el cliente al servidor:


6. Carga de archivos

Hacer carga o upload de archivos desde un cliente hacia nuestro servidor, ya sea para almacenarlos o para procesarlos, es una operación que no siempre es sencilla de realizar. Sin embargo Struts 2 cuenta con una manera que hace que lograr esto sea tan sencillo como el resto de las cosas que hasta ahora hemos aprendido.

Struts 2 proporciona el soporte para la carga de archivos conforme a la especificación de HTML, esto nos permite subir uno o varios archivos desde el cliente al servidor.

Cuando un archivo es cargado, este será almacenado en un directorio temporal por el interceptor correspondiente (fileUpload). El archivo deberá entonces ser procesado o movido a otra ubicación, por nuestro Action, ya que al terminar la petición el interceptor se encargará de eliminar este archivo temporal.

Veamos cómo realizar la carga de archivos con un ejemplo.

Lo primero que haremos será crear un nuevo directorio en las páginas web, llamado "carga". Dentro de este directorio crearemos una nueva JSP llamada "formulario.jsp":



Como hasta ahora, debemos indicar en la JSP que haremos uso de la biblioteca de etiquetas de Struts 2:


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


Lo siguiente es crear un formulario. Este será un poco distinto a los que hemos creado hasta ahora. Lo primero es que los datos del formulario deben codificarse de una forma especial antes de que estos sean enviados al servidor. Afortunadamente es el navegador el que se encarga de hacer esta codificación, lo único que nosotros debemos hacer es indicar, usando el atributo "enctype" del formulario, cuál codificación será; cuando cargamos archivos estos deben ir codificados en "multipart/form-data".

Además los datos deben ser enviados por POST (en los formularios HTML el método por default para enviar datos es GET (por la URL), pero en Struts 2 por default se envían por POST (en el cuerpo del mensaje de la petición), así que no debemos agregarle ninguna cosa extra en este caso). El formulario, hasta ahora, se ve de la siguiente forma:


<s:form enctype="multipart/form-data">
</s:form>


Los archivos son cargados con un tipo de campo especial el cual es generado usado la etiqueta "<s:file />". Esta etiqueta funciona de la misma forma que las demás que hemos venido usando hasta el momento:


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


En este caso he llamado a mi archivo "archivo" pero podría tener cualquier nombre, como "imagen", "reporte", "datos", etc.

Agregaré otro campo solo para que vemos que podemos subir archivos junto con datos "planos" del formulario:


<s:form action="cargaArchivo" enctype="multipart/form-data">
    <s:file name="archivo" label="Archivo" />
    <s:textfield name="autor" label="Autor" />
</s:form>


Terminaremos el formulario agregando un botón de envío de formulario y una etiqueta que nos mostrará cualquier error que pudiera ocurrir en el proceso de carga:


<s:actionerror />
<s:form action="cargaArchivo" enctype="multipart/form-data">
    <s:file name="archivo" label="Archivo" />
    <s:textfield name="autor" label="Autor" />
    <s:submit value="Enviar" />
</s:form>


Como ven, el formulario es tan sencillo como todos los que hemos estado haciendo a lo largo del tutorial.

Ahora crearemos el Action que se encargará de procesar los datos de este formulario, y que es donde se encuentra la parte interesante del ejemplo ^^.

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


public class CargaArchivo extends ActionSupport
{
}


Agregaremos una propiedad de tipo String para almacenar el nombre del autor:


public class CargaArchivo extends ActionSupport
{
    private String autor;
}


Ahora viene la parte importante, para poder obtener una referencia al archivo, debemos colocar, en nuestro Action, una propiedad de tipo "File":


public class CargaArchivo extends ActionSupport
{
    private String autor;
    private File archivo;
}


Con esto, cada vez que se cargue un archivo usando el formulario, la variable de tipo "File" que tenemos, hará referencia a este archivo. Lo que nos queda es agregar los respectivos setters para las dos propiedades anteriores:


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

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

    public void setAutor(String autor)
    {
        this.autor = autor;
    }
}


Ahora sobrecargaremos el método "execute" de nuestro Action para mover el archivo a ua nueva ubicación en nuestro sistema de archivos:


@Override
public String execute() throws Exception
{
    File nuevoArchivo = new File("/", archivo.getName());
    archivo.renameTo(nuevoArchivo);
        
    return SUCCESS;
}


Podemos ver que colocaremos el nuevo archivo en el directorio raíz del sistema operativo y que el nombre del archivo será el mismo que el del archivo que estamos subiendo.

Al terminar el proceso mostraremos un poco de información con respecto al archivo. La información del archivo se verá en la página que enviemos como resultado "SUCCESS", por lo que deberemos proporcionar los getters correspondientes para esta información; en este caso mostraremos el nombre del archivo y la ruta en la que queda almacenado:


public String getNombre()
{
    return archivo.getName();
}
    
public String getRuta()
{
    return archivo.getAbsolutePath();
}


Como ven estamos obteniendo la información directamente del objeto File que representa al archivo que estamos cargando.

Crearemos la página que mostrará los resultados por lo que en el directorio "carga" creamos una nueva JSP llamada "archivoCargado". Dentro de este archivo hacemos indicamos usando la directiva "taglib" que haremos uso de la biblioteca de etiquetas de Struts 2. Después, usando la etiqueta "<s:property>" mostraremos los dos valores de los atributos anteriores:


Nombre: <s:property value="nombre" /><br />
Ruta: <s:property value="ruta" />


Anotaremos este clase como ya sabemos hacerlo, para que el framework la trate como un Action:


@Namespace(value = "/carga")
@Action(value = "cargaArchivo", results =
{
    @Result(location = "/carga/archivoCargado.jsp"),
    @Result(name="input", location = "/carga/formulario.jsp")
}) 
public class CargaArchivo extends ActionSupport


Ya tenemos todo asi que procedemos a ejecutar la aplicación y a ingresar a la siguiente dirección:


http://localhost:8080/formularios/carga/formulario.jsp


Con lo que veremos el formulario que creamos:



Podemos ver que en el campo de tipo "file" que declaramos, existe un pequeño botón que dice "Examinar...". Al presionar este botón se abrirá un cuadro de dialogo que nos permitirá seleccionar un archivo; en mi caso seleccionaré una imagen al azar de mi biblioteca de imágenes:



Cuando presionemos el botón "Enviar" de nuestro formulario, veremos en la página resultante una salida similar a la siguiente:



Podemos notar antes que nada que el archivo que habíamos subido, llamado "logoJT.png", ahora se llama "upload_5829876d_132a34219c9__7ffb_00000000.tmp". Esto ocurre porque cuando enviamos un archivo usando un formulario, el archivo no viaja como tal por la red; son sus bytes los que son enviados (lo sé técnicamente sería lo mismo ya que un archivo es un conjunto de bytes), el interceptor "fileUpload" toma estos bytes y los coloca en un archivo nuevo al cual le coloca un nombre extraño.

Si observamos el directorio a donde hemos movido el archivo que habíamos obtenido notaremos que nuestro archivo efectivamente ha quedado ahí, pero con el nuevo nombre (y extensión) que le ha colocado el interceptor:



Podemos ver que no tenemos información del nombre original del archivo ni de su tipo de contenido. Como podemos imaginar, ambos son datos muy importantes que nos pueden servir en algún momento.

Afortunadamente para nosotros, Struts 2 proporciona una forma en la que podemos obtener esta información de una forma fácil y sencilla (como nos tiene mal acostumbrados este framework ^^).

Para obtener esta información debemos proporcionar dos atributos extra para el archivo (con sus correspondientes getters). El nombre de estos atributos (en realidad solo los setters, pero creo que es más fácil entender de esta forma) debe seguir una cierta convención si queremos que Struts 2 proporcione la información de manera correcta.

Para obtener el nombre original del archivo debemos proporcionar un atributo, de tipo String, cuyo identificador sea "<nombre_del_campo_del_archivo>FileName"; o sea que si el identificador del campo del archivo es "documento", el identificador del campo para el nombre del archivo debe ser "documentoFileName", si el identificador del campo es "miArchivo", el campo para el nombre debe ser "miArchivoFileName".

Para obtener el tipo de contenido del archivo, o sea el "content type", debemos hacer algo similar y proporcionar un campo cuyo nombre sea "<nombre_del_campo_del_archivo>ContentType". Los campos para estos dos datos, junto con sus setters, queda de la siguiente forma:


private String archivoFileName;
private String archivoContentType;

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

public void setArchivoFileName(String archivoFileName)
{
    this.archivoFileName = archivoFileName;
}


Ahora con estos datos podemos modificar un poco nuestro Action dejando el método "execute" de la siguiente forma:


public String execute() throws Exception
{
    File nuevoArchivo = new File("/", archivoFileName);
    archivo.renameTo(nuevoArchivo);
        
    return SUCCESS;
}


Ahora el nombre con el que guardaremos el archivo que recibimos, es el mismo nombre del archivo original.

Agregaremos también un getter para cada una de las propiedades que no hemos utilizado aún, o sea para "autor" y "archivoContentType" para poder leerlos desde la JSP del resultado:


public String getArchivoContentType()
{
    return archivoContentType;
}

public String getAutor()
{
    return autor;
}


Al final, nuestra clase "CargaArchivo" queda de la siguiente forma:


@Namespace(value = "/carga")
@Action(value = "cargaArchivo", results =
{
    @Result(location = "/carga/archivoCargado.jsp"),
    @Result(name="input", location = "/carga/formulario.jsp")
}) 
public class CargaArchivo extends ActionSupport
{
    private String autor;
    private File archivo;
    private String archivoFileName;
    private String archivoContentType;

    @Override
    public String execute() throws Exception
    {
        File nuevoArchivo = new File("/", archivoFileName);
        archivo.renameTo(nuevoArchivo);
        
        return SUCCESS;
    }

    public String getArchivoContentType()
    {
        return archivoContentType;
    }

    public String getAutor()
    {
        return autor;
    }

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

    public void setArchivoFileName(String archivoFileName)
    {
        this.archivoFileName = archivoFileName;
    }

    public String getNombre()
    {
        return archivoFileName;
    }
    
    public String getRuta()
    {
        return archivo.getAbsolutePath();
    }
    
    public void setArchivo(File archivo)
    {
        this.archivo = archivo;
    }

    public void setAutor(String autor)
    {
        this.autor = autor;
    }
}


Debemos modificar ahora la JSP "archivoCargado" del directorio "carga" para que quede de la siguiente forma:


Nombre: <s:property value="nombre" /><br />
Ruta: <s:property value="ruta" /><br />
Autor: <s:property value="autor" /><br />
Content Type: <s:property value="archivoContentType" />


Con todos los datos que necesitamos. volvemos a ejecutar la aplicación, y debemos ver el mismo formulario de las últimas veces:



Subimos nuevamente nuestro archivo, y al presionar el botón "Enviar" deberemos ver la siguiente pantalla:



Como podemos observar, ahora se está indicando el nombre original de la imagen, y el tipo de archivo que se subió (que en este caso es "image/png"). Si vamos nuevamente al directorio raíz de nuestro sistema operativo veremos que ahora la imagen se ha almacenado de forma correcta, y podemos ver un preview de la misma:



Con lo que podemos comprobar que la carga se realizó de forma correcta ^_^.



Hagamos una segunda prueba, intentemos subir el siguiente archivo (como tip para lo que viene a continuación, fíjense en el tamaño del archivo).



Al tratar de enviar este archivo veremos que obtenemos... un mensaje de error:



Lo que el mensaje básicamente dice es que el tamaño de nuestro archivo excede el tamaño máximo de 2MB que Struts 2 tiene configurado por defecto. ¿Qué podemos hacer entonces para subir archivos que sean más grandes? Como deben estarse imaginando, Struts 2 proporciona una forma de configurar el tamaño máximo de los archivos que se pueden cargar, a través de una constante o a través de un parámetro del interceptor "fileUpload". Estos dos valores no son exactamente para lo mismo, pero lo explicaremos en su debido momento.

Primero veamos cómo establecer este valor como una constante.

Como recordarán, del primer tutorial de la serie, hay dos formas de definir las constantes de Struts 2, la primera es en el archivo "struts.xml" que normalmente usamos cuando realizamos una configuración con XML. El nombre de la constante que debemos definir es "struts.multipart.maxSize".

Esta variable quedaría de la siguiente forma en el archivo "struts.xml" si quisiéramos que el tamaño máximo del archivo fuera de 10MB:


<constant name="struts.multipart.maxSize" value="10485760" />


En donde el valor indica el peso máximo que puede tener el archivo el bytes (así es, leyeron bien: bytes). El valor es calculado de la siguiente forma:


10 * 1024 * 1024
//MB   KB   Bytes


Si no estamos usando un archivo de configuración, como es nuestro caso, la constante se define como un parámetro de inicialización en el filtro de Struts 2:


<filter>
    <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
    <init-param>
        <param-name>struts.multipart.maxSize</param-name>
        <param-value>5097152</param-value>
    </init-param>
</filter>


Si intentamos nuevamente subir nuestro archivo, veremos la siguiente salida:



Podemos ver que en esta ocasión logramos subir el archivo de forma correcta.

El utilizar la constante anterior modifica el límite del tamaño de los archivos en toda la aplicación. Esto es bueno si deseamos aumentar el límite en el tamaño de los archivos que se vaya a subir en cualquier formulario de la aplicación. ¿Pero qué pasaría si necesitáramos que en algún formulario particular el tamaño máximo de los archivos a subir fuera menor que en el resto de la aplicación? Para estos casos es que existe una forma para especificar un límite para un Action particular, estableciendo el parámetro "maximumSize" del interceptor "fileUpload" en el Action que queremos modificar.

Ojo con lo dicho anteriormente, esta segunda forma lo único que permite es hacer que el tamaño máximo para un Action particular sea menor que el resto de la aplicación, lo contrario (que el tamaño máximo para un Action sea mayor que para el resto de la aplicación) no se puede hacer.

Para personalizar el valor de este interceptor para un Action, cuando trabajamos con el archivo "struts.xml", usamos el elemento "<interceptor-ref>" para indicar qué interceptores son los que queremos aplicar a un Action particular y los valores de los parámetros de dichos interceptores. Por ejemplo, para el Action "cargaArchivo", si estuviéramos trabajando con un el archivo "struts.xml" quedaría de la siguiente forma para que el tamaño máximo de un archivo sea de 2MB (2* 1024 * 1024):


<action name="cargaArchivo" class="com.javatutoriales.struts2.formularios.actions.CargaArchivo">
    <interceptor-ref name="fileUpload">
        <param name="maximumSize">2097152</param>
    </interceptor-ref> 
    <interceptor-ref name="defaultStack"/>

    <result>/carga/archivoCargado.jsp</result>
    <result name="input">/carga/formulario.jsp</result>

</action>


Para cuando trabajamos con anotaciones la configuración queda de la siguiente forma:

@InterceptorRefs(value =
{
    @InterceptorRef(value = "fileUpload", params =
    {
        "maximumSize", "2097152"
    }),
    @InterceptorRef(value = "defaultStack")
})


Como de esta forma lo que hacemos es indicar los interceptores que se le aplicarán a este Action, debemos decir que además del interceptor "fileUpload" queremos aplicar el resto de los interceptores que se aplican normalmente a un Action y que se encuentran en el "defaultStack", como explicamos en el primer tutorial de la serie.

Si intentamos subir nuevamente nuestro archivo obtendremos el siguiente mensaje de error:



Podemos ver que este mensaje es ligeramente distinto al que habíamos obtenido anteriormente, pero nos sirve para comprobar que efectivamente el límite se ha modificado.

Si estamos realizando la carga de varios archivos, el tamaño máximo será el de la suma del tamaño de todos los archivos, y no de cada archivo individual. Además el tamaño máximo de archivos que el framework soporta, según la documentación oficial, es de 2GB.

Adicionalmente a restringir el tamaño máximo de los archivos que podemos cargar, Struts 2 nos permite limitar también el tipo (content-type) de los archivos que se cargarán, estableciendo el parámetro "allowedTypes" del interceptor "fileUpload". Supongamos que para el Action anterior, solo queremos permitir que se carguen archivos de imagen en formato "png". Para lograr esto debemos configurar el interceptor "fileUpload" para nuestro Action de la siguiente forma:

Si usamos el archivo "struts.xml":

<action name="cargaArchivo" class="com.javatutoriales.struts2.formularios.actions.CargaArchivo">
    <interceptor-ref name="fileUpload">
        <param name="maximumSize">2097152</param>
        <param name="allowedTypes">image/png</param>
    </interceptor-ref> 
    <interceptor-ref name="defaultStack"/>

    <result>/carga/archivoCargado.jsp</result>
    <result name="input">/carga/formulario.jsp</result>

</action>


Si usamos anotaciones:


@InterceptorRefs(value =
{
    @InterceptorRef(value = "fileUpload", params =
    {
        "maximumSize", "2097152", "allowedTypes","image/png"
    }),
    @InterceptorRef(value = "defaultStack")
})


Al intentar subir cualquier archivo que no sea una imagen en formato png, obtendremos el siguiente error:



Podemos indicar varios formatos permitidos separando cada elemento con comas, por ejemplo:


"image/jpg, image/png, image/ico"


Para terminar de hablar de carga de archivos debemos saber cómo personalizar los mensajes de error que se generan al cargar archivos. Los mensajes que vimos anteriormente, aunque bastante claros tal vez no sean los que querremos que nuestros usuarios finales vean en la aplicación. Para personalizar estos mensajes tenemos tres llaves que nos permiten indicar los mensajes que queremos mostrar en cada caso. Estas llaves son:

  • struts.messages.error.uploading - Un error general que ocurre y que impide subir un archivo
  • struts.messages.error.file.too.large - Ocurre cuando un archivo cargado es más grande que el tamaño máximo especificado
  • struts.messages.error.content.type.not.allowed - Ocurre cuando el archivo cargado no es del tipo permitido (content-type) para la carga


Estas llaves debemos colocarlas en un archivo de propiedades que contendrá los mensajes de error de la aplicación. Para crear este archivo hacemos clic derecho sobre el nodo "Source Package" del panel de proyectos. En el menú contextual que aparece seleccionamos la opción "New -> Properties File..." (si no tienen esa opción en el menú, seleccionen la opción "Other..." y en la ventana que se abre seleccionen la categoría "Other" y el tipo de archivo "Properties File"):



Llamaremos a este archivo "struts-mensajes" (el IDE se encargará de colocar automáticamente la extensión .properties). Damos clic en el botón "Finish" y veremos aparecer en el editor nuestro archivo de propiedades. En este archivo colocaremos los textos que los usuarios verán en caso de que ocurra algún error, por ejemplo podemos poner:


struts.messages.error.file.too.large=El archivo proporcionado supera el tamaño máximo
struts.messages.error.content.type.not.allowed=El archivo proporcionado no es del tipo adecuado


Ya que tenemos definidos los mensajes, lo siguiente que debemos hacer es indicarle a Struts 2 dónde se localiza este archivo. Para ello (si, adivinaron) usamos una constante para indicar el nombre del archivo (el cual será buscado a partir del directorio raíz de los paquetes de la aplicación, o sea en el nodo "Source Packages". La constante que usamos es "struts.custom.i18n.resources", y como ya hemos visto, podemos colocarle en el archivo "struts.xml" de la sigueinte forma:


<constant name="struts.custom.i18n.resources" value="struts-mensajes" />


O como un parámetro de inicialización en el filtro de struts:


<filter>
    <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
    <init-param>
        <param-name>struts.custom.i18n.resources</param-name>
        <param-value>struts-mensajes</param-value>
    </init-param>
</filter>


Cuando volvamos a ejecutar la aplicación veremos los siguientes mensajes de error:



Esto ya está mejor, ya que podemos colocar un texto tan descriptivo como queramos, sin embargo ahora hemos perdido algo de información importante que los otros mensajes nos daban, como el nombre del archivo que estamos subiendo, y algunos parámetros particulares para cada error.

Afortunadamente una vez más Struts 2 nos da una solución a esto ya que dentro de los mensajes podemos colocar algo llamado "placeholders" o contenedores, que básicamente es un espacio en nuestro mensaje donde Struts 2 se encargará de colocar un parámetro. Los parámetros que proporciona cada tipo de error son:

    • Para "struts.messages.error.file.too.large":
    • 0 - Nombre del parámetro que causó el error (en nuestro ejemplo "archivo")
    • 1 - Nombre del archivo que causó el error
    • 2 - Nombre del archivo en el servidor (el archivo temporal creado por el interceptor)
    • 3 - Tamaño del archivo cargado
    • Para "struts.messages.error.content.type.not.allowed":
    • 0 - Nombre del parámetro que causó el error (en nuestro ejemplo "archivo")
    • 1 - Nombre del archivo que causó el error
    • 2 - Nombre del archivo en el servidor (el archivo temporal creado por el interceptor)
    • 3 - Content-type del archivo cargado


Como podemos ver, en ambos casos los parámetros son prácticamente los mismos.

Podemos modificar los mensajes anteriores para que queden de la siguiente forma:


struts.messages.error.file.too.large=El archivo "{1}" supera el tamaño máximo permitido 2097152 bytes (2MB). El tamaño de su archivo es de {3} bytes
struts.messages.error.content.type.not.allowed=El archivo "{1}" no es de un tipo permitido (image/png). Su archivo es de tipo "{3}"


Con lo que obtendríamos los siguientes mensajes de error:



Que ya son mucho más claros para nuestros usuarios finales ^_^.

Como una nota final sobre la carga de archivos debemos decir que existe una constante más, "struts.multipart.saveDir", que podemos usar para indicar dónde queremos que se guarden los archivos temporales de las cargas que genera Struts 2. El uso de esta variable sale del propósito de este tutorial ^_^.

Ahora que hemos hablado bastante de la carga de archivos, pasemos al último tema del tutorial. Veamos como envías archivos desde nuestro servidor a los clientes usando Struts 2:


7. Descarga de Archivos

El poder enviar información binaria o archivos a los usuarios, aunque no es estrictamente parte del trabajo con formularios, es también una parte importante del desarrollo de aplicaciones web ya que tiene muchas aplicaciones prácticas como el enviar un reporte que generamos de forma dinámica, o un archivo que tengamos almacenado en algún lugar, como una base de datos o un directorio externo de la aplicación, o también mostrar imagenes que se hayan cargado o generado de forma dinámica en el sistema.

El realizar la descarga o envío de archivos es una tarea muy simple de hacer cuando trabajamos con Struts 2, ya que nos proporciona un tipo de resultado especial que permite enviar bytes directamente al "ServletOutputStream" del "HttpServletResponse".

Dedicaremos un tutorial completo a hablar de tipos de resultados, así que no entraré en muchos detalles de cómo funcionan estos, pero debemos saber que existen varios tipos de resultados, cada uno de los cuales altera de alguna forma la respuesta que enviamos al cliente. Podemos establecer algunos parámetros para indicar alguna información a nuestro result, entre otras cosas.

El result que nos ayudará en este caso es llamado "Stream Result", y básicamente es un result que permite enviar un flujo de bytes al cliente. Los parámetros que acepta este result son:

  • contentType: El tipo de contenido que se está enviando al usuario, por default es "text/plain"
  • contentLength: En número de bytes que se le enviarán al usuario (el navegador lo usa para mostrar de forma correcta una barra de progreso).
  • contentDisposition: Establece la cabecera "content disposition" que especifica el nombre del archivo, por lo regular ponemos algo como "attachment;filename="documento.pdf"" que muestra un cuadro de dialogo para guardar el documento, su valor por default es "inline" que nos muestra el documento en el mismo navegador
  • inputName: El nombre del "InputStream" que tiene los bytes del archivo que enviaremos al usuario (quedará más claro con el ejemplo), por default es "inputStream"
  • bufferSize: El tamaño del buffer que se usa para del flujo de entrada al de salida, por default es "1024"
  • allowCaching: Indica si el archivo debe ser guardado en caché por el cliente, por default es "true"
  • contentCharSet: El tipo de caracteres que tiene el contenido que enviamos al archivo, no tiene valor por default y si no lo establecemos no pasa nada ^_^.


Todos los atributos anteriores son opcionales, y por lo regular solo establecemos dos o tres.

Veremos tres ejemplos de este tipo de result.

Lo primero que haremos es crear un nuevo directorio, dentro de las páginas, llamado "descarga":



Para el primer ejemplo tomaremos un archivo que se encuentra en un directorio de nuestra aplicación y se lo enviaremos al usuario. Pueden tomar cualquier archivo que les guste, de preferencia alguno que puedan ver en el navegador como unPDF. Yo tomaré un archivo al azar de mi máquina y lo colocaré en el directorio raíz de la aplicación web, o sea en el directorio "web" de la aplicación que estamos desarrollando:



Abriré este archivo solo para ver el contenido y que cuando lo descarguemos podamos ver que es el mismo:



Ahora crearemos una nueva clase llamada "MuestraArchivo", en el paquete "actions". Esta clase extenderá de "ActionSupport":


public class MuestraArchivo extends ActionSupport
{
}


Lo primero que debemos hacer es colocar en nuestro Action un atributo de tipo "java.io.InputStream". Esta variable contendrá los bytes del archivo que se le enviará al cliente. Existen muchas formas de obtener una referencia a un "InputStream" de manera que solo tengamos que indicar la fuente donde está el archivo, y alguna clase especial se encargue del resto. En el peor de los casos tendremos que obtener estos bytes uno a uno y colocarlos en un objeto especial, pero tampoco es tan complicado; nosotros usaremos las dos formas en distintos ejemplos.

Si le damos a esta variable el nombre de "inputStream" no tendremos que declarar nada más en la configuración del result. Como es lo más fácil para el momento, lo haremos así y agregaremos el correspondiente getter de la propiedad:


public class MuestraArchivo extends ActionSupport
{
    private InputStream inputStream;

    public InputStream getInputStream()
    {
        return inputStream;
    }
}


Ahora sobre-escribiremos el método "execute" para poder obtener un flujo de lectura ("InputStream") a un archivo, usando una clase especial, un "FileInputStream". Para obtener este flujo lo único que debemos saber es la ubicación del archivo. Aquí hay que hacer un paréntesis para explicar un poco cómo se obtienen las rutas dentro de nuestra aplicación cuando estamos trabajando con aplicaciones web.

Dentro de la especificación de Servlets y JSPs se indica que cuando se hace la instalación o deploy de una aplicación web en un servidor (más específicamente en un contenedor de Servlets y JSPs) este debe quedar en un directorio de la máquina donde está instalado el servidor. ¿En cuál directorio?... esa es la cuestión interesante.

En la especificación no se indica dónde debe quedar cada aplicación que despleguemos, así que una vez instalada la aplicación, no podemos estar seguros de en dónde se encuentra físicamente dentro del servidor, por lo tanto si quisiéramos simplemente leer un archivo de nuestra aplicación no podríamos hacerlo ya que no sabríamos que ruta indicar; y todo esto en nuestro servidor local, si hiciéramos la instalación en un servidor remoto, rentado, o del cliente, sería aún peor ya que tal vez ni siquiera sabríamos el sistema operativo del mismo.

Para remediar esto, la especificación también proporciona una forma de obtener la ruta a cualquier recurso de nuestra aplicación. Para hacer esto debemos obtener una referencia al "ServletContext" de la aplicación y luego usar su método "getRealPath" indicando el recurso que del cual queremos obtener la ruta, iniciando desde la raíz de la aplicación (lo que vendría siendo el directorio "web" en NetBeans).

Ahora bien, como estamos usando un framework que se encarga de ocultar algunos detalles de la implementación web de la aplicación (como todo buen framework), no podemos obtener de una forma directa el "ServletContext" ya que no tenemos ninguna referencia a la interface de Servlets dentro de nuestros Actions.

Para remediar esto, Struts 2 nos proporciona una clase auxiliar que ayuda a obtener la referencia al "ServletContext" de una forma sencilla (de hecho también podemos obtener la referencia al "HttpServletRequest" y al "HttpServletResponse"). Esta clase es "ServletActionContext", y tiene una serie de métodos estáticos que permiten obtener referencias a los objetos anteriores.

Después de toda esa explicación, espero que quede claro el siguiente paso dentro de nuestro ejemplo.

Ya tenemos declarada una instancia de "InputStream" y hemos colocado un archivo .pdf, llamado "Ingenieria de software", en la raíz de la aplicación web, ahora crearemos un flujo de entrada para leer ese archivo y así enviarlo al cliente. Hacerlo en realidad es más fácil que decirlo, ya que Java proporciona una clase llamada "FileInputStream" que hace todo esto de forma automática, lo único que debemos hacer es indicarle la ruta en la que está almacenado nuestro archivo, de la siguiente forma:


@Override
public String execute() throws Exception
{
    String ruta = ServletActionContext.getServletContext().getRealPath("/Ingenieria de software.pdf");

    inputStream = new FileInputStream(ruta);

    return SUCCESS;
}


Esto es todo lo que debemos hacer para poder enviar el archivo al usuario, el framework se encargará de hacer el resto.

El paso final en este Action es colocar las anotaciones, que en este momento ya debemos conocer de memoria, para indicarle a Struts 2 que debe tratar esta clase como un Action. Primero que nada indicamos el namespace en el que se colocará al Action, y se le dará un nombre al mismo:


@Namespace(value = "/descarga")
@Action(value = "muestraArchivo)
public class MuestraArchivo extends ActionSupport


Lo siguiente es indicar el resultado de la ejecución de esté Action, la respuesta que será enviada al cliente, para lo cual usamos el atributo "results" y la anotación "@Result". En esta ocasión, como estamos usando un tipo distinto de result, debemos indicarlo dentro de la anotación usando su atributo "type". En este caso debemos indicar que el result es de tipo "stream" o flujo de bytes, de la siguiente forma:


@Namespace(value = "/descarga")
@Action(value = "muestraArchivo", results =
{
    @Result(type = "stream")
})
public class MuestraArchivo extends ActionSupport


El último paso es establecer alguno de los parámetros en el result, para eso usamos el atributo "params" de esta anotación. Este atributo recibe como argumento un arreglo de cadenas, donde los elementos nones representan el nombre del argumento que se quiere establecer, y los pares representan el valor de dicho argumento. En este ejemplo solo usaré el atributo "contentType" para indicar que el tipo de archivo que regresaré al usuario es un archivo pdf ("application/pdf"). Si están usando un archivo de algún otro tipo, una búsqueda rápida en Google les dará el tipo de contenido de su archivo:


@Namespace(value = "/descarga")
@Action(value = "muestraArchivo", results =
{
    @Result(type = "stream", params =
    {
        "contentType", "application/pdf"
    })
})
public class MuestraArchivo extends ActionSupport


Nuestra clase "MuestraArchivo" queda de la siguiente forma:


@Namespace(value = "/descarga")
@Action(value = "muestraArchivo", results =
{
    @Result(type = "stream", params =
    {
        "contentType", "application/pdf"
    })
})
public class MuestraArchivo extends ActionSupport
{
    private InputStream inputStream;

    @Override
    public String execute() throws Exception
    {
        String ruta = ServletActionContext.getServletContext().getRealPath("/Ingenieria de software.pdf");

        inputStream = new FileInputStream(ruta);

        return SUCCESS;
    }

    public InputStream getInputStream()
    {
        return inputStream;
    }
}


Si estuviéramos trabajando con archivos de configuración, el Action quedaría de la siguiente forma:


<action name="muestraArchivo" class="com.javatutoriales.struts2.formularios.actions.MuestraArchivo ">
    <result type="stream">
        <param name="contentType">application/pdf</param>
    </result>
</action>


Para este tipo de result, hablando estrictamente, no se necesita una página para mostrarlo, basta con colocar el nombre del Action en la barra de direcciones del navegador; nosotros crearemos una solo para ver cómo podemos invocarlos. Dentro del directorio "descarga" de las páginas web del proyecto creamos una nueva JSP llamada "archivo". Dentro de esta JSP, indicamos que haremos uso de la biblioteca de etiquetas de Struts 2, con la directiva "taglib" correspondiente:


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


Ahora usaremos la etiqueta "<s:a>" para crear un enlace a nuestro Action. Esta etiqueta es smuy fácil de utilizar, solo hay que indicar el nombre del Action, en su atributo "action", su namespace, en su atributo "namespace", junto con el texto que tendrá el enlace, de la siguiente forma:


<s:a action="muestraArchivo" namespace="/descarga">Ver archivo</s:a>


Ahora que está todo listo, ejecutamos nuestra aplicación e ingresamos a la siguiente dirección:


http://localhost:8080/formularios/descarga/archivo.jsp


Con lo que debemos ver la siguiente página:



Lo único que tiene esta página es un enlace a nuestro Action, cuando entramos en la misma deberemos ver el siguiente resultado:



Como podemos ver, el documento se ha incrustado en nuestro navegador, por lo que nuestro primer ejemplo ha funcionado de forma correcta ^_^.

En el ejemplo anterior, el archivo que se envío al usuario se mostró dentro del mismo navegador. Esto nos sirve con ciertos tipos de archivos y bajo ciertas circunstancias, pero no siempre querremos que el usuario vea los archivos en su navegador, algunas veces querremos que sea forzoso que los descargue a su computadora.

En nuestro segundo ejemplo veremos cómo hacer este cambio. Afortunadamente es algo muy sencillo de hacer, basta con agregar otro parámetro, "contentDisposition", a nuestro result. "contentDisposition", como pueden ver en la lista anterior de parámetros, especifica cómo será enviado este archivo al cliente, si "inline" (para mostrarse en el navegador) o "attachment" (para que el archivo sea descargado en la máquina del cliente). Ambas opciones nos permiten indicar un "filename" que es el nombre que tendrá el archivo al ser enviado.

Como el código del Action que se necesita para hacer este ejemplo, es muy parecido al anterior, crearé una nueva clase llamada "DescargaArchivo" y copiaré el código anterior.

Agregaremos este parámetro a nuestro result, y como nombre del archivo colocaremos simplemente "tutorial.pdf":


@Namespace(value = "/descarga")
@Action(value = "descargaArchivo", results =
{
    @Result(type = "stream", params =
    {
        "contentType", "application/pdf",    
        "contentDisposition","attachment;filename=\"tutorial.pdf\""
    })
})


Con archivos XML quedaría de la siguiente forma:


<result type="stream">
    <param name="contentType">application/pdf</param>
    <param name="contentDisposition">attachment;filename="tutorial.pdf"</param>
</result>


Agregamos la liga correspondiente a este nuevo Action en la página "archivo.jsp":


<s:a action="descargaArchivo" namespace="/descarga">Descargar archivo</s:a>


Con este simple cambio, al volver a ejecutar nuestra aplicación y entrar en nuestro Action, ahora en lugar de ver el documento en pantalla veremos el cuadro de diálogo del navegador que nos pregunta qué queremos hacer con el archivo:



Podemos ver en la imagen anterior, que el nombre que tendrá el archivo es el mismo nombre que estamos regresando como el "filename". En esta ocasión hemos puesto el nombre de forma estática, pero esta no será siempre la forma en la que querremos indicar el nombre del archivo, en la mayoría de las ocasiones este nombre deberá ser generado de forma dinámica. Para lograr esto, todas las propiedades de todos los results de Struts 2 permiten establecer sus valores usando expresiones, esto es colocar una marca que indique que esa propiedad deberá ser leída de algún getter de la clase.

En este caso indicaremos que el nombre del archivo deberá ser obtenido usando el método "getNombreArchivo" de la clase:


@Result(type = "stream", params =
{
    "contentType", "application/pdf",
    "contentDisposition","attachment;filename=\"${nombreArchivo}\""
})


Podemos ver que solo colocamos "nombreArchivo" entre los signos "${" y "}" (que indican que lo que esté contenido entre ambos es una expresión) y el framework automáticamente sabrá que debe buscar un getter.

Con archivos XML queda exactamente igual:


<result type="stream">
    <param name="contentType">application/pdf</param>
    <param name="contentDisposition">attachment;filename="${nombreArchivo}"</param>
</result>


En este caso, nuestro método "getNombreArchivo" regresará una cadena estática, pero en una aplicación real esta podría ser generada de forma dinámica:


public String getNombreArchivo()
{
    return "documento.pdf";
}


Ejecutamos nuevamente la aplicación y podremos ver que ahora el nombre del archivo está siendo efectivamente leído a través del getter correspondiente:



Hasta ahora nuestra descarga parece realizarse de forma correcta, pero si prestamos un poco de atención al cuadro de descarga podremos ver que hay algo que no nos agrada mucho:



¿Lo han notado? Y no, no me refiero a mi velocidad de descarga (aunque a mí no me agrada mucho que digamos ^^). El cuadro de descarga nos dice que no conoce el tamaño del archivo que estamos enviando al cliente, solo sabe la cantidad de información que ya ha descargado. Esto es muy molesto para los clientes, porque no saben cuánto tiempo deberán esperar para descargar su archivo.

Remediemos este pequeño error, para eso usaremos un parámetro más que nos proporciona el result "stream", "contentLength". Este parámetro, como su nombre lo indica, permite especificar cuál es el tamaño del archivo que estamos enviando, para que el navegador pueda colocar la barra de progreso de descarga del archivo de forma correcta.

Para usar esta propiedad deberemos hacer unos pequeños cambios en nuestro código. Antes que nada debemos agregar un atributo, con su correspondiente getter, que contenga el número de bytes que pesa nuestro archivo:


private long bytesArchivo;

public long getBytesArchivo()
{
    return bytesArchivo;
}


Ahora debemos modificar el código del método "execute" para que obtenga el dato anterior, para eso haremos uso de otro de los constructores de la clase "FileInputStream" que hemos estado usando para leer el archivo. Este segundo constructor recibe, en vez de la ruta en la que se encuentra el archivo a leer, un objeto de tipo "File" que representa el archivo; por lo que ahora crearemos un objeto de tipo "File" y se lo pasaremos al constructor de "FileInputStream":


@Override
public String execute() throws Exception
{
    String ruta = ServletActionContext.getServletContext().getRealPath("/Ingenieria de software.pdf");

    File archivo = new File(ruta);
        
    inputStream = new FileInputStream(archivo);

    return SUCCESS;
}


¿Por qué hemos hecho esto? Bueno, porque la clase "File" contiene un método que nos permite obtener justamente el tamaño del archivo, de la siguiente forma:


@Override
public String execute() throws Exception
{
    String ruta = ServletActionContext.getServletContext().getRealPath("/Ingenieria de software.pdf");

    File archivo = new File(ruta);
    bytesArchivo = archivo.length();
        
    inputStream = new FileInputStream(archivo);

    return SUCCESS;
}


Con esto hemos terminado con las modificaciones al método "execute" y lo único que falta es agregar el parámetro correspondiente a nuestro result, para esto nuevamente haremos uso de una expresión:


@Result(type = "stream", params =
{
    "contentType", "application/pdf",
           "contentDisposition","attachment;filename=\"${nombreArchivo}\"",
    "contentLength", "${bytesArchivo}"
})


Con archivos XML queda de la siguiente forma:


<result type="stream">
    <param name="contentType">application/pdf</param>
    <param name="contentDisposition">attachment;filename="${nombreArchivo}"</param>
    <param name="contentLength">${bytesArchivo}</param>
</result>


Y esto es todo. Ahora cuando volvamos a intentar descargar nuestro archivo obtendremos el siguiente dialogo de descargar:



En esta ocasión se indica el tamaño del archivo y el tiempo que hace falta para concluir con la descarga, por lo que nuestro segundo ejemplo ha funcionado correctamente ^^.

Ahora pasaremos a ver el tercer y último ejemplo. En este veremos un uso aún más común del result "stream": enviar imágenes del servidor al navegador y mostrárselas al usuario.

Para este último ejemplo del tutorial crearemos una nueva clase llamada "GeneradorImagenes", dentro del paquete "actions", esta clase extenderá de "ActionSupport":


public class GeneradorImagen extends ActionSupport
{
}


Como regresaremos un flujo de bytes al usuario, debemos declarar una variable de tipo "InputStream" con su correspondiente getter. El identificador de esta variable puede ser cualquiera que queramos, en mi caso lo llamaré "imagenDinamica":


private InputStream imagenDinamica;

public InputStream getImagenDinamica()
{
    return imagenDinamica;
}


Ahora viene la parte interesante, generaremos la imagen de manera dinámica y la guardaremos de forma que al ser enviada al navegador este pueda entenderla para mostrarla.

Sobre-escribimos el método "execute" e indicamos cuál será el alto y el ancho de nuestra imagen:


@Override
public String execute() throws Exception
{
    final int ANCHO_IMAGEN = 260;
    final int ALTO_IMAGEN = 130;
}


El hecho de que las variables estén marcadas como final es solo para asegurarme de que su valor no cambie durante la ejecución del método.

Ahora crearemos un objeto que nos permita manipular de una forma sencilla imágenes generadas por nosotros mismos. Para esto haremos uso de la clase "BufferedImage". Esta clase recibe en su constructor el ancho y alto de la imagen, además de indicar el tipo de la imagen que será creada, en nuestro caso será una sencilla imagen en RGB:


BufferedImage imagen = new BufferedImage(ANCHO, ALTO, BufferedImage.TYPE_INT_RGB);


Existen muchas formas de establecer los colores de cada uno de los pixeles de una "BufferedImage": podemos obtener un objeto "Graphics2D" usando su método "createGraphics()" y posteriormente dibujar sobre este usando el API 2D de Java. Aunque la forma más rápida para este ejemplo es usar directamente su método "setRGB". A este método se le indica la posición del pixel que queremos establecer, en coordenadas "x,y", y el color del pixel en exadecimal, o sea "0xFF0000" para el rojo, "0x00FF00" para el verde, y "0x0000FF" para el azul.

Crearemos dos ciclos, uno para recorrer todos las filas de la imagen, y otro para recorrer todas sus columnas. Dentro del ciclo más anidado estableceremos el color del pixel apropiado usando una instancia de la clase "Color" que recibe el valor correspondiente al rojo, verde, azul, y la transparencia del color. Los valores deben están dentro del un rango de 0 a 255, siendo 0 el tono más obscuro (negro) y 255 el más claro (blanco). Si el color se sale del rango la imagen no se mostrará, por lo que usaremos el operador modulo "%" para hacer que los valores se queden en este rango. Como el valor del pixel debe ser pasado como un entero, usaremos al final el método "getRGB()" de "Color" para obtener el valor correspondiente al color que estamos estableciendo:


for(int alto = 0; alto < ALTO_IMAGEN; alto++)
{
    for(int ancho = 0; ancho < ANCHO_IMAGEN; ancho++)
    {
        imagen.setRGB(ancho, alto, new Color((ancho*alto)%255, (ancho*alto)%255, (ancho*alto)%255, 255).getRGB());
    }
}


También pudimos haber hecho algo más sencillo como:


imagen.setRGB(ancho, alto, ancho*alto);


Pero no se obtiene el mismo resultado, y es un poco menos claro lo que está pasando ^_^.

Nuestra imagen ya tiene color, ahora debemos transformarla a un formato que pueda ser entendido por un navegador web. Para esto haremos uso de la clase "ImageIO", la cual tiene un método estático, "write", que permite convertir las imágenes en formatos "png", "jpg", "gif" y "bmp". Este método recibe como parámetros la imagen que queremos dibujar, el formato en el queremos que quede la imagen, y un "OutputStream" en donde quedarán los bytes de la imagen.

Para el ultimo parámetro usaremos un tipo de "OutputStream" que nos permita recuperar los bytes de esa imagen de una forma simple, por lo que usaramos un objeto de tipo "ByteArrayOutputStream":


ByteArrayOutputStream bytesImagen = new ByteArrayOutputStream();
ImageIO.write(imagen, "png", bytesImagen);


El último paso es crear el "InputStream" desde el cual Struts 2 leerá nuestra imagen. La imagen ya está en un arreglo de bytes, el del objeto de tipo "ByteArrayOutputStream", por lo que usaremos un objeto que nos permita usar este arreglo de bytes para crear un "InputStream". Usaremos un objeto de tipo "ByteArrayInputStream" que en su constructor recibe un arreglo de bytes, en este caso el arreglo de bytes que representa nuestra imagen:


imagenDinamica = new ByteArrayInputStream(bytesImagen.toByteArray());


Eso es todo. Nuestro método "execute" queda de la siguiente forma:

public String execute() throws Exception
{
    final int ANCHO_IMAGEN = 260;
    final int ALTO_IMAGEN = 130;
        
    BufferedImage imagen = new BufferedImage(ANCHO_IMAGEN, ALTO_IMAGEN, BufferedImage.TYPE_INT_RGB);
        
    for(int alto = 0; alto < ALTO_IMAGEN; alto++)
    {
        for(int ancho = 0; ancho < ANCHO_IMAGEN; ancho++)
        {
            imagen.setRGB(ancho, alto, new Color((ancho*alto)%255, (ancho*alto)%255, (ancho*alto)%255, 255).getRGB());
        }
    }
        
    ByteArrayOutputStream bytesImagen = new ByteArrayOutputStream();
    ImageIO.write(imagen, "png", bytesImagen);
    
    imagenDinamica = new ByteArrayInputStream(bytesImagen.toByteArray());
        
    return SUCCESS;
}


Como ven, es más sencillo de lo que parecía ^_^.

Para terminar con nuestro Action debemos colocar las anotaciones que indican que esta clase debe ser tratada como un Action de Struts 2. Casi todas ya las hemos visto hasta el cansancio, por lo que solo comentaré que nuestro result debe ser de tipo "stream" y que le estableceremos dos parámetros: "inputName" que indica el nombre de la propiedad de tipo "InputStream" que contiene los bytes que serán regresados al usuario, en nuestro caso esta es "imagenDinamica".

El otro parámetro que debemos establecer es el "contentType" que indica el formato de nuestra imagen, en este caso "image/png". También podríamos indicar el tamaño de la imagen usando el parámetro "contentLength", pero esto queda como ejercicio para el lector ^_^!.


@Namespace(value = "/descarga")
@Action(value = "imagenGenerada", results =
{
    @Result(type = "stream", params =
    {
        "inputName", "imagenDinamica",
        "contentType", "image/png"
    })
})


Con archivos XML quedaría de la siguiente forma:

<result type="stream">
    <param name="inputName">imagenDinamica</param>
    <param name="contentType">application/pdf</param>
</result>


La clase "GeneradorImagen" queda finalmente de la siguiente forma:

@Namespace(value = "/descarga")
@Action(value = "imagenGenerada", results =
{
    @Result(type = "stream", params =
    {
        "inputName", "imagenDinamica",
        "contentType", "image/png"
    })
})
public class GeneradorImagen extends ActionSupport
{
    private InputStream imagenDinamica;
    
    @Override
    public String execute() throws Exception
    {
        final int ANCHO_IMAGEN = 260;
        final int ALTO_IMAGEN = 130;
        
        BufferedImage imagen = new BufferedImage(ANCHO_IMAGEN, ALTO_IMAGEN, BufferedImage.TYPE_INT_RGB);
        
        for(int alto = 0; alto < ALTO_IMAGEN; alto++)
        {
            for(int ancho = 0; ancho < ANCHO_IMAGEN; ancho++)
            {
                imagen.setRGB(ancho, alto, new Color((ancho*alto)%255, (ancho*alto)%255, (ancho*alto)%255, 255).getRGB());
            }
        }
        
        ByteArrayOutputStream bytesImagen = new ByteArrayOutputStream();
        ImageIO.write(imagen, "png", bytesImagen);
    
        imagenDinamica = new ByteArrayInputStream(bytesImagen.toByteArray());
        
        return SUCCESS;
    }

    public InputStream getImagenDinamica()
    {
        return imagenDinamica;
    }  
}


El último paso consiste en indicar en una JSP que debe mostrar una imagen usando este Action. Para eso usaremos la etiqueta "<img>" de HTML. Para no crear una nueva JSP colocaremos esta etiqueta en la página "archivo.jsp" que hemos estado usando. En esta etiqueta debemos indicar que la fuente (el source) de la imagen en nuestro Action, de la siguiente forma:


<img src="imagenGenerada.action" alt="imagen" />


Así de simple. Ahora si volvemos a ejecutar nuestra aplicaciones, debemos ver la siguiente salida



No es la mejor imagen del mundo, pero es dinámica y es nuestra ^_^

Con esto damos fin a este largo tutorial en el que hemos visto todo lo que debemos saber para el trabajo de formularios con Struts 2.

Cualquier duda, comentario, sugerencia, aclaración o corrección pueden dejar un comentario o enviar un correo a "programadorjavablog@gmail.com".

Saludos y gracias.

Descarga los archivos de este tutorial desde aquí:



Entradas Relacionadas: