
- Ruby on Rails
- Symfony
- Grails
- Y quizá algunos otros menos conocidos
Como resultado al final disponer de un framework que tenga scaffolding con el tiempo puede no suponer tanto ahorro de tiempo como puede parecer en un principio. Tapestry no proporciona un scaffolding para hacer estos CRUD aunque si proporciona varios componentes avanzados que ayudan mucho a realizarlos (Grid, BeanEditor). A continuación pondré un ejemplo de una funcionalidad similar a un CRUD realizada con el framework Apache Tapestry. En el ejemplo se podrá mantener una tabla de una base de datos pudiendo dar de alta una nuevos registros, eliminarlos, modificarlos y ver sus datos en una tabla con paginación Ajax. En el se verá que todo esto no supone más de 200 líneas de código Java y 80 líneas de código de presentación en tml. Necesitará solo dos archivos, uno para código Java, otro para el de presentación tml e increiblemente ninguno para código Javascript a pesar de hacer la paginación via Ajax. En estos números no estoy incluyendo varias clases de apoyo como la entidad a persistir o su DAO (Data Access Object) ya que no son propios de un único scaffolding sino que se podría utilizar para todos los que tengamos en la aplicación.
En la entrada sobre persistencia JPA usando Tapestry ya expliqué como construir el servicio y la transaccionalidad. En el siguiente código utilizaré ese servicio DAO para el acceso a la base de datos, no cambia nada. Me centraré en poner el código necesario para la página que necesita proporcionar la funcionalidad del scaffolding.
A continuación el código Java en la que principalmente Tapestry se encarga de llamar al método que actúa de escuchador para cada evento que se produzca en la página, en función del evento se llamará a la operación DAO adecuada y actualizar el estado del controlador.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.tapestry.jpa.pages.admin; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import javax.persistence.EntityManager; | |
import org.apache.tapestry5.Block; | |
import org.apache.tapestry5.ComponentResources; | |
import org.apache.tapestry5.SymbolConstants; | |
import org.apache.tapestry5.annotations.Cached; | |
import org.apache.tapestry5.annotations.Component; | |
import org.apache.tapestry5.annotations.Property; | |
import org.apache.tapestry5.beaneditor.BeanModel; | |
import org.apache.tapestry5.corelib.components.Form; | |
import org.apache.tapestry5.grid.GridDataSource; | |
import org.apache.tapestry5.ioc.annotations.Inject; | |
import org.apache.tapestry5.ioc.annotations.Symbol; | |
import org.apache.tapestry5.services.BeanModelSource; | |
import org.apache.tapestry5.services.TranslatorSource; | |
import es.com.blogspot.elblogdepicodev.tapestry.jpa.dao.ProductoDAO; | |
import es.com.blogspot.elblogdepicodev.tapestry.jpa.entities.Producto; | |
import es.com.blogspot.elblogdepicodev.tapestry.jpa.misc.JPAGridDataSource; | |
import es.com.blogspot.elblogdepicodev.tapestry.jpa.misc.Pagination; | |
public class ProductoAdmin { | |
private enum Modo { | |
ALTA, EDICION, LISTA | |
} | |
@Inject | |
private ProductoDAO dao; | |
@Inject | |
private EntityManager entityManager; | |
@Inject | |
@Symbol(SymbolConstants.TAPESTRY_VERSION) | |
@Property | |
private String tapestryVersion; | |
@Inject | |
private TranslatorSource translatorSource; | |
@Inject | |
private BeanModelSource beanModelSource; | |
@Inject | |
private Block edicionBlock, listaBlock; | |
@Inject | |
private ComponentResources resources; | |
@Component | |
private Form form; | |
private Modo modo; | |
@Property | |
private Producto producto; | |
void onActivate(Long id, Modo modo) { | |
setModo(modo, (id == null) ? null : dao.findById(id)); | |
} | |
Object[] onPassivate() { | |
return new Object[] { (producto == null) ? null : producto.getId(), (modo == null) ? null : modo.toString().toLowerCase() }; | |
} | |
void setupRender() { | |
if (modo == null) { | |
setModo(Modo.LISTA, null); | |
} | |
} | |
void onPrepareForSubmitFromForm() { | |
onPrepareForSubmitFromForm(null); | |
} | |
void onPrepareForSubmitFromForm(Long id) { | |
if (id != null) { | |
// Si se envía un id se trata de una edición, buscarlo | |
producto = dao.findById(id); | |
} | |
if (producto == null) { | |
producto = new Producto(); | |
} | |
} | |
Object onCanceledFromForm() { | |
setModo(Modo.LISTA, null); | |
return ProductoAdmin.class; | |
} | |
void onSuccessFromForm() { | |
dao.persist(producto); | |
setModo(Modo.LISTA, null); | |
} | |
void onNuevo() { | |
setModo(Modo.ALTA, null); | |
} | |
void onEditar(Long id) { | |
setModo(Modo.EDICION, dao.findById(id)); | |
} | |
void onEliminarTodos() { | |
dao.removeAll(); | |
setModo(Modo.LISTA, null); | |
} | |
void onEliminar(Long id) { | |
producto = dao.findById(id); | |
dao.remove(producto); | |
setModo(Modo.LISTA, null); | |
} | |
public boolean hasProductos() { | |
return getSource().getAvailableRows() > 0; | |
} | |
public GridDataSource getSource() { | |
return new JPAGridDataSource<Producto>(entityManager, Producto.class) { | |
@Override | |
public List<Producto> find(Pagination pagination) { | |
return dao.findAll(pagination); | |
} | |
}; | |
} | |
public BeanModel<Producto> getModel() { | |
BeanModel<Producto> model = beanModelSource.createDisplayModel(Producto.class, resources.getMessages()); | |
model.exclude("id"); | |
model.add("action", null).label("").sortable(false); | |
return model; | |
} | |
public Block getBlock() { | |
switch (modo) { | |
case ALTA: | |
case EDICION: | |
return edicionBlock; | |
default: | |
case LISTA: | |
return listaBlock; | |
} | |
} | |
// La anotacion @Cached permite cachar el resultado de un método de forma | |
// que solo se evalúe | |
// una vez independientemente del número de veces que se llame en la | |
// plantilla de visualización. | |
@Cached | |
public Map<String, String> getLabels() { | |
Map<String, String> m = new HashMap<String, String>(); | |
switch (modo) { | |
case ALTA: | |
m.put("titulo", "Alta producto"); | |
m.put("guardar", "Crear producto"); | |
break; | |
case EDICION: | |
m.put("titulo", "Modificación producto"); | |
m.put("guardar", "Modificar producto"); | |
break; | |
default: | |
} | |
return m; | |
} | |
private void setModo(Modo modo, Producto producto) { | |
switch (modo) { | |
case ALTA: | |
this.producto = new Producto(); | |
break; | |
case EDICION: | |
if (producto == null) { | |
modo = Modo.ALTA; | |
this.producto = new Producto(); | |
} else { | |
this.producto = producto; | |
} | |
break; | |
default: | |
case LISTA: | |
this.producto = null; | |
break; | |
} | |
this.modo = modo; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html t:type="layout" | |
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" | |
xmlns:p="tapestry:parameter"> | |
Versión: <b>${tapestryVersion}</b><br/> | |
<t:holaMundo/><br/> | |
<t:delegate to="block"/> | |
<t:block id="listaBlock"> | |
<h1>Lista de productos</h1> | |
<t:grid source="source" row="producto" model="model" rowsPerPage="2" lean="true" inPlace="true" class="table table-bordered table-condensed"> | |
<p:nombreCell> | |
<t:eventlink event="editar" context="producto.id">${producto.nombre}</t:eventlink> | |
</p:nombreCell> | |
<p:actionCell> | |
<t:eventlink event="eliminar" context="producto.id" class="btn btn-danger">Eliminar</t:eventlink> | |
</p:actionCell> | |
<p:empty> | |
<p>No hay productos.</p> | |
</p:empty> | |
</t:grid> | |
<t:eventlink event="nuevo" class="btn btn-primary">Nuevo producto</t:eventlink> | |
<t:if test="hasProductos()"><t:eventlink event="eliminarTodos" class="btn btn-danger">Eliminar todos</t:eventlink></t:if> | |
</t:block> | |
<t:block id="edicionBlock"> | |
<t:remove> | |
En otros frameworks la lógica para obtener el título del bloque según se trate de un alta o una modificación, | |
probablemente se hiciese metiendo lógica en la plantilla de presentación, dado que Tapestry permite llamar a métodos | |
de la clase Java asociada al componente es mejor dejar esa lógica en el código Java de esta manera la plantilla será más | |
sencilla y clara además de aprovecharnos del compilador. labels es un método definido en la página admin.producto | |
que devuelve un mapa. | |
</t:remove> | |
<h1>${labels.get('titulo')}</h1> | |
<t:form t:id="form" context="producto.id" validate="producto" clientValidation="none" class="form-horizontal"> | |
<t:errors class="literal:alert alert-error"/> | |
<div class="control-group"> | |
<t:label for="nombre" class="control-label"/> | |
<div class="controls"> | |
<input t:type="textfield" t:id="nombre" value="producto.nombre" size="100" label="Nombre"/> | |
</div> | |
</div> | |
<div class="control-group"> | |
<t:label for="descripcion" class="control-label"/> | |
<div class="controls"> | |
<input t:type="textarea" t:id="descripcion" value="producto.descripcion" label="Descripción"/> | |
</div> | |
</div> | |
<div class="control-group"> | |
<t:label for="cantidad" class="control-label"/> | |
<div class="controls"> | |
<input t:type="textfield" t:id="cantidad" value="producto.cantidad" size="4" label="Cantidad"/> | |
</div> | |
</div> | |
<div class="control-group"> | |
<t:label for="fecha" class="control-label"/> | |
<div class="controls"> | |
<input t:type="textfield" t:id="fecha" type="date" value="producto.fecha" label="Fecha"/> | |
</div> | |
</div> | |
<div class="control-group"> | |
<div class="controls"> | |
<input t:type="submit" class="btn btn-primary" value="prop:labels.get('guardar')"/> | |
<t:if test="producto.id"><t:eventlink event="eliminar" context="producto.id" class="btn btn-danger">Eliminar</t:eventlink></t:if> | |
<input t:type="submit" class="btn" value="Cancelar" mode="cancel"/> | |
</div> | |
</div> | |
</t:form> | |
</t:block> | |
</html> |
Finalmente, como comenté un una entrada anterior sobre seguridad sobre XSS (Cross Site Scripting) el mantenimiento está protegido ante este tipo de ataques (puedes probar crear un producto con <script>alert(1);</script>), para ello no hemos hecho nada especial.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.tapestry.jpa.misc; | |
import java.util.List; | |
import javax.persistence.EntityManager; | |
import javax.persistence.criteria.CriteriaBuilder; | |
import javax.persistence.criteria.CriteriaQuery; | |
import org.apache.tapestry5.grid.GridDataSource; | |
import org.apache.tapestry5.grid.SortConstraint; | |
@SuppressWarnings("rawtypes") | |
public abstract class JPAGridDataSource<T> implements GridDataSource { | |
private EntityManager entityManager; | |
private Class type; | |
private int start; | |
private List<T> results; | |
public JPAGridDataSource(EntityManager entityManager, Class type) { | |
this.entityManager = entityManager; | |
this.type = type; | |
} | |
@Override | |
public int getAvailableRows() { | |
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); | |
CriteriaQuery<Long> cq = cb.createQuery(Long.class); | |
cq.select(cb.count(cq.from(type))); | |
return entityManager.createQuery(cq).getSingleResult().intValue(); | |
} | |
@Override | |
public void prepare(int start, int end, List<SortConstraint> sort) { | |
Pagination pagination = new Pagination(start, end, Sort.fromSortConstraint(sort)); | |
this.start = start; | |
results = find(pagination); | |
} | |
public abstract List<T> find(Pagination pagination); | |
@Override | |
public Object getRowValue(int i) { | |
return results.get(i - this.start); | |
} | |
@Override | |
public Class getRowType() { | |
return type; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.tapestry.jpa.misc; | |
import java.util.ArrayList; | |
import java.util.List; | |
import javax.persistence.criteria.CriteriaBuilder; | |
import javax.persistence.criteria.Order; | |
import javax.persistence.criteria.Root; | |
public class Pagination { | |
private int start; | |
private int end; | |
private List<Sort> sort; | |
public Pagination(int start, int end, List<Sort> sort) { | |
this.start = start; | |
this.end = end; | |
this.sort = sort; | |
} | |
public int getStart() { | |
return start; | |
} | |
public void setStart(int start) { | |
this.start = start; | |
} | |
public int getEnd() { | |
return end; | |
} | |
public void setEnd(int end) { | |
this.end = end; | |
} | |
public List<Sort> getSort() { | |
return sort; | |
} | |
public void setSort(List<Sort> sort) { | |
this.sort = sort; | |
} | |
@SuppressWarnings("rawtypes") | |
public List<Order> getOrders(Root root, CriteriaBuilder cb) { | |
List<Order> orders = new ArrayList<Order>(); | |
for (Sort s : sort) { | |
Order o = s.getOrder(root, cb); | |
if (o != null) { | |
orders.add(o); | |
} | |
} | |
return orders; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.tapestry.jpa.misc; | |
import java.util.ArrayList; | |
import java.util.List; | |
import javax.persistence.criteria.CriteriaBuilder; | |
import javax.persistence.criteria.Order; | |
import javax.persistence.criteria.Root; | |
import org.apache.tapestry5.grid.SortConstraint; | |
public class Sort { | |
private String property; | |
private Direction direction; | |
public Sort(String property, Direction direction) { | |
this.property = property; | |
this.direction = direction; | |
} | |
public String getProperty() { | |
return property; | |
} | |
public void setProperty(String property) { | |
this.property = property; | |
} | |
public Direction getDirection() { | |
return direction; | |
} | |
public void setDirection(Direction direction) { | |
this.direction = direction; | |
} | |
@SuppressWarnings("rawtypes") | |
public Order getOrder(Root root, CriteriaBuilder builder) { | |
switch (direction) { | |
case ASCENDING: | |
return builder.asc(root.get(property)); | |
case DESCENDING: | |
return builder.desc(root.get(property)); | |
default: | |
return null; | |
} | |
} | |
public static List<Sort> fromSortConstraint(List<SortConstraint> sort) { | |
List<Sort> cs = new ArrayList<Sort>(); | |
for (SortConstraint s : sort) { | |
String property = s.getPropertyModel().getPropertyName(); | |
Direction direction = Direction.UNSORTED; | |
switch (s.getColumnSort()) { | |
case ASCENDING: | |
direction = Direction.ASCENDING; | |
break; | |
case DESCENDING: | |
direction = Direction.DESCENDING; | |
break; | |
default: | |
} | |
Sort c = new Sort(property, direction); | |
cs.add(c); | |
} | |
return cs; | |
} | |
} |
Como en el resto de entradas el código fuente completo lo puedes encontrar en mi repositorio de GitHub. Si quieres probarlo en tu equipo lo puedes hacer de forma muy sencilla con los siguientes comandos y sin instalar nada previamente (salvo java y git). Si no dispones de git para clonar mi repositorio de GitHub puedes obtener el código fuente del repositorio en un archivo zip con el anterior enlace.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ git clone git://github.com/picodotdev/elblogdepicodev.git | |
$ cd elblogdepicodev/TapestryJPA | |
$ ./gradlew tomcatRun | |
# Abrir en el navegador http://localhost:8080/TapestryJPA/admin/producto |
Referencia:
Documentación sobre Apache Tapestry