Validaciones de atributos con Hibernate Validator

logo java

El objetivo de la especificación Bean Validation es establecer un estándar para la definición e implementación de restricciones en clases Java del tipo “el atributo nombre no puede ser nulo”, “el atributo cantidad debe ser un número positivo mayor que 10”, etc. La aplicación más evidente de estas restricciones es la realización de validaciones sobre los datos de entrada de un sistema, ya sean proporcionados por un usuario a través de un formulario o de forma automática por otro proceso. Al tratarse de un estándar compatible con Java SE puede ser utilizado en cualquier aplicación e integrado fácilmente con Spring, Struts 2 o JSF.

Bean Validation es una especificación así que para poder utilizarla tenemos que recurrir a un producto que la implemente. La implementación de referencia, y la única oficialmente certificada para la versión más reciente de la especificación, es Hibernate Validator. Otra alternativa, y que personalmente nunca he utilizado, es Oval.

En este pequeño tutorial veremos cómo empezar a utilizar este estándar en nuestras aplicaciones Java de la mano de Hibernate Validator. Una vez adquiridos estos conocimientos podemos continuar con la Creación de validadores con Bean Validation\Hibernate Validator.

Lo primero que tenemos que hacer es incluir en el classpath de nuestra aplicación las librerías necesarias para utilizar Hibernate Validator. En nuestro proyecto de ejemplo con Maven tendremos el siguiente pom.xml.

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.danielme.blog.validation</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>1.0</version>
    <name>Hibernate Validator</name>
    <packaging>jar</packaging>

    <description>JSR 380 with Hibernate Validator</description>
    <url>https://danielme.com/2018/10/08/validaciones-con-hibernate-validator</url>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <mavencompiler.version>3.2</mavencompiler.version>
        <hibernate.validator.version>6.0.13.Final</hibernate.validator.version>
        <java.el.api.version>3.0.0</java.el.api.version>
        <java.el.impl.version>2.2.6</java.el.impl.version>
        <junit.version>4.12</junit.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>${hibernate.validator.version}</version>
        </dependency>

        <dependency>
            <groupId>javax.el</groupId>
            <artifactId>javax.el-api</artifactId>
            <version>${java.el.api.version}</version>
        </dependency>

        <dependency>
            <groupId>org.glassfish.web</groupId>
            <artifactId>javax.el</artifactId>
            <version>${java.el.impl.version}</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>


    </dependencies>

    <build>

        <plugins>

            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${mavencompiler.version}</version>
                <configuration>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <source>${jre.version}</source>
                    <target>${jre.version}</target>
                </configuration>
            </plugin>

        </plugins>

    </build>

</project>

La especificación Bean Validation define un conjunto de anotaciones que proporcionan una colección de restricciones genéricas y básicas y que podemos comprobar en la documentación. Estas restricciones que obtenemos de serie se complementan con otras anotaciones añadidas por Hibernate y que por tanto están fuera del estándar.

Vamos a crear un Bean denominado User como el siguiente.

Cursos aplicaciones móviles

package com.danielme.blog.validation.model;

public class User {

    private String name;
    private String email;
    private Integer phoneNumber;
    private boolean legalAge;
    private String personalBlog;
    private List<String> tags;

    ...getters and setters

}

Ahora vamos a ir definiendo restricciones de forma individual sobre cada atributo. El ejemplo no pretende ser exhaustivo sino una demostración básica de cómo utilizar Hibernate Validator aplicado a los atributos de una clase. También se pueden aplicar a parámetros y retornos de métodos; esta funcionalidad será tratada en un futuro tutorial.

  • El nombre es un campo obligatorio con un tamaño comprendido entre 5 y 50 caracteres.
    @NotNull
    @Size(min = 5, max = 50)
    private String name;
    

    En este ejemplo definimos el tamaño del String, pero si simplemente queremos que contenga al menos un carácter podemos utilizar @NotBlank.

  • El email es obligatorio y debe tener el correspondiente formato.
    @NotNull
    @Email
    private String email;
    
  • El número de teléfono es opcional pero si se proporciona debe ser un número de 9 dígitos que empiece por seis.
    @Min(600000000)
    @Max(699999999)
    private Integer phoneNumber;
    

    Como alternativa se puede utilizar la anotación @Range propia de Hibernate.

  • Como alternativa al número de teléfono y ejemplo de expresiones regulares, podemos definir la misma restricción del siguiente modo.
    @Pattern(regexp = "[6][0-9]{8}")
    private String phoneNumberAsString;
    
  • El consentimiento de edad legal debe ser siempre true.
    @AssertTrue
    private boolean legalAge;
    
  • La dirección del blog debe ser una url válida.
    @URL
    private String personalBlog;
    

    Esta anotación es propia de Hibernate.

  • Algunas restricciones pueden ser aplicadas a colecciones. Por ejemplo, vamos a exigir que la lista de tags no esté vacía (aunque sí se permite que pueda null) y tenga entre 3 y 10 elementos.
    @NotEmpty
    @Size(min = 3, max = 10)
    private List<String> tags;
    

El cumplimiento de estas restricciones por parte de cualquier instancia de la clase User se realiza con una clase de tipo Validator la cual se construye con una clase de tipo ValidatorFactory.

Nota: si usamos Bean Validation de forma integrada con un framework como por ejemplo Spring MVC la validación de las restricciones se invocará según defina esa integración.

ValidatorFactory validationfactory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();

Probemos el funcionamiento con el siguiente test de JUnit 4.

package com.danielme.blog.validation;

import static org.junit.Assert.assertEquals;

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.User;

public class ValidatorTest {

    private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    @Test
    public void testEmpty() {
        Set<ConstraintViolation<User>> violations = validator.validate(new User());
        assertEquals(4, violations.size());
        violations.stream().forEach(v -> System.out.println(
                v.getPropertyPath() + " : " + v.getMessageTemplate() + " = " + v.getMessage()));
    }
}

Si lo ejecutamos veremos que no se cumplen las restricciones @NotNull

legalAge : {javax.validation.constraints.AssertTrue.message} = must be true
name : {javax.validation.constraints.NotNull.message} = must not be null
email : {javax.validation.constraints.NotNull.message} = must not be null

Vemos que por defecto las anotaciones proporcionan un mensaje de error genérico y descriptivo para el tipo de error identificado por una clave, pero generalmente vamos a querer definir nuestros propios mensajes de error personalizados. Esto podemos hacerlo utilizando el atributo message de las anotaciones.

@NotNull(message = "User name cannot not be empty")
@Size(min = 5, max = 50, message = "The length of the name must be between 5 and 50 characters")
private String name;

Sin embargo definir el mensaje que verá el usuario tal cual me parece una mala práctica ya que habitualmente tendremos que localizar nuestras aplicaciones en varios idiomas, además del hecho de tener los mensajes mezclados dentro del código fuente lo que obliga a recompilarlo ante cualquier cambio en los mismos. Por ello recomiendo utilizar una clave que permita identificar el mensaje de error a mostrar, y obtener el texto de ese mensaje en el idioma adecuado a partir de ficheros de localización. Esto último se puede configurar al integrarse Bean Validation con Spring o Struts 2.

Siguiendo esta estrategia voy a definir los mensajes para el bean User.

public class User {

    @NotNull(message = "user.name.empty")
    @Size(min = 5, max = 50, message = "user.name.size")
    private String name;

    @NotNull(message = "user.email.empty")
    @Email(message = "user.email.mask")
    private String email;

    @Min(value = 600000000, message = "user.phoneNumber.min")
    @Max(value = 699999999, message = "user.phoneNumber.max")
    private Integer phoneNumber;

    @Pattern(regexp = "[6][0-9]{8}", message = "user.phoneNumberAsString.mask")
    private String phoneNumberAsString;

    @AssertTrue(message = "user.legalAge.false")
    private boolean legalAge;

    @URL(message = "user.personalBlog.url")
    private String personalBlog;

    @NotEmpty(message = "user.tags.empty")
    @Size(min = 3, max = 10, message = "user.tags.size")
    private List<String> tags;

Ahora ampliamos el test con algún caso más para ver el funcionamiento de las validaciones.

    @Test
    public void testNotNull() {
        Set<ConstraintViolation<User>> violations = validator.validate(buildValidUser());
        assertTrue(violations.isEmpty());
    }

    @Test
    public void testEmail() {
        User user = buildValidUser();
        user.setEmail("mail.com");
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        assertEquals("user.email.mask", violations.iterator().next().getMessage());
    }

    @Test
    public void testUrlError() {
        User user = buildValidUser();
        user.setPersonalBlog("www.danielme.com");
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        assertEquals("user.personalBlog.url", violations.iterator().next().getMessage());
    }

    @Test
    public void testUrlSucces() {
        User user = buildValidUser();
        user.setPersonalBlog("https://www.danielme.com");
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        assertTrue(violations.isEmpty());
    }
    
    @Test
    public void testEmptyTags() {
        User user = buildValidUser();
        user.setTags(null);
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        assertEquals("user.tags.empty", violations.iterator().next().getMessage());
    }

    private User buildValidUser() {
        User user = new User();
        user.setName("Harris");
        user.setEmail("harris@gmail.com");
        user.setLegalAge(true);
        List<String> tags = new LinkedList<>();
        tags.add("Java");
        tags.add("Spring");
        tags.add("Android");
        user.setTags(tags);
        return user;
    }

Validaciones en cascada

Bean Validation permite la aplicación de restricciones en cascada para las relaciones que una clase con otras. Por ejemplo, vamos a crear la siguiente clase.

package com.danielme.blog.validation.model;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class Address {

    @NotNull(message = "address.line1.empty")
    @Size(min = 5, max = 50, message = "address.line1.size")
    private String line1;

    private String line2;

    @NotNull(message = "address.zipcode.empty")
    @Size(min = 2, max = 10, message = "address.zipcode.size")
    private String zipCode;

    @NotNull(message = "address.countrycode.empty")
    @Size(min = 2, max = 2, message = "address.countrycode.size")
    private String countryCode;

    ...setters and getters
}

Y la relacionamos con User.

private Address address;

public Address getAddress() {
      return address;
  }
public void setAddress(Address address) {
      this.address = address;
  }

En el siguiente test se crea un User sin errores de validación y se relaciona con un objeto de tipo Address que no verifica las restricciones de sus atributos. Comprobamos que la validación de User no devuelve ninguna violación.

 @Test
    public void testAddress() {
        User user = buildValidUser();
        Address address = new Address();
        user.setAddress(address);
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        assertTrue(violations.isEmpty());
    }

Si queremos que al validar un objeto de tipo User también se apliquen las restricciones del objeto Address que pueda contener basta con anotar el atributo con @Valid.

@Valid
private Address address;

Ahora actualizamos el test para comprobar que recibimos los tres errores esperados en Address.

    @Test
    public void testAddress() {
        User user = buildValidUser();
        Address address = new Address();
        user.setAddress(address);
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        assertEquals(3, violations.size());
        violations.stream()
                .forEach(v -> System.out.println(v.getPropertyPath() + " : " + v.getMessage()));
    }
address.line1 : address.line1.empty
address.zipCode : address.zipcode.empty
address.countryCode : address.countrycode.empty

Grupos

La especificación Bean Validation permite organizar las restricciones de una clase en grupos de tal modo que podemos elegir qué grupo de restricciones queremos validar cada vez que realizamos una validación de un objeto de la clase. De hecho, todas las restricciones para las que no definamos su agrupación están asociadas al grupo Default.

Vamos a crear la siguiente clase que es una copia de la clase Address.

package com.danielme.blog.validation.model;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class AddressWithGroup {

    @NotNull(message = "address.line1.empty")
    @Size(min = 5, max = 50, message = "address.line1.size")
    private String line1;

    private String line2;

    @NotNull(message = "address.zipcode.empty")
    @Size(min = 2, max = 10, message = "address.zipcode.size")
    private String zipCode;

    @NotNull(message = "address.countrycode.empty")
    @Size(min = 2, max = 2, message = "address.countrycode.size")
    private String countryCode;
    
...getters y setters
}

Obsérvese que el código de país es obligatorio, pero supongamos que no siempre queremos aplicar esa restricción. Lo que haremos es crear dos grupos de restricciones:

  1. Validaciones por defecto: se validan todas las restricciones.
  2. Grupo “Sin país”: no queremos que se aplique la restricción @NotNull para país. Esto es, tenemos que agrupar todas las demás.

Para indicar los grupos a los que pertenecen las restricciones usamos el atributo groups. Los grupos se definen simplemente creando una interfaz vacía. La interfaz para el grupo Default ya la tenemos y se aplica de forma automática a todas las restricciones para las que no definimos explícitamente su grupo, así que creamos una interfaz denominada por ejemplo NoCountry y definimos los grupos.

public class AddressWithGroup {

    public interface NoCountry {
    }

    @NotNull(message = "address.line1.empty", groups = { NoCountry.class, Default.class })
    @Size(min = 5, max = 50, message = "address.line1.size", groups = { NoCountry.class,
            Default.class })
    private String line1;

    private String line2;

    @NotNull(message = "address.zipcode.empty", groups = { NoCountry.class, Default.class })
    @Size(min = 2, max = 10, message = "address.zipcode.size", groups = { NoCountry.class,
            Default.class })
    private String zipCode;

    @NotNull(message = "address.countrycode.empty")
    @Size(min = 2, max = 2, message = "address.countrycode.size", groups = { NoCountry.class,
            Default.class })
    private String countryCode;

Obsérvese que la validación por defecto incluye todas las restricciones de la clase, por lo que el siguiente test será válido.

@Test
public void testAddressWithGroupDefault() {
    AddressWithGroup address = new AddressWithGroup();
    Set<ConstraintViolation<AddressWithGroup>> violations = validator.validate(address);
    assertEquals(3, violations.size());
    violations.stream()
              .forEach(v -> System.out.println(v.getPropertyPath() + " : " + v.getMessage()));
}
zipCode : address.zipcode.empty
line1 : address.line1.empty
countryCode : address.countrycode.empty

Pero si sólo se validan las restricciones del grupo NoCountry el test equivalente es el siguiente

@Test
public void testAddressWithGroupNoCountry() {
    AddressWithGroup address = new AddressWithGroup();
    Set<ConstraintViolation<AddressWithGroup>> violations = validator.validate(address, NoCountry.class);
    assertEquals(2, violations.size());
    violations.stream()
                .forEach(v -> System.out.println(v.getPropertyPath() + " : " + v.getMessage()));
    }
zipCode : address.zipcode.empty
line1 : address.line1.empty

De este modo el @NotNull de countryCode no se valida al no formar parte del grupo indicado (NoCountry.class).

Código de ejemplo
El código de ejemplo del tutorial completo se encuentra en GitHub e incluye el código del tutorial Creación de validadores con Bean Validation\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.