Tapestry 5, parte 3 - Componentes, inyección, internacionalización

Continuando con la cátedra de Tapestry 5, vamos a ver ahora tres cosas sencillas pero bastante ilustrativas del poder de este framework: cómo funcionan los componentes (unidades funcionales que pueden ir contenidas en páginas o dentro de otros componentes); inyección de objetos en páginas y componentes, y cómo internacionalizar una página o componente.

Los componentes son fragmentos de páginas que van dentro de otros componentes o dentro de páginas. Un ejemplo muy típico es este: queremos dar a nuestro sitio una apariencia uniforme, con hojas de estilos, una serie de ligas en la parte superior, algunos datos de copyright y demás en la parte inferior, etc. Todas las páginas deben salir igual. Esto en Tapestry se resuelve muy fácil con un solo componente que podemos llamar Marco, que además va a tener la peculiaridad de que va a envolver a su componente o página padre:

<!-- Marco.tml -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"
  xmlns:p="tapestry:parameter">
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
  <title>${title}</title>
</head>
<body>
<!-- imaginense que aqui van unas ligas, encabezados, etc etc -->

<!-- ahora aqui vamos a incluir el cuerpo de la pagina -->
<t:body />

<!-- y al final el pie de pagina -->
<div>Copyright 2009 blablabla</div>
</body></html>

Las partes interesantes de lo anterior son:

  • El encabezado tiene un ${title} que a continuación vamos a ver cómo va a llegar al componente (será un parámetro que vamos a pasarle desde la página)
  • El tag de es para incluir el cuerpo del componente o página que nuestro Marco va a envolver.

Ahora vamos a ver la clase que acompaña a este HTML, que será org.ejemplo.componentes.Marco (siguiendo el ejemplo que comencé en el post anterior):

@IncludeStylesheet("context:/estilos.css")
public class Marco {
  @Property
  @Parameter(required=true, defaultPrefix=BindingConstants.LITERAL)
  private String title; //El titulo de la pagina

}

Eso es lo único que necesitamos. Estamos declarando una propiedad title que es una cadena y además indicamos que es un parámetro obligatorio. En cuanto a lo del defaultPrefix, esto es muy interesante: En Tapestry, cuando pasamos parámetros a componentes, lo que se hace es realmente un binding entre el parámetro y su valor, y hay varios maneras de realizar esto, por lo que normalmente los valores se pasan de forma prefijo:valor, pero en los parámetros se pueden especificar prefijos default para cada uno. En el caso del titulo de la página, tiene sentido indicar que el prefijo default sea un valor literal, es decir que no se evalua sino se toma tal cual está escrito en el HTML. Otros prefjios son:

  • prop: Para ligar el valor del parámetro con una propiedad del componente o página donde reside.
  • asset: Para ligar el valor del parámetro con un activo del componente o página donde reside. Un activo puede ser por ejemplo una imagen.
  • message: Para ligar el valor del parámetro con un mensaje del componente o página donde reside. Cada página o componente puede tener una serie de mensajes que son muy útiles porque es la manera más sencilla de internacionalizar una página.

Hay otros tipos de binding más sofisticados, por el momento dejémoslo así, sin embargo algo muy importante y que es parte del poder que nos ofrece Tapestry, es que podemos crear nuestros propios bindings y realmente no es muy complicado hacerlo.

También definimos en la clase Marco que queremos usar una hoja de estilos llamada "estilos.css", que se encuentra en el directorio raíz de los archivos de web (el famoso "context" que se usa en proyectos web, no sólo de Tapestry; es el que contiene el subdirectorio WEB-INF). Esto es interesante porque a pesar de que una hoja de estilos es algo de interfaz, se define con una simple anotación en la clase Java, para poder indicar si sale de un archivo del proyecto que viene en el web context o podría venir en el classpath (se pone el prefijo "classpath:" en ese caso).

Ahora vamos a ver cómo se puede usar nuestro componente Marco. Regresando al ejemplo de las páginas Hola y Nombre, ahora les podemos dar una apariencia uniforme simplemente haciendo esto:

<!-- Nombre -->
<html t:type="marco" title="Captura de nombre"
  xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"
  xmlns:p="tapestry:parameter">
<t:form t:id="saluda">
  <t:textfield t:id="nombre" t:value="nombre" size="30" maxlength="80" />
  <t:submit t:id="saludar" t:value="Saludar" />
</t:form>
</html>

Aquí podemos ver que envolvemos lo que ya teníamos con un tag de HTML pero que le agregamos dos atributos importantes:

  • t:type="marco" para indicar que realmente el tag de HTML se va a convertir en el componente Marco. Al momento de hacer el render de la página, Tapestry va a hacer primero el render del componente Marco hasta donde llega al tag de , en ese momento va a sustituir dicho tag con el resto de la página Nombre, hasta llegar al y eso lo va a sustituir con el resto de Marco (lo que sigue de en Marco.tml).
  • title="Captura de Nombre" que es el parámetro "title" que definimos en el componente Marco. Así de fácil es pasarle un parámetro; no le ponemos prefijo porque ya sabemos que es literal porque así está definido el componente.

Para la otra página vamos a hacer algo un poco distinto:

<!-- Hola -->
<html t:type="layout" title="message:titulo"
  xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"
  xmlns:p="tapestry:parameter">
<t:if test="nombre">
Hola, ${nombre}!!!
<p:else>No sé a quién saludar!!! Quién eres?</p:else>
</t:if>
<t:pagelink page="nombre">Poner otro nombre</t:pagelink>
</html>

Aquí estamos poniéndole el prefijo message al valor de título, de manera que esta vez no estamos pasando la cadena "titulo" al Marco, sino que Tapestry va a buscar un mensaje con llave "titulo" entre los mensajes de la página Hola. Los mensajes los ponemos bajo el directorio resources del proyecto, en la misma estructura de la página, es decir resources/org/ejemplo/pages y ahí dentro van los archivos. Digo archivos porque podemos tener varios, simplemente hay que ponerles el mismo nombre de la página o componente y opcionalmente un sufijo con el idioma (y hasta el país). Los archivos siguen el formato de "properties" estándar de Java. Entonces podemos declarar los siguientes archivos:

Marco_en.properties
titulo=Hello there

Marco_es_MX.properties
titulo=Qué pasó mano

Marco_es_ES.properties
titulo=Qué hay tío

Marco_fr.properties
titulo=Bon jour mon ami

Marco.properties
titulo=Hola

El archivo que no tiene sufijo de idioma ni país, es el que se usa por default cuando un usuario entra a ver la página en un idioma que no tenemos disponible (por ejemplo ruso, japonés, italiano, etc). Si alguien entra con su navegador puesto para español de México, verá el saludo "Qué pasó mano" pero si entra con español de España va a ver el saludo "Qué hay tío" y si entra con español de cualquier otro país (si es que viene especificado por el navegador) verá también el valor default que es "Hola". Así que ahora ya tenemos una aplicación internacionalizada.

Los mensajes también pueden llevar parámetros tipo printf, que resolvemos desde nuestra clase Java. El manejar los mensajes desde la clase nos permite hacer más dinámico el contenido. Por ejemplo en vez de solamente tener "titulo", podríamos tener dos mensajes, por ejemplo "titulo-am" y "titulo-pm" para decir "buenos días" o "buenas tardes" o "buenas noches". Tendríamos que tener entonces los archivos de mensajes así (voy a poner solamente el default):

titulo-am=Buenos Dias!
titulo-pm=Buenas %s!
tardes=tardes
noches=noches

Y metemos algo de código a la clase:

import org.apache.tapestry5.ioc.Messages;

public class Hola {

  @Inject
  private Messages mensajes;
  @Inject
  private org.slf4j.Logger log;

  public String getPageTitle() {
    long now = System.currentTimeMillis();
    long milis = now % 86400000;
    if (milis < 43200000) {
      return mensajes.get("titulo-am");
    } else {
      String parte = milis > 68400000 ? "noches" : "tardes";
      log.debug("Vamos a decir 'buenas {}'", parte);
      return mensajes.format("titulo-pm", mensajes.get(parte));
    }
  }

}

y cambiamos el binding del título de la página (no hay que olvidar usar el binding apropiado porque el default es "literal"):

<html t:type="layout" title="prop:pageTitle"
  xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"
  xmlns:p="tapestry:parameter">

El manejador de mensajes para la página lo va a inyectar Tapestry en la variable mensajes, por la anotación @Inject que le puse. No necesito indicar más; por el tipo de variable, Tapestry sabe lo que debe poner ahí. Lo mismo va para el Logger (Tapestry usa SLF4J); Tapestry inyecta un logger ya configurado para nuestro componente. En cuanto al código, simplemente vemos la hora a ver si es entre las 0:00 y las 12 PM para decir "buenos días", o si es entre las 12:00 y las 19:00 para decir "buenas tardes", o decir "buenas noches" si es después de las 7 PM. El mensaje lo construimos tomando primero la parte de "tardes" o "noches" del mismo catálogo de mensajes.

La próxima vez hablaré de cómo integrar Tapestry con Spring, para ya empezar a hacer cosas más interesantes, como por ejemplo mostrar una lista de objetos (que puede venir de una base de datos o donde sea) con una liga para ver dicho objeto en otra página. Falta ver muchas cosas, como integración con AJAX o simplemente incluir JavaScript en nuestras páginas o componentes. Mientras tanto pueden revisar la referencia de componentes de Tapestry 5 para que vean que incluye desde los componentes más simples como distintos tipos de ligaas hasta editores de objetos completos con validación.

Siguiente: Parte 4 - Integración con Spring