Creación de validadores con Bean Validation\Hibernate Validator

logo java

Una vez que hemos empezado a utilizar la especificación Bean Validation para definir restricciones sobre los atributos de clases Java, en algún momento nos encontraremos con la necesidad de validar restricciones que no sean proporcionadas ni por el estándar Bean Validation ni por Hibernate Validator y que no pueden definirse utilizando expresiones regulares con @Pattern. Por ejemplo, realizar alguna comprobación en una base de datos.

Afortunadamente Bean Validation está diseñada para ser totalmente extensible y la implementación de validadores personalizados para nuestras aplicaciones es una tarea sencilla tal y como veremos en este tutorial.

El primer validador

Empecemos creando una restricción lo más simple posible para familiarizarnos con el proceso de creación de restricciones o validadores personalizados. Este proceso consiste en dos pasos:

  1. Crear la anotación. Veamos el siguiente ejemplo:
    package com.danielme.blog.validation.custom;
    
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    import javax.validation.Constraint;
    import javax.validation.Payload;
    
    import com.danielme.blog.validation.custom.impl.DummyValidator;
    
    @Target({ ElementType.FIELD })
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = DummyValidator.class)
    public @interface Dummy {
    
        String message() default "{com.danielme.blog.validation.custom.DummyValidator.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
    }
    

    He definido una anotación aplicable a atributos (@Target) e indicado con @Constraint la clase que implementa la lógica de este validador y que veremos a continuación. Asimismo, la anotación tiene los tres atributos que se esperan de los validadores con valores por defecto incluyendo la clave del mensaje de error.

  2. Implementar el validador. En concreto, hay que implementar la interfaz ConstraintValidator para la anotación de la validación y la clase del elemento que se valida. En el método isValid recibimos el elemento a validar y tenemos que implementar la validación propiamente dicha.
    package com.danielme.blog.validation.custom.impl;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    
    import com.danielme.blog.validation.custom.Dummy;
    
    public class DummyValidator implements ConstraintValidator<Dummy, String> {
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return "dummy".equals(value);
        }
    
    }
    
    
  3. Y con esto ya tenemos nuestra validación lista para ser aplicada.

    package com.danielme.blog.validation.model;
    
    import com.danielme.blog.validation.custom.Dummy;
    import com.danielme.blog.validation.custom.SameValue;
    import com.danielme.blog.validation.custom.StringRange;
    
    public class CustomValidators {
    
        @Dummy
        private String dummy;
    
        public String getDummy() {
            return dummy;
        }
    
        public void setDummy(String dummy) {
            this.dummy = dummy;
        }   
    
    }
    

    Los siguientes tests de JUnit4 nos permiten comprobar el correcto funcionamiento de nuestro código.

    package com.danielme.blog.validation;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertTrue;
    
    import java.util.Set;
    
    import javax.validation.ConstraintViolation;
    import javax.validation.Validation;
    import javax.validation.Validator;
    
    import org.junit.Test;
    
    import com.danielme.blog.validation.model.CustomValidators;
    
    public class CustomValidatorsTest {
    
        private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
    
        @Test
        public void testDummyNoOk() {
            CustomValidators customValidators = new CustomValidators();
            Set<ConstraintViolation<CustomValidators>> violations = validator
                    .validate(customValidators);
            assertEquals(1, violations.size());
            assertEquals("{com.danielme.blog.validation.custom.DummyValidator.message}",
                    violations.iterator().next().getMessage());
        }
    
        @Test
        public void testDummyOk() {
            CustomValidators customValidators = new CustomValidators();
            customValidators.setDummy("dummy");
            Set<ConstraintViolation<CustomValidators>> violations = validator
                    .validate(customValidators);
            assertTrue(violations.isEmpty());
        }
    
    }
    

    Cursos aplicaciones móviles

    Validadores parametrizables

    Es posible añadir parámetros a aquellos validadores en los que los necesitemos simplemente definiéndolos en la anotación. Vamos a crear un validador que compruebe que el contenido de una cadena esté dentro de un conjunto de valores personalizable para que el validador sea lo más genérico y reutilizable posible.

    Al definir la anotación, añadimos un array para contener los valores. No le damos valor por defecto para que sea obligatorio proporcionar esos valores en la anotación.

    package com.danielme.blog.validation.custom;
    
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    import javax.validation.Constraint;
    import javax.validation.Payload;
    
    import com.danielme.blog.validation.custom.impl.StringRangeValidator;
    
    @Target({ ElementType.FIELD })
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = StringRangeValidator.class)
    public @interface StringRange {
    
        String[] values();
    
        String message() default "{com.danielme.blog.validation.custom.StringRange.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
    }
    

    Para acceder a los atributos de la anotación se sobrescribe el método initialize.

    package com.danielme.blog.validation.custom.impl;
    
    import java.util.Arrays;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    
    import com.danielme.blog.validation.custom.StringRange;
    
    public class StringRangeValidator implements ConstraintValidator<StringRange, String> {
    
        private String[] values;
    
        @Override
        public void initialize(StringRange constraintAnnotation) {
            values = constraintAnnotation.values();
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return value == null || Arrays.stream(values).anyMatch(value::equals);
        }
    
    }
    

    Ahora usamos la anotación del siguiente modo:

    @StringRange(values = { "Spain", "Mexico" })
    private String country;
    

    y probamos con los tests correspondientes.

        @Test
        public void testRangeOk() {
            CustomValidators customValidators = new CustomValidators();
            customValidators.setDummy("dummy");
            customValidators.setCountry("Spain");
    
            Set<ConstraintViolation<CustomValidators>> violations = validator
                    .validate(customValidators);
            
            assertTrue(violations.isEmpty());
        }
    
        @Test
        public void testRangeNoOk() {
            CustomValidators customValidators = new CustomValidators();
            customValidators.setCountry("Germany");
            customValidators.setDummy("dummy");
    
            Set<ConstraintViolation<CustomValidators>> violations = validator
                    .validate(customValidators);
    
            assertEquals(1, violations.size());
            assertEquals("{com.danielme.blog.validation.custom.StringRange.message}",
                    violations.iterator().next().getMessage());
        }
    

    Validadores sobre múltiples campos

    Un escenario que podemos encontrar es tener que definir una validación que afecte a más de un atributo de la clase. El ejemplo más típico es el de un formulario de registro en el que por seguridad el usuario tiene que proporcionar por ejemplo dos veces su contraseña o número de teléfono.

    La creación de la validación anterior es análoga a los ejemplos que hemos visto previamente pero definiendo ahora la anotación a nivel de clase y no de atributo. Para hacer el validador genérico, lo definiremos para la clase Object, es decir, para cualquier clase, y en la anotación indicaremos los nombres de los atributos que deben ser iguales. De este modo, la anotación quedaría así:

    package com.danielme.blog.validation.custom;
    
    import java.lang.annotation.RetentionPolicy;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    import javax.validation.Constraint;
    import javax.validation.Payload;
    
    import com.danielme.blog.validation.custom.impl.SameValueValidator;
    
    @Target({ ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = SameValueValidator.class)
    public @interface SameValue {
    
        String field1();
    
        String field2();
    
        String message() default "{com.danielme.blog.validation.custom.SameValue.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
    }
    
    

    La implementación tiene la particularidad de que debemos acceder a los atributos de la clase mediante reflection. Asimismo la he codificado de tal modo que sólo se compruebe la igualdad de los campos si ambos no son nulos porque la nulidad de los mismos se puede validar con la anotación @NonNull.

    package com.danielme.blog.validation.custom.impl;
    
    import java.lang.reflect.Field;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    
    import com.danielme.blog.validation.custom.SameValue;
    
    public class SameValueValidator implements ConstraintValidator<SameValue, Object> {
    
        private String field1;
        private String field2;
    
        @Override
        public void initialize(SameValue constraintAnnotation) {
            field1 = constraintAnnotation.field1();
            field2 = constraintAnnotation.field2();
        }
    
       @Override
        public boolean isValid(Object object, ConstraintValidatorContext context) {
            try {
                Object value1 = getValue(object, field1);
                Object value2 = getValue(object, field2);
    
                return value1 == null || value2 == null || value1.equals(value2);
            } catch (Exception ex) {
                throw new IllegalArgumentException(
                        "Cannot read '" + field1 + "', '" + field2 + "' fields from Object " + object,
                        ex);
            }
        }
    
        private Object getValue(Object object, String fieldName) throws NoSuchFieldException,
                SecurityException, IllegalArgumentException, IllegalAccessException {
            Field field = object.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(object);
        }
    
    }
    
    

    Ahora aplicamos la validación a la clase.

    @SameValue(field1 = "field1", field2 = "field2")
    public class CustomValidators {  
    
        private String field1;
    
        private String field2;
    
    

    Los tests correspondientes.

        @Test
        public void testSameValueNoOk() {
            CustomValidators customValidators = new CustomValidators();
            customValidators.setDummy("dummy");
            customValidators.setField1("value1");
            customValidators.setField2("value2");
            
            Set<ConstraintViolation<CustomValidators>> violations = validator
                    .validate(customValidators);
            
            assertEquals(1, violations.size());
            assertEquals("{com.danielme.blog.validation.custom.SameValue.message}",
                    violations.iterator().next().getMessage());
        }
        
        @Test
        public void testSameValueOk() {
            CustomValidators customValidators = new CustomValidators();
            customValidators.setDummy("dummy");
            customValidators.setField1("value");
            customValidators.setField2("value");
            
            Set<ConstraintViolation<CustomValidators>> violations = validator
                    .validate(customValidators);
            
            assertTrue(violations.isEmpty());
        }
    

    Una solución alternativa para evitar el uso de reflection es utilizar una interfaz que defina los métodos que devuelven los valores de los dos campos a validar.

    package com.danielme.blog.validation.custom;
    
    public interface SameValueFields {
    
        Object getField1();
    
        Object getField2();
    }
    

    Ahora la anotación no recibe los nombres de los campos.

    package com.danielme.blog.validation.custom;
    
    import java.lang.annotation.RetentionPolicy;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    import javax.validation.Constraint;
    import javax.validation.Payload;
    
    import com.danielme.blog.validation.custom.impl.SameValueViaInterfaceValidator;
    
    @Target({ ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = SameValueViaInterfaceValidator.class)
    public @interface SameValueViaInterface {
    
        String message() default "{com.danielme.blog.validation.custom.SameValue.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
    }
    

    La implementación se crea para validar un objeto que implemente la interfaz lo que evita utilizar reflection.

    package com.danielme.blog.validation.custom.impl;
    
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    
    import com.danielme.blog.validation.custom.SameValueFields;
    import com.danielme.blog.validation.custom.SameValueViaInterface;
    
    public class SameValueViaInterfaceValidator
            implements ConstraintValidator<SameValueViaInterface, SameValueFields> {
    
        @Override
        public boolean isValid(SameValueFields object, ConstraintValidatorContext context) {
    
            return object.getField1() == null || object.getField2() == null
                    || object.getField1().equals(object.getField2());
        }
    
    }
    

    Por último, para aplicar el validador además de añadir la anotación a la clase también tendremos que implementar la interfaz.

    @SameValueViaInterface
    public class CustomValidators implements SameValueFields {
    

    Paso de parámetros externos

    Una funcionalidad muy interesante de Hibernate Validator y que está fuera del estándar consiste en el envío de parámetros (payload) a los validadores en el momento de construir el objeto validador. Esto permite parametrizar el comportamiento del validador en tiempo de ejecución lo que nos permite crear validadores dinámicos y flexibles. En la documentación oficial encontramos el siguiente ejemplo, obsérvese que el payload puede ser el idioma que el usuario de nuestra aplicación tenga seleccionado en ese momento:

     ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
                .configure()
                .constraintValidatorPayload( "US" )
                .buildValidatorFactory();
    
        Validator validator = validatorFactory.getValidator();
    

    También se puede definir el payload en cada validador de forma específica en lugar de en la factoría para todos los validadores:

    HibernateValidatorFactory hibernateValidatorFactory = Validation.byDefaultProvider()
            .configure()
            .buildValidatorFactory()
            .unwrap( HibernateValidatorFactory.class );
    
    Validator validator = hibernateValidatorFactory.usingContext()
            .constraintValidatorPayload( "US" )
            .getValidator();
    

    El payload lo podemos obtener dentro de nuestro validador con el siguiente código.

     @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            String countryCode = context
                    .unwrap( HibernateConstraintValidatorContext.class )
                    .getConstraintValidatorPayload( String.class );
            
            ...resto del código
        }
    

    Código de ejemplo
    El código de ejemplo del tutorial completo se encuentra en GitHub (incluye también el código del tutorial Validaciones con Hibernate Validator). Para más información sobre cómo utilizar GitHub consultar este artículo.

    Master Pyhton, Java, Scala or Ruby

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.