Construir Aplicaciones Web con WebWork2

Puedes encontrar la versión original de este tutorial en Inglés en:

http://theserverside.com/

Este artículo te llevará a través del desarrollo de la aplicación wafer weblog utilizando la versión WebWork 2.x y cubrirá muchas de las características básicas de WW2.

Wafer es un proyecto de investigación que compara muchos de los marcos de trabajo de aplicaciones web de código abierto que están disponibles utilizando una aplicación de ejemplo común. Este proyecto de investigación está diseñado para comparar los marcos de trabajo de aplicaciones a nivel de campo especificando una aplicación de ejemplo para que las características de la aplicación sean irrelevantes y los méritos de cada marco de trabajo sean realmente el foco.

Introducción

Empecemos configurando nuestro proyecto. Tan fácil como suena, algunas veces no es tan simple. Sabes qué ficheros necesitas, la estructura de directorios correcta y qué modificaciones necesita Ant (que siempre son un reto cuando empiezas con un nuevo marco de trabajo). Desafortunadamente WW2 (WebWork2) no proporciona mucha ayuda con esto pero ningún otro marco de trabajo lo hace, por eso la forma más fácil es crear una estructura típica de J2EE, como la puedes encontrar en weblog application.

Mientras escribo este artículo se ha liberado una nueva herramienta para realizar esto, llamada megg. Esta herramienta crea un proyecto WW2 con casi todo lo que se necesita para empezar, como una completa estructura de directorios para src, config, jsp, html, lib y más. Incluso va más allá, al crear una sencilla aplicación HelloWorld con un test JUnit y tiene un fichero de construcción Ant completamente funcional ya adaptado con el nombre de tu aplicación web!.

Configuración

El primer lugar para empezar para aquellos familiarizados con Struts sería la parte de configuración. Al igual que otros marcos de trabajo, WW2 tiene un fichero XML para controlar las acciones y salidas. Abajo tienes una porción del fichero xwork.xml de la aplicación wafer:

<action name="RegisterAction" 
class="com.flyingbuttress.wafer.actions.RegisterAction">
            <result name="input" type="dispatcher">
                <param name="location">/register.jsp</param>
            </result>
            <result name="success" type="chain">
                <param name="actionName">LoginAction</param>
            </result>
            
            <interceptor-ref name="defaultStack"/>
            <interceptor-ref name="validation"/>
            <interceptor-ref name="workflow"/>             
   </action>

El formato está bastante claro. El nombre de la action es el nombre de referencia para esa acción que es cómo el nombre que usaremos en nuestra URL/HTML como /HelloWorld.do. El result define que tipo de vista debería reenviar esta acción y tiene un nombre y un tipo. El nombre (input, success, error, etc.) necesita mapearse con un valor de retorno de la Action y el campo type define qué tipo de View va a ser está acción, abajo puedes ver el listado actual de tipos:

  • Dispatcher - enrutado JSP.
  • Redirect - reenvío redireccionado.
  • Velocity - para una vista de plantilla de velocity.
  • Chain - para enrutar a otra clase Action.

Si necesitas crear un tipo de resultado único como una salida Excel, puedes crear fácilmente tu propio tipo de resultado implementando el interface Result, añadiendo el código de implementación y añadiendo el resultado a la lista de resultados posibles en el fichero webwork-default.xml.

Interceptores

La siguiente pieza lógica a discutir son las clases Action; sin embargo, es casi imposible continuar sin explicar el concepto de los Interceptores. La mayoría del trabajo que hace WW2 lo hace mediante Interceptores. Personalmente encuentro que la implementación de los interceptores es uno de los factores únicos que diferencia a WW2 del resto de marcos de trabajo. En la superficie, los interceptores son como filtros. Otros los han comparado con "Practical AOP". A pesar del nombre, la característica es muy buena.

Se puede llamar a los interceptores antes de llamar a la acción, después de llamarla o antes y después. Tienen acceso total a la clase Action y al entorno de ejecución permitiendote llamar a los métodos de la clase Action o trabajar con su entorno. Puedes ver un gran de ejemplo en el TimerInterceptor. Antes de llamar a la acción toma un timestamp, luego, después de completar la acción obtiene otro timestamp y calcula el tiempo que tardó en ejecutarla.

Otra característica de los interceptores es la facilidad con la que podemos configurar la pila de interceptores. Cada <action> del fichero xwork.xml puede tener una o muchas etiquetas <interceptor-ref> asociadas con esa acción. Una práctica mejor es referenciar una pila de interceptores, donde podríamos querer llamar a algo más que unos pocos interceptores al igual que se hace en "defaultStack" en la aplicación wafer. CUIDADO: la ordenación de la pila es muy importante; se llaman en el orden en que son definidos, por eso si un interceptor está relacionado con otro que ya ha sido llamado lo mejor es tener este interceptor encima en la pila! Para una completa lista de interceptores puedes ver el fichero webwork-default.xml.

Struts también tiene interceptores, como http://struts.sourceforge.net/saif/index.html; sin embargo, muchos consideran que se pasa de conceptual. En general, parece que proporciona funcionalidades similares pero tiene mucho que recorrer antes de estar listo para su consumo masivo y podría tardar en integrarse en la construcción principal de Struts. Crear tu Propio Interceptor

Basta de charla, creemos uno! Todos los interceptores deben implementar el interface Interceptor que básicamente tiene 3 métodos, init, destroy e intercept. Para nuestra necesidades hemos extendido AroundInterceptor que nos permite llamar a un método antes y después. Creemos un Interceptor que requiera que el usuario se identifique para ejecutar una Action. Hará esto chequeando si el objeto user está en la sesión antes de se ejecute Action si no se encuentra el usuario se envía a la página de login.

public class AuthorizeInterceptor extends AroundInterceptor {
	private static final Log log = LogFactory.getLog(LoggingInterceptor.class);
	private boolean loggedIn = false;

	protected void before(ActionInvocation invocation) throws Exception
	{
		User u = null; 
		ActionContext ctx = ActionContext.getContext();
		Map session = ctx.getSession();
		u = (User)session.get("user");
		
		if(u == null)
		{
			log.info("User not logged in");
			loggedIn = false;
		}
		else
			loggedIn = true;
	}
	
	protected void after(ActionInvocation invocation, 
          String result) throws Exception
	{
		log.info("After");
	}
	
	public String intercept(ActionInvocation invocation) throws Exception {
		
		before(invocation);
		if(loggedIn == false)
			return Action.LOGIN;    // send em back to the login screen
		else
		{
			String result = invocation.invoke();
			after(invocation, result);
			
			return result;
		}	
	}
}

Se llama al método before antes de llamar al método intercept permitiéndonos comprobar si el usuario se ha identificado en esta sesión y si no es así devuelve el valor global de login, "login" que está definido en el <global-results> del fichero xwork.xml. Luego tenemos que añadir una definición de interceptor a nuestro fichero xwork.xml:

<interceptors>
<!-- custom created Interceptor for checking if a user has already logged -->
	<interceptor name="login"
        	class="com.flyingbuttress.wafer.interceptor.AuthorizeInterceptor"/>	
</interceptors>

Ahora ya tenemos un AuthorizationInterceptor. Para todas nuestras classes Action que queramos ocultar detrás de un login, sólo tenemos que referenciar este interceptor en la configuración de la acción, como se muestra abajo, y ya tenemos una Action de seguridad básica:

<action name="ShowCommentsAction" class="com.flyingbuttress.wafer.actions.ShowCommentsAction">
            <result name="success" type="dispatcher">
                <param name="location">/comments.jsp</param>
            </result>
            <interceptor-ref name="login"/>
            <interceptor-ref name="defaultStack"/>
</action>

También puedes crear un interceptor que llame a un método de tu clase Action antes o después de la sentencia execute de la misma forma que el interceptor "workflow" llama a validate() de la clase Action antes de ejecutarse. Podríamos decir que este concepto se podría aplicar como llamar al método init() de tu clase Action antes de llamar al método execute. Riesgos de los Interceptores

Me gustan los interceptores pero también son una de las partes más duras para operar con ellas satisfactoriamente. El problema es que algunos interceptores tienen dependencias de otros interceptores por eso debes asegurarte de tener asignados a tu clase Action los interceptores correctos y es crucial que estén en el orden correcto. Para mantener la simplicidad sólo hemos referenciado el interceptor de pila estándar llamado defaultStack. Todos los demás interceptores los he llamado por su nombre en la llamada <action/>. Para los más veteranos en WebWork esta es una forma demasiado complicada para llamar a inteceptores pero encuentro que puede ayudar a mantener la cosas claras.

Clases Action

Actions, Controllers, y Commands son piezas con las que tratan los desarrolladores. En el mundo WebWork se llaman Actions y hay básicamente dos tipos: "dirigidas al campo" y "dirigidas al modelo". Piensa en las acciones dirigidas al campo como si fuera el estilo Controller-as-Model; esta probablemente es la mejor opción para páginas con modelos muy pequeños. La mayoría de la aplicación wafer web log se hizo utilizando este modelo.

El tipo dirigido al modelo es donde el modelo es su propio POJO. Este estilo es mejor para grandes modelos y promueve una mejor reutilización del código.

Para definir una clase Action, sin importar los tipos mencionados arriba, tenemos que extender la clase ActionSupport o implementar el interface Action. En el desarrollo de la aplicación elegimos extender ActionSupport debido a todas las características de ayuda como el manejo de errores y el logging. Empecemos con una acción conducida por el campo, RegisterAction.java:

public class RegisterAction extends ActionSupport {
    String username, email, firstname, lastname, password;
    private User tempUser;

    public String getUserName() {
       return username;
    }

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

    public String getEmail() {
        return email;
    }

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

    public String getFirstName() {
        return firstname;
    }

    public void setFirstName(String firstname) {
        this.firstname = firstname;
    }

    public String getLastName() {
        return lastname;
    }

    public void setLastName(String lastname) {
        this.lastname = lastname;
    }

    public String getPassword() {
        return password;
    }

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

    public String execute() throws Exception {
		
        if (hasErrors())
            return INPUT;
        else {
            tempUser = WebLogSystem.getUserStore().create(this.username,this.password );
            tempUser.setFirstName(this.getFirstName());
            tempUser.setLastName(this.getLastName());
            tempUser.setEmail(this.getEmail());
            tempUser.save();
            return SUCCESS;
        }
    }
	
    /**
     * Do business logic validation by checking to see if the user entered is
     * already in the database.
     */
    public void validate() {
        LOG.info("Validating the registration page");
        try{
            if(WebLogSystem.getUserStore().verify(this.username)) {
                this.addFieldError("Username", "Someone already has that name");
            }
        }
        catch(Exception e) {
            e.printStackTrace();
        }
    }
}

El único método que tienes que implementar es execute(). Se llama a este método cada vez que se invoca a tu acción y tiene un valor de retorno String. Hay valores por defecto definidos en el Interface Action con valores como success, input, none y error y estos valores mapean directamente a los nombres de los campos result del fichero xwork.xml.

Validación

WW2 tiene tanto validación UI como de datos. La validación UI (Interface de Usuario) básicamente chequea para ver si los tipos y rangos de los valores de un campo son correctos. Por ejemplo, si un campo numérico es realmente un número o si una fecha es realmente una fecha válida, etc. La validación de datos es cuando necesitamos chequear si el valor dado es válido entre, digamos, una lista de posibles elecciones, quizás requiriendo una búsqueda en una base de datos. Este tipo de validación podría utilizarse en el código postal donde no es suficiente con que el número esté dentro de un rango dado, y tenemos que chequear una lista de códigos válidos para ver si realmente es bueno. Validación de Interface de Usuario

Este tipo de validación se hace en un fichero XML que se define en el mismo paquete que la clase Action y que debe llamarse <ActionClassName>-validation.xml. Abajo hay un ejemplo del RegisterAction-validation.xml:

<validators>
    <field name="userName">
        <field-validator type="requiredstring">
            <message>You must enter a value for username</message>
        </field-validator>
    </field>
    <field name="email">
        <field-validator type="requiredstring">
            <message>You must enter a value for email</message>
        </field-validator>
    </field>
    <field name="email">
        <field-validator type="email">
            <message>You must enter a valid email</message>
        </field-validator>
    </field>
    <field name="firstName">
        <field-validator type="requiredstring">
            <message>You must enter a value for first name</message>
        </field-validator>
    </field>
    <field name="lastName">
        <field-validator type="requiredstring">
            <message>You must enter a value for last name</message>
        </field-validator>
    </field>
    <field name="password">
        <field-validator type="requiredstring">
            <message>You must enter a value for password</message>
        </field-validator>
    </field>
</validators>

Aquí podemos ver una imagen de una validación fallida:

WW2 viene con muchos validadores de campo diferentes, como fecha, email, entero, string, etc. y todos los validadores están definidos en el fichero validator.xml; sin embargo, si tu tienes una necesidad única es muy fácil crear tu propio validador de campo implementando el interface Validator y añadiendo la referencia al fichero validator.xml. La validación de interface de usuario se llama con el interceptor validation, por eso para activar la validación también debes referenciar ese interceptor en la configuración de tu acción al igual que la clase RegisterAction en el fichero xwork.xml para la aplicación wafer weblog. Validación de Datos

La validación de datos implica que el programador escriba algo de código para chequear un escenario como "¿Es este un código postal válido? Déjame consultar la base de datos". Para hacer esto creamos en nuestra clase Action un método sin argumentos llamado validate y ponemos en él toda la funcionalidad que necesitemos. Luego implementamos el interface Validatable referenciando el interceptor workflow. Este interceptor llamará al método validate de la clase Action antes de que se ejecute la acción, permitiendonos añadir errores al contexto si es necesario.

Inversión de Control

IoC es un patrón de diseño que favorece el acoplamiento ligero entre clases y componentes. Actualmente, cuando nuestro código tiene algunas clases que dependen de otras para operar, sus dependencias están fuertemente acopladas con la clase. ¿Entonces, por qué es interesante IoC? Porque promueve un buen diseño separando las clases mediante interfaces e implementaciones y permite al contenedor manejar el ciclo de vida de nuestros componentes.

La mejor forma de describirlo es con un ejemplo. Digamos que nuestra compañia crea escalas para pesar humanos y aliens y que estas escalas se venderán en la Tierra, en Venus y en Marte. El problema es que la gravedad es diferente en estos planetas, por eso para poder asegurar que realmente conocen su peso verdadero en términos de libras terrestes, las escalas tendrán que ser flexibles para manejar esta necesidad. Los ingredientes para realizar el trabajo IoC son los siguientes:

  • components.xml (fichero de configuración IoC)
  • Scale.java (Interface para todos los componentes).
  • ScaleAware.java (Interface para la clase Action).
  • MarsScaleImpl.java (componente).
  • VenusScaleImpl.java (componente).
  • EarthScaleImpl.java (componente).
  • ScaleAction.java (clase Action).

Echemos un vistazo a component.xml:

<components>
    <component>
        <scope>application</scope>
        <class>com.flyingbuttress.scale.MarsScale</class>
        <enabler>com.flyingbuttress.scale.ScaleAware</enabler>
    </component>
</components>

Aquí hemos definido el ámbito del componente, la clase de implementación y el interface que notifica al contenedor que cualquier clase Action que implemente este interface tiene una dependencia de la clase de arriba. Veamos cómo se hace esto en la clase Action:

public class ScaleAction implements Action, ScaleAware
{
	private Scale scale;
	
	public void setScale(Scale scale)
	{
		this.scale = scale;
	}
	public String execute() throws Exception
	{
		System.out.println("The weight of you is:" + scale.getWeight());
		return SUCCESS;
	}
}

Ahora el contenedor ve que esto implementa ScaleAware; por lo tanto, llamará a setScale y lo pasará en la clase de implementación mediante el interface. Ahora todas aquellas escalas vendidas en Marte, lo único que necesitan hacer es poner la definición de la clase seleccionada como MarsScale en components.xml y los de la tierra como EarthScale. Hay muchas razones diferentes para hacer esto, además del comercio interestelar, pero la forma de implementación es la mima. Si se da la oportunidad todo el control de usuarios de la aplicación wafer weblog se podría hacer utilizando IoC.

La capacidad de IoC es interesante pero debes tener cuidado de no tratarla como una solución para un problema. IoC no funciona en todos los lados, por lo tanto utilizala apropiadamente, o podría ser un fracaso.

Trabajar con la Vista JSP

Las Etiquetas

La forma más común en la que la mayoría de los marcos de trabajo envían y reciben información de una página JSP es mediante un librería de etiquetas. Algunos usan JSTL mientras otros como Webwork tienen su propio conjunto de etiquetas. La mayoría de las páginas JSP de la aplicación wafer weblog utilizan etiquetas WW que encontramos muy útiles y fáciles de utilizar. Aquí tenemos un utilización sencilla de una etiqueta:

<ww:property value="user.firstName" /> 

o:

<ww:textfield label="First Name" name="firstName" ></ww:textfield>

El primer ejemplo llama al método getUser() de la clase Action que llamó a esta página, luego llama a getFirstName() sobre ese objeto. El segundo ejemplo creara una caja de entrada etiquetada como ‘First Name’ con name para la caja de entrada para firstName. Aunque parece que esta etiqueta no hace demasiado (cualquiera puede crear un sencilla etiqueta input de HTML) maneja los mensajes de error en línea que encuentro muy útiles (mira la página que hemos visto arriba). JSTL

Si tu eres una persona que adora los estándares puede utilizar JSTL. La siguiente etiqueta hará lo mismo que la primera que hemos visto en la sección anterior:

<c:out value=${user.firstName}/> 
Ognl y el OgnlValueStack

Ognl (Object Graphical Navigation Language) es parecido a JSTL excepto en que al contrario que JSTL que principalmente se utiliza para obtener cosas, Ognl también se puede utilizar para seleccionar cosas. Con Ognl podemos crear un mapa al vuelo como este:

<ww:select label="’Gender’" name="’gender’" list="#{‘true’ : ‘Male’, ‘false’ : ‘Female’}"/>

También podemos crear valores de salida para el ActionContext como:

<ww:property value="#name" />

donde name se configuró en la clase Action como:

ActionContext ctx = ActionContext.getContext();
ctx.put("name", otherUser.getUsername());

Ognl también se preocupa de una trivial y completa conversión de tipos. Por ejemplo, podemos pasarle en una caja de texto el valor "10/14/1971" y él lo convierte a un objeto Date utilizando un método accesor setDate como lo haríamos en la clase Action. Si tenemos la necesidad o el deseo podremos crear nuestros propios conversores de tipos para objetos personalizados.

Por último, el poder que Ognl proporciona a Xwork está en OgnlValueStack, que básicamente es una pila para almacenar valores almacenados en la petición. Si se utiliza con el interceptor parámetrizado podemos situar todos los parámetros de un formulario en la pila para recuperarlos posteriormente en el código. Esta es otra característica que actúa como un componente J2EE (HttpRequest) pero no es igual, lo que lo diferencia del API Servlet. Una forma bonita de utilizar OgnlValueStack es simplificando un gran clase Actión Controller-as-model. Digamos que tenemos una clase Action que mapea un formulario que tiene 30 parámetros. Esto significa que si estás utilizando el patrón Controller-as-Model tendrás como mínimo 30 métodos accesores para almacenar parámetros en tu clase. Pero con OgnlValueStack simplemente tendrás una llamada como ésta:

String bla = (String) stack.findValue("bla");

Quizás no sea tan limpio como los métodos accesores pero es una forma diferente para gente diferente.

Otras Características no Mencionadas en Detalle

Empaquetar Action

Esto permite empaquetar un conjunto de clases Action e incluir el fichero xwork.xml como un include en el fichero xwork.xml maestro. También podemos hacer lo mismo para las vistas Velocity, que si se utilizan juntas nos permiten descomponer tu aplicación para compartir más fácilmente las piezas de funcionalidad. Componentes UI y Componentes Personalizados

Webwork nos permite crear componentes de interface de usuario reutilizables y con pieles como esos calendarios que muchos weblogs tienen hoy en día. Espacios de Nombres y Paquetes

Podemos empaquetar las configuraciones de xwork.xml en paquetes que se pueden utilizar para extender otros paquetes, obtener acceso a todas las acciones, interceptores, etc. Añade espacios de nombres con el paquete y podrás crear alias para las acciones con diferentes nombres dandote la habilidad para tener que el RegisterAction.action en un espacio de nombres apunte a una clase diferente en otro espacio de nombres.

¿Por qué me gusta WebWork2?

Aquí tienes una lista de posibles razones por las que podrías estar interesado en utilizar WW2:

  • Se construye con interfaces en lugar de con clases concretas.
  • Te gustarán algunas de sus características no incluidas en los "otros" marcos de trabajo como IoC o los Interceptores.
  • Estás buscando un MVC que no está atado al entorno J2EE, haciendo más sencillas el testeo de unidades junto con otras tareas.
  • Muchas compañias están cambiando de Struts a Webwork como su estándar.
  • Bebes Pepsi en lugar de CocaCola. ;-)

Conclusión

Construir la aplicación Wafer weblog fue realmente un sueño con WebWork. Su utilización de interfaces e interceptores lo hace sencillo y flexible y hace díficil no soñar despierto con todas las formas en las que se podría trabajar con estos conceptos. La comunidad crece con lideres en lo que confiar. En mi artículo de Maverick mencioné que si vas a aprender un marco de trabajo quizás deberías elegir Struts en vez de Maverick. Esta vez, digo que deberías elegir WebWork.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP
SIGUIENTE ARTÍCULO