openxava / documentación / Lección 26: Referencias y colecciones

Curso: 1. Primeros pasos | 2. Modelo básico del dominio (1) | 3. Modelo básico del dominio (2) | 4. Refinar la interfaz de usuario | 5. Desarrollo ágil | 6. Herencia de superclases mapedas | 7. Herencia de entidades | 8. Herencia de vistas | 9. Propiedades Java | 10. Propiedades calculadas | 11. @DefaultValueCalculator en colecciones | 12. @Calculation y totales de colección | 13. @DefaultValueCalculator desde archivo | 14. Evolución del esquema manual | 15. Cálculo de valor por defecto multiusuario | 16. Sincronizar propiedades persistentes y calculadas | 17. Lógica desde la base de datos | 18. Validando con @EntityValidator | 19. Alternativas de validación  | 20. Validación al borrar  21. Anotación Bean Validation propia | 22. Llamada REST desde una validación  | 23. Atributos en anotaciones  | 24. Refinar el comportamiento predefinido | 25. Comportamiento y lógica de negocio | 26. Referencias y colecciones | A. Arquitectura y filosofía | B. Java Persistence API | C. Anotaciones | D. Pruebas automáticas

Tabla de contenidos

Lección 26: Referencias y colecciones
Refinar el comportamiento de las referencias
Las validaciones están bien, pero no son suficientes
Refinar la acción para buscar una referencia con una lista
Buscar la referencia tecleando en los campos
Refinar la acción para buscar cuando se teclea la clave
Refinar el comportamiento de las colecciones
Refinar la lista para añadir elementos a la colección
Refinar la acción que añade elementos a la colección
Resumen
En lecciones anteriores aprendiste como añadir tus propias acciones. Sin embargo, esto no es suficiente para personalizar del todo el comportamiento de tu aplicación, porque la interfaz de usuario generada, en concreto la interfaz de usuario para referencias y colecciones, tiene un comportamiento estándar que a veces no es el más conveniente.
Por fortuna, OpenXava proporciona muchas formas de personalizar el comportamiento de las referencias y colecciones. En esta lección aprenderás como hacer algunas de estas personalizaciones, y como esto añade valor a tu aplicación.

Refinar el comportamiento de las referencias

Posiblemente te hayas dado cuenta de que el módulo Pedido tiene un pequeño defecto: el usuario puede añadir cualquier factura que quiera al pedido actual, aunque el cliente de la factura sea diferente. Esto no es admisible. Arreglémoslo.

Las validaciones están bien, pero no son suficientes

El usuario sólo puede asociar un pedido a una factura si ambos, factura y pedido, pertenecen al mismo cliente. Esto es lógica de negocio específica de tu aplicación, por tanto el comportamiento estándar de OpenXava no lo resuelve.
La siguiente imagen muestra cómo se produce un error de validación cuando el cliente de la factura es incorrecto:
references-collections_es010
Ya que esto es lógica de negocio la vamos a poner en la capa del modelo, es decir, en las entidades. Lo haremos añadiendo una validación. Así obtendrás el efecto de la figura de arriba.
Ya sabes como añadir esta validación a tu entidad Pedido. Se trata de añadir un método anotado con @AssertTrue:
public class Pedido {

    ...

    // Este método ha de devolver true para que este pedido sea válido
    @AssertTrue(message="cliente_pedido_factura_coincidir") 
    private boolean isClienteFacturaCoincide() {
    	return factura == null || // factura es opcional
    		factura.getCliente().getNumero() == getCliente().getNumero();
    }

}
También has de añadir el mensaje a src/main/resources/i18n/facturacion-messages_es.properties:
cliente_pedido_factura_coincidir=El cliente de la factura y del pedido han de coincidir
Aquí comprobamos que el cliente de la factura es el mismo que el del pedido. Esto es suficiente para preservar la integridad de los datos, pero la validación sola es una opción bastante pobre desde el punto de vista del usuario.

Refinar la acción para buscar una referencia con una lista

Aunque la validación impide que el usuario pueda asignar una factura incorrecta a un pedido, lo tiene difícil a la hora de escoger una factura correcta. Porque cuando pulsa para buscar una factura, todas las facturas existentes se muestran. Vamos a mejorar esto para mostrar solo las facturas del cliente del pedido visualizado, de esta manera:
references-collections_es020.png
Para definir nuestra propia acción de búsqueda para la referencia a factura usaremos la anotación @SearchAction. Aquí tienes la modificación necesaria en la clase Pedido:
public class Pedido extends DocumentoComercial {
 
    @ManyToOne
    @ReferenceView("SinClienteNiPedidos") 
    @OnChange(MostrarOcultarCrearFactura.class) 
    @SearchAction("Pedido.buscarFactura") // Define nuestra acción para buscar facturas
    Factura factura; 
    
    ...
	
}
De esta forma tan simple definimos la acción a ejecutar cuando el usuario pulsa en el botón de la linterna para buscar una factura. El argumento usado para @SearchAction, Pedido.buscarFactura, es el nombre calificado de la acción, es decir la acción buscarFactura del controlador Pedido definido en el archivo controladores.xml.
Ahora tenemos que editar controladores.xml y añadir la definición de nuestra nueva acción:
<controlador nombre="Pedido">

    ...
	
    <accion nombre="buscarFactura"
        clase="com.tuempresa.facturacion.acciones.BuscarFacturaDesdePedido"
        oculta="true" icono="magnify"/>
        <!--
        oculta="true" : Para que no se muestre en la barra de botones del módulo
        icono="magnify" : La misma imagen que la de la acción estándar
        -->
	
</controlador>
Nuestra acción hereda de ReferenceSearchAction como se muestra en el siguiente código:
package com.tuempresa.facturacion.acciones; // En el paquete 'acciones'

import org.openxava.actions.*; // Para usar ReferenceSearchAction

public class BuscarFacturaDesdePedido
    extends ReferenceSearchAction { // Lógica estándar para buscar una referencia

    public void execute() throws Exception {
        int numeroCliente =
            getView().getValueInt("cliente.numero"); // Lee de la vista el número
                                                  // de cliente del pedido actual
        super.execute(); // Ejecuta la lógica estándar, la cual muestra un diálogo
        if (numeroCliente > 0) { // Si hay cliente los usamos para filtrar
            getTab().setBaseCondition("${cliente.numero} = " + numeroCliente);
        }
    }
}
Observa como usamos getTab().setBaseCondition() para establecer una condición en la lista para escoger la referencia. Es decir, desde una ReferenceSearchAction puedes usar getTab() para manipular la forma en que se comporta la lista.
Si no hay cliente no añadimos ninguna condición por tanto se mostrarían todas las facturas, esto ocurre cuando el usuario escoge la factura antes que el cliente.

Buscar la referencia tecleando en los campos

La lista para escoger una referencia ya funciona bien. Sin embargo, queremos dar al usuario la opción de escoger una factura sin usar la lista, simplemente tecleando el año y el número. Muy útil si el usuario conoce de antemano que factura quiere.
OpenXava provee esa funcionalidad por defecto. Si los campos @Id son visualizados en la referencia serán usados para buscar, en caso contrario OpenXava usa el primer campo visualizado para buscar. Aunque en nuestro caso esto no es tan conveniente, porque el primer campo visualizado es el año, y buscar una factura sólo por el año no es muy preciso. La siguiente imagen muestra el comportamiento por defecto junto con una alternativa más conveniente:
references-collections_es030.png
Afortunadamente es fácil indicar que campos queremos usar para buscar desde la perspectiva del usuario. Esto se hace por medio de la anotación @SearchKey. Edita la clase DocumentoComercial (recuerda, el padre de Pedido y Factura) y añade esta anotación a las propiedades anyo y numero:
abstract public class DocumentoComercial extends Eliminable {

    @SearchKey // Añade esta anotación aquí
    @Column(length=4)
    @DefaultValueCalculator(CurrentYearCalculator.class) 
    int anyo;
 
    @SearchKey // Añade esta anotación aquí
    @Column(length=6)
    @ReadOnly
    int numero;
	
    ...
	
}
De esta forma cuando el usuario busque un pedido o una factura desde una referencia tiene que teclear el año y el número, y la entidad correspondiente será recuperada de la base de datos y rellenará la interfaz de usuario. Ahora es fácil para el usuario escoger una factura desde un pedido sin usar la lista de búsqueda, simplemente tecleando el año y el número.

Refinar la acción para buscar cuando se teclea la clave

Ahora que obtener una factura tecleando el año y el número funciona queremos refinarlo para ayudar al usuario a hacer su trabajo de forma más eficiente. Por ejemplo, sería útil que si el usuario todavía no ha escogido al cliente para el pedido y escoge una factura, el cliente de esa factura sea asignado automáticamente al pedido actual. La siguiente imagen visualiza el comportamiento deseado:
references-collections_es040.png
Por otra parte, si el usuario ya ha seleccionado un cliente para el pedido, si no coincide con el de la factura, ésta será rechazada y se visualizará un mensaje de error, tal como se muestra aquí:
references-collections_es050.png
Para definir este comportamiento especial hemos de añadir una anotación en la referencia factura de Pedido. @OnChangeSearch permite definir nuestra propia acción para hacer la búsqueda de la referencia cuando su clave cambia en la interfaz de usuario. Puedes ver la referencia modificada:
public class Pedido extends DocumentoComercial {
 
    @ManyToOne
    @ReferenceView("SinClienteNiPedidos") 
    @OnChange(MostrarOcultarCrearFactura.class) 
    @OnChangeSearch(BuscarAlCambiarFactura.class) // Añade esta anotación
    @SearchAction("Pedido.buscarFactura") 
    Factura factura; 
    
    ...
	
}
A partir de ahora cuando un usuario teclee un nuevo año y número para la factura, BuscarAlCambiarFactura se ejecutará. En esta acción se han de leer los datos de la factura de la base de datos y actualizar la interfaz de usuario. A continuación el código de la acción:
package com.tuempresa.facturacion.acciones; // En el paquete 'acciones'

import java.util.*;
import org.openxava.actions.*; // Para usar OnChangeSearchAction
import org.openxava.model.*;
import org.openxava.view.*;
import com.tuempresa.facturacion.modelo.*;

public class BuscarAlCambiarFactura  
    extends OnChangeSearchAction { // Lógica estándar para buscar una referencia cuando
                                   // los valores clave cambian en la interfaz de usuario (1)
    public void execute() throws Exception {
        super.execute(); // Ejecuta la lógica estándar (2)
        Map clave = getView() // getView() aquí es la de la referencia, no la principal(3)
            .getKeyValuesWithValue();
        if (clave.isEmpty()) return;  // Si la clave está vacía no se ejecuta más lógica
        Factura factura = (Factura) // Buscamos la factura usando la clave tecleada (4)
            MapFacade.findEntity(getView().getModelName(), clave);
        View vistaCliente = getView().getRoot().getSubview("cliente"); // (5)
        int numeroCliente = vistaCliente.getValueInt("numero");
        if (numeroCliente == 0) { // Si no hay cliente lo llenamos (6)
            vistaCliente.setValue("numero", factura.getCliente().getNumero());
            vistaCliente.refresh();
        } 
        else { // Si ya hay un cliente verificamos que coincida con el cliente de la factura (7)
            if (numeroCliente != factura.getCliente().getNumero()) {
                addError("cliente_factura_no_coincide", 
                    factura.getCliente().getNumero(), factura, numeroCliente);
                getView().clear();
            }
        }
    }
}	
Dado que la acción desciende de OnChangeSearchAction (1) y usamos super.execute() (2) se comporta de la forma estándar, es decir, cuando el usuario teclea el año y el número los datos de la factura se recuperan y rellenan la interfaz de usuario. Después, usamos getView() (3) para obtener la clave de la factura visualizada y así encontrar su correspondiente entidad usando MapFacade (4). Desde dentro de una OnChangeSearchAction getView() devuelve la subvista de la referencia, y no la vista global. Por lo tanto, en este caso getView() es la vista de la referencia a factura. Esto permite crear acciones @OnChangeSearch más reutilizables. Has de escribir getView().getRoot().getSubview("cliente") (5) para acceder a la vista del cliente. 
Para implementar el comportamiento visualizado en la imagen anterior, la acción pregunta si no hay cliente (numeroCliente == 0) (6). Si éste es el caso rellena los datos del cliente desde el cliente de la factura. En caso contrario implementa la lógica de la imagen de arriba verificando que el cliente del pedido actual coincide con el cliente de la factura recuperada.
Nos queda un pequeño detalle, el texto del mensaje. Añade la siguiente entrada al archivo facturacion-messages_es.properties de la carpeta src/main/resources/i18n:
cliente_factura_no_coincide=Cliente Nº {0} de la factura {1} no coincide con el cliente Nº {2} del pedido actual
Una cosa interesante de @OnChangeSearch es que también se ejecuta si la factura se escoge desde la lista, porque en este caso el año y el número también cambian. Por ende, este es un lugar centralizado donde refinar la lógica para recuperar la referencia y rellenar la vista.

Refinar el comportamiento de las colecciones

Podemos refinar las colecciones de la misma forma que hemos hecho con las referencias. Esto es muy útil, porque nos permite mejorar el comportamiento actual del módulo Factura. El usuario sólo puede añadir un pedido a una factura si la factura y el pedido pertenecen al mismo cliente. Además, el pedido tiene que estar entregado y no tener todavía factura.

Refinar la lista para añadir elementos a la colección

Actualmente cuando el usuario trata de añadir pedidos a la factura todos los pedidos están disponibles. Vamos a mejorar esto para mostrar solo los pedidos del cliente de la factura, entregados y todavía sin factura, tal como se muestra:
references-collections_es060.png
Usaremos la anotación @AddAction para definir nuestra propia acción que muestre la lista para añadir pedidos. El siguiente código muestra la modificación necesaria en la clase Factura:
public class Factura extends DocumentoComercial {
 
    @OneToMany(mappedBy="factura")
    @CollectionView("SinClienteNiFactura") 
    @AddAction("Factura.anyadirPedidos") // Define nuestra propia acción para añadir pedidos
    Collection<Pedido> pedidos;
	
    ...
	
}
De esta forma tan sencilla definimos la acción a ejecutar cuando el usuario pulsa en el botón para añadir pedidos. El argumento usado para @AddAction, Factura.anyadirPedidos, es el nombre calificado de la acción, es decir la acción añadirPedidos del controlador Factura tal como se ha definido en el archivo controladores.xml.
Ahora hemos de editar controladores.xml para añadir el controlador Factura (todavía no existe) con nuestra acción:
<controlador nombre="Factura">
    <hereda-de controlador="Facturacion"/>

    <accion nombre="anyadirPedidos"
        clase="com.tuempresa.facturacion.acciones.IrAnyadirPedidosAFactura"
        oculta="true" icono="table-row-plus-after"/>
        <!--
        oculta="true" : No se mostrará en la barra de botones del módulo
        icono="table-row-plus-after" : La misma imagen que la acción estándar
        -->

</controlador>
Este es el código de la acción:
package com.tuempresa.facturacion.acciones; // En el paquete 'acciones'

import org.openxava.actions.*; // Para usar GoAddElementsToCollectionAction

public class IrAnyadirPedidosAFactura
    extends GoAddElementsToCollectionAction { // Lógica estándar para ir a la lista que
                                              // permite añadir elementos a la colección
    public void execute() throws Exception {
        super.execute(); // Ejecuta la lógica estándar, la cual muestra un diálogo
        int numeroCliente =
            getPreviousView() // getPreviousView() es la vista principal (estamos en un diálogo)
                .getValueInt("cliente.numero"); // Lee el número de cliente de la
                                                // factura actual de la vista
        getTab().setBaseCondition( // La condición de la lista de pedidos a añadir
            "${cliente.numero} = " + numeroCliente +
            " and ${entregado} = true and ${factura} is null"
        );
    }
}
Fíjate como usamos getTab().setBaseCondition() para establecer la condición de la lista para escoger la entidades a añadir. Es decir, desde una GoAddElementsToCollectionAction puedes usar getTab() para manipular la forma en que la lista se comporta.

Refinar la acción que añade elementos a la colección

Una mejora interesante para la colección de pedidos sería que cuando el usuario añada pedidos a la factura actual, las líneas de detalle de estos pedidos se copien automáticamente a la factura.
No podemos usar @AddAction para esto, porque es la acción que muestra la lista de elementos a añadir a la colección. Pero no es la acción que añade los elementos. En esta sección aprenderemos como definir la acción que realmente añade los elementos:
references-collections_es070.png
Por desgracia, no hay una anotación para definir directamente esta acción de añadir. Sin embargo, no es una tarea demasiado difícil, solo hemos de refinar la acción @AddAction instruyéndola para mostrar nuestro propio controlador y en este controlador podemos poner las acciones que queramos. Dado que ya hemos definido nuestra @AddAction en la sección anterior solo hemos de añadir un nuevo método a la ya existente IrAnyadirPedidosAFactura. Añade el siguiente método getNextController() a tu acción:
public class IrAnyadirPedidosAFactura ... {

    ...

    public String getNextController() { // Añadimos este método
        return "AnyadirPedidosAFactura"; // El controlador con las acciones disponibles
    }                                    // en la lista de pedidos a añadir
}
Por defecto las acciones en la lista de entidades a añadir (los botones AÑADIR y CANCELAR) son del controlador estándar de OpenXava AddToCollection. Sobrescribir getNextController() en nuestra acción nos permite definir nuestro propio controlador en su lugar. Añade en controladores.xml la siguiente definición para nuestro controlador propio para añadir elementos:
<controlador nombre="AnyadirPedidosAFactura">
    <hereda-de controlador="AddToCollection" /> <!-- Extiende del controlador estándar -->
	
    <!-- Sobrescribe la acción para añadir -->
    <accion nombre="add"
        clase="com.tuempresa.facturacion.acciones.AnyadirPedidosAFactura" />
		
</controlador>
De esta forma la acción para añadir pedidos a la factura será AnyadirPedidosAFactura. Recuerda que el objetivo de nuestra acción es añadir los pedidos a la factura de la manera convencional, pero también copiar las líneas de estos pedidos a la factura. Este es el código de la acción:
package com.tuempresa.facturacion.acciones; // En el paquete 'acciones'

import java.rmi.*;
import java.util.*;
import javax.ejb.*;
import org.openxava.actions.*; // Para usar AddElementsToCollectionAction
import org.openxava.model.*;
import org.openxava.util.*;
import org.openxava.validators.*;
import com.tuempresa.facturacion.modelo.*;

public class AnyadirPedidosAFactura
    extends AddElementsToCollectionAction { // Lógica estándar para añadir
                                            // elementos a la colección
    public void execute() throws Exception {
        super.execute(); // Usamos la lógica estándar "tal cual"
        getView().refresh(); // Para visualizar datos frescos, incluyendo los importes
    }                        // recalculados, que dependen de las líneas de detalle

    protected void associateEntity(Map clave) // El método llamado para asociar
        throws ValidationException, // cada entidad a la principal, en este caso para
            XavaException, ObjectNotFoundException,// asociar cada pedido a la factura
            FinderException, RemoteException
    {
        super.associateEntity(clave); // Ejecuta la lógica estándar (1)
        Pedido pedido = (Pedido) MapFacade.findEntity("Pedido", clave); // (2)
        pedido.copiarDetallesAFactura(); // Delega el trabajo principal en la entidad (3)
    }
}
Sobrescribimos el método execute() sólo para refrescar la vista después del proceso. Realmente, lo que nosotros queremos es refinar la lógica de asociar un pedido a la factura. La forma de hacer esto es sobrescribiendo el método associateEntity(). La lógica aquí es simple, después de ejecutar la lógica estándar (1) buscamos la entidad Pedido correspondiente y entonces llamamos al método copiarDetallesAFactura() de ese Pedido. Por suerte ya teníamos un método para copiar detalles desde una entidad Pedido a la Factura especificada, simplemente llamamos a este método.
Ahora solo has de crear una factura nueva, escoger un cliente y añadir pedidos. Es incluso más fácil de usar que el modo lista del módulo Pedido ya que el módulo Factura solo se muestran los pedidos adecuados al cliente.

Resumen

Esta lección te ha mostrado como refinar el comportamiento estándar de las referencias y colecciones para que tu aplicación se adapte a las necesidades del usuario. Aquí sólo has visto algunos ejemplos ilustrativos. OpenXava ofrece muchas más posibilidades para refinar el comportamiento de las colecciones y referencias, con anotaciones como @ReferenceView, @ReadOnly, @NoFrame, @NoCreate, @NoModify, @NoSearch, @AsEmbedded, @SearchAction, @DescriptionsList, @LabelFormat, @Action, @OnChange, @OnChangeSearch, @Editor, @CollectionView, @EditOnly, @ListProperties, @RowStyle, @EditAction, @ViewAction, @NewAction, @SaveAction, @HideDetailAction, @RemoveAction, @RemoveSelectedAction, @ListAction, @DetailAction o @OnSelectElementAction. Mira las secciones Personalización de referencia y Personalización de colección de la guía de referencia.
Y por si esto fuera poco, siempre tienes la opción definir tu propio editor para referencias o colecciones. Los editores te permiten crear una interfaz de usuario personalizada para visualizar y editar la referencia o colección.
Esta flexibilidad te permite usar la generación automática de la interfaz gráfica para prácticamente cualquier caso posible en las aplicaciones de gestión de la vida real.

Descargar código fuente de esta lección

¿Problemas con la lección? Pregunta en el foro