1. 序章

チュートリアルJavaBean Validation Basics では、 JSR380を使用してjavax検証をさまざまなタイプに適用する方法を説明しました。 また、チュートリアル Spring MVC Custom Validation では、カスタム検証を作成する方法を説明しました。

この次のチュートリアルでは、カスタムアノテーションを使用した列挙型の検証の構築に焦点を当てます。

2. 列挙型の検証

残念ながら、ほとんどの標準アノテーションはenumsに適用できません。

たとえば、 @Pattern アノテーションを列挙型に適用すると、HibernateValidatorで次のようなエラーが発生します。

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

アノテーションは特定の列挙型に固有である必要がありますが、もちろん、異なるバリデーター間でshareコードを使用できます。

5. 文字列が列挙型の値と一致することの検証

String に一致するように列挙型を検証する代わりに、逆のこともできます。 このために、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;

文字列(または任意のCharSequence)が列挙型に含まれているかどうかを確認するためにValueOfEnumValidatorを定義しましょう

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: [...]

もちろん、この例外を処理することはできます。 ただし、これではすべての違反を一度に報告することはできません。

値を列挙型にマッピングする代わりに、Stringにマッピングできます。 次に、バリデーターを使用して、列挙値のいずれかに一致するかどうかを確認します。

6. すべてをまとめる

これで、新しい検証のいずれかを使用してBeanを検証できます。 最も重要なことは、すべての検証がnull値を受け入れることです。 したがって、アノテーション@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検証のテスト

バリデーターをテストするために、新しく定義されたアノテーションをサポートするバリデーターを設定します。 すべてのテストでCustomerbeanを使用します。

まず、有効なCustomerインスタンスが違反を引き起こさないことを確認する必要があります。

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

次に、新しいアノテーションがnull値をサポートして受け入れるようにします。 違反は1回だけです。 これは、customerTypeOfSubset@NotNullアノテーションによって報告される必要があります。

@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.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. 結論

このチュートリアルでは、カスタムアノテーションとバリデーターを使用して列挙型を検証するための3つのオプションについて説明しました。

最初に、正規表現を使用して列挙型の名前を検証する方法を学びました。

次に、特定の列挙型の値のサブセットの検証について説明しました。 また、これを行うための一般的なアノテーションを作成できない理由についても説明しました。

最後に、文字列のバリデーターを作成する方法についても説明しました。 Stringが特定の列挙型の特定の値に準拠しているかどうかを確認するため。

いつものように、記事の完全なソースコードは、Githubから入手できます。