4.2 Tutorial de Tapestry Índice Introducción Páginas Ejemplo pojo-tapestrytutorial Estructura del subsistema Capa modelo Plantillas Componentes Validación de campos de entrada Clases Sintaxis de expansiones y valores de parámetros Catálogos de mensajes Formularios PageLink, Form, TextField, Label, If Propiedades Inyección de páginas Manejadores de eventos Ciclo de vida de las páginas Comunicación de datos entre páginas Objetivo Aprender los fundamentos básicos de Tapestry 5 para diseñar e implementar aplicaciones Web utilizando este framework Introducción (1) Proyecto Apache Framework Java open source para la construcción de aplicaciones Web Construido sobre la API estándar de Servlets La primera versión fue concebida por Howard Lewis Ship en el año 1999 La versión actual, que será la que nosotros estudiaremos, es la 5 Puede utilizarse dentro de cualquier servidor de aplicaciones Java EE “ligero” (solamente necesita soporte para Servlets) Orientado a componentes Enfoque: Una aplicación se compone de un conjunto de páginas Web, cada una de las cuales se construye a partir de componentes que pueden generar diversos eventos ante los que la página puede reaccionar Introducción (2) Algunas características Sigue el enfoque POJO Además de los componentes proporcionados por el framework permite que el desarrollador construya fácilmente sus propios componentes a medida Contiene componentes para interacciones AJAX Soporte para internacionalización Soporte para validación de datos de entrada Soporte para Inyección de Dependencias Integración con Hibernate Convenciones de nombrado Anotaciones Soporte para implementar aplicaciones Web CRUD directamente con Hibernate (sin Spring) Integración con Spring Útil cuando la lógica de negocio es más compleja y está implementada con Spring (como es nuestro caso) Introducción (y 3) Desde un punto de vista de alto nivel, una aplicación Tapestry puede ser vista como un conjunto de páginas interactivas que son gestionadas por el framework Estás páginas “recuerdan” los valores introducidos por el usuario en los campos de entrada Cuando el usuario realiza alguna acción, como por ejemplo un clic sobre un enlace o un botón, pueden reaccionar a esa acción ejecutando un método que actúa como manejador del evento producido por ese componente Desde un punto de vista de más bajo nivel, cuando la aplicación Web recibe una petición, utiliza la página apropiada para generar la respuesta (normalmente una página HTML) y se la envía al navegador Cuando el usuario realiza alguna acción sobre la página HTML recibida (como por ejemplo rellenar un campo de texto y pulsar un botón), entonces esa información es enviada de vuelta a la aplicación Tapestry, a través de otra petición Dependiendo de la lógica de la aplicación y de las acciones del usuario, la información es pasada a la misma página o a otra página para procesar la petición y generar la respuesta adecuada Páginas (1) Cada página tiene asociada una plantilla y una clase Java Ambas deben tener el mismo nombre El objetivo de la clase Java es recibir los eventos relativos a la página y el objetivo de la plantilla es generar el markup de la página Una plantilla es un documento XML que permite generar markup En el caso de una aplicación Web, el markup es normalmente HTML o XHTML, pero también podría ser, por ejemplo, XML (y en este último caso, la aplicación Web actuaría como un servicio Web REST) Tiene extensión .tml (Tapestry Markup Language) El XML incluirá referencias a componentes (proporcionados por el framework o desarrollados por el usuario) y también puede incluir expansiones (expresiones que permiten insertar valores de propiedades de la clase Java o textos internacionalizados en la plantilla) Cuando Tapestry genera el markup para una página, le pide a cada componente que esté presente en la plantilla de la página que genere el código correspondiente Páginas (y 2) Las clases son POJOs No extienden de ninguna clase ni implementan ninguna interfaz del framework Tienen propiedades para almacenar el estado de la página Pueden contener métodos para implementar la funcionalidad de la página Son invocados cuando se producen determinados eventos en algún componente de la página Se utilizan convenciones de nombrado o anotaciones para asociar métodos a eventos generados por componentes Cuando se genera el WAR de la aplicación Web, la clase (fichero .class) y la plantilla (fichero .tml) deben colocarse en el mismo directorio (que vendrá determinado por el paquete al que pertenece la página) pojo-tapestrytutorial Para el tutorial de Tapestry utilizaremos el módulo pojo-tapestrytutorial de los ejemplos Implementa las capas modelo y Web de la misma mini-aplicación bancaria implementada para el tutorial de Servlets y JSPs que, recordemos, tiene los siguientes casos de uso Crear una cuenta Buscar una cuenta a partir de su identificador Página principal del tutorial http://localhost:9090/pojo-tapestrytutorial Tapestry Tutorial Main Page Clic en Create Account Create Account Form Clic en Find Account Find Account Form Demo: Creación de una Cuenta http://localhost:9090/pojo-tapestrytutorial/createaccount Create Account Form Create Account Form Created Account Data Demo: Control de Errores Create Account Form El identificador de usuario y el balance son obligatorios Create Account Form El identificador de cuenta debe ser un número entero y >=0 Create Account Form El balance debe ser un número real y >=0 Demo: Búsqueda de una cuenta http://localhost:9090/pojo-tapestrytutorial/findaccount Find Account Form Find Account Form Account Data Demo: Control de Errores Find Account Form El identificador de cuenta es obligatorio Find Account Form El identificador de cuenta debe ser un número entero y >=0 Account Data Debe existir alguna cuenta con el identificador introducido Estructura del Subsistema (1) pojo-tapestrytutorial src/main java es.udc.pojo.tapestrytutorial model account accountservice web.pages *.java resources es/udc/pojo/tapestrytutorial/web/pages webapp/WEB-INF web.xml app.properties *.tml *.properties Estructura del Subsistema (y 2) Cada página Tiene su clase asociada en el paquete es.udc.pojo.tapestrytutorial.web.pages dentro del directorio de código fuente src/main/java Tiene su plantilla asociada en el directorio es/udc/pojo/tapestrytutorial/web/pages dentro del directorio de recursos src/main/resources De esta forma el fichero .tml de la plantilla correspondiente a cada página se copiará (durante la fase de procesamiento de recursos de Maven) al mismo directorio en el que se genera el fichero .class resultado de compilar la clase de la página (dentro de target/classes) Capa Modelo (1) La capa modelo de la aplicación es exactamente igual que la del subsistema pojo-servjsptutorial Recuérdese que para simplificar su implementación Se ha optado por la realización de la persistencia en memoria, en lugar de utilizar una BD Se ha definido la implementación del servicio directamente, sin crear una interfaz Capa Modelo (y 2) Modelado de entidades Account Representa una cuenta bancaria, con la misma información que la comentada para pojominibank Definición API modelo Account - accountId : Long - userId : Long - balance : double + Constructores + métodos get/set AccountServiceImpl Define los métodos createAccount y findAccount No utiliza DAOs porque mantiene las cuentas en memoria en lugar de hacerlas persistentes AccountServiceImpl - lastAccountId : long - accounts: Map<Long, Account> + createAccount(account : Account) : Account + findAccount(accountId : long) : Account web.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <!-<!-<!-- ====================================================================== --> Tapestry 5 web application configuration --> ====================================================================== --> <web-app> <display-name>POJO-Examples Tapestry Tutorial</display-name> <context-param> <param-name>tapestry.app-package</param-name> <param-value>es.udc.pojo.tapestrytutorial.web</param-value> </context-param> <filter> <filter-name>app</filter-name> <filter-class>org.apache.tapestry5.TapestryFilter</filter-class> </filter> <filter-mapping> <filter-name>app</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app> Filtros Disponibles desde la versión 2.3 de la API de Servlets Proporcionan un mecanismo estándar para interceptar peticiones y respuestas dentro de un contenedor de Servlets, para transformar o utilizar la información contenida en ellas Se definen con la etiqueta filter Permiten capturar una petición y cambiar el flujo de ésta (decidir a qué Servlet dirigirla, rechazarla, procesarla, etc.) También permiten realizar transformaciones sobre las respuestas generadas filter-name: Indica el nombre del filtro filter-class: Indica la clase Java que lo implementa La etiqueta filter-mapping indica cuando debe invocarse un filtro filter-name: Nombre del filtro a aplicar url-pattern: Patrón que indica para qué peticiones debe aplicarse Integración en un Contenedor de Servlets Tapestry se integra dentro de un contenedor de Servlets a través de un filtro En el fichero web.xml debe declararse el filtro org.apache.tapestry5.TapestryFilter Puede dársele cualquier nombre, aunque suele utilizarse app Este filtro procesa todas las peticiones relativas a Tapestry y pasa el resto de peticiones al contenedor de Servlets Cuando se accede al directorio raíz de una aplicación Tapestry entonces envía la petición a la página Index Además, para cada aplicación debe configurarse el paquete raíz de la aplicación a través de un parámetro con nombre tapestry.app-package Tapestry utiliza este paquete para localizar las clases relativas a las páginas y los componentes creados por el usuario utilizados en la aplicación Las clases de los componentes deben ir en el sub-paquete components Las clases de las páginas deben ir en el sub-paquete pages (en nuestro ejemplo es.udc.pojo.tapestrytutorial.web.pages) Index.tml (1) <!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" xml:space="preserve"> <head> <title>${message:title}</title> </head> <body text="#000000" bgcolor="#ffffff"> <div align="center"> <p><font color="#000099" size="+2" face="Arial, Helvetica, sans-serif"> <b>${message:title}</b></font><br/> </p> </div> Index.tml (y 2) <div align="center"> <a href="#" t:type="PageLink" t:page="CreateAccount">${message:createAccount}</a> <br/> <a href="#" t:type="PageLink" t:page="FindAccount">${message:findAccount}</a> </div> </body> </html> Plantillas (1) A través de la declaración DOCTYPE se hace referencia a la DTD de XHTML 1.0, y se indica además que el elemento raíz es html <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> El elemento raíz incluye la declaración de un espacio de nombres <html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd" xml:space="preserve"> Cualquier elemento o atributo que tenga prefijo t pertenece al espacio de nombres de Tapestry En el markup generado, no se incluye la importación del espacio de nombres de Tapestry Plantillas (y 2) Tapestry elimina todos los espacios y retornos de carro de la plantilla que sean innecesarios para la respuesta generada. Por ejemplo: Dentro de un bloque de texto las secuencias de espacios se sustituyen por un único espacio Los bloques de texto entre dos tags HTML, compuestos únicamente por espacios y retornos de línea, son eliminados Si se examina la respuesta generada (por ejemplo con la opción “Ver código fuente” de un navegador) puede verse que está todo en pocas líneas Para variar este comportamiento debe usarse el atributo XML estándar xml:space, para indicar a Tapestry que conserve los espacios y retornos de carro (xml:space="preserve") El markup generado no incluye este atributo Útil para depuración mientras se está desarrollando la aplicación Componentes Definición de componentes Es posible incluir componentes dentro de una plantilla utilizando el elemento correspondiente (dentro del espacio de nombres de Tapestry) <t:PageLink page="CreateAccount">${message:createAccount}</t:PageLink> Tiene el inconveniente potencial de que se pierde la capacidad de visualizar la plantilla, como una página HTML, en un navegador Otra alternativa consiste en incluir la definición del componente dentro de un elemento HTML estándar utilizando el atributo t:type para indicar el tipo del componente <a href="#" t:type="PageLink" t:page="CreateAccount">${message:createAccount}</a> En los ejemplos de la asignatura se ha seguido esta aproximación Los nombres de los componentes no son sensibles a mayúsculas/minúsculas Convenciones en el uso del espacio de nombres de Tapestry con atributos Para favorecer la legibilidad, en los ejemplos de la asignatura se han seguido las siguientes convenciones en el uso del espacio de nombres de Tapestry con atributos Cuando se usa un componente de Tapestry dentro de un elemento HTML => se ha usado el prefijo del espacio de nombres de Tapestry en los atributos propios del componente <a href="#" t:type="PageLink" t:page="CreateAccount”>...</a> Cuando se usa un componente de Tapestry directamente => no se ha utilizado el prefijo del espacio de nombres de Tapestry en los atributos del componente <t:if test="account">...</t:if> En cualquier caso => siempre se ha usado el prefijo del espacio de nombres de Tapestry para el atributo id en los componentes que lo necesitan Para el atributo especial id, el prefijo siempre es necesario <t:select t:id="language" model="languages"/> Componente PageLink Su propósito es visualizar un enlace que apunte hacia otra página de la aplicación Se define dentro de un elemento HTML de tipo a Debe declararse el atributo t:type con valor PageLink El atributo t:page sirve para indicar el nombre la página a la que se debe navegar cuando se hace clic en el enlace Se refiere al nombre lógico de la página En el markup generado sustituye el valor del atributo href por un valor apropiado (en función del valor del atributo t:page) y al igual que cualquier otro componente, elimina el atributo t:type Desde la plantilla de la página Index se ha utilizado este componente para generar dos enlaces hacia las páginas CreateAccount y FindAccount Sintaxis de expansiones y valores de parámetros (1) Los valores de los parámetros de los componentes se especifican con la sintaxis prefijo:valor y las expansiones mediante ${prefijo:valor} Entre otros, existen los siguiente prefijos literal: indica que valor debe interpretarse como un literal prop: indica que valor es el nombre de una propiedad de la clase de la página (se accederá a ella través del método get correspondiente) message: indica que valor es la clave de un mensaje del catálogo de mensajes Sintaxis de expansiones y valores de parámetros (2) En el caso de acceso a propiedades (prefijo prop) es posible Referirse a subpropiedades Ejemplo: propiedad.subpropiedad invocaría el método getSubpropiedad sobre el objeto devuelto por el método getPropiedad de la página Invocar a un método del objeto que no sea un getter (utilizando paréntesis a continuación del nombre del método) Ejemplo: propiedad.hashCode() invocaría el método hashCode sobre el objeto devuelto por el método getPropiedad de la página Invocar a un método del objeto pasándole parámetros (se indican separados por comas dentro de los paréntesis) Los parámetros son a su vez expresiones que pueden referirse a propiedades del objeto Sintaxis de expansiones y valores de parámetros (y 3) Prefijos por defecto Un parámetro puede definir un prefijo por defecto, y de hecho es lo habitual, de manera que en ese caso, el valor del parámetro se puede especificar sin incluir el prefijo La documentación de un componente especifica para cada parámetro su prefijo por defecto En el caso de las expansiones, el valor por defecto del prefijo es prop Catálogos de Mensajes (1) app.properties accountId-label=Account Identifier balance-label=Balance link-home=Home userId-label=User Identifier Index.properties createAccount=Create Account findAccount=Find Account title=Tapestry Tutorial Main Page Catálogos de Mensajes (2) Forman parte del soporte de internacionalización que proporciona Tapestry Permiten que los textos fijos de las plantillas, en lugar de escribirlos directamente en ellas, se escriban en un fichero de mensajes Ficheros .properties Existe un catálogo de mensajes general para toda la aplicación Mensajes visibles desde todas las páginas Debe colocarse dentro del directorio WEB-INF El nombre debe ser el mismo que el que se le dio al filtro de Tapestry en el fichero web.xml En el ejemplo debe llamarse, por tanto, app.properties En este fichero deben colocarse los mensajes que sean comunes a varias páginas Catálogos de Mensajes (3) Es posible definir un fichero de mensajes específicos para una página Debe tener el mismo nombre que la página y debe estar en el mismo directorio que ella (en la estructura de código fuente están colocados en el mismo directorio que la plantilla de la página) Los mensajes definidos en este fichero solo serán visibles para la página en cuestión Por ejemplo los mensajes definidos en Index.properties solamente son visibles para la página Index Los mensajes se referencian a través de una expansión utilizando el prefijo message seguido de la clave del mensaje en el fichero de mensajes La sintaxis es ${message:clave} Primero se busca la clave en el fichero de mensajes específico de la página (si existe) y si no se encuentra, entonces se busca en el fichero de mensajes general Catálogos de Mensajes (y 4) En el apartado 4.3 se explicará cómo pueden utilizarse los ficheros de mensajes junto con otras funcionalidades proporcionadas por Tapestry para visualizar una aplicación en diferentes idiomas Index.java package es.udc.pojo.tapestrytutorial.web.pages; public class Index {} La clase asociada a la página Index no define ninguna propiedad ni ningún método CreateAccount.tml (1) <!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" xml:space="preserve"> <head> <title>${message:title}</title> </head> <body> <form t:type="Form" t:id="createAccountForm"> <t:errors/> <table width="100%" border="0" align="center" cellspacing="12"> CreateAccount.tml (2) <!-- User Identifier --> <tr> <th align="right" width="50%"> <t:label for="userId"/> </th> <td align="left"> <input t:type="TextField" t:id="userId" t:validate="required, min=0" size="16" maxlength="16" /> </td> </tr> <!-- Balance --> <tr> <th align="right" width="50%"> <t:label for="balance"/> </th> <td align="left"> <input t:type="TextField" t:id="balance" t:validate="required,min=0" size="16" maxlength="16"/> </td> </tr> CreateAccount.tml (y 3) <!-- Create button --> <tr> <td width="50%"></td> <td align="left" width="50%"> <input type="submit" value="${message:create}"/> </td> </tr> </table> </form> </body> </html> CreateAccount.properties create=Create title=Create Account Form Componente Form En la plantilla se ha incluido un componente Form (t:type="Form"), embebido dentro de una etiqueta form HTML, que dentro contiene otros componentes Este componente genera un formulario HTML En la clase de la página puede haber definidos manejadores asociados a los eventos que genera este componente Hay una serie de eventos que se generan cuando se visualiza el componente y otros cuando se envía el formulario (submit) El componente contiene un elemento HTML input de tipo submit, que sirve para enviar el formulario Se le ha asignado un identificador a través del atributo t:id Como se verá más adelante, este identificador puede ser utilizado desde el código de la clase de la página para indicar cuales son los manejadores de eventos de este formulario t:id es un atributo especial que tienen todos los componentes y su valor identifica al componente de manera única dentro de la página Componente TextField (1) Dentro del formulario hay dos componentes de tipo TextField embebidos dentro de dos elementos input HTML (t:type="TextField") Este componente está ligado a una propiedad de la clase de la página El atributo t:value sirve para indicar el nombre de la propiedad a la que está asociado Si no existe ese atributo entonces t:id es el que indica el nombre de la propiedad a la que está asociado La clase debe tener métodos get y set para acceder al valor de la propiedad y establecerlo Por ejemplo, el primer TextField tiene el valor userId para la propiedad t:id, por tanto estará asociado a la propiedad userId, lo que implica que la clase de la página debe tener los métodos getUserId y setUserId Componente TextField (y 2) Dentro del formulario hay dos componentes de tipo TextField embebidos dentro de dos elementos input HTML (cont) Este componente se visualiza como un campo de texto (un input de tipo text), conteniendo el valor de la propiedad a la que está asociado Accede a la propiedad de la clase para recuperar su valor a través del método get correspondiente Cuando se envía el formulario al servidor, se le asigna el valor que contenga en ese momento el campo de texto a la propiedad a la que está asociada Establece el valor de la propiedad en la clase a través del método set correspondiente Componente Label El componente Label genera el tag HTML <label> asociado a un campo de entrada de un formulario En CreateAccount.tml se han utilizado dos componentes Label para visualizar la etiqueta asociada a los dos campos de texto del formulario En este caso el componente se ha declarado directamente a través del elemento t:label El atributo for indica el identificador del componente cuya etiqueta debe ser visualizada (en nuestro caso los identificadores de los componentes TextField) Para obtener el valor de la etiqueta asociada a un componente TextField Primero se mira si tiene un atributo t:label y si existe se usa su valor Puede ser constante o leerse de un fichero de mensajes Si no existe ese atributo se genera en función del identificador del componente (atributo t:id) Si existe una clave en el fichero de mensajes id-label (sustituyendo id por el identificador del componente) se usa el valor que tenga asignado (por ejemplo userId-label=User identifier) Si no, se genera en función del valor del identificador dividiéndolo en palabras (por ejemplo userId se dividiría como “User Id”) Validación de campos de formularios (1) Tapestry proporciona un conjunto de validadores que pueden ser aplicados a los campos de entrada de un formulario a través del atributo t:validate Para los campos de texto userId y balance se han utilizado los validadores required: Comprueba que el valor asignado al campo no sea nulo ni la cadena vacía min: Comprueba que el valor numérico asignado al campo no sea menor que la cantidad indicada (0 en este caso) Por defecto, cuando se produce algún error de validación en el envío de un formulario se vuelve a mostrar la misma página que lo contiene Este comportamiento puede variarse proporcionando un manejador para el evento adecuado Validación de campos de formularios (y 2) Los validadores básicos proporcionados por Tapestry realizan las validaciones en el navegador (JavaScript) y también en el lado servidor (por seguridad y por si el cliente tiene deshabilitado JavaScript) Para visualizar los mensajes de error (lado servidor) asociados a los validadores utilizados dentro de un formulario se emplea el componente Errors (elemento t:errors) Otros validadores proporcionados por Tapestry son Debe colocarse dentro del componente Form que incluye a los componentes que tienen los validadores Es recomendable utilizarlo siempre por si el navegador tiene deshabilitado JavaScript minlength/maxlength: Comprueban que la longitud de la cadena asignada al campo no sea menor/mayor que la cantidad indicada max: Comprueba que el valor numérico asignado al campo no sea mayor que la cantidad indicada regexp: Comprueban que la cadena asignada al campo concuerda con un patrón Es posible añadir nuevos validadores CreateAccount.java public class CreateAccount { @Property private Long userId; @Property private Double balance; @InjectPage private AccountCreated accountCreated; Object onSuccess() { Account account = new Account(userId, balance); new AccountServiceImpl().createAccount(account); accountCreated.setAccountId(account.getAccountId()); return accountCreated; } } Propiedades La clase CreateAccount contiene dos propiedades asociadas a los dos campos de texto presentes en la plantilla de la página: userId y balance Como se había dicho con anterioridad el acceso a esas propiedades se hace a través de métodos get/set La anotación @Property permite anotar campos para los que deben crearse métodos get/set De esta forma no es necesario escribir esos métodos Si se usa la anotación, los métodos get/set no pueden existir Inyección de páginas Utilizando el patrón de inyección de dependencias, la anotación @InjectPage permite inyectar una página en una propiedad de otra página Tiene un atributo opcional (value) que debe ser utilizado cuando el tipo de la página a inyectar no puede ser determinado (e.g. el tipo de la propiedad en la que se inyecta es una interfaz que cumple la página a ser inyectada) Formularios El componente Form genera una serie de eventos (tanto a la hora de visualizarse como a la hora de hacer un submit de él) para los cuales se puede proporcionar un manejador en la clase de la página que contiene el formulario En CreateAccount se proporciona un manejador para el evento “success”, que es emitido una vez que el formulario se ha enviado al servidor y sus campos de entrada se han validado correctamente Para procesar un evento, existen dos posibilidades Definir un método con el formato onXxx, siendo Xxx el nombre del evento (e.g. onSuccess) Usar la anotación @OnEvent sobre el nombre de un método (que puede tener un nombre arbitrario) NOTA: en el código de los ejemplos, se ha preferido la primera opción (convención de nombrado) El método onSuccess actúa como manejador del evento “success” para todos los formularios de la página Si hubiese más de un formulario podría especificarse un manejador diferente para cada uno en función de su identificador (atributo t:id) A través del nombre del método: onSuccessFromCreateAccountForm Anotando el método: @OnEvent(value = “success", component = “createAccountForm") Manejadores de eventos (1) Un manejador de un evento puede devolver Nada (tipo de retorno void o cuando devuelve null): En este caso la misma página vuelve a ser visualizada Una nueva página de la aplicación a ser visualizada String: Debe ser el nombre lógico de la página Class: La clase de la página Una instancia de la clase de la página (útil cuando la página debe ser configurada de alguna manera) Link: Una implementación de la interfaz Link que será convertida a una URL a la cual será redirigido el cliente URL: El cliente será redirigido a esa URL StreamResponse: Un objeto de tipo Stream que permite enviar datos binarios al cliente (e.g. fichero PDF) Cualquier otro objeto devuelto producirá un error Manejadores de eventos (y 2) El manejador del evento “success” de la página CreateAccount (método onSuccess) Crea un objeto Account a partir de las propiedades userId y balance de la página Llama al servicio del modelo para crear la cuenta Utiliza la instancia de la página de tipo AccountCreated que ha sido inyectada y establece el identificador de la cuenta que se acaba de crear a través del método setAccountId Serán los valores (ya validados) que se hayan introducido en los campos de texto del formulario Esa página lo necesita puesto que muestra el identificador de la cuenta creada Devuelve la instancia de la página de tipo AccountCreated Por tanto, esa será la siguiente página a ser visualizada AccountCreated.java package es.udc.pojo.tapestrytutorial.web.pages; public class AccountCreated { private Long accountId; public Long getAccountId() { return accountId; } public void setAccountId(Long accountId) { this.accountId = accountId; } Long onPassivate() { return accountId; } void onActivate(Long accountId) { this.accountId = accountId; } } NOTA: No se ha utilizado la anotación @Property sobre el campo accountId porque desde la clase CreateAccount se llama al método setAccountId (y por tanto el método debe existir en tiempo de compilación) Ciclo de vida de las páginas Para mejorar el rendimiento Tapestry mantiene un pool de instancias para cada página Cuando se solicita una página, obtiene una instancia del pool para atender esa petición Una vez atendida la petición la página es devuelta al pool Antes de devolverla al pool las variables de la instancia son inicializadas para borrar los datos específicos de esa petición Hay que tener en cuenta que cada vez que se trabaja con una página puede ser una instancia distinta Cuando el manejador del evento “success” de la página CreateAccount devuelve una instancia de la página AccountCreated (en la que ha establecido el identificador de la cuenta que se ha creado) Realmente lo que se hace es indicarle al navegador que haga una nueva petición sobre la página AccountCreated Cuando esa petición llega a la aplicación, se coge una instancia de la página AccountCreated del pool, y la utiliza para generar la respuesta PROBLEMA: La instancia que se ha cogido del pool no tiene establecido el identificador de la cuenta que se ha creado !!! Comunicación de datos entre páginas Existen dos mecanismos que permiten pasar datos entre páginas Se puede hacer que un campo de la página sea persistente Los campos persistentes se anotan con @Persist El valor de estos campos se guarda en la sesión (HttpSession) Cada vez que se obtiene del pool una página que tenga un campo persistente se busca en la sesión el valor del campo y se establece en la página Desventajas: Menor escalabilidad al hacer uso de la sesión para almacenar datos No es posible hacer bookmarks a esas páginas Se puede utilizar el “contexto de activación de páginas” Requiere más código pero no utiliza la sesión y permite hacer bookmarks a las páginas Es la opción elegida en el ejemplo Contexto de Activación de Páginas (1) Para hacer una página “activable” basta con añadirle los métodos onActivate y onPassivate Cuando una página implementa estos métodos Antes de ser devuelta al pool se invoca el método onPassivate Este método debe devolver los datos que necesitará la página para visualizar los datos deseados Estos datos se incluyen en la URL a la que se redirige el cliente junto con el nombre de la página No se hace uso de la sesión Es posible hacer un bookmark a esa URL Cuando se recibe la petición del cliente, se obtiene una nueva instancia del pool y se llama al método onActivate pasándole los datos que vienen incluidos en la petición Recibe, por tanto, los datos que devuelve onPassivate Contexto de Activación de Páginas (y 2) El método onPassivate de AccountCreated devuelve el identificador de la cuenta creada Cuando el método onSuccess de la clase CreateAccount devuelve la instancia de AccountCreated (suponiendo que el identificador de la cuenta creada es 1) la URL a la que se redirige al cliente es http://host:port/pojo-tapestrytutorial/accountcreated/1 El método onActivate recibe el identificador de la cuenta creada y lo almacena en la variable accountId (invocando al método setAccountId) Los datos que devuelve onPassivate y recibe onActivate tienen que ser tipos simples (o sus contrapartidas objetuales) o String Es posible pasar más de un dato entre páginas onPassivate puede devolver un Object[] onActivate puede recibir más de un parámetro Cada parámetro, de izquierda a derecha, corresponde a un elemento del array Object[], y Tapestry hace automáticamente un cast al tipo de parámetro AccountCreated.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" xml:space="preserve"> <head> <title>${message:title}</title> </head> <body text="#000000" bgcolor="#ffffff"> <div align="center"> <p> <font color="#000099" face="Arial, Helvetica, sans-serif"> <b>${message:bodyMessage-1} ${accountId} ${message:bodyMessage-2}</b> </font> </p> </div> <br/> <a href="#" t:type="PageLink" t:page="Index">${message:link-home}</a> <br/> </body> </html> AccountCreated.properties bodyMessage-1=Account number bodyMessage-2=created sucessfully title=Created Account Data Creación de una cuenta (AccountCreated) En AccountCreated.tml se usa la expansión ${accountId} y por tanto se llama al método getAccountId de la clase AccountCreated, que en este caso contendrá el identificador de la cuenta que se ha creado Recuérdese que ese valor se le pasó a través del mecanismo de activación de páginas FindAccount.tml (1) <!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" xml:space="preserve"> <head> <title>${message:title}</title> </head> <body text="#000000" bgcolor="#ffffff"> <form t:type="Form" t:id="findAccountForm"> <t:errors/> <table width="100%" border="0" align="center" cellspacing="12"> FindAccount.tml (2) <!-- Account Identifier --> <tr> <th align="right" width="50%"> <t:label for="accountId"/> </th> <td align="left"> <input t:type="TextField" t:id="accountId" t:validate="required, min=0" size="16" maxlength="16"/> </td> </tr> <!-- Search button --> <tr> <td width="50%"></td> <td align="left" width="50%"> <input type="submit" value="${message:find}"/> </td> </tr> </table> </form> </body> </html> FindAccount.java public class FindAccount { @Property private Long accountId; @InjectPage private AccountDetails accountDetails; Object onSuccess() { accountDetails.setAccountId(accountId); return accountDetails; } } Búsqueda de una cuenta (FindAccount) FindAccount.tml Contiene un formulario con un campo de texto para introducir el identificador de la cuenta buscada Se valida que tenga algún valor y que sea mayor que 0 FindAccount.java Tiene una propiedad para almacenar el identificador de la cuenta buscada Se le inyecta una página de tipo AccountDetails (utilizada para mostrar los detalles de una cuenta) Tiene un manejador para el evento “success” que utiliza la página inyectada para establecerle el identificador de la cuenta buscada y la devuelve como resultado AccountDetails.java public class AccountDetails { private Long accountId; private Account account; public void setAccountId(Long accountId) { this.accountId = accountId; } public Account getAccount() { return account; } void onActivate(Long accountId) { this.accountId = accountId; try { account = new AccountServiceImpl().findAccount(accountId); } catch (InstanceNotFoundException e) { } } Long onPassivate() { return accountId; } } Búsqueda de una cuenta (AccountDetails.java) Utiliza el contexto de activación de páginas En el método onPassivate se devuelve el identificador de la cuenta buscada Cuando se envía el formulario de búsqueda de cuenta, suponiendo que se está buscando la cuenta con identificador 1, la URL a la que se redirige el cliente será http://host:port/pojo-tapestrytutorial/accountdetails/1 En el método onActivate Es posible hacer un bookmark sobre esa URL Se llama al modelo para recuperar una cuenta con el identificador especificado por el usuario Si la cuenta se encuentra, se guarda en la variable account Si la cuenta no se encuentra, se captura la excepción y no se hace nada (simplemente se deja a null la variable account) Define el método getAccount para permitir el acceso a la propiedad account AccountDetails.tml (1) <!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" xml:space="preserve"> <head> <title>${message:title}</title> </head> <body text="#000000" bgcolor="#ffffff"> <div align="center"> <p> <font color="#000099" face="Arial, Helvetica, sans-serif"> <b>${message:header}</b> </font> </p> </div> <div align="center"> AccountDetails.tml (2) <t:if test="account"> <table border="1" align="center" width="35%"> <tr> <th width="60%">${message:accountId-label}</th> <td width="30%" align="center">${account.accountId}</td> </tr> <tr> <th width="60%">${message:userId-label}</th> <td width="30%" align="center">${account.userId}</td> </tr> <tr> <th width="60%">${message:balance-label}</th> <td width="30%" align="center">${account.balance}</td> </tr> </table> <p:else> <font color="#000099" face="Arial, Helvetica, sans-serif"> <b>${message:accountNotFound}</b> </font> </p:else> </t:if> AccountDetails.tml (3) </div> <br/> <a href="#" t:type="PageLink" t:page="Index">${message:link-home}</a> <br/> </body> </html> Componente If El componente If muestra su contenido si se cumple una condición El parámetro test recibe un valor de tipo booleano Si es true se muestra el contenido del componente El prefijo por defecto para este parámetro es prop Permite otros tipos de valores no booleanos Object: true si no Number: true si no Collection: true String: true si no es null es 0 si no está vacía es la cadena vacía y no es el literal “false” Tiene un parámetro opcional else que permite proporcionar un contenido alternativo a ser mostrado si la condición no se cumple El espacio de nombres tapestry:parameter sirve para pasar parámetros “de bloque” a componentes En Tapestry, un bloque (block) es un trozo de plantilla Normalmente se le asigna el prefijo “p” Con este espacio de nombres definido, los parámetros de bloque se pasan a través de un elemento que se construye usando el prefijo “p:” seguido del nombre del parámetro <p:else>…</p:else> Búsqueda de una cuenta (AccountDetails.tml) Utiliza un componente If para determinar si la propiedad account de la página (accedida a través del método getAccount) es distinta de null Si es distinta de null se usan expansiones para mostrar los valores de los campos de la cuenta Recuérdese que el prefijo por defecto del parámetro test del componente If es prop Por ejemplo ${account.accountId} inserta el resultado de invocar a getAccount().getAccountId() Si es null significa que no se ha encontrado ninguna cuenta con el identificador introducido por el usuario y se muestra un mensaje indicando que no se ha podido encontrar dicha cuenta