Tapestry 5, parte 4 – Integración con Spring

Al principio de esta serie, mencioné que Tapestry trae su propio manejador para inyección de dependencias. Sin embargo, muchos de nosotros ya estamos familiarizados con Spring, y en el caso de integración adicional con Hibernate y cosas así, ya tenemos configuraciones algo complejas en un ApplicationContext y queremos conservarlas. Así que es importante poder usar Spring con Tapestry.

Pues bien, es muy sencillo. Primero que nada, integramos la parte de Spring como en cualquier aplicación web. Suponiendo que tenemos un solo archivo de configuración spring.xml dentro de WEB-INF:

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>/WEB-INF/spring.xml</param-value>
</context-param>
...
<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

Además de esto, debemos cambiar el TapestryFilter por la clase org.apache.tapestry5.spring.TapestrySpringFilter (viene en la librería tapestry-spring). Y por último, aquí hay algo que hicieron muy bien en Tapestry 5.0 pero que a mi manera de ver complicaron innecesariamente en Tapestry 5.1. El tema es algo enredoso pero lo principal es que para la 5.1 quisieron hacer que se pudieran inyectar componentes de Hive (el manejador de Tapestry) en componentes de Spring, pero a cambio de eso se perdió algo muy importante que fue la capacidad de inyectar componentes de Spring específicamente por su nombre (eso se tenía en 5.0). Esto último es muy necesario en el caso por ejemplo de los DAO's de Hibernate que se envuelven en proxies de Spring para manejo de las sesiones. De modo que vamos a agregar algo más al web.xml para tener el comportamiento de Tapestry 5.0 en cuanto a Spring:

<context-param>
  <param-name>tapestry.use-external-spring-context</param-name>
  <param-value>true</param-value>
</context-param>

Bien, ahora estamos listos. Eso es todo, de verdad; no hay que hacer más. Los beans definidos en el spring.xml estarán disponibles para nuestras páginas y componentes por medio de Tapestry. Por ejemplo, supongamos que definimos en nuestro application context de Spring un DAO de Hibernate que lee una lista de usuarios. Como somos muy limpios para nuestra implementación, tenemos una interfaz abstracta que define los métodos del DAO, luego tenemos aparte la implementación de dicha interfaz usando Hibernate y la envolvemos en un proxy de Spring para que nos maneje las sesiones de Hibernate. Pero pues desde Tapestry no tenemos por qué saber nada de Hibernate, de modo que vamos a apuntar a la pura interfaz. Entonces hacemos una página Usuarios que va a presentar una lista de usuarios, de la siguiente forma:

<!-- Usuario -->
<html t:type="layout" title="Lista de Usuarios"
  xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"
  xmlns:p="tapestry:parameter">

<table border="1">
<tr><th>Clave</th><th>email</th><th>Nombre</th></tr>
<t:loop element="tr" source="users" value="user">
  <td><t:pagelink page="user/show" context="user.uid">${user.uid}</t:pagelink></td>
  <td>${user.email}</td><td>${user.nombre}</td>
</t:loop>
</table>

</html>

En esta página tenemos un componente de Loop que va a tomar una lista llamada "users" y va a iterar sobre ella, poniendo en cada iteración el elemento actual en la variable "user". Cada iteración se va a presentar en texto HTML como un elemento "tr" y dentro de la iteración tenemos un componente PageLink que va a llevarnos a la página Usuario, y tiene como contexto la propiedad "uid" de la variable "user". De modo que ahora el código de Usuarios.java es así:

public class Usuarios {
  @Inject
  @Service("userDao")
  private UserDAO dao; //Con esto vamos a inyectar el bean "userDao" de Spring

  @Property
  private User user;

  public List<User> getUsers() {
    return dao.getAllUsers();
  }

}

Como podemos ver, no tenemos que preocuparnos por cómo obtener el DAO de usuarios; Tapestry lo va a sacar el ApplicationContext de Spring, donde lo tenemos definido como "userDao" y lo va a poner en la variable "dao" de nuestra página. Lo único que tenemos que hacer es invocar el método getAllUsers() del DAO (que es imaginario, lo sé, pero no es difícil entender que tienen un método public List<User> getAllUsers() o sí?).

Como podrán ver, la liga tiene una referencia a la página "user/show"; esto es un poquito de REST que maneja Tapestry. En realidad se hará una traducción y se va a buscar la página ShowUser. De ese modo podemos tener páginas como ShowUser, DeleteUser, EditUser, CreateUser, etc y las podemos invocar con "user/show", "user/delete", etc.

Y ahora solamente falta la página de ShowUser, que será invocada si se da click a la liga que muestra la clave (uid) de un usuario. Dado que la liga la definimos con la propiedad "context" y le pusimos que el contexto es la clave del usuario (user.uid), cuando se invoque la página vamos a poder leer ese valor para obtener el usuario indicado y desplegarlo. Por ejemplo:

<!-- ShowUser.tml -->
<html t:type="layout" title="Lista de Usuarios"
  xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"
  xmlns:p="tapestry:parameter">

Seleccionaste al usuario ${user.name} con clave ${user.uid} y correo electrónico ${user.email}:

<t:beandisplay object="user" />

</html>

public class ShowUser {

  @Property
  private User user;
  @Inject
  @Service("userDao")
  private UserDAO dao;

  void onActivate(int uid) {
    user = dao.getUserByUid(uid);
  }

}

Qué estamos haciendo aquí? Pues la página ShowUser tiene una propiedad user, la cual se lee en la página para mostrar sus datos. El componente BeanDisplay es muy interesante porque sabe mostrar todas las propiedades de un objeto de Java.

Lo importante aquí es el método onActivate(int). Esto es nuevamente convención sobre configuración. Cualquier página y componente puede tener un método onActivate() que es invocado cuando se vuelve activa dicha página/componente; pero en este caso, dado que la página es invocada con un elemento de contexto (la clave del usuario), podemos implementar una variante del método que trae como parámetro un entero; Tapestry se encargará de convertir el dato de contexto que viene en el URL al tipo apropiado, y posteriormente invocará nuestro método. Ahora solamente tenemos que obtener el usuario con esa clave y asignarlo a nuestra propiedad user.

En este caso el método onActivate() es tipo void, pero qué pasa si alguien invoca directamente por URL la página ShowUser si que exista un usuario? puede haber un error. Por ello podemos adicionalmente implementar el método onActivate() sin parámetro, para que se invoque cuando alguien quiere entrar a esta página sin especificar un usuario, en cuyo caso queremos redirigirlos a la lista de usuarios, por lo que el método queda así:

Object onActivate() {
  return Usuarios.class;
}

Con eso se va a redirigir al usuario a la página Usuarios; dado que no tenemos que pasarle ningún tipo de parámetro, es suficiente con devolver la clase, no tenemos que inyectar la página como hicimos otras veces, y así ya vimos dos maneras de pasar valores entre páginas: primero mostré cómo se puede obtener la página como un objeto desde otra página y así se invocan métodos y se devuelve para que se redirija al usuario al nuevo destino, y ahora vemos cómo se puede pasar información contextual en una liga a otra página (y cómo recibir dicha información en la página destino).

Siguiente: Sesiones, validaciones y AJAX