Validações para tipos de enumeração

Validações para tipos de enumeração

1. Introdução

No tutorialJava Bean Validation Basics, vimos como podemos aplicar validaçõesjavax usandoJSR 380 para vários tipos. E no tutorialSpring MVC Custom Validation, vimos como criar validações personalizadas.

Neste próximo tutorial,we’ll focus on building validations for enums using custom annotations.

2. Validando Enums

Unfortunately, most standard annotations can not be applied to enums.

Por exemplo, ao aplicar a anotação@Pattern a um enum, recebemos um erro como este com o Validador Hibernate:

javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint
 'javax.validation.constraints.Pattern' validating type 'org.example.javaxval.enums.demo.CustomerType'.
 Check configuration for 'customerTypeMatchesPattern'

Na verdade, as únicas anotações padrão que podem ser aplicadas a enum são@NotNull e@Null.

3. Validando o Padrão de um Enum

Vamos começar definindo uma anotação para validar o padrão de um enum:

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = EnumNamePatternValidator.class)
public @interface EnumNamePattern {
    String regexp();
    String message() default "must match \"{regexp}\"";
    Class[] groups() default {};
    Class[] payload() default {};
}

Agora podemos simplesmente adicionar essa nova anotação usando uma expressão regular em nosso enumCustomerType:

@EnumNamePattern(regexp = "NEW|DEFAULT")
private CustomerType customerType;

Como podemos ver, a anotação não contém realmente a lógica de validação. Portanto, precisamos fornecer umConstraintValidator:

public class EnumNamePatternValidator implements ConstraintValidator> {
    private Pattern pattern;

    @Override
    public void initialize(EnumNamePattern annotation) {
        try {
            pattern = Pattern.compile(annotation.regexp());
        } catch (PatternSyntaxException e) {
            throw new IllegalArgumentException("Given regex is invalid", e);
        }
    }

    @Override
    public boolean isValid(Enum value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        Matcher m = pattern.matcher(value.name());
        return m.matches();
    }
}

Neste exemplo, a implementação é muito semelhante ao validador padrão@Pattern. However, this time, we match the name of the enum.

4. Validando um Subconjunto de um Enum

Combinar um enum com uma expressão regular não é seguro para o tipo. Instead, it makes more sense to compare with the actual values of an enum.

No entanto, devido às limitações das anotações, essa anotação não pode ser tornada genérica. Isso ocorre porque os argumentos para anotações podem ser apenas valores concretos de uma enum específica, não instâncias da classe pai da enum.

Vejamos como criar uma anotação de validação de subconjunto específico para nossaCustomerType enum:

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = CustomerTypeSubSetValidator.class)
public @interface CustomerTypeSubset {
    CustomerType[] anyOf();
    String message() default "must be any of {anyOf}";
    Class[] groups() default {};
    Class[] payload() default {};
}

Essa anotação pode então ser aplicada a enums do tipoCustomerType:

@CustomerTypeSubset(anyOf = {CustomerType.NEW, CustomerType.OLD})
private CustomerType customerType;

Next, we need to define the CustomerTypeSubSetValidator to check whether the list of given enum values contains the current one:

public class CustomerTypeSubSetValidator implements ConstraintValidator {
    private CustomerType[] subset;

    @Override
    public void initialize(CustomerTypeSubset constraint) {
        this.subset = constraint.anyOf();
    }

    @Override
    public boolean isValid(CustomerType value, ConstraintValidatorContext context) {
        return value == null || Arrays.asList(subset).contains(value);
    }
}

Embora a anotação deva ser específica para um determinado enum, podemos, é claro, codificarshare entre diferentes validadores.

5. Validando se uma string corresponde a um valor de um Enum

Em vez de validar um enum para corresponder aString, também poderíamos fazer o oposto. Para isso, podemos criar uma anotação que verifique seString é válido para um determinado enum.

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = ValueOfEnumValidator.class)
public @interface ValueOfEnum {
    Class> enumClass();
    String message() default "must be any of enum {enumClass}";
    Class[] groups() default {};
    Class[] payload() default {};
}

Esta anotação pode ser adicionada a um campoString e podemos passar qualquer classe enum.

@ValueOfEnum(enumClass = CustomerType.class)
private String customerTypeString;

Let’s define the ValueOfEnumValidator to check whether the String (or any CharSequence) is contained in the enum:

public class ValueOfEnumValidator implements ConstraintValidator {
    private List acceptedValues;

    @Override
    public void initialize(ValueOfEnum annotation) {
        acceptedValues = Stream.of(annotation.enumClass().getEnumConstants())
                .map(Enum::name)
                .collect(Collectors.toList());
    }

    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        return acceptedValues.contains(value.toString());
    }
}

Essa validação pode ser especialmente útil ao trabalhar com objetos JSON. Como a seguinte exceção aparece, ao mapear um valor incorreto de um objeto JSON para uma enumeração:

Cannot deserialize value of type CustomerType from String value 'UNDEFINED': value not one
 of declared Enum instance names: [...]

É claro que podemos lidar com essa exceção. No entanto, isso não nos permite denunciar todas as violações de uma só vez.

Em vez de mapear o valor para um enum, podemos mapeá-lo para umString. Em seguida, usaríamos nosso validador para verificar se ele corresponde a qualquer um dos valores enum.

6. Trazendo tudo junto

Agora podemos validar beans usando qualquer uma de nossas novas validações. Mais importante ainda, todas as nossas validações aceitam valores denull. Consequentemente, também podemos combiná-lo com a anotação@NotNull:

public class Customer {
    @ValueOfEnum(enumClass = CustomerType.class)
    private String customerTypeString;

    @NotNull
    @CustomerTypeSubset(anyOf = {CustomerType.NEW, CustomerType.OLD})
    private CustomerType customerTypeOfSubset;

    @EnumNamePattern(regexp = "NEW|DEFAULT")
    private CustomerType customerTypeMatchesPattern;

    // constructor, getters etc.
}

Na próxima seção, veremos como podemos testar nossas novas anotações.

7. Testando nossas validações Javax para Enums

Para testar nossos validadores, vamos configurar um validador, que suporta nossas anotações recém-definidas. Usaremos o beanCustomer para todos os nossos testes.

Primeiro, queremos ter certeza de que uma instânciaCustomer válida não causa nenhuma violação:

@Test
public void whenAllAcceptable_thenShouldNotGiveConstraintViolations() {
    Customer customer = new Customer();
    customer.setCustomerTypeOfSubset(CustomerType.NEW);
    Set violations = validator.validate(customer);
    assertThat(violations).isEmpty();
}

Em segundo lugar, queremos que nossas novas anotações suportem e aceitem os valoresnull. Esperamos apenas uma única violação. Isso deve ser relatado emcustomerTypeOfSubset pela anotação@NotNull :

@Test
public void whenAllNull_thenOnlyNotNullShouldGiveConstraintViolations() {
    Customer customer = new Customer();
    Set violations = validator.validate(customer);
    assertThat(violations.size()).isEqualTo(1);

    assertThat(violations)
      .anyMatch(havingPropertyPath("customerTypeOfSubset")
      .and(havingMessage("must not be null")));
}

Por fim, validamos nossos validadores para relatar violações, quando a entrada não é válida:

@Test
public void whenAllInvalid_thenViolationsShouldBeReported() {
    Customer customer = new Customer();
    customer.setCustomerTypeString("invalid");
    customer.setCustomerTypeOfSubset(CustomerType.DEFAULT);
    customer.setCustomerTypeMatchesPattern(CustomerType.OLD);

    Set violations = validator.validate(customer);
    assertThat(violations.size()).isEqualTo(3);

    assertThat(violations)
      .anyMatch(havingPropertyPath("customerTypeString")
      .and(havingMessage("must be any of enum class org.example.javaxval.enums.demo.CustomerType")));
    assertThat(violations)
      .anyMatch(havingPropertyPath("customerTypeOfSubset")
      .and(havingMessage("must be any of [NEW, OLD]")));
    assertThat(violations)
      .anyMatch(havingPropertyPath("customerTypeMatchesPattern")
      .and(havingMessage("must match \"NEW|DEFAULT\"")));
}

8. Conclusão

Neste tutorial,we covered three options to validate enums using custom annotations and validators.

Primeiro, aprendemos como validar o nome de uma enum usando uma expressão regular.

Segundo, discutimos uma validação para um subconjunto de valores de uma enum específica. Também explicamos por que não podemos criar uma anotação genérica para fazer isso.

Por fim, também vimos como criar um validador para strings. Para verificar se umString está de acordo com um valor particular de um determinado enum.

Como sempre, o código-fonte completo do artigo está disponívelover on Github.