Tapestry 5, parte 5 - Sesiones, validaciones, AJAX

Una parte importante de cualquier aplicación web seria, es la manera en que se almacenan variables de sesión. Por ejemplo, una aplicación que requiere login y se crea una sesión para el usuario; se requiere validar el usuario/password y cuando es un usuario válido, guardar sus datos en la sesión y las páginas que solamente son para usuarios válidos necesitan redirigir al usuario al login para que solamente usuarios registrados puedan ver la página.

Otro ejemplo es un simple blog como éste, en el que solamente los usuarios registrados pueden dejar comentarios. Vamos ver este ejemplo. Primero, supongamos que ya tenemos la página que muestra el blog y los comentarios, y tenemos un componente para dejar un nuevo comentario pero solamente a los usuarios ya registrados.

Para empezar, podemos hacer una clase abstracta para las páginas que requieren de un usuario registrado, y ponerla (siguiendo los ejemplos anteriores) en el paquete org.ejemplo.base (porque no es una página realmente, solamente una superclase para ciertas páginas):

public class InnerPage {
  @SessionState
  private User user;
  private boolean userExists;

  public void setUsuario(User value) {
    user = value;
  }
  public User getUsuario() {
    return userExists ? user : null;
  }

}

Nuestra página de blog puede heredar de InnerPage y con ello puede manejar un usuario en la sesión. Las variables que se desean almacenar en sesión simplemente se deben marcar con la anotación @SessionState. Pero hay que tener cuidado; si nuestro código hace referencia a esa variable, Tapestry va a crear una instancia de la misma y nos la va a asignar. Esto puede ser muy cómodo en varias ocasiones pero puede ser un problema en este caso; por ello tenemos una bandera que se llama igual que nuestra variable de sesión pero con sufijo "Exists"; Tapestry detecta esto y la vuelve true cuando ya se ha asignado un valor (manual o automáticamente) a nuestra variable de sesión. La duración de la sesión se marca de manera normal en el web.xml.

Una cosa importante: en cualquier página o componente que tenga una variable tipo User marcada con la anotación @SessionState, Tapestry pondrá el valor apropiado; es decir si ya tenemos una página donde asignamos un usuario a esa variable, ese valor se está asociando con la sesión del usuario, y por lo tanto cualquier otra página que tenga una variable similar (aunque no herede de nuestra InnerPage) se le asignará el valor correspondiente con la sesión. Internamente Tapestry está manejando las sesiones y cuando una página necesita una variable de sesión se le asigna en el momento apropiado.

Ahora vamos a hacer el componente para dejar comentarios en el blog. Primero, el HTML:

<t:if test="usuario">

<t:form t:id="comments"> <t:errors/>
<t:label for="comment" />
<t:textarea t:id="comment" value="comment" validate="required,minLength=20,maxLength=500" cols="40" rows="5" />
<t:submit t:id="submitComment" value="Enviar" />
</t:form>

<p:else>

<t:form t:id="login"> <t:errors/>
<t:label for="username" />
<t:textfield t:id="username" value="uname" validate="required" maxlength="20" />
<t:label for="passwd" />
<t:textfield t:id="passwd" value="pass" maxlength="20" />
<t:submit t:id="login" value="Login" />
</t:form>

</p:else></t:if>

El elemento label se va a presentar como un texto con el nombre del elemento especificado. En el campo de comentario y de nombre de usuario estamos pidiendo validación required, que va a forzar al usuario a teclear un valor. Hay otras validaciones como regexp, minlength, maxlength, email, max, min (estos dos últimos para campos numéricos).

El elemento errors va a presentar todos los errores de validación cuando la forma se envia al server con errores (aunque Tapestry va a presentar mensajes de error en el campo apropiado al momento de dejar cada campo, usando JavaScript).

El código de nuestro componente es así:

public class Comment extends InnerPage { //InnerPage ya trae el manejo de usuario

  @Inject
  @Service("userDao")
  private UserDAO dao;
  @Property
  private String comment;

  @Property
  @Persist //esta anotacion es para que cuando reaparezca la página no se pierda este valor
  private String uname;
  @Property
  private String pass;

  @OnEvent(value="success", component="login") //Esto es equivalente a llamar el metodo onSuccessFromLogin
  void login() {
    //Solamente se va a invocar este metodo cuando el username tenga un valor, por las validaciones
    User u = dao.validateLogin(uname, pass);
    if (u != null) {
      setUsuario(u); //Con esto ya tendremos usuario cuando se haga el render de la página y ya aparecerá el campo para dejar el comentario
    }
  }

  @OnEvent(value="success", component="comments") //Equivalente a onSuccessFromComments (lleva el nombre de la forma)
  void comenta() {
    //Aqui se pasa la variable "comment" al manejador del blog para agregar el comentario; esta un poco fuera del alcance del ejemplo
  }

}

Nuevamente podemos ver que el código es bastante simple. Con las validaciones que Tapestry nos ofrece, ya no tenemos que verificar si el comentario fue demasiado corto o demasiado largo, o si teclearon un nombre de usuario en el login. El componente va a pedir un login cuando el usuario que lo ve no tiene una sesión activa, pero va a mostrar un campo para dejar comentarios a los usuarios que ya tengan una sesión activa. Al teclear usuario y password, si se pasan las validaciones necesarias, se invocará el método login, porque lo asociamos con ese evento usando la anotación @OnEvent y ahi se obtiene el usuario si es que puso bien sus datos, guardándolo en la variable de la sesión que definimos en la superclase y por lo tanto cuando se haga el render de la página, nuestro <t:if> ya va a evaluar "true" su prueba de que el usuario exista y presentará la forma de comentarios.

Las formas en Tapestry tienen un funcionamiento un poco peculiar pero bastante conveniente: cuando se hace el submit, se realizan las validaciones, se invoca al método asociado con el evento, y se envía una redirección de HTTP al cliente para que vuelva a pedir la página. Esto evita los típicos problemas que ocurren cuando el usuario le da "back" a su navegador; no se reenvían datos porque la página que ven como resultado del submit ya no fue directamente resultado del submit sino resultado de una redirección. Es por esto que si se tienen variables que se quieren conservar en este ciclo, se deben anotar con el @Persist, para que cuando se haga el render después de la redirección, todavía se tengan los valores requeridos.

Supongamos que ahora nos dicen que el componente no debe ocupar tanto espacio en la página. Hay usuarios que solamente leen el blog pero no quieren dejar comentarios, por lo tanto la forma de login o el campo para dejar comentarios no lo necesitan ver; solamente debería aparecer a quienes quieren dejar un comentario. Por lo tanto debería haber una liga que diga "Dejar un comentario" y al darle click ya debe aparecer la forma de login o el campo para dejar el comentario, todo esto sin necesidad de recargar la página completa. Pues con Tapestry es muy fácil realizar estos cambios a nuestro componente, solamente necesitamos poner nuestras formas dentro de una zona, poner una liga asociada con dicha zona, y tener acceso en el componente a dicha zona para mostrarla cuando sea necesario:

<t:actionlink t:id="comentar" zone="czone">Dejar un comentario</t:actionlink>
<t:zone t:id="czone" id="czone" t:visible="false">
  <!-- Y aqui dentro va lo que ya teniamos del componente -->
</t:zone>
public class Comment extends InnerPage {

  //esto va despues de las variables que ya teniamos
  @InjectComponent
  private Zone czone;

  Object onActionFromComentar {
    return czone.getBody();
  }

  //y aqui van los metodos que ya teniamos
}

Cuando usamos el atributo "zone" de una liga de Tapestry, estamos indicando que no debe cargar la página completa sino que es una petición AJAX, que debe hacer a nuestra página o componente para obtener un valor de retorno y desplegarlo en la zona correspondiente dentro de la página o componente. Concretamente, la liga de "comentar" causa un evento de acción en nuestro componente; se invoca onActionFromComentar y ahí simplemente devolvemos el contenido de la zona "czone", que inicialmente está marcada como invisible. Podríamos devolver otra cosa ahí, como un String con HTML y eso se despliega en esa zona; o algún otro componente de la página al cual tengamos acceso desde el método onActionFromComentar.

En casos de JavaScript más complejo, podemos tener métodos en nuestra página o componente que devuelvan estructuras de JSON; Tapestry ofrece manejo de JSON en su paquete org.apache.tapestry5.json, con las clases JSONArray, JSONLiteral y JSONObject y la interfaz JSONString. La página oficial de Tapestry ofrece más información respecto a JavaScript. El ejemplo del campo de texto con autocomplete ilustra lo sencillo que es usar AJAX dentro de Tapestry y el buen soporte que el framework ofrece, como la facilidad de distinguir si una petición es normal (página completa) o AJAX.

De hecho la última versión de Tapestry tiene una característica bastante buena de AJAX: Se pueden incluir varios scripts en la página y Tapestry los va a combinar todos para que solamente se tenga referencia a uno solo en la página que se devuelve al cliente, de modo que se evitan problemas de incluir scripts duplicados, además de reducir el número de peticiones que el cliente debe hacer al servidor. Para incluir un archivo de JavaScript en una página se puede poner una anotación en la clase:

@IncludeJavaScriptLibrary("funciones.js")
public class MiPagina {
}

En este caso el archivo funciones.js se busca dentro del classpath junto con el componente, o dentro del directorio raíz de recursos web (el que contiene el directorio WEB-INF). Algo interesante es que se pueden tener versiones internacionales de los scripts; puede haber un funciones_es.js, funciones_fr.js y el default funciones.js y se utilizará la versión apropiada según el idioma del navegador del usuario.

Y pues con esto concluye la introducción a Tapestry. Hay muchos temás más, pero considero que ya no son parte propiamente de una introducción; lo que he cubierto en estos 5 artículos es la funcionalidad básica del framework, con el objetivo de ilustrar la facilidad de uso, a pesar de que exista una cierta curva de aprendizaje, que creo que más bien es una curva de des-aprendizaje, porque hay que olvidarse de todas las malas costumbres y experiencias que hemos adquirido con el uso de JSP y frameworks basados en JSP.