Рубрики
Без рубрики

Проверки для типов перечислений

Проверки для типов перечислений

Автор оригинала: Martin van Wingerden.

1. введение

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

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

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

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

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

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'

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

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

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

@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 {};
}

Теперь мы можем просто добавить эту новую аннотацию с помощью регулярного выражения в наш тип Customer enum:

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

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

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();
    }
}

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

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

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

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

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

@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 {};
}

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

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

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

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);
    }
}

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

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

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

@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 {};
}

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

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

Давайте определим Значение EnumValidator , чтобы проверить, содержится ли Строка (или любая CharSequence) в перечислении :

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());
    }
}

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

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

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

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

6. Сведение Всего Этого Воедино

Теперь мы можем проверить бобы, используя любую из наших новых проверок. Самое главное, что все наши проверки принимают значения null . Следовательно, мы также можем объединить его с аннотацией @Nonnull :

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 не вызывает никаких нарушений:

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

Во-вторых, мы хотим, чтобы наши новые аннотации поддерживали и принимали значения null . Мы ожидаем только одного нарушения. Об этом следует сообщить в customerTypeOfSubset с помощью аннотации @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")));
}

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

@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 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\"")));
}

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

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

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

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

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

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