«1. Введение

В учебнике Основы проверки Java Bean мы увидели, как мы можем применять проверки javax с использованием JSR 380 к различным типам. А в учебнике Spring MVC Custom Validation мы увидели, как создавать собственные проверки.

В этом следующем руководстве мы сосредоточимся на создании проверок для перечислений с использованием пользовательских аннотаций.

2. Проверка перечислений

К сожалению, большинство стандартных аннотаций нельзя применить к перечислениям.

Например, при применении аннотации @Pattern к перечислению мы получаем ошибку, подобную этой, с помощью Hibernate Validator: .

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

3. Проверка шаблона перечисления

Давайте начнем с определения аннотации для проверки шаблона перечисления:

Теперь мы можем просто добавить эту новую аннотацию, используя регулярное выражение, в наше перечисление CustomerType :

@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<? extends Payload>[] payload() default {};
}

Как мы видим, аннотация на самом деле не содержит логики проверки. Поэтому нам нужно предоставить ConstraintValidator:

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

В этом примере реализация очень похожа на стандартный валидатор @Pattern. Однако на этот раз мы сопоставляем имя перечисления.

public class EnumNamePatternValidator implements ConstraintValidator<EnumNamePattern, Enum<?>> {
    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();
    }
}

4. Проверка подмножества перечисления

Сопоставление перечисления с регулярным выражением небезопасно для типов. Вместо этого имеет смысл сравнивать с фактическими значениями перечисления.

Однако из-за ограничений аннотаций такую ​​аннотацию нельзя сделать универсальной. Это связано с тем, что аргументы для аннотаций могут быть только конкретными значениями определенного перечисления, а не экземплярами родительского класса перечисления.

Давайте посмотрим, как создать специальную аннотацию проверки подмножества для нашего перечисления CustomerType:

Затем эту аннотацию можно применить к перечислениям типа CustomerType:

@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<? extends Payload>[] payload() default {};
}

Далее нам нужно определить CustomerTypeSubSetValidator, чтобы проверить, содержит ли список заданных значений перечисления текущее значение:

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

Хотя аннотация должна быть специфичной для определенного перечисления, мы, конечно, можем совместно использовать код между разными валидаторами.

public class CustomerTypeSubSetValidator implements ConstraintValidator<CustomerTypeSubset, CustomerType> {
    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);
    }
}

5. Проверка соответствия строки значению перечисления

Вместо проверки соответствия перечисления строке можно сделать и наоборот. Для этого мы можем создать аннотацию, которая проверяет, допустима ли строка для определенного перечисления.

Эту аннотацию можно добавить к полю String, и мы можем передать любой класс enum.

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

Давайте определим ValueOfEnumValidator для проверки того, содержится ли String (или любая CharSequence) в перечислении:

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

Эта проверка может быть особенно полезна при работе с объектами JSON. Потому что возникает следующее исключение при отображении неверного значения из объекта JSON в перечисление:

public class ValueOfEnumValidator implements ConstraintValidator<ValueOfEnum, CharSequence> {
    private List<String> 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());
    }
}

Мы, конечно, можем обработать это исключение. Однако это не позволяет сообщать обо всех нарушениях сразу.

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

Вместо сопоставления значения с перечислением мы можем сопоставить его со строкой. Затем мы использовали бы наш валидатор, чтобы проверить, соответствует ли он какому-либо из значений перечисления.

6. Собираем все вместе

Теперь мы можем проверять bean-компоненты, используя любую из наших новых проверок. Самое главное, все наши проверки принимают нулевые значения. Следовательно, мы также можем комбинировать его с аннотацией @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.
}

7. Тестирование наших валидаций Javax для перечислений

Чтобы протестировать наши валидаторы, мы настроим валидатор, который поддерживает наши вновь определенные аннотации. Мы будем клиентом для всех наших тестов.

Во-первых, мы хотим убедиться, что действительный экземпляр Customer не вызывает никаких нарушений:

Во-вторых, мы хотим, чтобы наши новые аннотации поддерживали и принимали нулевые значения. Мы ожидаем только одно нарушение. Об этом должно быть сообщено в аннотации @NotNull для customerTypeOfSubset:

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

Наконец, мы проверяем наши валидаторы, чтобы сообщать о нарушениях, когда ввод недействителен:

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

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

8. Заключение

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

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

    assertThat(violations)
      .anyMatch(havingPropertyPath("customerTypeString")
      .and(havingMessage("must be any of enum class com.baeldung.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\"")));
}

В этом руководстве , мы рассмотрели три варианта проверки перечислений с помощью пользовательских аннотаций и валидаторов.

«Во-первых, мы научились проверять имя перечисления с помощью регулярного выражения.

Во-вторых, мы обсудили проверку подмножества значений конкретного перечисления. Мы также объяснили, почему мы не можем создать общую аннотацию для этого.

Наконец, мы также рассмотрели, как создать валидатор для строк. Чтобы проверить, соответствует ли строка определенному значению данного перечисления.

Как всегда, полный исходный код статьи доступен на Github.

«