viernes, 28 de enero de 2011

Componente lista para Tapestry 5 (paginable y anidable)

Tapestry 5 viene con muchos componetes útiles,  varios tipos de enlaces, campos de formulario, tabla paginada, bucles, gestión de errores y otros muchos otros que se le pueden
añadir mediante librerías externas. Sin embargo, hay uno que he echado en falta que es una lista de elementos paginada y que podamos usarla de forma anidada. Lo más parecido que hay es el componente Grid sin embargo este presenta los datos en una tabla y puede que no nos sirva.

Una de las cosas buenas de Tapestry es que es muy sencillo crear un nuevo componente. Asi que tomando como bse el código del componente Grid, ya que tiene muchas similitudes con él, he desarrollado un componente lista paginada.

El componente Lista está formado por las siguientes clases:

com.blogspot.elblogdepicodev.tapestry.components.Lista: representa la clase principal del componente.
com.blogspot.elblogdepicodev.tapestry.components.ListaPager: genera una lista de enlaces a las páginas de la lista según los datos devueltos por el objeto ListaDataSource y emite los eventos de cambio de página.
com.blogspot.elblogdepicodev.tapestry.components.ListaRows: procesa los elementos de la lista para la página que se está visualizando.

A partir del componente Gird, he eliminado lo que no era necesario y añadido el soporte para poder anidar varios componentes Lista. Su uso podría ser tan sencillo como lo siguiente, aunque se pueden pasar otros parámetros como el número de elementos en cada página y algunos otros parámetros que también existen en el componente Grid como inPlace, rowIndex,y rowClass:

<t:lista source="list1" row="x1" rowindex="i1" rowsperpage="literal:3" inplace="true">
  <t:lista source="list2" row="x2" rowindex="i2" rowsperpage="literal:10"> 
    ${x1} x ${x2} = ${multiplica(x1, x2)}
  </t:lista>
</t:lista>

Sencillo para todo lo que hace, ¿no?. Piensa como lo podrías hacer con otro framework ¿tendrías que copiar y pegar código para poder reutilizar la funcionalidad? ¿tendrías que manejar parámetros de la request y variables en sesión? Pues eso es una de las cosas que me gusta de Tapestry que te puedes olvidar de todo esto es usar el componente y listo no hace falta saber como funciona internamente de eso ya se encarga el componente. El resultado sería este.


Otra clase importante es ListaDataSource en la cual se apoya la clase Lista para obtener los datos a mostrar.

com.blogspot.elblogdepicodev.tapestry.misc.ListaDataSource: es una interfaz que permitirá al componente lista obtener los datos a mostrar.

package com.blogspot.elblogdepicodev.tapestry.misc;

import org.apache.tapestry5.ValueEncoder;

public interface ListaDataSource {
 /**
  * Devuelve el número de elementos totales en la lista.
  */
    int getAvailableRows();

 /**
  * Permite preparar el modelo para mostrar los elementos desde el índice de inicio hasta el índice de fin.
  */
    void prepare(int startIndex, int endIndex);

 /**
  * Obtiene el objeto de índice indicado.
  */
    Object getRowValue(int index);

 /**
  * Devuelve la clase de los objetos de la lista.
  */
    @SuppressWarnings("rawtypes")
    Class getRowType();
    
    @SuppressWarnings("rawtypes")
 /**
  * Encoder para el objeto devuelto por el método getContext().
  */
    ValueEncoder getEncoder();

 /**
  * Dato asociado a los elementos lista, usado como clave para persistir la página actual de la lista a visualizar y permitir el anidamiento de componentes Lista.
  */
    Object getContext();
}

La clase ListaDataSource es una interfaz y por tanto necesitamos alguna implementación de ella para poder usar el componente Lista. Con las siguientes dos abarcamos muchos de los casos que podemos necesitar.

com.blogspot.elblogdepicodev.tapestry.misc.CollectionListaDataSource: crea un objeto ListaDataSource a partir de una colección de objetos.
com.blogspot.elblogdepicodev.tapestry.misc.HibernateListaDataSource: crea un objeto ListaDataSource que obtendrá los datos de una sesión de hibernate.

Otra de las buenas características de Tapestry es el concepto de Coercer que básicamente es una forma que tiene Tapestry de convertir un objeto en otro, por ejemplo un int a un String, un String con cierto formato a una lista, etc... en el caso que nos ocupa una Collection en un CollectionDataSource, de esta forma en el parámetro source del componente Lista podremos pasarle también un objeto que implemente la interfaz Collection y Tapestry buscará una forma de convertir esa Collection a un objeto que implemente el tipo del parámetro (ListaDataSource). Para ello debemos indicarle a Tapestry en la clase del módulo de la aplicación el coercer necesario para ello.

package com.blogspot.elblogdepicodev.tapestry.services;

public class AppModule {
...
@SuppressWarnings("rawtypes")
public static void contributeTypeCoercer(Configuration configuration) {
    Coercion coercion = new Coercion() {
        public CollectionListaDataSource coerce(Collection input) {
            return new CollectionListaDataSource(input);
        }
    };

    configuration.add(new CoercionTuple(Collection.class, CollectionListaDataSource.class, coercion));

    add(configuration, ListaPagerPosition.class);
}
...
}

El código del componente lista es demasiado como para ponerlo en esta entrada pero podéis descargarlo desde el apartado referencia. Si a alguien le parece interesante puede sentirse libre de usarlo y modificarlo.

Referencia:
Documentación sobre Apache Tapestry
Código fuente del componente lista paginada para Tapestry 5 (source code)

miércoles, 12 de enero de 2011

Componente cache para Tapestry 5

En el desarrollo de un proyecto web suele ser interesante tener alguna forma de cachear el contenido de ciertas regiones dinámicas de la página que puede ser costoso generarlas pero que es poco habitual que varíen su contenido, como por ejemplo, el menú, el pie de página u otras partes comunes. El cachear estas regiones ayuda a generar la página más rápidamente y supone un ahorro de recursos del servidor con lo que conseguimos dos importantes cosas: evitar que el usuario pierda el interés y se vaya de nuestra web porque la página tarda mucho en cargarse y poder atender a más usuarios con los mismos recursos del servidor.

Para Tapestry 4 había disponible un componente cache en la librería tapfx pero estos componentes no son compatibles con Tapestry 5. Con lo que he tenido la necesidad de desarrollar uno compatible con esta versión (y me ha sorprendido lo sencillo que me ha resultado como veréis por el número de lineas del mismo).

El componente Cache que he desarrollado hace uso de la librería estándar de facto para esta funcionalidad en Java ehcache, a la que si tenemos necesidad posteriormente podemos añadirle características de cache distribuida con terracotta de forma simple y sin afectar al código del componente.

Sin más vayamos a ver el código del componente:

package com.blogspot.elblogdepicodev.tapestry.components;

import java.util.Collections;

import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;

import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.dom.Element;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.RequestGlobals;

public class Cache {

    @Inject
    @Property
    private RequestGlobals requestGlobals;
    
    @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.LITERAL)
    private String cacheName;
    
    @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.LITERAL)
    private String key;
    
    @Parameter(value = "false", defaultPrefix = BindingConstants.PROP)
    private boolean disabled;
    
    @Inject
    private CacheManager cacheManager;

    boolean beforeRenderBody(MarkupWriter writer) {
        if (!disabled) {
            Ehcache cache = cacheManager.getEhcache(cacheName);
            net.sf.ehcache.Element element = (net.sf.ehcache.Element) cache.get(key + "-" + requestGlobals.getRequest().getLocale().toString());
            if (element != null) {
                String value = (String) element.getValue();
                writer.writeRaw(value);
                return false;
            }
            writer.element("cache", Collections.EMPTY_LIST.toArray());
        }
        return true;        
    }
    
    void afterRenderBody(MarkupWriter writer) {
        if (!disabled) {
            Element e = writer.getElement();
            if (e.getName().equals("cache")) {
                String value = e.getChildMarkup();
                writer.end();
                e.pop();
            
                net.sf.ehcache.Element element = new net.sf.ehcache.Element(key + "-" + requestGlobals.getRequest().getLocale().toString(), value);
                Ehcache cache = cacheManager.getEhcache(cacheName);
                cache.put(element);
            }
        }
    }
}

Como se puede ver en el código el componente tiene 3 parámetros: cacheName para saber en que cache de ehcache se cacheará el contenido html, key para identificar el contenido en la cache y disabled para habilitar la cache o deshabilitarla según alguna condición.

La funcionanilidad está dividida en dos métodos: beforeRenderBody y afterRenderBody. En el primero lo que se hace es comprobar si se habilita el uso de la cache, si es que no se devuelve true para que se procesen el cuerpo del componente, si está habilitada la cache (disabled == false) se comprueba si en la cache indicada por el parámetro cacheName existe una clave indicada por el parámetro key, si existe el contenido html del cuerpo del componente se ha generado anteriormente y ya está cachedo por lo que no es necesario volver a procesarlo y se escribe directamente el contenido, finalmente se devuelve false para que no se procese el cuerpo del componente. Si se llama al método afterRenderBody es que se ha procesado el cuerpo del componente, si la cache está habilitada tendremos que almacenar el contenido html generado por los componentes del cuerpo del componente cache para posteriores usos, se obtiene la cache y en la clave indicada junto con el locale en el que se ha generado el contenido se guarda el html del cuerpo.

Otra parte del uso de este componente es definir el servicio CacheManager que se inyecta en el componente. Para ello tendremos que definir un método (buildCacheManager) en la clase del módulo de la aplicación para construir el servicio y poder inyectarlo en el componente.

package com.blogspot.elblogdepicodev.tapestry.services;

import java.util.Collection;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.config.CacheConfiguration;
import net.sf.ehcache.constructs.blocking.BlockingCache;

import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.annotations.SubModule;
import org.apache.tapestry5.ioc.services.Coercion;
import org.apache.tapestry5.ioc.services.CoercionTuple;
import org.apache.tapestry5.util.StringToEnumCoercion;

...

public class AppModule {

    ...
    
    public static CacheManager buildCacheManager() {
        CacheConfiguration defaultCacheConfig = new CacheConfiguration("defaultCache", 10000);
        defaultCacheConfig.setDiskStorePath("java.io.tmpdir");
        defaultCacheConfig.setEternal(false);
        defaultCacheConfig.setTimeToIdleSeconds(120);
        defaultCacheConfig.setTimeToLiveSeconds(120);
        defaultCacheConfig.setOverflowToDisk(false);
        defaultCacheConfig.setDiskPersistent(false);
        defaultCacheConfig.setMemoryStoreEvictionPolicy("LRU");
        Cache defaultCache = new Cache(defaultCacheConfig);

        CacheConfiguration fragmentosTapestryConfig = new CacheConfiguration("fragmentos-tapestry", 50);
        fragmentosTapestryConfig.setEternal(false);
        fragmentosTapestryConfig.setTimeToIdleSeconds(1800);
        fragmentosTapestryConfig.setTimeToLiveSeconds(3600);
        fragmentosTapestryConfig.setOverflowToDisk(false);
        fragmentosTapestryConfig.setDiskPersistent(false);
        fragmentosTapestryConfig.setMemoryStoreEvictionPolicy("LRU");
        Cache fragmentosTapestryCache = new Cache(fragmentosTapestryConfig);        
        
        CacheManager cacheManager = new CacheManager();
        cacheManager.addCache(defaultCache);
        cacheManager.addCache(fragmentosTapestryCache);
        
        Cache cache = cacheManager.getCache("fragmentos-tapestry");
        BlockingCache fragmentosTapestryBlockingCache = new BlockingCache(cache);
        cacheManager.replaceCacheWithDecoratedCache(cache, fragmentosTapestryBlockingCache);
        
        return cacheManager;
    }
}

La configuración del ehcache puede hacerse definiendo un archivo ehcache.xml o como he preferido en este caso de forma programática, aquí se definen dos cache: la cache por defecto y una cache para los fragmentos html que cacheará el componente Cache.

El uso del componente cache sería tan sencillo como:

<t:cache cacheName="fragmentos-tapestry" key="ejemplo">
    [Componentes de tapestry que generarán en contenido HTML que se cacheará]
</t:cache>

Y esto es todo, unas 65 líneas de código que pueden mejorar notablemente el tiempo de respuesta de nuestras aplicaciones del ya de por si excelente rendimiento que ofrece Tapestry. Si este componente te ha resultado interesante o puede serte útil para tu proyecto siente libre de usarlo, modificarlo o proponer mejoras, solo pido que dejes un comentario en esta entrada para conocer a otras personas que usan Tapestry o tienen interés en él.

Referencia:
Documentación sobre Apache Tapestry
http://ehcache.org/
http://www.terracotta.org/
http://andyhot.di.uoa.gr/tapfx/app
http://tapfx.sourceforge.net/