Patrón múltiples vistas de un mismo dato en Tapestry

Escrito por el , actualizado el .
blog-stack java planeta-codigo programacion tapestry
Comentarios

Un proyecto grande contendrá muchos archivos de código fuente, poseer gran cantidad de archivos puede ser una molestia al trabajar con ellos al tener que buscarlos o abrilos. En el caso de las aplicaciones web puede darse el caso de que un mismo dato tenga un archivo diferente por cada forma de visualizarlo, para reducir el número de archivos en estos casos uso el siguiente patrón cuando trabajo con Apache Tapestry con el soporte que ofrece pero que puede ser igualmente aplicado de forma similar a otros frameworks.

Apache Tapestry

Al desarrollar una aplicación web puede que necesitemos mostrar un mismo dato de diferentes formas. Una posibilidad es crear una vista por cada forma diferente que se haya de mostrar el dato. Sin embargo, de esta forma tendremos que crear un archivo diferente por cada forma a visualizar, si esto mismo nos ocurre en múltiples datos nos encontraremos en la situación de que el número de archivos del proyecto crecerá suponiendo una pequeña molestia tener que trabajar con tantos, también y peor aún es que múltiples archivos relacionados no lo estarán salvo que les demos una nomenclatura similar para mantenerlos ordenados por nombre y sean fáciles de encontrar si queremos abrir varios.

Tener tantos archivos puede ser una molestia que denomino de microgestión, esto es, tener muchos archivos pequeñitos. Para evitar microgestionar podemos tener una única vista que con un parámetro determine la forma de representar el dato, mientras que el contenido del archivo tenga alta cohesión me parece adecuado e incluso mejor ya que las diferentes vistas muy posiblemente serán parecidas con lo que quizá dupliquemos algo de código que será mejor tenerlo en un mismo archivo que en varios diferentes.

En este artículo comentaré una forma de como realizar esto usando el framework Apache Tapestry con las posibilidad que ofrece que a mi me ha resultado muy práctica, algo similar podría usarse en otros frameworks.

En Tapestry en una vista se pueden tener múltiples componentes Block cuya misión es agrupar otros componentes que como resultado de procesarse producirán el html. Por otra parte está el componente Delegate que indicándole en el parámetro to un componente Block lo procesa emitiendo el contenido html que generen los componentes que contenga. Teniendo en el código Java asociado al componente que mostrará el dato de diferentes formas un método que con cierta lógica devuelva un componente Block a visualizar podemos conseguir el objetivo.

En la siguiente vista de un artículo usada en el agregador de bitácoras Blog Stack se ve que en el archivo tml de la vista hay varios componentes Block y un componente Delegate tal y como he comentado que se puede hacer. El componente bloque excerptBlock es muy similar al componente fullBlock diferenciándose en que el primero emite un extracto del contenido del artículo con ${contentExcerpt} […] y el segundo el artículo completo con <t:outputraw value=“content”/>.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<t:container xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd" xmlns:p="tapestry:parameter">

<t:delegate to="block"/>

<t:block id="excerptBlock">
    <article t:type="any" itemscope="" itemtype="http://schema.org/BlogPosting">
        <header><t:outputraw value="getTag('open')"/><a t:type="any" href="${post.url}" target="target" itemprop="sameAs">${post.title}</a><t:outputraw value="getTag('close')"/></header>

        <p class="post-info">
            <span itemprop="dateModified" datetime="${data.get('microdataDate')}">${data.get('date')}</span>,
            <span>fuente <a t:type="any" href="${source.pageUrl}" target="target">${source.name}</a></span><t:if test="labels">,</t:if>
            <t:if test="labels">
                etiquetas
                <t:loop source="labels" value="label"><a t:type="pagelink" page="label" context="labelContext"><span itemprop="articleSection">${label.name}</span></a>&nbsp;</t:loop>
            </t:if>
        </p>

        <p itemprop="description" class="text-justify">
            ${contentExcerpt} [...]
        </p>
        <p>
            <a t:type="any" href="${post.url}" target="target" itemprop="sameAs">Leer artículo completo</a>
        </p>
    </article>
</t:block>

<t:block id="fullBlock">
    <article t:type="any" itemscope="" itemtype="http://schema.org/BlogPosting">
        <header><t:outputraw value="getTag('open')"/><a t:type="any" href="${post.url}" target="target" itemprop="sameAs">${post.title}</a><t:outputraw value="getTag('close')"/></header>

        <p class="post-info" style="font-weight: bold;">
            <span itemprop="dateModified" datetime="${data.get('microdataDate')}">${data.get('date')}</span>,
            <span>fuente <a t:type="any" href="${source.pageUrl}" target="target">${source.name}</a></span><t:if test="labels">,</t:if>
            <t:if test="labels">
                etiquetas
                <t:loop source="labels" value="label"><a t:type="any" href="${labelAbsoluteUrl}"><span itemprop="articleSection">${label.name}</span></a>&nbsp;</t:loop>
            </t:if>
        </p>

        <p itemprop="description" class="text-justify">
            <t:outputraw value="content"/>
        </p>
    </article>
</t:block>

</t:container>

En la clase Java asociada al componente está el método getBlock que determina el bloque a mostrar. En este caso la lógica es muy sencilla, en base a un parámetro que recibe el componente (mode) indicando la vista del dato que se quiere se devuelve el componente Block adecuado. Las referencias a los componentes Block presentes en la vista se puede inyectar usando la anotación @Inject junto con @Component usando el mismo identificativo en la vista y en el nombre de la propiedad para la referencia del componente.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package info.blogstack.components;

import info.blogstack.misc.Globals;
import info.blogstack.misc.Utils;
import info.blogstack.persistence.jooq.Keys;
import info.blogstack.persistence.jooq.tables.records.LabelRecord;
import info.blogstack.persistence.jooq.tables.records.PostRecord;
import info.blogstack.persistence.jooq.tables.records.SourceRecord;
import info.blogstack.persistence.records.AppPostRecord;
import info.blogstack.services.MainService;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.Block;
import org.apache.tapestry5.annotations.Cached;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.internal.services.LinkSource;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

public class PostComponent {

    private DateTimeFormatter DATETIME_FORMATTER = DateTimeFormat.forPattern("EEEE, dd 'de' MMMM 'de' yyyy").withLocale(Globals.LOCALE);
    private DateTimeFormatter MICRODATA_DATETIME_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm");
    
    enum Mode {
        HOME, POST, ARCHIVE, NEWSLETTER, DEFAULT
    }
    
    private static int NUMBER_LABELS = 4;
    
    @Parameter
    @Property
    private PostRecord post;
    
    @Parameter(value = "default", defaultPrefix = BindingConstants.LITERAL)
    @Property
    private Mode mode;
    
    @Property
    private LabelRecord label;
    
    @Inject
    private MainService service;
    
    @Inject
    private LinkSource linkSource;
    
    @Inject
    private Block excerptBlock;
    
    @Inject
    private Block fullBlock;
    
    public Object[] getContext() {
        return Utils.getContext(post, post.fetchParent(Keys.POST_SOURCE_ID));
    }
    
    public Block getBlock() {
        switch (mode) {
            case HOME:           
            case ARCHIVE:
            case POST:
            case DEFAULT:
                return excerptBlock;
            case NEWSLETTER:
                return fullBlock;
            default:
                throw new IllegalArgumentException();
            
        }
    }
    
    public String getTag(String key) {
        Map<String, String> m = new HashMap<String, String>();
        m.put("h1:open", "<h1>");
        m.put("h1:close", "</h1>");
        m.put("h2:open", "<h2>");
        m.put("h2:close", "</h2>");

        String tag = null;
        switch (mode) {
            case HOME:           
            case ARCHIVE:
            case NEWSLETTER:
            case DEFAULT:
                tag = "h2";
                break;
            case POST:
                tag = "h1";
                break;
            default:
                throw new IllegalArgumentException();
            
        }
        
        String k = String.format("%s:%s", tag, key);
        return m.get(k);
    }
    
    @Cached(watch = "post")
    public List<LabelRecord> getLabels() {
        return service.getLabelDAO().findByPost(post, NUMBER_LABELS, true);
    }
    
    @Cached(watch = "post")
    public String getContentExcerpt() {
        AppPostRecord apost = post.into(AppPostRecord.class);
        return apost.getContentExcerpt();
    }
    
    @Cached(watch = "post")
    public String getContent() {
        AppPostRecord apost = post.into(AppPostRecord.class);
        return apost.getContent();
    }

    @Cached(watch = "post")
    public Map<String, Object> getData() {
        AppPostRecord apost = post.into(AppPostRecord.class);
        Map<String, Object> datos = new HashMap<>();
        if (apost.getPublishdate() != null) {
            datos.put("date", DATETIME_FORMATTER.print(apost.getPublishdate()));
            datos.put("microdataDate", MICRODATA_DATETIME_FORMATTER.print(apost.getPublishdate()));
        }
        if (apost.getUpdatedate() != null) {
            datos.put("date", DATETIME_FORMATTER.print(apost.getUpdatedate()));
            datos.put("microdataDate", MICRODATA_DATETIME_FORMATTER.print(apost.getUpdatedate()));
        }
        return datos;
    }
    
    public String getTarget() {
        return (mode == Mode.POST) ? null : "_blank";
    }
    
    public SourceRecord getSource() {
        return post.fetchParent(Keys.POST_SOURCE_ID);
    }
    
    public Object[] getLabelContext() {
        return Utils.getContext(label);
    }
    
    public String getLabelAbsoluteUrl() {
        return linkSource.createPageRenderLink("label", true, getLabelContext()).toAbsoluteURI();     
    }
}
Portada libro: PlugIn Tapestry

Libro PlugIn Tapestry

Si te interesa Apache Tapestry descarga gratis el libro de más de 300 páginas que he escrito sobre este framework en el formato que prefieras, PlugIn Tapestry: Desarrollo de aplicaciones y páginas web con Apache Tapestry, y el código de ejemplo asociado. En el libro comento detalladamente muchos aspectos que son necesarios en una aplicación web como persistencia, pruebas unitarias y de integración, inicio rápido, seguridad, formularios, internacionalización (i18n) y localización (l10n), AJAX, ... y como abordarlos usando Apache Tapestry.