ViewBaseAction tiene una propiedad
view que puedes usar desde dentro de
execute() mediante
getView(). Este objeto del tipo
org.openxava.view.View permite manejar la interfaz de usuario, en este caso borramos los datos visualizados usando
getView().clear().
También usamos
addMessage(). Todos los mensajes añadidos con
addMessage() se mostrarán al usuario al final de la ejecución de la acción. Puedes, bien añadir el mensaje a mostrar, o bien un id de una entrada en
src/main/resources/i18n/facturacion-messages_es.properties.
La siguiente imagen muestra el comportamiento del módulo
Factura después de añadir la acción de borrar personalizada:
Por supuesto, este es un comportamiento tonto. Añadamos el comportamiento real. Para marcar como borrada la factura actual sin borrarla realmente, necesitamos añadir una nueva propiedad a
Factura. Llamémosla
eliminado:
@Hidden // No se mostrará por defecto en las vistas y los tabs
@Column(columnDefinition="BOOLEAN DEFAULT FALSE") // Para llenar con falses en lugar de con nulos
boolean eliminado;
Como ves, es una propiedad booleana simple y llana. El único detalle es el uso de la anotación
@Hidden. Indica que cuando una vista o lista tabular por defecto sea generada la propiedad
eliminado no se mostrará; aunque si la pones explícitamente en
@View(members=) o
@Tab(properties=) sí que se mostrará. Usa esta anotación para marcar aquellas propiedades de uso interno del programador pero que no tienen sentido para el usuario final.
Usamos
@Column(columnDefinition=) para llenar la columna con
falses en lugar de con nulos. Aquí puedes poner la definición SQL de la columna para enviar a la base de datos. Es más sencillo que actualizar la base de datos pero el código es más dependiente de la base de datos.
Ya estamos preparados para escribir el código real de la acción:
public void execute() throws Exception {
Factura factura = XPersistence.getManager().find(
Factura.class,
getView().getValue("oid")); // Leemos el id de la vista
factura.setEliminado(true); // Modificamos el estado de la entidad
addMessage("object_deleted", "Factura"); // El mensaje de confirmación de borrado
getView().clear(); // Borramos la vista
}
El efecto visual es el mismo, se ve un mensaje y la vista se borra, pero en este caso hacemos algo de lógica. Buscamos la entidad
Factura asociada con la vista actual y entonces cambiamos el valor de su propiedad
eliminado. No necesitas hacer nada más, porque OpenXava confirma automáticamente la transacción JPA. Es decir, puedes leer cualquier objeto y modificar su estado en una acción, y cuando la acción finalice los cambios se almacenarán en la base de datos.
Pero hemos dejado algunos cabos sueltos. El botón de "borrar" sigue en la vista después de haber borrado la entidad, es decir, cuando no hay un objeto seleccionado, además si el usuario lo pulsa la instrucción para buscar fallará y un mensaje un tanto técnico e ininteligible se le mostrará a nuestro desamparado usuario. Podemos refinar este caso no mostrando el botón, tal como cuando pulsamos el botón
Nuevo. Observa la ligera modificación al método
execute():
public void execute() throws Exception {
// ...
getView().clear();
getView().setKeyEditable(true); // Crearemos una nueva entidad
}
Con
getView().setKeyEditable(true) indicamos que creamos una nueva entidad y como nuestra acción
delete tiene el atributo
disponible-en-nuevo="false", entonces, el botón de borrar no se mostrará.
Ahora que ya sabes como escribir tus propias acciones personalizadas, es tiempo de aprender como escribir código genérico.
Acciones genéricas
El código actual de
EliminarFactura refleja la forma típica de escribir acciones. Es código concreto que accede directamente a entidades concretas para manipularlas.
Pero a veces puedes encontrarte alguna lógica en tu acción susceptible de ser usada y reusada por toda tu aplicación, incluso en todas tus aplicaciones. En este caso, puedes utilizar algunas técnicas para crear código más reutilizable y así convertir tus acciones personalizadas en acciones genéricas.
Aprendamos estas técnicas para escribir código más genérico en nuestras acciones.
Código genérico con MapFacade
Imagínate que quieres usar tu
EliminarFactura también para pedidos. Es más, imagínate que quieres usarla para cualquier entidad de la aplicación con una propiedad
eliminado. Es decir, quieres una acción para marcar como borrada, en lugar de borrarla de la base de datos, no solo facturas sino cualquier entidad. En este caso, el código actual de tu acción no es suficiente. Necesitas un código más genérico.
Puedes conseguir una acción más genérica usando la clase de OpenXava llamada
MapFacade.
MapFacade (del paquete
org.openxava.model) te permite manejar el estado de tus entidades usando mapas, esto es conveniente ya que
View trabaja con mapas. Además, los mapas son más dinámicos que los objetos y por tanto más apropiados para crear código genérico.
Reescribamos nuestra acción para borrar. Primero, renombremos
EliminarFactura (una acción para borrar objetos de tipo
Factura) como
EliminarParaFacturacion (la acción para borrar objetos en la aplicación
facturacion). Esto implica que tienes que cambiar la entrada para la acción en
controladores.xml, para cambiar el nombre de la clase. Tal como se muestra a continuación:
<accion nombre="delete"
modo="detail" confirmar="true"
clase="com.tuempresa.facturacion.acciones.EliminarParaFacturacion"
icono="delete"
disponible-en-nuevo="false"
atajo-de-teclado="Control D"/>
Ahora, renombra tu
EliminarFactura como
EliminarParaFacturacion y reescribe su código:
package com.tuempresa.facturacion.acciones;
import java.util.*;
import org.openxava.actions.*;
import org.openxava.model.*;
public class EliminarParaFacturacion extends ViewBaseAction {
public void execute() throws Exception {
Map<String, Object> valores =
new HashMap<>(); // Los valores a modificar en la entidad
valores.put("eliminado", true); // Asignamos true a la propiedad 'eliminado'
MapFacade.setValues( // Modifica los valores de la entidad indicada
getModelName(), // Un método de ViewBaseAction
getView().getKeyValues(), // La clave de la entidad a modificar
valores); // Los valores a cambiar
resetDescriptionsCache(); // Reinicia los caches para los combos
addMessage("object_deleted", getModelName());
getView().clear();
getView().setKeyEditable(true);
getView().setEditable(false); // Dejamos la vista como no editable
}
}
Esta acción hace lo mismo que la anterior, pero no tiene ninguna referencia a la entidad
Factura. Por tanto, es genérica, puedes usarla con
Pedido,
Autor o cualquier otra entidad siempre y cuando tenga una propiedad
eliminado. El truco está en
MapFacade la cual permite modificar una entidad a partir de mapas. Puedes obtener esos mapas directamente de la vista (usando
getView().getKeyValues() por ejemplo) o puedes crearlos de una manera genérica, como en el caso del mapa
valores.
Adicionalmente puedes ver dos pequeñas mejoras sobre la versión antigua. Primero, llamamos a
resetDescriptionsCache(), un método de
BaseAction. Este método borra el caché usado para los combos. Cuando modificas una entidad, si quieres que los combos reflejen los cambios en la sesión actual has de llamar a este método. Segundo, llamamos a
getView().setEditable(false). Esto inhabilita los controles de la vista, para impedir que el usuario rellene datos en la vista. Para crear una nueva entidad el usuario tiene que pulsar el botón
Nuevo.
Ahora tu acción está lista para ser usada por cualquier otra entidad. Podríamos copiar y pegar el controlador
Factura como
Pedido en
controladores.xml. De esta forma, nuestra lógica genérica para borrar se usaría para
Pedido. ¡Espera un momento! ¿He dicho “copiar y pegar”? No queremos arder en el fuego eterno del infierno, ¿verdad? Así que usaremos una forma más automática de insuflar nuestra nueva acción a todos lo módulos. Aprendámoslo en la siguiente sección.
Cambiar el controlador por defecto para todos los módulos
Si usas
EliminarParaFacturacion solo para
Factura entonces definirla en el controlador
Factura de
controladores.xml es una buena táctica. Pero, recuerda que hemos mejorado esta acción precisamente para hacerla reutilizable, por tanto reutilicémosla. Vamos a asignar un controlador a todos los módulos de un solo golpe.
El primer paso es cambiar el nombre del controlador de
Factura a
Facturacion:
<controlador nombre="Facturacion">
<hereda-de controlador="Typical"/>
<accion nombre="delete" modo="detail" confirmar="true"
clase="com.tuempresa.facturacion.acciones.EliminarParaFacturacion"
icono="delete"
disponible-en-nuevo="false"
atajo-de-teclado="Control D"/>
</controlador>
Como ya sabes, cuando usas el nombre de una entidad, como
Factura, como nombre de controlador, ese controlador será usado por defecto en el módulo de esa entidad. Por lo tanto, si cambiamos el nombre del controlador, este controlador no se usará para la entidad. De hecho el controlador
Facturacion no es usado por ningún módulo, porque no hay ninguna entidad llamada "Facturacion".
Queremos que el controlador
Facturacion sea el controlador usado por defecto por todos los módulos de la aplicación. Para hacer esto hemos de modificar el archivo
aplicacion.xml que esta en la carpeta
src/main/resources/xava de tu aplicación. Dejándolo así:
<?xml version = "1.0" encoding = "ISO-8859-1"?>
<!DOCTYPE aplicacion SYSTEM "dtds/aplicacion.dtd">
<aplicacion nombre="facturacion">
<!--
Se asume un módulo por defecto para cada entidad con el
controlador de <modulo-defecto/>
-->
<modulo-defecto>
<controlador nombre="Facturacion" />
</modulo-defecto>
</aplicacion>
De esta forma tan simple todos los módulos de tu aplicación ahora usarán
Facturacion en lugar de
Typical como controlador por defecto. Trata de ejecutar tu módulo
Factura y verás como la acción se ejecuta al borrar un elemento.
Puedes probar el módulo
Pedido también, pero no funcionará porque no tiene la propiedad
eliminado. Podríamos añadir la propiedad
eliminado a
Pedido y funcionaría con nuestro nuevo controlador, pero en vez de “copiar y pegar” la propiedad
eliminado en todas nuestras entidades, vamos a usar una técnica mejor. Veámoslo en la siguiente sección.
Volvamos un momento al modelo
Tu tarea ahora sería añadir la propiedad
eliminado a todas las entidades para que la
EliminarParaFacturacion funcione. Esta es una buena ocasión para usar herencia y así poner el código común en el mismo sitio, en lugar de usar el infame “copiar y pegar”.
Primero quita la propiedad
eliminado de
Factura:
public class Factura extends DocumentoComercial {
//@Hidden // No se mostrará por defecto en las vistas y los tabs
//@Column(columnDefinition="BOOLEAN DEFAULT FALSE")
//boolean eliminado;
// El resto del código...
}
Y ahora crea una nueva superclase mapeada llamada
Eliminable en el paquete
com.tuempresa.facturacion.modelo:
package com.tuempresa.facturacion.modelo;
import javax.persistence.*;
import org.openxava.annotations.*;
import lombok.*;
@MappedSuperclass @Getter @Setter
public class Eliminable extends Identificable {
@Hidden
@Column(columnDefinition="BOOLEAN DEFAULT FALSE")
boolean eliminado;
}
Eliminable es una superclase mapeada. Recuerda, una superclase mapeada no es una entidad, es una clase con propiedades, métodos y anotaciones de mapeo para ser usada como superclase para entidades.
Eliminable extiende de
Identificable, por tanto cualquier entidad que extienda
Eliminable tendrá las propiedades
oid y
eliminado.
Ahora puedes convertir cualquiera de tus entidades actuales en
Eliminable, solo has de cambiar
Identificable por
Eliminable como superclase. Hagámoslo con
DocumentoComercial:
// abstract public class DocumentoComercial extends Identificable {
abstract public class DocumentoComercial extends Eliminable {
// El resto del código...
}
Dado que
Factura y
Pedido son
DocumentoComercial, ahora puedes usar tu controlador
Facturacion con la
EliminarParaFaturacion contra ellos.
Nos queda un sutil detalle. La entidad
Pedido tiene un método
@PreRemove para hacer una validación al borrar. Esta validación puede impedir el borrado. Podemos mantener esta validación para nuestro borrado personalizado simplemente sobrescribiendo el método
setEliminado() de
Pedido:
public class Pedido extends DocumentoComercial {
// ...
@PreRemove
private void validarPreBorrar() { // Ahora este método no se ejecuta
if (factura != null) { // automáticamente ya que el borrado real no se produce
throw new javax.validation.ValidationException(
XavaResources.getString("no_puede_borrar_pedido_con_factura"));
}
}
public void setEliminado(boolean eliminado) {
if (eliminado) validarPreBorrar(); // Llamamos a la validación explícitamente
super.setEliminado(eliminado);
}
}
Con este cambio la validación funciona igual que en el caso de un borrado de verdad, así preservamos el comportamiento original intacto.
Metadatos para un código más genérico
Con tu actual código de
Factura y
Pedido el funcionamiento es bueno. Aunque si tratas de borrar una entidad de cualquier otro módulo, recibirás un feo mensaje de error. La figura siguiente muestra lo que ocurre cuando intentas borrar un
Cliente.
Sí, si tu entidad no tiene una propiedad
eliminado, la acción de borrar falla miserablemente. Es verdad que gracias a la clase
Eliminable puedes añadir la propiedad
eliminado a todas tus entidades fácilmente, pero puede ser que quieras tener entidades que puedan marcarse como borradas (
Eliminable) y entidades que sean borradas de verdad de la base de datos. Queremos que la acción funcione bien en todos los casos.
OpenXava almacena metadatos para todas tus entidades y puedes acceder a estos metadatos desde tu código. Esto te permite, por ejemplo, averiguar si la entidad tiene una propiedad
eliminado.
El siguiente código muestra una modificación en la acción para preguntar si la entidad tiene una propiedad
eliminado, si no el proceso de borrado no se realiza:
public void execute() throws Exception {
if (!getView().getMetaModel() // Metadatos de la entidad actual
.containsMetaProperty("eliminado")) // ¿Tiene una propiedad 'eliminado'?
{
addMessage( // De momento, mostramos un mensaje si la propiedad 'eliminado' no está
"No eliminado, ésta no tiene propiedad eliminado");
return;
}
// El resto del código...
}
La clave aquí es
getView().getMetaModel() que devuelve un objeto
MetaModel del paquete
org.openxava.model.meta. Este objeto contiene metadatos sobre la entidad actualmente visualizada en la vista. Puedes preguntar por propiedades, referencias, colecciones, métodos y otra metainformación sobre la entidad. Consulta la
API de MetaModel para aprender más. En este caso preguntamos si la propiedad
eliminado existe.
De momento solo mostramos un mensaje. Mejorémoslo para borrar de verdad la entidad.
Llamar a otra acción desde una acción
Queremos que cuando la entidad no tenga una propiedad
eliminado sea borrada de la base de datos de la manera habitual. Nuestra primera opción es escribir nosotros mismos la lógica de borrado, realmente no es una tarea complicada. Sin embargo, es mucho mejor usar la lógica estándar de borrado de OpenXava, así no necesitamos escribir ninguna lógica de borrado y usamos un código más refinado y probado.
Para hacer esto OpenXava permite llamar a una acción desde dentro de una acción, simplemente llama a
executeAction() indicando el nombre calificado de la acción, es decir, el nombre del controlador y el nombre de la acción. En nuestro caso para llamar a la acción estándar de OpenXava para borrar usaríamos
executeAction("CRUD.delete"). El siguiente código muestra
EliminarParaFacturacion modificada para llamar a la acción estándar de OpenXava para borrar.
package com.tuempresa.facturacion.acciones;
import java.util.*;
import org.openxava.actions.*;
import org.openxava.model.*;
public class EliminarParaFacturacion extends ViewBaseAction {
public void execute() throws Exception {
if (!getView().getMetaModel().containsMetaProperty("eliminado")) {
executeAction("CRUD.delete"); // LLamamos a la acción estándar
return; // de OpenXava para borrar
}
// Cuando "eliminado" existe usamos nuestra propia lógica de borrado
Map<String, Object> valores = new HashMap<>();
valores.put("eliminado", true);
MapFacade.setValues(getModelName(), getView().getKeyValues(), valores);
resetDescriptionsCache();
addMessage("object_deleted", getModelName());
getView().clear();
getView().setKeyEditable(true);
getView().setEditable(false);
}
}
Simplemente llamamos a
executeAction(“CRUD.delete”) si queremos que la acción por defecto para borrar de OpenXava se ejecute. Así, escribimos nuestra propia lógica de borrado (en este caso marcar una propiedad con
true) para algunos casos y “dejamos pasar” la lógica estándar para los demás.
Ahora puedes usar tu
EliminarParaFacturacion contra cualquier entidad. Si la entidad tiene una propiedad
eliminado se marcará como borrada, en caso contrario se borrará físicamente de la base de datos.
Este ejemplo te muestra como usar
executeAction() para refinar la lógica estándar de OpenXava. Otra forma de hacerlo es mediante la herencia. Veamos cómo en la siguiente sección.
Refinar la acción de búsqueda por defecto
EliminarParaFacturacion ahora funciona bastante bien, aunque no tiene demasiada utilidad. Es inútil marcar como borrados los objetos, si el resto de la aplicación no es consciente de ello. Es decir, hemos de modificar otras partes de la aplicación para que traten los objetos “marcados como borrados” como si no existieran.
El lugar más obvio para empezar es la acción de búsqueda. Si borras una factura y después tratas de buscarla, no deberías encontrarla. La siguiente figura muestra como funciona la búsqueda en OpenXava.
La primera cosa que puedes observar en la figura anterior es que buscar en modo detalle es más flexible de lo que parece. El usuario puede introducir cualquier valor en cualquier campo, o combinación de campos, y pulsar en el botón de refrescar. Entonces el primer objeto cuyos valores coinciden es cargado en la vista.
Puedes pensar: Bueno, puedo refinar la acción
CRUD.refresh de la misma forma que he refinado
CRUD.delete. Por supuesto, puedes hacerlo así. Y funcionaría; cuando el usuario pulsara en la acción del modo detalle tu código se ejecutaría. Aunque, aquí hay un detalle un tanto sutil. La lógica de buscar no se llama sólo desde el modo detalle, sino también desde otros puntos del módulo OpenXava. Por ejemplo, cuando el usuario escoge un detalle, la acción
List.viewDetail coge la clave de la fila, la pone en la vista de detalle y después ejecuta la acción de buscar.
Para hacerlo bien, hemos de poner la lógica para buscar en un módulo, en la misma acción, y todas las acciones que necesiten buscar encadenarán con esta acción. Tal como muestra la anterior figura.
Esto queda más claro si ves el código de la acción estándar
CRUD.refresh, que es
org.openxava.actions.SearchAction cuyo código se muestra a continuación:
public class SearchAction extends BaseAction
implements IChainAction { // Encadena con otra acción
public void execute() throws Exception { // No hace nada
}
public String getNextAction() throws Exception { // De IChainAction
return getEnvironment() // Para acceder a las variables de entorno
.getValue("XAVA_SEARCH_ACTION");
}
}
Como ves, la acción estándar para buscar en modo detalle no hace nada, simplemente redirige a otra acción. Esta otra acción se define en una variable de entorno llamada
XAVA_SEARCH_ACTION, que lee usando
getEnvironment(). Por la tanto, si quieres refinar la lógica de búsqueda de OpenXava la mejor manera es definiendo tu acción como valor para
XAVA_SEARCH_ACTION. Hagámoslo pues de esta manera.
Para dar valor a la variable de entorno edita el archivo
controladores.xml en la carpeta
src/main/resources/xava de tu proyecto y añade al principio la línea
<var-entorno /> como ves a continuación:
...
<controladores>
<!-- Para definir un valor global para una variable de entorno -->
<var-entorno
nombre="XAVA_SEARCH_ACTION"
valor="Facturacion.buscarExcluyendoEliminados" />
<controlador nombre="Facturacion">
...
De esta forma el valor para la variable de entorno
XAVA_SEARCH_ACTION en cualquier módulo será “Facturacion.buscarExcluyendoEliminados”, por lo tanto la lógica de búsqueda para todos los módulos estará en esta acción.
El siguiente paso lógico es definir esta acción en el controlador "Facturacion" del mismo
controladores.xml:
<controlador nombre="Facturacion">
...
<accion nombre="buscarExcluyendoEliminados"
oculta="true"
clase="com.tuempresa.facturacion.acciones.BuscarExcluyendoEliminados" />
<!-- oculta="true" : Así la acción no se mostrará en la barra de botones -->
</controlador>
Y ahora es el momento para escribir la clase de implementación. En este caso solo queremos refinar la lógica de búsqueda, es decir, la búsqueda se ha de hacer de la forma convencional, con la excepción de las entidades con una propiedad
eliminado cuyo valor sea
true. Para hacer este refinamiento vamos a usar herencia. El siguiente código muestra la acción:
package com.tuempresa.facturacion.acciones;
import java.util.*;
import javax.ejb.*;
import org.openxava.actions.*;
public class BuscarExcluyendoEliminados
extends SearchByViewKeyAction { // La acción estándar de OpenXava para buscar
private boolean esEliminable() { // Pregunta si la entidad tiene una propiedad 'eliminado'
return getView().getMetaModel()
.containsMetaProperty("eliminado");
}
protected Map getValuesFromView() // Coge los valores visualizados desde la vista
throws Exception // Estos valores se usan como clave al buscar
{
if (!esEliminable()) { // Si no es 'eliminable' usamos la lógica estándar
return super.getValuesFromView();
}
Map<String, Object> valores = super.getValuesFromView();
valores.put("eliminado", false) ; // Llenamos la propiedad 'eliminado' con false
return valores;
}
protected Map getMemberNames() // Los miembros a leer de la entidad
throws Exception
{
if (!esEliminable()) { // Si no es 'eliminable' ejecutamos la lógica estándar
return super.getMemberNames();
}
Map<String, Object> miembros = super.getMemberNames();
miembros.put("eliminado", null); // Queremos obtener la propiedad 'eliminado'
return miembros; // aunque no esté en la vista
}
protected void setValuesToView(Map valores) // Asigna los valores desde
throws Exception // la entidad a la vista
{
if (esEliminable() && // Si tiene una propiedad 'eliminado' y
(Boolean) valores.get("eliminado")) { // vale true
throw new ObjectNotFoundException(); // lanzamos la misma excepción que
// OpenXava lanza cuando el objeto no se encuentra
}
else {
super.setValuesToView(valores); // En caso contrario usamos la lógica estándar
}
}
}
La lógica estándar para buscar está en la clase
SearchByViewKeyAction. Básicamente, la lógica de esta clase consiste en coger los valores de la vista, si la propiedad
id está presente buscará por id, en caso contrario coge todos los valores en la vista para usar en la condición de búsqueda, devolviendo el primer objeto que coincida con la condición. Queremos usar este mismo algoritmo cambiando solo algunos detalles sobre la propiedad
eliminado. Por tanto, en vez de sobrescribir el método
execute(), que contiene la lógica de búsqueda, sobrescribimos tres métodos protegidos, que son llamados desde
execute() y contienen algunos puntos susceptibles de ser refinados.
Después de estos cambios prueba tu aplicación, y verás como cuando tratas de buscar una factura o un pedido, si están borrados no se muestran. Incluso si escoges una factura o pedido borrado desde el modo lista se producirá un error y no verás los datos en modo detalle.
Has visto como al definir una variable de entorno
XAVA_SEARCH_ACTION en
controladores.xml estableces la lógica de búsqueda de una manera global, es decir, para todos los módulos a la vez. Si lo que quieres es definir una acción de búsqueda para un módulo en particular, simplemente define la variable de entorno en la definición del módulo en
aplicacion.xml, tal como mostramos a continuación:
<modulo nombre="Producto">
<!--Para dar un valor local a la variable de entorno para este módulo -->
<var-entorno
nombre="XAVA_SEARCH_ACTION"
valor="Producto.buscarPorNumero"/>
<modelo nombre="Producto"/>
<controlador nombre="Producto"/>
<controlador nombre="Facturacion"/>
</modulo>
De esta forma para el módulo
Producto la variable de entorno
XAVA_SEARCH_ACTION valdrá
“Producto.buscarPorNumero”. Es decir, las variables de entorno son locales a los módulos. Aunque definas un valor por defecto en
controladores.xml, siempre tienes la opción de sobrescribirlo para un módulo concreto. La variables de entorno son una forma práctica de configurar tu aplicación declarativamente.
No queremos una forma especial de búsqueda para
Producto, por tanto no añadas esta definición de módulo a tu
aplicacion.xml. Este código solo era para ilustrar el uso de
<var-entorno /> en los módulos.
Modo lista
Ya casi tenemos el trabajo hecho. Cuando el usuario borra una entidad con una propiedad
eliminado la entidad se marca como borrada en vez de ser borrada físicamente de la base de datos. Y si el usuario trata de buscar una entidad “marcada como borrada” no puede verla en modo detalle. Aunque, el usuario todavía puede ver las entidades “marcadas como borradas” en modo lista, y lo que es peor si borra las entidades desde modo lista, éstas son efectivamente borradas de la base de datos. Atemos estos cabos sueltos.
Filtrar datos tabulares
Solo las entidades con su propiedad
eliminado igual a
false tienen que ser mostradas en modo lista. Esto es muy fácil de conseguir usando la anotación
@Tab. Esta anotación te permite definir la forma en que los datos tabulares (los datos mostrados en modo lista) son visualizados y te permite además definir una condición. Por tanto, añadir esta anotación a las entidades que tengan una propiedad
eliminado es suficiente para conseguir nuestro objetivo, tal como se muestra a continuación:
@Tab(baseCondition = "${eliminado} = false")
public class Factura extends DocumentoComercial { ... }
@Tab(baseCondition = "${eliminado} = false")
public class Pedido extends DocumentoComercial { ... }
Y de esta forma tan sencilla el modo lista no mostrará las entidades “marcadas como borradas”.
Acciones de lista
El único detalle que nos queda es el borrar las entidades desde modo lista, éstas han de marcarse como borradas si procede. Vamos a refinar las acciones estándares
CRUD.deleteSelected y
CRUD.deleteRow de la misma manera que hemos hecho con
CRUD.delete.
Primero, sobrescribimos la acciones
deleteSelected y
deleteRow para nuestra aplicación. Añade la siguiente definición de acción a tu controlador
Facturacion definido en
controladores.xml:
<controlador nombre="Facturacion">
<hereda-de controlador="Typical"/>
<!-- ... -->
<accion nombre="deleteSelected" modo="list" confirmar="true"
procesar-elementos-seleccionados="true"
icono="delete"
clase="com.tuempresa.facturacion.acciones.EliminarSeleccionadoParaFacturacion"
atajo-de-teclado="Control D"/>
<accion nombre="deleteRow" modo="NONE" confirmar="true"
clase="com.tuempresa.facturacion.acciones.EliminarSeleccionadoParaFacturacion"
icono="delete"
en-cada-fila="true"/>
</controlador>
La acciones estándar para borrar entidades desde modo lista son
deleteSelected (para borrar las filas seleccionadas) y
deleteRow (la acción que aparece en cada fila). Estas acciones están definidas en el controlador
CRUD.
Typical extiende de
CRUD y
Facturacion extiende
Typical; así que el controlador
Facturacion incluye por defecto estas acciones. Dado que hemos definido unas acciones con los mismos nombres, nuestras acciones sobrescriben las estándares. Es decir, de ahora en adelante la lógica para borrar las filas seleccionadas en modo lista está en la clase
EliminarSeleccionadoParaFacturacion. Fíjate como la lógica para ambas acciones están en una única clase Java. El código es el siguiente:
package com.tuempresa.facturacion.acciones;
import org.openxava.actions.*;
import org.openxava.model.meta.*;
public class EliminarSeleccionadoParaFacturacion
extends TabBaseAction // Para trabajar con datos tabulares (lista) por medio de getTab()
implements IChainActionWithArgv { // Encadena con otra acción, indicada con getNextAction()
private String nextAction = null; // Para almacenar la siguiente acción a ejecutar
public void execute() throws Exception {
if (!getMetaModel().containsMetaProperty("eliminado")) {
nextAction="CRUD.deleteSelected"; // 'CRUD.deleteSelected' se ejecutará
// cuando esta acción finalice
return;
}
marcarEntidadesSeleccionadasComoEliminadas(); // La lógica para marcar las
// filas seleccionadas como objetos borrados
}
private MetaModel getMetaModel() {
return MetaModel.get(getTab().getModelName());
}
public String getNextAction() // Obligatorio por causa de IChainAction
throws Exception
{
return nextAction; // Si es nulo no se encadena con ninguna acción
}
public String getNextActionArgv() throws Exception {
return "row=" + getRow(); // Argumento a enviar a la la acción encadenada
}
private void marcarEntidadesSeleccionadasComoEliminadas() throws Exception {
// ...
}
}
Puedes ver como esta acción es bastante parecida a
EliminarParaFacturacion. Si las entidades no tienen la propiedad
eliminado encadena con la acción estándar, en caso contrario ejecuta su propia lógica para borrar las entidades. Aunque en este caso usamos
IChainActionWithArgv en lugar del más sencillo
executeAction() porque necesitamos enviar un argumento a la acción encadenada. Generalmente las acciones para modo lista extienden de
TabBaseAction, así puedes usar
getTab() para obtener el objeto
Tab asociados a la lista. Un
Tab (de
org.openxava.tab) te permite manipular los datos tabulares. Por ejemplo en el método
getMetaModel() preguntamos al
Tab el nombre del modelo para obtener el
MetaModel correspondiente.
Si la entidad tiene una propiedad
eliminado entonces se ejecuta nuestra propia lógica de borrado. Esta lógica está en el método
marcarEntidadesSeleccionadasComoEliminadas() que puedes ver a continuación:
private void marcarEntidadesSeleccionadasComoEliminadas() throws Exception {
Map<String, Object> valores = new HashMap<>(); // Valores a asignar a cada entidad para marcarla
valores.put("eliminado", true); // Pone 'eliminado' a true
Map<String, Object>[] clavesSeleccionadas = getSelectedKeys(); // Obtenemos las filas seleccionadas
if (clavesSeleccionadas != null) {
for (int i = 0; i < clavesSeleccionadas.length; i++) { // Iteramos sobre las filas seleccionadas
Map<String, Object> clave = clavesSeleccionadas[i]; // Obteniendo la clave de cada entidad
try {
MapFacade.setValues( // Modificamos cada entidad
getTab().getModelName(),
clave,
valores);
}
catch (javax.validation.ValidationException ex) { // Si se produce una ValidationException..
addError("no_delete_row", i, clave);
addError("remove_error", getTab().getModelName(), ex.getMessage()); // ...mostramos el mensaje
}
catch (Exception ex) { // Si se lanza cualquier otra excepción, se añade
addError("no_delete_row", i, clave); // un mensaje genérico
}
}
}
getTab().deselectAll(); // Después de borrar deseleccionamos la filas
resetDescriptionsCache(); // Y reiniciamos el caché de los combos para este usuario
}
Como ves la lógica es un simple bucle sobre las claves de las filas seleccionadas, y en cada iteración ponemos a
true la propiedad
eliminado usando el método
MapFacade.setValues(). Atrapamos las excepciones dentro de la iteración del bucle, así si hay algún problema borrando la entidad, esto no afecta al borrado de las demás entidades. Hemos hecho un pequeño refinamiento para el caso de
ValidationException, añadiendo el mensaje de validación (
ex.getMessage()) a los errores a mostrar al usuario.
Al final deseleccionamos todas las filas mediante
getTab().deselectAll(), porque estamos borrando filas, por tanto si no eliminamos la selección, esta se habría recorrido después de la ejecución de la acción.
Hemos llamado a
resetDescriptionsCache() para actualizar las entidades borradas en todos los combos de la actual sesión de usuario. Los combos, es decir las referencias marcadas con
@DescriptionsList, usan el
@Tab de la entidad referenciada para filtrar los datos. Es decir, si tuvieras un combo de facturas o pedidos con la condición
“deleted = false” en el
@Tab, en este caso el contenido del combo cambiaría.
Ahora ya tienes refinada del todo la forma en que tu aplicación borra las entidades. Aunque aún nos quedan cosas interesantes por hacer.
Reutilizar el código de las acciones
Ahora tu aplicación marca como borradas las facturas y pedidos en vez de borrarlos. La ventaja de este enfoque es que el usuario puede restaurar en cualquier momento una factura o pedido borrado por error. Para que esta característica sea útil de verdad has de proporcionar al usuario una herramienta para restaurar las entidades borradas. Vamos a crear un módulo papelera para
Factura y otro para
Pedido para traer los documentos borrados de vuelta a la vida.
Propiedades para crear acciones reutilizables
La papelera que queremos es como la que puedes ver en la siguiente figura. Es una lista de facturas o pedidos donde el usuario pueda seleccionar varias y pulsar en el botón
Restaurar, o simplemente pulsar en el vínculo
Restaurar en la fila del documento que quiera restaurar:
La lógica de esta acción de restaurar es simplemente poner la propiedad
eliminado de las entidades seleccionadas a
false. Es decir, es exactamente la misma lógica que usamos para borrar, pero poniendo
false en vez de
true. Dado que nuestra conciencia no nos permite copiar y pegar, vamos a reutilizar nuestro código actual. La forma de reutilizar es añadiendo una propiedad
restaurar a la acción
EliminarSeleccionadoParaFacturacion, para poder restaurar las entidades borradas.
El siguiente código muestra lo necesario para añadir una propiedad
restaurar a la acción:
public class EliminarSeleccionadoParaFacturacion ... {
//...
@Getter @Setter
boolean restaurar; // Una nueva propiedad
private void marcarEntidadesSeleccionadasComoEliminadas() throws Exception {
Map<String, Object> valores = new HashMap<String, Object>();
// valores.put("eliminado", true); // Pone 'eliminado' a true // En lugar de un true fijo, usamos
valores.put("eliminado", !isRestaurar()); // el valor de la propiedad 'restaurar';
// ...
}
Como puedes ver solo hemos añadido una propiedad
restaurar y el uso de su complemento como nuevo valor para la propiedad
eliminado en la entidad. Es decir, si
restaurar es
false, el caso por defecto, un
true se grabará en
eliminado, así tu acción de borrar borrará. Pero si
restaurar es
true la acción guardará
false en la propiedad
eliminado de la entidad, y por tanto la factura, pedido o cualquier otra entidad estará de nuevo disponible en la aplicación.
Para usar esta acción como una acción para restaurar has de definirla en
controladores.xml, tal como muestra el siguiente código:
<controlador nombre="Papelera">
<accion nombre="restaurar" modo="list"
clase="com.tuempresa.facturacion.acciones.EliminarSeleccionadoParaFacturacion">
<poner propiedad="restaurar" valor="true"/> <!-- Pone la propiedad restaurar a true -->
<!-- antes de llamar al método execute() de la acción -->
</accion>
</controlador>
A partir de ahora puedes referenciar a la acción
Papelera.restaurar cuando necesites una acción para restaurar. Estás reutilizando el mismo código para borrar y restaurar, gracias al elemento
<poner /> de
<accion /> que te permite configurar las propiedades de la acción.
Usemos esta nueva acción de restaurar en los nuevos módulos papelera.
Módulos personalizados
Como ya sabes, OpenXava genera un módulo por defecto para cada entidad de tu aplicación. Aunque, siempre tienes la opción de definir los módulos a mano, bien para refinar el comportamiento del módulo para cierta entidad, o bien para definir una funcionalidad completamente nueva sobre esa entidad. En este caso vamos a crear dos nuevos módulos,
PapeleraFactura y
PapeleraPedido, para restaurar los documentos borrados. Usaremos el controlador
Papelera en ellos. El siguiente código muestra la definición de módulos en el archivo
aplicacion.xml:
<aplicacion nombre="facturacion">
<modulo-defecto>
<controlador nombre="Facturacion"/>
</modulo-defecto>
<modulo nombre="PapeleraFactura">
<var-entorno nombre="XAVA_LIST_ACTION"
valor="Papelera.restaurar"/> <!-- La acción a mostrar en cada fila -->
<modelo nombre="Factura"/>
<tab nombre="Eliminado"/> <!-- Para mostrar solo las entidades borradas -->
<controlador nombre="Papelera"/> <!-- Con solo una acción: restaurar -->
</modulo>
<modulo nombre="PapeleraPedido">
<var-entorno nombre="XAVA_LIST_ACTION" valor="Papelera.restaurar"/>
<modelo nombre="Pedido"/>
<tab nombre="Eliminado"/>
<controlador nombre="Papelera"/>
</modulo>
</aplicacion>
Estos módulos van contra
Factura y
Pedido, pero definen una acción especial como acción de fila usando la variable de entorno
XAVA_LIST_ACTION. La siguiente figura muestra
PapeleraFactura:
Varias definiciones de datos tabulares por entidad
Otro detalle importante es que solo las entidades borradas se muestran en la lista. Esto es posible porque definimos un
@Tab específico indicando su nombre para el módulo. El siguiente código detalla como escoger el
@Tab para un módulo:
<modulo nombre="...">
...
<tab nombre="Eliminado"/> <!-- "Eliminado" es un @Tab definido en la entidad -->
...
</modulo>
Por supuesto, has de tener un
@Tab llamado “Eliminado” en tus entidades
Pedido y
Factura. Tal como se muestra a continuación:
@Tab(baseCondition = "${eliminado} = false") // Tab sin nombre, es el de por defecto
@Tab(name="Eliminado", baseCondition = "${eliminado} = true") // Tab con nombre
public class Factura extends DocumentoComercial { ... }
@Tab(baseCondition = "${eliminado} = false")
@Tab(name="Eliminado", baseCondition = "${eliminado} = true")
public class Pedido extends DocumentoComercial { ... }
Usamos el
@Tab sin nombre como lista por defecto para
Factura y
Pedido, pero tenemos un
@Tab llamado
"Eliminado" que puedes usar para generar una lista con solo las filas borradas. En este caso lo usamos para los módulos papelera. Ahora puedes probar tus nuevos módulos, si no los ves en el menú prueba cerrar sesión y volver a identificarte.
Obsesión por reutilizar
¡Bien hecho! El código de
EliminarSeleccionadoParaFacturacion puede borrar y restaurar entidades, y hemos añadido la capacidad de restaurar con solo un poco más de código, sin copiar y pegar.
Y ahora un enjambre de perniciosos pensamientos bullen en tu cabeza. Seguramente estés pensando “Esta acción no es únicamente para borrar, sino también para borrar y restaurar”, y entonces, “Espera un momento, lo que es en realidad es una acción para actualizar la propiedad
eliminado de la entidad actual”, y tu siguiente pensamiento será “Con tan solo un poco más podemos actualizar cualquier propiedad de la entidad”.
Sí, estás en lo cierto. Con facilidad podemos crear una acción más genérica, una
ActualizarPropiedad por ejemplo, y usarla para declarar tus acciones
deleteSelected y
restaurar, tal como se muestra a continuación:
<accion nombre="deleteSelected" modo="list" confirmar="true"
class="com.tuempresa.facturacion.acciones.ActualizarPropiedad"
atajo-de-teclado="Control D">
<poner propiedad="propiedad" valor="eliminado" />
<poner propiedad="valor" valor="true" />
</accion>
<accion nombre="restaurar" modo="list"
class="com.tuempresa.facturacion.acciones.ActualizarPropiedad">
<poner propiedad="propiedad" valor="eliminado" />
<poner propiedad="valor" valor="false" />
</accion>
Aunque parezca una buena idea, no vamos a crear esta flexible
ActualizarPropiedad. Porque cuanto más flexible sea tu código, más sofisticado será. Y no queremos código sofisticado. Queremos código sencillo, y aunque el código sencillo es algo imposible de conseguir, hemos de esforzarnos por que nuestro código sea lo más sencillo posible. El consejo es: crea código reutilizable solo cuando éste simplifique tu aplicación en el presente.
Resumen
El comportamiento estándar de OpenXava solo es el punto de partida. Usando la acción de borrar como excusa, hemos explorado algunas formas de refinar los detalles del comportamiento de la aplicación. Con las técnicas de esta lección no solo puedes refinar la lógica de borrado, sino también definir completamente la forma en que una aplicación OpenXava funciona. Así, tienes la posibilidad de adaptar el comportamiento de tu aplicación para cubrir las expectativas de tus usuarios.
El comportamiento por defecto de OpenXava es limitado: solo mantenimientos y listados. Si quieres una aplicación que de verdad aporte valor a tu usuario necesitas añadir funcionalidad específica que le ayude a resolver sus problemas. Haremos esto en la próxima lección.
Descargar código fuente de esta lección
¿Problemas con la lección? Pregunta en el foro ¿Ha ido bien? Ve a la lección 25