No, un tag JSP o un tag de Grails no es equivalente a un componente de Tapestry

Escrito por el .
java opinion planeta-codigo tapestry programacion
Enlace permanente Comentarios

Alguna vez que he dado una presentación sobre Apache Tapestry después de la misma me comentaron que eso mismo que explicaba se podía hacer con el framework que esa persona usaba. En un proyecto la tecnología no es es lo más importante pero es una herramienta que puede facilitar en gran medida el desarrollo. Respecto a los componentes de Tapestry alguien puede pensar que son iguales a los tag que existen en las tecnologías de presentación como JSP o Grails. En este artículo comentaré algunas diferencias importantes que los hace más y muy interesantes junto con otras características de framework.

Apache Tapestry

Java

Viendo el panel Kanban de la herramienta de peticiones JIRA que usamos para registrar y priorizar las siguiente tareas en la empresa que trabajo hay unas cuantas que consisten en dado un listado de compras poder realizar operaciones sobre múltiples filas sin salir de la pantalla del listado. La necesidad es evitar que los usuarios de la aplicación hagan las acciones de forma individual de forma repetitiva, evitarles esto harán que sean más productivos y podrán desarrollar su trabajo mejor y más rápido. Así de sencillo, aparentemente.

Esta necesidad que en la realidad será implementada con Grails quería compararla con una implementación equivalente usando Apache Tapestry porque como en muchas otras necesidades intuyo que con Tapestry implementarlas es significativamente más sencillo y con un resultado de implementación como en este caso con el que quedo más a gusto.

La necesidad

Definiendo más la necesidad hasta ahora cada fila del listado tiene un conjunto de botones para realizar acciones individuales y ahora se quiere al final del listado otro conjunto de botones para realizar acciones sobre las compras que se seleccionen de forma múltiple. Para seleccionar las compras se usará un checkbox colocado al inicio de cada fila. Para algunas acciones el usuario ha de introducir información adicional cosa que hará con un diálogo modal que ya existe pero que hasta ahora solo permitía hacer la acción de forma individual. Las mismas acciones se realizarán en varias páginas de la aplicación (después de la acción se deberá volver a la página en la que se estaba), solo se podrán hacer las acciones múltiples si en todas las compras seleccionadas es posible realizar esa acción y el contenido de los diálogos solicitando información adicional podrán depender de las compras seleccionadas. Las acciones en el ejemplo serán habilitar o deshabilitar. Determinar las acciones posibles de una compra es compleja y saber si una acción es posible no depende solo de información en la propia compra sino de otras entidades del sistema, en el ejemplo no será así pero se tendrá en cuenta en la implementación.

Esta sería una imagen del prototipo de los botones para hacer acciones múltiples, seleccionar compras y el diálogo modal para introducir información adicional.

Listado de productos Modal solicitando información adicional

Listado y modal de la necesidad expuesta

En la necesidad real las filas son compras pero en el ejemplo usaré la entidad Product. Las acciones en el ejemplo serán habilitar para la que no será necesaria información adicional, la otra acción será deshabilitar para la que se necesitará introducir una razón con un modal.

Las posibilidades

Para implementar técnicamente el que solo se puedan hacer las acciones múltiples según los productos seleccionadas se habilitarán o deshabilitarán los botones con JavaScript sin peticiones AJAX adicionales al servidor para ello toda la información necesaria deberá estar en el cliente. En este caso bastará habilitar o deshabilitar cada botón según si esa acción es posible en todas los productos seleccionadas pero eso podría no bastar ya que se podría requerir que productos fuesen del mismo vendedor. En el ejemplo con un atributo en un elemento HTML de la fila que contenga las acciones posibles separadas por comas bastará. De esta forma no habrá que hacer consultas adicionales al servidor mediante peticiones AJAX en cada nueva selección.

Sin embargo, como el contenido de los diálogos depende del producto o productos seleccionadas se hará una petición AJAX para obtener su contenido. De esta forma el contenido de los diálogos no tendrá que estar precargado (el número de acciones podría ser una decena) en el cliente ni generarlo con JavaScript en cliente que sería algo más complicado que usar la propia tecnología para generar contenido que está presente en el servidor y posiblemente más propenso a errores por usar JavaScript.

La implementación con Apache Tapestry

Definida la necesidad y unas pocas notas voy a poner el código de como con Apache Tapestry implementar la solución. La página del listado será la siguiente. En el checkbox de selección se añade el atributo data-product-actions con las acciones posibles que se obtienen del servicio AppService con el método getAvaliableActions. El componente de Tapestry actions generará el código de los botones tanto para los individuales en su uso <t:actions> con el parámetro product como múltiples en su uso con el parámetro type.

 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
package io.github.picodotdev.blogbitix.tapestrymultipleactions.pages;

import io.github.picodotdev.blogbitix.tapestrymultipleactions.entities.Product;
import io.github.picodotdev.blogbitix.tapestrymultipleactions.services.AppService;
import io.github.picodotdev.blogbitix.tapestrymultipleactions.services.ProductRepository;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.beaneditor.BeanModel;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.BeanModelSource;

import java.util.List;
import java.util.stream.Collectors;

public class Index {

	@Property
	private Product product;

    @Inject
    private AppService service;

    @Inject
    private ProductRepository repository;

	@Inject
	private BeanModelSource beanModelSource;

	@Inject
	private ComponentResources resources;

    public List<Product> getProducts() {
        System.out.print(1);
    	return repository.findAll();
    }

	public BeanModel<Product> getModel() {
		BeanModel<Product> model = beanModelSource.createDisplayModel(Product.class, resources.getMessages());
		model.add("select", null).label("").sortable(false);
		model.add("action", null).label("Actions").sortable(false);
		model.include("select", "name", "stock", "state", "action");
        model.reorder("select", "name", "stock", "state", "action");
		return model;
	}

	public String getActions(Product product) {
	    return service.getAvaliableActions(product).stream().map(a -> a.name().toString().toLowerCase()).collect(Collectors.joining(","));
    }
}
Index.java
 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
<!DOCTYPE html>
<html t:type="layout" title="Products" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd" xmlns:p="tapestry:parameter">

<h1>Products</h1>
<t:grid t:id="grid" source="products" row="product" model="model" rowsPerPage="10" lean="true" class="table table-hover">
    <p:selectCell>
        <input t:type="any" type="checkbox" name="product" value="prop:product.id" data-product-actions="prop:getActions(product)"/>
    </p:selectCell>
    <p:nameCell>
        ${product.name}
    </p:nameCell>
    <p:actionCell>
        <t:actions product="product"/>
    </p:actionCell>
    <p:empty>
        <p>No products.</p>
    </p:empty>
</t:grid>
<div class="btn-toolbar">
    <t:actions type="literal:product"/>
</div>

<t:disableProductsModal/>

</html>
Index.tml

El código para mostrar las acciones con botones para un determinado producto o para los productos es el siguiente. El mismo componente se encargará de realizar en el servidor la acción habilitar que no necesita modal. Con un poco de JavaScript, jQuery y Underscore se habilitarán o deshabilitarán los botones y se mostrará el diálogo para la acción deshabilitar.

 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
package io.github.picodotdev.blogbitix.tapestrymultipleactions.components;

import io.github.picodotdev.blogbitix.tapestrymultipleactions.entities.Product;
import io.github.picodotdev.blogbitix.tapestrymultipleactions.misc.Utils;
import io.github.picodotdev.blogbitix.tapestrymultipleactions.services.ProductRepository;
import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.Block;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.services.TypeCoercer;
import org.apache.tapestry5.services.RequestGlobals;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;

import java.util.List;

public class Actions {

    public enum Type {
        PRODUCT
    }

    @Parameter(defaultPrefix = BindingConstants.PROP)
    @Property
    private Product product;

    @Parameter
    private Type type;

    @Property
    private String ids;

    @Inject
    private ProductRepository repository;

    @Inject
    private TypeCoercer coercer;

    @Inject
    private Block productActionsBlock, productsActionsBlock;

    @Inject
    private RequestGlobals globals;

    @Environmental
    private JavaScriptSupport javascriptSupport;

    void afterRender() {
        if (product != null || type == Type.PRODUCT) {
            javascriptSupport.require("app/actions");
        }
    }

    void onSuccessFromProductForm() {
        repository.enable(product);
    }

    void onSuccessFromProductsForm() {
        repository.enable(getProducts());
    }

    public Block getBlock() {
        if (product != null) {
            return productActionsBlock;
        }
        switch (type) {
            case PRODUCT: return productsActionsBlock;
        }
        return null;
    }

    public String getEnabled(Product product) {
        return (product.isEnabled()) ? "disabled" : null;
    }

    public String getDisabled(Product product) {
        return (product.isDisabled()) ? "disabled" : null;
    }

    private List<Product> getProducts() {
        return Utils.getProducts(ids, coercer, repository);
    }
}
Actions.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!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 t:id="productActionsBlock">
    <t:form t:id="productForm">
        <t:hidden id="id" value="product"/>
        <input t:type="any" type="submit" value="Enable" disabled="prop:getEnabled(product)" data-product-action="enable" class="btn btn-primary btn-xs"/>
        <input t:type="any" type="button" value="Disable" disabled="prop:getDisabled(product)" data-product-action="disable" class="btn btn-danger btn-xs"/>
    </t:form>
</t:block>

<t:block t:id="productsActionsBlock">
    <t:form t:id="productsForm">
        <t:hidden id="ids" value="ids"/>
        <input t:type="any" type="button" value="Enable" disabled="disabled" data-products-action="enable" class="btn btn-primary"/>
        <input t:type="any" type="button" value="Disable" disabled="disabled" data-products-action="disable" class="btn btn-danger"/>
    </t:form>
</t:block>

</t:container>
Actions.tml
 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
define("app/actions", ["jquery", "underscore", "app/modals"], function($, _, modals) {
    function ProductActions() {
        var that = this;

        $("input[type='checkbox'][name='product']").on('change', function() {
            var actions = that.getAttribute(that.getCheckedCheckboxes(), 'data-product-actions');
            actions = _.map(actions, function(actions) {
                return actions.split(',');
            });
            actions = _.intersection.apply(_, actions);
            $("input[data-products-action]").attr('disabled', 'disabled');
            _.each(actions, function(action) {
                $("input[data-products-action='" + action + "']").removeAttr('disabled');
            });
        });

        $("input[type='button'][data-products-action='enable']").on('click', function() {
            var ids = that.getAttribute(that.getCheckedCheckboxes(), 'value');
            $(this).closest('form').find("#ids").val(ids.join(','));
            $('#productsForm')[0].submit();
        });

        $("input[type='button'][data-product-action='disable']").on('click', function() {
            var id = $(this).closest('form').find("#id").val();
            new modals.DisableProductsModal().show([id]);
        });

        $("input[type='button'][data-products-action='disable']").on('click', function() {
            var ids = that.getAttribute(that.getCheckedCheckboxes(), 'value');
            new modals.DisableProductsModal().show(ids);
        });

        $("input[type='checkbox'][name='product']").trigger('change');
    }

    ProductActions.prototype.getCheckedCheckboxes = function() {
        return $("input[type='checkbox'][name='product']:checked");
    };

    ProductActions.prototype.getAttribute = function(elements, attribute) {
        return elements.map(function() {
            return $(this).attr(attribute);
        }).get();
    };

    function init(spec) {
        new ProductActions(spec);
    }

    init();
});
actions.js

El código del modal para deshabilitar sería el siguiente. En el método show recibe los ids de los productos a deshabilitar y recupera del servidor el contenido de diálogo con una petición AJAX. El componente del modal se encargará de hacer el deshabilitado de los productos y la recarga de la página si finaliza correctamente o de mostrar los errores de validación que se produzcan si no se ha introducido el motivo.

  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
package io.github.picodotdev.blogbitix.tapestrymultipleactions.components;

import io.github.picodotdev.blogbitix.tapestrymultipleactions.entities.Product;
import io.github.picodotdev.blogbitix.tapestrymultipleactions.misc.Utils;
import io.github.picodotdev.blogbitix.tapestrymultipleactions.services.ProductRepository;
import org.apache.commons.lang3.StringUtils;
import org.apache.tapestry5.Block;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.corelib.components.Select;
import org.apache.tapestry5.corelib.components.Zone;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.services.TypeCoercer;
import org.apache.tapestry5.services.RequestGlobals;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;

import java.util.List;
import java.util.stream.Collectors;

public class DisableProductsModal {

    @Property
    private String ids;

    @Property
    private ProductRepository.DisableReason reason;

    @Property
    private String url;

    @Inject
    private ProductRepository repository;

    @Inject
    private RequestGlobals requestGlobals;

    @Inject
    private TypeCoercer coercer;

    @Inject
    private Block reloadBlock;

    @Inject
    private ComponentResources componentResources;

    @Environmental
    private JavaScriptSupport javascriptSupport;

    @Component
    private Form form;

    @Component
    private Select select;

    @Component
    private Zone zone;

    public ProductRepository.DisableReason[] getModel() {
        return ProductRepository.DisableReason.values();
    }

    void setupRender() {
        url = componentResources.createEventLink("show").toAbsoluteURI();
    }

    public boolean isRender() {
        return requestGlobals.getRequest().isXHR();
    }

    public String getProductsLabel() {
        List<Product> products = getProducts();
        return String.format("%s (%s)", products.stream().map(p -> p.getName()).collect(Collectors.joining(", ")), products.size());
    }

    public Object onShow() {
        ids = requestGlobals.getRequest().getParameter("ids");
        return zone;
    }

    private void onValidateFromForm() {
        if (StringUtils.isBlank(ids)) {
            form.recordError("A product is required.");
        }
        if (reason == null) {
            form.recordError(select, "Reason is required.");
        }
    }

    private void onSuccessFromForm() {
        repository.disable(getProducts(), reason);
    }

    private Object onSubmitFromForm() {
        if (form.getHasErrors()) {
            return zone.getBody();
        }
        return reloadBlock;
    }

    private List<Product> getProducts() {
        return Utils.getProducts(ids, coercer, repository);
    }
}
DisableProductsModal.java
 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">

<div t:type="any" id="disableProductsModal" data-url="prop:url" class="modal fade" tabindex="-1" role="dialog">
    <t:zone t:id="zone" id="disableProductsModalZone">
        <t:if test="render">
            <div class="modal-dialog">
                <t:form t:id="form" zone="disableProductsModalZone" class="form-horizontal">
                    <t:hidden id="ids" value="ids"/>
                    <div class="modal-content">
                        <div class="modal-header">
                            <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                            <h4 class="modal-title">Disable products</h4>
                        </div>
                        <div class="modal-body">
                            <div id="errors">
                                <t:errors/>
                            </div>
                            <div class="form-group">
                                <label class="col-sm-2 control-label">Products</label>
                                <div class="col-sm-10">
                                    <p class="form-control-static">${productsLabel}</p>
                                </div>
                            </div>
                            <div class="form-group">
                                <t:label for="select" class="col-sm-2 control-label"/>
                                <div class="col-sm-10">
                                    <t:select t:id="select" value="reason" model="model" label="Reason" class="form-control"/>
                                </div>
                            </div>
                        </div>
                        <div class="modal-footer">
                            <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                            <t:submit class="btn btn-primary" value="Save"/>
                        </div>
                    </div>
                </t:form>
            </div>
        </t:if>
    </t:zone>
</div>

<t:block t:id="reloadBlock">
    <script type="text/javascript">window.location.reload();</script>
</t:block>

</t:container>
DisableProductsModal.tml
 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
define("app/modals", ["jquery", "bootstrap/modal"], function($, modal) {
    function DisableProductsModal() {
    }

    DisableProductsModal.prototype.show = function(ids) {
        var that = this;

        $.get({
            url: $('#disableProductsModal').attr('data-url'),
            data: {
                'ids': ids.join(',')
            },
            success: function(html) {
                that.reset();
                $('#disableProductsModal').html(html.content);
                $('#disableProductsModal').modal('show');
            }
        });
    };

    DisableProductsModal.prototype.reset = function() {
        $('#errors', '#disableProductsModal').remove();
    };

    return {
        DisableProductsModal: DisableProductsModal
    };
});
modals.js
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run

Algunas diferencias con Servlets/JSP y Grails

La tecnología de presentación de páginas web Java con Java Server Pages o JSP permiten encapsular con un tag la generación de un trozo de HTML no en el propio JSP sino en ese tag que en código Java pudiendo incluir la llamada a un JSP. Los tags y librerías de tags son una forma de reutilizar esas partes de generación de código en el mismo proyecto y entre proyectos. Los tags además son una forma de abstraernos del funcionamiento interno del tag haciendo que solo necesitemos conocer sus parámetros.

Si usamos JSP usar librerías de tags es una buena idea, sin embargo, tiene algunas limitaciones como que requieren un archivo descriptor en formato XML que las defina y aunque pudiendo saber que parámetros definen y cuáles son requeridos no define el tipo de los parámetros que requiere. Los archivos XML en la época actual han caído en desuso porque son propensos a errores, errores que no son detectados hasta tiempo de ejecución, de los peores tipos de errores. Por otro lado, que los tags no especifiquen el tipo de parámetro que requiere cada uno hace que debamos inspeccionar el código fuente del tag con lo que la ventaja de abstraerse del funcionamiento no es del todo completa. Si por algún cambio el tipo de parámetro cambia hay que adaptar todos los usos del tag, si alguno no se hace nuevamente se producirán errores en tiempo de ejecución.

Grails usa GSP, una tecnología de presentación similar a los JSP. También dispone de tags que no requieren definir los tags en un archivo XML simplificando su uso pero que igualmente adolecen de algunos problemas como los JSP. Por un lado, los tags de Grails no disponen un mecanismo para hacer requerido un determinado parámetro con lo que deberemos incluir la comprobación con código nosotros, tampoco define el tipo de parámetros que requiere. También aunque hacer más simple su desarrollo al no tener un descriptor XML como en los tag JSP hace que haya que inspeccionar el código fuente para saber qué parámetros tiene, si son requeridos y cuál es el tipo del parámetro. Todo esto hace que puedan producirse errores en tiempo de ejecución y errores que no son producidos hasta que se ejercita el tag con un mal uso o un uso desactualizado al igual que usando los tag JSP.

En Apache Tapestry todo son componentes, las páginas también son componentes con la característica de que no están embebidos en otro componente. Un componente de Apache Tapestry sería similar a un tag de JSP o un tag de Grails, con ciertas similitudes pero no iguales en aspectos importantes. De pronto, un componente de Tapestry define los parámetros que necesita y si son requeridos pero también define el tipo del parámetro. Como se aprecia en las páginas de documentación de los componentes integrados de serie en Apache Tapestry se puede conocer esta información sin necesidad de conocer el código fuente del componente, documentación que podemos generar para los componentes que nosotros desarrollemos. Los parámetros, si son requeridos y sus tipos forman el contrato del componente y es lo único que deberemos conocer para usarlos, su funcionamiento interno nos es irrelevante que incluye el código JavaScript que necesite, podría que CSS y literales internacionalizados.

Pero esas no son las únicas diferencias con los tags de JSP o de Grails y es que estas son solo tecnologías de presentación, la V del patrón MVC. Los componentes de Tapestry aparte de encapsular la lógica de presentación también pueden encapsular lógica de controlador, en el conocido patrón MVC además de V pueden ser C con lo que encapsulan aún más funcionalidad. La lógica de presentación y controlador en los JSP y Grails está separada pero ambas lógicas no son independientes, están relacionadas, en Tapestry está encapsulada en el mismo componente.

Los componentes de Tapestry usan el modelo pull en vez del modelo push haciendo innecesario construir un objeto Map que pasar a la vista, haciendo que sea la plantilla la que solicite al controlador los datos que necesita y haciendo que el controlador no sepa que datos necesita la vista. El controlador solo deberá tener las propiedades y métodos que necesite la vista. Dado que en las plantillas tml de la vista no se pueden incluir expresiones complejas hace que no contengan lógica que estará en el controlador asociado que es código Java donde tendremos la ayuda del compilador para detectar errores.

Para volver a la misma página en Spring MVC, Struts o Grails posiblemente deberíamos recibir además información para retornar a la misma página en la que estábamos cosa que es innecesaria en Tapestry por su concepto de contexto de activación de página y el patrón Redirect-After-Post hará que al recargar la página por código con window.localtion.reload(); después de una petición POST el navegador no muestre un diálogo modal informando al usuario de que se reenviarán datos.

Diálogo recargar después de petición POST en Firefox

Diálogo recargar después de petición POST en Firefox

React y Polymer son tecnologías de cliente en algunos aspectos similares a los componentes de Apache Tapestry pero con la diferencia de que unos son para el navegador del cliente y otros para el servidor, nada nos impide en la misma aplicación usar en el cliente React y Polymer y en el servidor Apache Tapestry. Nótese en el código del caso anterior que Tapestry ofrece integración con JavaScript de un modo que no existe ni en Spring MVC, Struts o Grails e incorpora de serie RequireJS, Underscore y jQuery, un componente de Tapestry puede requerir la cargar de un recurso de JavaScript y desde el componente es posible pasar datos al JavaScript usando el servicio JavaScriptSupport.

Esto es solo un pequeño ejemplo de las posibilidades de Apache Tapestry me dejo muchas otras como los eventos, translators, encoders, coerces, librerías de componentes, inversion of control, AJAX, validaciones de formularios, … En un proyecto las herramientas no son lo más importante pero el lenguaje de programación, framework y librerías importan, hay 10 razones para seguir usando Java y varios motivos para elegir Apache Tapestry.

Finalizando

Lamentablemente hasta el momento no he tenido una oportunidad laboral de comprobar y demostrar que como en este ejemplo pero basado en una necesidad real que con Tapestry la implementación de la solución es más sencilla, menos propensa a errores y que la productividad no está relacionado con escribir unas pocas líneas de código menos con un lenguaje menos verboso o dejar de escribir puntos y comas al final de las líneas, más aún con las novedades de Java 8. Quizá un día llegue esa oportunidad :|.

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.



Comparte el artículo: