openxava / documentación / Lección 6: Validación avanzada

Curso: 1. Primeros pasos | 2. Modelar con Java | 3. Pruebas automáticas | 4. Herencia | 5. Lógica de negocio básica | 6. Validación avanzada | 7. Refinar el comportamiento predefinido | 8. Comportamiento y lógica de negocio | 9. Referencias y colecciones | A. Arquitectura y filosofía | B. Java Persistence API | C. Anotaciones

Tabla de contenidos

Lección 6: Validación avanzada
Alternativas de validación
Añadir la propiedad entregado a Pedido
Validar con @EntityValidator
Validar con métodos de retrollamada JPA
Validar en el setter
Validar con Bean Validation
Validar al borrar con @RemoveValidator
Validar al borrar con un método de retrollamada
¿Cuál es la mejor forma de validar?
Crear tu propia anotación de Bean Validation
Usar un Bean Validation en tu entidad
Definir tu propia anotación ISBN
Usa Apache Commons Validator para implementar la lógica
Llamar a un servicio web REST para validar el ISBN
Añadir atributos a tu anotación
Pruebas JUnit
Probar la validación al añadir a una colección
Probar validación al asignar una referencia y al borrar
Probar el Bean Validation propio
Resumen
De momento solo hemos hecho validaciones básicas usando la anotación @Required de OpenXava. A veces es necesario escribir nuestra propia lógica de validación. En el apéndice C se explica como funciona la validación. Aquí vamos a añadir validaciones con lógica propia a tu aplicación.

Alternativas de validación

Vamos a refinar tu código para que el usuario no pueda asignar pedidos a una factura si los pedidos no han sido entregados todavía. Es decir, solo los pedidos entregados pueden asociarse a una factura. Aprovecharemos la oportunidad para explorar diferentes formas de hacer esta validación.

Añadir la propiedad entregado a Pedido

Para hacer esto, lo primero es añadir una nueva propiedad a la entidad Pedido. La propiedad entregado:
private boolean entregado;
 
public boolean isEntregado() {
    return entregado;
}
 
public void setEntregado(boolean entregado) {
    this.entregado = entregado;
}
Además es necesario añadir la propiedad entregado a la vista. Modifica la vista Pedido como muestra el siguiente código:
@Views({
    @View( extendsView="super.DEFAULT",
        members="entregado; factura { factura } " // Propiedad 'entregado' añadida
    ),
    ...
})
public class Pedido extends DocumentoComercial
Ahora tienes una nueva propiedad entregado que el usuario puede marcar para indicar que el pedido ha sido entregado. Ejecuta el nuevo código y marca algunos de los pedidos existentes como entregados.
Nota:
Al arrancar la aplicación se añadirá la columna entregado a la tabla DocumentoComercial, ésta tendrá el valor null para todos los registros. Debemos actualizar esta columna con las siguiente sentencia SQL:
UPDATE DocumentoComercial
SET entregado = false

Validar con @EntityValidator

En tu aplicación actual el usuario puede añadir cualquier pedido que le plazca a una factura usando el módulo Factura, y puede asignar una factura a cualquier pedido desde el módulo Pedido. Vamos a restringir esto. Solo los pedidos entregados podrán añadirse a una factura.
La primera alternativa que usaremos para implementar esta validación es mediante @EntityValidator. Esta anotación te permite asignar a tu entidad una clase con la lógica de validación deseada. Anotemos tu entidad Pedido tal como muestra el siguiente código:
@EntityValidator(
    value=ValidadorEntregadoParaEstarEnFactura.class, // Clase con la lógica de validación
    properties= {
        @PropertyValue(name="anyo"), // El contenido de estas propiedades
        @PropertyValue(name="numero"), // se mueve desde la entidad 'Pedido'
        @PropertyValue(name="factura"), // al validador antes de
        @PropertyValue(name="entregado") // ejecutar la validación
})
public class Pedido extends DocumentoComercial {
Cada vez que un objeto Pedido se crea o modifica un objeto del tipo ValidadorEntregadoParaEstarEnFactura es creado, entonces las propiedades anyo, numero, factura y entregado se rellenan con las propiedades del mismo nombre del objeto Pedido. Después de eso, el método validate() del validador se ejecuta. Puedes ver el código del validador:
package com.tuempresa.facturacion.validadores; // En el paquete 'validadores'
 
import com.tuempresa.facturacion.modelo.*;
import org.openxava.util.*;
import org.openxava.validators.*;
 
public class ValidadorEntregadoParaEstarEnFactura
    implements IValidator { // ha de implementar 'IValidator'
 
    private int anyo; // Propiedades a ser inyectadas desde Pedido
    private int numero;
    private boolean entregado;
    private Factura factura;
 
    @Override
    public void validate(Messages errors)
        throws Exception { // La lógica de validación
        if (factura == null) return;
        if (!entregado) {
            errors.add( // Al añadir mensajes a 'errors' la validación fallará
                "pedido_debe_estar_entregado", // Un id del archivo i18n
                anyo, numero); // Argumentos para el mensaje
        }
    }
    // Getters y setters para anyo, numero, entregado y factura
    ...
}
La lógica de validación es extremadamente fácil, si una factura está presente y este pedido no ha sido servido añadimos un mensaje de error, por tanto la validación fallará. Has de añadir el mensaje de error en el archivo Facturacion/i18n/MensajesFacturacion_es.properties. Tal como muestra a continuación:
# Mensajes  para la aplicación Facturacion
pedido_debe_estar_entregado=Pedido {0}/{1} debe estar entregado para ser añadido a una Factura
Ahora puedes intentar añadir pedidos a una factura con la aplicación, verás como los pedidos no servidos son rechazados. Como se ve en la siguiente figura:
validation_es010.png
Ya tienes tu validación hecha con @EntityValidator. No es difícil, pero es un poco verboso, porque necesitas escribir una clase nueva solo para añadir 2 línea de lógica. Aprendamos otras formas de hacer esta misma validación.

Validar con métodos de retrollamada JPA

Vamos a probar otra forma más sencilla de hacer esta validación, simplemente moviendo la lógica de validación desde la clase validador a la misma entidad Pedido, en este caso a un método @PreUpdate.
Lo primero es eliminar la clase ValidadorEntregadoParaEstarEnFactura de tu proyecto. También quita la anotación @EntityValidator de tu entidad Pedido:
// @EntityValidator( // Eliminar '@EntityValidator'
//    value=ValidadorEntregadoParaEstarEnFactura.class,
//    properties= {
//        @PropertyValue(name="anyo"),
//        @PropertyValue(name="numero"),
//        @PropertyValue(name="factura"),
//        @PropertyValue(name="entregado")
// })
public class Pedido extends DocumentoComercial {
Acabamos del eliminar la validación. Ahora, vamos a añadirla de nuevo, pero ahora dentro de la misma clase Pedido. Escribe el método validar() que se muestra a continuación dentro de tu clase Pedido:
@PreUpdate
private void validar() throws Exception {
    if (factura != null && !isEntregado()) { // La lógica de validación
        // La excepción de validación del entorno Bean Validation
        throw new javax.validation.ValidationException(
            XavaResources.getString( // Para leer un mensaje i18n
                "pedido_debe_estar_entregado",
                getAnyo(),
                getNumero())
        );
    }
}
Antes de grabar un pedido esta validación se ejecutará, si falla una ValidationException será lanzada. Esta excepción es del marco de validación Bean Validation, de esta forma OpenXava sabe que es una excepción de validación. Así con solo un método dentro de tu entidad tienes la validación hecha.

Validar en el setter

Otra alternativa para hacer tu validación es poner tu lógica de validación dentro del método setter. Es un enfoque simple y llano. Para probarlo, quita el método validar() de tu entidad Pedido y modifica el método setFactura() de la forma que mostramos a continuación:
public void setFactura(Factura factura) {
    if (factura != null && !isEntregado()) { // La lógica de validación
        // La excepción de validación del entorno Bean Validation
        throw new javax.validation.ValidationException(
            XavaResources.getString( // Para leer un mensaje i18n
                "pedido_debe_estar_entregado",
                getAnyo(),
                getNumero())
        );
    }
    this.factura = factura; // La asignación típica del setter
}
Esto funciona exactamente como las dos opciones anteriores. Es parecida a la alternativa del @PreUpdate, solo que no depende de JPA, es una implementación básica de Java.

Validar con Bean Validation

Como opción final vamos a hacer la más breve. Consiste en poner tu lógica de validación dentro de un método booleano anotado con la anotación de Bean Validation @AssertTrue.
Para implementar esta alternativa primero quita la lógica de validación del método setFactura(). Después, añade isEntregadoParaEstarEnFactura() a tu entidad Pedido, como se muestra a continuación:
@AssertTrue(  // Antes de grabar confirma que el método devuelve true, si no lanza una excepción
    message="pedido_debe_estar_entregado" // Mensaje de error en caso retorne false
)
private boolean isEntregadoParaEstarEnFactura() { // ...
    return factura == null || isEntregado(); // La lógica de validación
}
En las formas anteriores de validación nuestro mensaje de error era construído mediante dos argumentos, anyo y numero, que en nuestro archivo i18n son representados por {0}/{1} respectivamente. Para el caso de validación con @AssertTrue no podemos pasar estos dos argumentos para construir nuestro mensaje de error, sino que podemos declarar propiedades y propiedades calificadas del bean validado en la definición del mensaje. Veamos como modificar la definición del mensaje:
# Mensajes  para la aplicación Facturacion
pedido_debe_estar_entregado=Pedido {anyo}/{numero} debe estar entregado para ser añadido a una Factura
OpenXava llenará {anyo}/{numero} con los valores de anyo y numero que tenga el Pedido que está siendo actualizado y no cumple la condición de validación.
Esta es la forma más simple de validar, porque solo anotamos el método con la validación, y es el entorno Bean Validation el responsable de llamar este método al grabar, y lanzar la excepción correspondiente si la validación no pasa.

Validar al borrar con @RemoveValidator

Las validaciones que hemos visto hasta ahora se hacen cuando la entidad se modifica, pero a veces es útil hacer la validación justo al borrar la entidad, y usar la validación para vetar el borrado de la misma.
Vamos a modificar la aplicación para impedir que un usuario borre un pedido si éste tiene una factura asociada. Para hacer esto anota tu entidad Pedido con @RemoveValidator, como se muestra a continuación:
@RemoveValidator(ValidadorBorrarPedido.class) // La clase con la validación
public class Pedido extends DocumentoComercial {
Ahora, antes de borrar un pedido la lógica de ValidadorBorrarPedido se ejecuta, y si la validación falla el pedido no se borra. Veamos el código de este validador:
package com.tuempresa.facturacion.validadores; // En el paquete 'validadores'
 
import com.tuempresa.facturacion.modelo.*;
import org.openxava.util.*;
import org.openxava.validators.*;
 
public class ValidadorBorrarPedido
    implements IRemoveValidator { // Ha de implementar 'IRemoveValidator'
 
    private Pedido pedido;
 
    @Override
    public void setEntity(Object entity) // La entidad a borrar se inyectará...
        throws Exception // ...con este método antes de la validación
    {
        this.pedido = (Pedido) entity;
    }
 
    @Override
    public void validate(Messages errors) // La lógica de validación
        throws Exception
    {
        if (pedido.getFactura() != null) {
            // Añadiendo mensajes a 'errors' la validación fallará y el
            // borrado se abortará
            errors.add("no_puede_borrar_pedido_con_factura");
        }
    }
}
La lógica de validación está en el método validate(). Antes de llamarlo la entidad a validar es inyectada usando setEntity(). Si se añaden mensajes al objeto errors la validación fallará y la entidad no se borrará. Has de añadir el mensaje de error en el archivo Facturacion/i18n/MensajesFacturacion_es.properties:
no_puede_borrar_pedido_con_factura=Pedido asociado a factura no puede ser eliminado
Ahora si intentas borrar un pedido con una factura asociada obtendrás un mensaje de error y el borrado no se producirá.
Puedes ver que usar un @RemoveValidator no es difícil, pero es un poco verboso. Has de escribir una clase nueva solo para añadir un simple if. Examinemos una alternativa más breve:

Validar al borrar con un método de retrollamada

Vamos a probar otra forma más simple de hacer esta validación al borrar, moviendo la lógica de validación desde la clase validador a la misma entidad Pedido, en este caso en un método @PreRemove.
El primer paso es eliminar la clase ValidadorBorrarPedido de tu proyecto. Además quita la anotación @RemoveValidator de tu entidad Pedido:
// @RemoveValidator(ValidadorBorrarPedido.class) // Quitamos '@RemoveValidator'
public class Pedido extends DocumentoComercial {
Hemos quitado la validación. Añadámosla otra vez, pero ahora dentro de la misma clase Pedido. Añade el método validarPreBorrar() a la clase Pedido, como se muestra a continuación:
@PreRemove
private void validarPreBorrar() {
    if (factura != null) { // La lógica de validación
        throw new javax.validation.ValidationException( // Lanza una excepción runtime
            XavaResources.getString( // Para obtener un mensaje de texto
                "no_puede_borrar_pedido_con_factura"));
    }
}
Antes de borrar un pedido esta validación se efectuará, si falla se lanzará una ValidationException. Puedes lanzar cualquier excepción runtime para abortar el borrado. Tan solo con un método dentro de la entidad tienes la validación hecha.

¿Cuál es la mejor forma de validar?

Has aprendido varias formas de hacer la validación sobre tus clases del modelo. ¿Cuál de ellas es la mejor? Todas ellas son opciones válidas. Depende de tus circunstancias y preferencias personales. Si tienes una validación que no es trivial y es reutilizable en varios puntos de tu aplicación, entonces usar un @EntityValidator y @RemoveValidator es una buena opción. Por otra parte, si quieres usar tu modelo fuera de OpenXava y sin JPA, entonces el uso de la validación en los setters es mejor.
En nuestro caso particular hemos optado por @AssertTrue para la validación “el pedido ha de estar servido para estar en una factura” y por @PreRemove para la validación al borrar. Ya que son las alternativas más simples que funcionan.

Crear tu propia anotación de Bean Validation

Las técnicas mencionadas hasta ahora son muy útiles para la mayoría de las validaciones de tus aplicaciones. Sin embargo, a veces te encuentras con algunas validaciones que son muy genéricas y quieres usarlas una y otra vez. En este caso definir tu propia anotación de Bean Validation puede ser una buena opción. Definir un Bean validation es más largo y engorroso que lo que hemos visto hasta ahora, pero usarlo y reusarlo es simple, tan solo añadir una anotación a tu propiedad o clase.
Vamos a aprender como crear un Bean Validation.

Usar un Bean Validation en tu entidad

Es superfácil. Simplemente anota tu propiedad como ves a a continuación:
@ISBN // Esta anotación indica que esta propiedad tiene que validarse como un ISBN
private String isbn;
Solo con añadir @ISBN a tu propiedad, y ésta será validada justo antes de que la entidad se grabe en la base de datos, ¡genial! El problema es que @ISBN no está incluida como un validador predefinido en el marco de validación Bean Validation. Esto no es un gran problema, si quieres una anotación @ISBN, hazla tú mismo. De hecho, vamos a crear la anotación de validación @ISBN en esta sección.
Antes de nada, añadamos una nueva propiedad isbn a Producto. Edita tu clase Producto y añádele el siguiente código:
@Column(length=13)
private String isbn;
 
public String getIsbn() {
    return isbn;
}
 
public void setIsbn(String isbn) {
    this.isbn = isbn;
}
Ejecuta el módulo Producto con tu navegador. Sí, la propiedad isbn ya está ahí. Ahora, puedes añadir la validación.

Definir tu propia anotación ISBN

Creemos la anotación @ISBN. Primero, crea un paquete en tu proyecto llamado com.tuempresa.facturacion.anotaciones, entonces sigue las instrucciones de la siguiente figura para crear una nueva anotación llamada ISBN.
validation_es020.png
Edita el código de tu recién creada anotación ISBN y déjala así:
package com.tuempresa.facturacion.anotaciones; // En el paquete 'anotaciones'
 
import java.lang.annotation.*;
 
import javax.validation.*;
 
import com.tuempresa.facturacion.validadores.*;
 
@Constraint(validatedBy = ValidadorISBN.class)
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ISBN {
 
    Class<?>[] groups() default{};
    Class<? extends Payload>[] payload() default{};
    String message() default "No existe ISBN"; // Definimos un mensaje por defecto
}
Como puedes ver, es una definición de anotación normal y corriente, solo que usas @Constraint para indicar la clase con la lógica de validación. Escribamos la clase ValidadorISBN.

Usa Apache Commons Validator para implementar la lógica

Vamos a escribir la clase ValidadorISBN con la lógica de validación para un ISBN. En lugar de escribir nosotros mismos la lógica para validar un ISBN usaremos el proyecto Commons Validator de Apache. Commons Validator contiene algoritmos de validación para direcciones de correo electrónico, fechas, URL y así por el estilo. El commons-validator.jar se incluye por defecto en los proyectos OpenXava, por tanto lo puedes usar sin ninguna configuración adicional.
El código para ValidadorISBN lo puedes ver a continuación:
package com.tuempresa.facturacion.validadores; // En el paquete 'validadores'
 
import javax.validation.*;
 
import com.tuempresa.facturacion.anotaciones.*;
import org.openxava.util.*;
 
public class ValidadorISBN implements ConstraintValidator<ISBN, Object> {
 
    private static org.apache.commons.validator.routines.ISBNValidator
        validador = // De 'Commons Validator'
            new org.apache.commons.validator.routines.ISBNValidator();
 
    @Override
    public void initialize(ISBN isbn) {
 
    }
 
    @Override
    public boolean isValid(Object value,
        ConstraintValidatorContext contexto) { // Contiene la lógica de validación
        if (Is.empty(value)) return true;
        return validador.isValid(value.toString()); // Usa 'Commons Validator'
    }
}
Como ves, la clase validador tiene que implementar ConstraintValidator del paquete javax.validation. Esto fuerza a tu validador a implementar initialize() e isValid(). El método isValid() contiene la lógica de validación. Fíjate que si el elemento a validar está vacío asumimos que es válido, porque validar si un valor está presente es responsabilidad de otras anotaciones, como @Required, y no de @ISBN.
En este caso la lógica de validación es sencillísima, porque nos limitamos a llamar al validador ISBN de Apache Commons Validator.
@ISBN está listo para usar. Para hacerlo anota tu propiedad isbn con él. Puedes ver cómo:
@Column(length=13) @ISBN
private String isbn;
Ahora, puedes probar tu módulo, y verificar que el ISBN que introduces se valida correctamente. Enhorabuena, has escrito tu primer Bean Validation. No ha sido tan difícil: una anotación, una clase.
Este @ISBN es suficientemente bueno para usarlo en la vida real, sin embargo, vamos a mejorarlo un poco más, y así tendremos la posibilidad de experimentar con algunas posibilidades interesantes.

Llamar a un servicio web REST para validar el ISBN

Aunque la mayoría de los validadores tienen una lógica simple, puedes crear validadores con una lógica compleja si lo necesitas. Por ejemplo, en el caso de nuestro ISBN, queremos, no solo verificar el formato correcto, sino también comprobar que existe de verdad un libro con ese ISBN. Una forma de hacer esto es usando servicios web.
Como seguramente ya sepas, un servicio web es una funcionalidad que reside en un servidor web y que tú puedes llamar desde tu programa. La forma tradicional de desarrollar servicios web es mediante los estándares WS-*, como SOAP, UDDI, etc. Aunque, hoy en día, la forma más simple de desarrollar servicios es REST. REST consiste básicamente en usar la ya existente “forma de trabajar” de internet para comunicación entre programas. Llamar a un servicio REST consiste en usar una URL web convencional para obtener un recurso de un servidor web. Este recurso usualmente contiene datos en formato XML, HTML, JSON, etc. En otras palabras, los programas usan internet de la misma manera que lo hacen los usuarios con sus navegadores.
Hay bastantes sitio con servicios web SOAP y REST para consultar el ISBN de un libro, pero no suele ser gratis. Por tanto, vamos a usar una alternativa más barata, que va a ser llamar a un sitio web convencional para hacer la búsqueda del ISBN, y examinar después la página resultado para determinar si la búsqueda ha funcionado. Algo así como un servicio web pseudo-REST.
Para llamar a la página web usaremos el marco de trabajo HtmlUnit. Aunque el principal cometido de este marco de trabajo sea crear pruebas para tus aplicaciones web, puedes usarlo para leer cualquier página web. Lo usaremos porque es más fácil que otras librerías con este propósito, como por ejemplo Apache Commons HttpClient. Observa lo simple que es leer una página web con HtmlUnit:
WebClient cliente = new WebClient();
HtmlPage pagina = (HtmlPage) cliente.getPage("http://www.openxava.org/");
Después de esto, puedes usar el objeto pagina para manipular la página leída.
OpenXava usa HtmlUnit como marco subyacente para las pruebas, por tanto ya está incluido en OpenXava, pero no se incluye por defecto en las aplicaciones OpenXava, así que tienes que incluirlo tú mismo en tu aplicación. Para hacerlo copia los archivos cssparser.jar, htmlunit-core-js.jar, htmlunit.jar, httpclient.jar, httpcore.jar, httpmime.jar, nekothml.jar, sac.jar, xalan.jar, xercesImpl.jar, xml-apis.jar, desde la carpeta Openxava/lib a la carpeta Facturacion/web/WEB-INF/lib.
Modifiquemos ValidadorISBN para que haga uso de este servicio REST. Puedes ver el resultado:
package com.tuempresa.facturacion.validadores; // En el paquete 'validadores'
 
import javax.validation.*;
 
import org.apache.commons.logging.*; // Para usar 'Log'
import com.tuempresa.facturacion.anotaciones.*;
import org.openxava.util.*;
 
import com.gargoylesoftware.htmlunit.*; // Para usar 'HtmlUnit'
import com.gargoylesoftware.htmlunit.html.*;
 
public class ValidadorISBN implements ConstraintValidator<ISBN, Object> {
 
    private static Log log = LogFactory.getLog(ValidadorISBN.class); // Instanciamos un 'Log'
    private static org.apache.commons.validator.routines.ISBNValidator
        validador = new org.apache.commons.validator.routines.ISBNValidator();
 
    @Override
    public void initialize(ISBN isbn) {
 
    }
 
    @Override
    public boolean isValid(Object value,
        ConstraintValidatorContext contexto) { // Contiene la lógica de validación
        if (Is.empty(value)) return true;
        if (!validador.isValid(value.toString())) return false;
        return existeIsbn(value); // Aquí hacemos la llamada REST
    }
 
    private boolean existeIsbn(Object isbn) {
        String mensajeError = "can't be found"; // Mensaje de búsqueda fallida en 'bookfinder4u'
        try {
            WebClient cliente = new WebClient();
            HtmlPage pagina = (HtmlPage) cliente.getPage( // Llamamos a
                "http://www.bookfinder4u.com/" + // bookfinder4u
                "IsbnSearch.aspx?isbn=" + // con una url para buscar
                isbn + "&mode=direct"); // por ISBN
            return pagina.asText() // Comprueba si la página resultante
                .indexOf(mensajeError) < 0; // no tiene el mensaje de error
        } catch (Exception ex) {
            log.warn("Imposible conectar a bookfinder4u " +
                "para validar ISBN. Validación falló", ex);
            return false; // Si hay algún error asumimos que la validación falló
        }
    }
}
Simplemente buscamos una página usando como argumento en la URL el ISBN. Si la página resultante no contiene el mensaje de error "can't be found" la búsqueda ha sido satisfactoria. El método pagina.asText() devuelve el contenido de la página HTML sin las marcas HTML, es decir, con solo la información textual.
Puedes usar este truco con cualquier sitio que te permita hacer búsquedas, así puedes consultar virtualmente millones de sitios web desde tu aplicación. En un servicio REST más puro el resultado hubiera sido un documento JSON o XML en vez de uno HTML, pero hubieras tenido que pasar por caja.
Prueba ahora tu aplicación y verás como si introduces un ISBN no existente la validación falla.

Añadir atributos a tu anotación

Creas una anotación Bean Validation cuando quieres reutilizar la validación varias veces, usualmente en varios proyectos. En este caso, necesitas hacer tu validación adaptable, para que sea reutilizable de verdad. Por ejemplo, en el proyecto actual buscar en www.bookfinder4u.com el ISBN es conveniente, pero en otro proyecto, o incluso en otra entidad de tu actual proyecto, puede que no quieras hacer esa búsqueda. Necesitas hacer tu anotación más flexible.
La forma de añadir esta flexibilidad a tu anotación de validación es mediante los atributos. Por ejemplo, podemos añadir un atributo de búsqueda booleano a nuestra anotación ISBN para poder escoger si queremos buscar el ISBN en internet para validar o no. Para hacerlo, simplemente añade el atributo buscar al código de la anotación ISBN, tal como muestra el siguiente código:
public @interface ISBN {
 
    boolean buscar() default true; // Para (des)activar la búsqueda web al validar
    // ... el resto del código
}
Este nuevo atributo buscar puede leerse de la clase validador. Míra como:
public class ValidadorISBN implements ConstraintValidator<ISBN, Object> {
    // ...
    private boolean buscar; // Almacena la opción buscar
 
    @Override
    public void initialize(ISBN isbn) { // Lee los atributos de la anotación
        this.buscar = isbn.buscar();
    }
 
   @Override
    public boolean isValid(Object value, ConstraintValidatorContext contexto) {
        if (Is.empty(value)) return true;
        if (!validador.isValid(value.toString())) return false;
        return buscar ? existeIsbn(value) : true; // Usa 'buscar'
    }
    // ...
}
Aquí ves la utilidad del método initialize(), que lee la anotación para inicializar el validador. En este caso simplemente almacenamos el valor de isbn.buscar() para preguntar por él en isValid().
Ahora puedes escoger si quieres llamar a nuestro servicio pseudo-REST o no para hacer la validación ISBN. Mira como:
@ISBN(buscar=false) // En este caso no se hace una búsqueda en la web para validar el ISBN
private String isbn;
¡Enhorabuena! Has aprendido como crear tu propia anotación de Bean Validation, y de paso a usar la útil herramienta HtmlUnit.

Pruebas JUnit

Nuestra meta no es desarrollar una ingente cantidad de código, sino crear software de calidad. Al final, si creas software de calidad acabarás escribiendo más cantidad de software, porque podrás dedicar más tiempo a hacer cosas nuevas y excitantes, y menos depurando legiones de bugs. Y tú sabes que la única forma de conseguir calidad es mediante las pruebas automáticas, por tanto actualicemos nuestro código de prueba.

Probar la validación al añadir a una colección

Recuerda que hemos refinado tu código para que el usuario no pueda asignar pedidos a una factura si los pedidos no están servidos. Después de esto, tu actual testAnyadirPedidos() de PruebaFactura puede fallar, porque trata de añadir el primer pedido, y es posible que ese primer pedido no esté marcado como servido.
Modifiquemos la prueba para que funcione y también para comprobar la nueva funcionalidad de validación. Mira como:
public void testAnyadirPedidos() throws Exception {
    login("admin", "admin");
    assertListNotEmpty();
    execute("List.orderBy", "property=numero");
    execute("List.viewDetail", "row=0");
    execute("Sections.change", "activeSection=1");
    assertCollectionRowCount("pedidos", 0);
    execute("Collection.add",
        "viewObject=xava_view_section1_pedidos");
//  execute("AddToCollection.add", "row=0"); // Ahora no seleccionamos al azar
 
    seleccionarPrimerPedidoConEntregadoIgual("Entregado"); // Selecciona un pedido entregado
    seleccionarPrimerPedidoConEntregadoIgual(""); // Selecciona uno no entregado
    execute("AddToCollection.add"); // Tratamos de añadir ambos
    assertError( // Un error, porque el pedido no entregado no se puede añadir
        "¡ERROR! 1 elemento(s) NO añadido(s) a Pedidos de Factura");
    assertMessage( // Un mensaje de confirmación, porque el pedido entregado ha sido añadido
        "1 elemento(s) añadido(s) a Pedidos de Factura");
 
    assertCollectionRowCount("pedidos", 1);
    checkRowCollection("pedidos", 0);
    execute("Collection.removeSelected",
        "viewObject=xava_view_section1_pedidos");
    assertCollectionRowCount("pedidos", 0);
}
Hemos modificado la parte de la selección de pedidos a añadir, antes seleccionábamos el primero, no importaba si estaba servido o no. Ahora seleccionamos un pedido servido y otro no servido, de esta forma comprobamos que el pedido servido se añade y el no servido es rechazado.
La pieza que nos falta es la forma de seleccionar los pedidos. Esto es el trabajo del método seleccionarPrimerPedidoConEntregadoIgual(). Veámoslo:
private void seleccionarPrimerPedidoConEntregadoIgual(String value)
    throws Exception
{
    int c = getListRowCount(); // El total de filas visualizadas en la lista
    for (int i = 0; i < c; i++) {
        if (value.equals(
            getValueInList(i, 13))) // Obtenermos valor de la columna 'entregado'
            {
                checkRow(i);
                return;
            }
    }
    fail("Debe tener al menos una fila con entregado=" + value);
}
Aquí ves una buena técnica para hacer un bucle sobre los elementos visualizados de una lista para seleccionarlos y coger algunos datos, o cualquier otra cosa que quieras hacer con los datos de la lista.

Probar validación al asignar una referencia y al borrar

Desde el módulo Factura el usuario no pueda asignar pedidos a una factura si los pedidos no están servidos, por lo tanto, desde el módulo Pedido el usuario tampoco debe poder asignar una factura a un pedido si éste no está servido. Es decir, hemos de probar también la otra parte de la asociación. Lo haremos modificando el actual testPonerFactura() de PruebaPedido.
Además, aprovecharemos este caso para probar la validación al borrar que vimos en las secciones Validar al borrar con @RemoveValidator y Validar al borrar con un método de retrollamada. Allí modificamos la aplicación para impedir que un usuario borrara un pedido si éste tenía una factura asociada. Ahora probaremos este hecho.
Todo esto está en el testPonerFactura() mejorado que puedes ver a continuación:
public void testPonerFactura() throws Exception {
    login("admin", "admin");
    assertListNotEmpty();
    execute("List.orderBy", "property=numero"); // Establece el orden de la lista
    execute("List.viewDetail", "row=0");
    assertValue("entregado", "false"); // El pedido no debe estar entregado
    execute("Sections.change", "activeSection=1");
    assertValue("factura.numero", "");
    assertValue("factura.anyo", "");
    execute("Reference.search",
        "keyProperty=factura.anyo");
    String anyo = getValueInList(0, "anyo");
    String numero = getValueInList(0, "numero");
    execute("ReferenceSearch.choose", "row=0");
    assertValue("factura.anyo", anyo);
    assertValue("factura.numero", numero);
 
    // Los pedidos no entregados no pueden tener factura
    execute("CRUD.save");
    assertErrorsCount(1); // No podemos grabar porque no ha sido entregado
    setValue("entregado", "true");
    execute("CRUD.save"); // Con 'entregado=true' podemos grabar el pedido
    assertNoErrors();
 
    // Un pedido con factura no se puede borrar
    execute("Mode.list"); // Vamos al modo lista
    execute("CRUD.deleteRow", "row=0"); // y eliminanos el pedido grabado
    assertError("Imposible borrar Pedido por: " + // No podemos borrar porque tiene
        "Pedido asociado a factura no puede ser eliminado"); // una factura asociada
 
    // Restaurar los valores originales
    execute("List.viewDetail", "row=0");
    setValue("entregado", "false");
    setValue("factura.anyo", "");
    execute("CRUD.save");
    assertNoErrors();
}
La prueba original solo buscaba una factura, ni siquiera intentaba grabarla. Ahora, hemos añadido código al final para probar la grabación de un pedido marcado como servido, y marcado como no servido, de esta forma comprobamos la validación. Después de eso, tratamos de borrar el pedido, el cual tiene una factura, así probamos también la validación al borrar.

Probar el Bean Validation propio

Solo nos queda probar tu Bean Validation ISBN, el cual usa un servicio REST para hacer la validación. Simplemente hemos de escribir una prueba que trate de asignar un ISBN incorrecto, uno inexistente y uno correcto a un producto, y ver que pasa. Para hacer esto añadamos un método testValidarISBN() a PruebaProducto.
public void testValidarISBN() throws Exception {
    login("admin", "admin");
 
    // Buscar producto1
    execute("CRUD.new");
    setValue("numero", Integer.toString(producto1.getNumero()));
    execute("CRUD.refresh");
    assertValue("descripcion", "Producto JUNIT 1");
    assertValue("isbn", "");
 
    // Con un formato de ISBN incorrecto
    setValue("isbn", "1111");
    execute("CRUD.save"); // Falla por el formato (apache commons validator)
    assertError("1111 no es un valor válido para ISBN de Producto: " +
        "No existe ISBN");
 
    // ISBN no existe aunque tiene un formato correcto
    setValue("isbn", "9791034369997");
    execute("CRUD.save"); // Falla porque no existe ISBN (el servicio REST)
    assertError("9791034369997 no es un valor válido para ISBN de " +
        "Producto: No existe ISBN");
 
    // ISBN existe
    setValue("isbn", "9780932633439");
    execute("CRUD.save"); // No falla
    assertNoErrors();
}
Seguramente la prueba manual que hacías mientras estabas escribiendo el validador @ISBN era parecida a esta. Por eso, si hubieras escrito tu código de prueba antes que el código de la aplicación, lo hubieras podido usar mientras que desarrollabas, lo cual es más eficiente que repetir una y otra vez a mano las pruebas con el navegador.
Fíjate que si usas @ISBN(search=false) esta prueba no funciona porque no solo comprueba el formato sino que también hace la búsqueda con el servicio REST. Por tanto, has de usar @ISBN sin atributos para anotar la propiedad isbn y poder ejecutar esta prueba.
Ahora ejecuta todas las prueba de tu aplicación Facturación para verificar que todo sigue en su sitio.

Resumen

En esta lección has aprendido varias formas de hacer validación en una aplicación OpenXava. Además, ahora estás preparado para encapsular toda la lógica de validación reutilizable en anotaciones usando Bean Validation.
La validación es una parte importante de la lógica de tu aplicación, y te ánimo a que la pongas en el modelo, es decir en las entidades; tal y como esta lección ha mostrado. Aun así, a veces es conveniente poner algo de lógica fuera de las clases del modelo. Aprenderás a hacer esto en las siguientes lecciones.

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 7