Перейти к основному содержимому

Валидации для типов Enum

· 6 мин. чтения

1. Введение

В учебнике Java Bean Validation Basics мы увидели, как мы можем применять проверки 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.foreach.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<? extends Payload>[] payload() default {};
}

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

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

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

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

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

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

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

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

Давайте посмотрим, как создать конкретную аннотацию проверки подмножества для нашего перечисления 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 {};
}

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

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

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

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. Проверка соответствия строки значению перечисления

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

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

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

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

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

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

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

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

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

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

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

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

@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.foreach.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 .