カスタムバリデーションで「javax.validation.ValidationException: HV000033」が発生する原因と対策


個人的にちょっとハマったのと、Googleで検索しても他にドキュメントがなかったので
備忘録としてメモしておく。

### 使っているFramework/Libraryのバージョン
 * Spring Framework

4.2.2.RELEASE

* Hibernate Validator

5.2.2.Final

### 書いたコード
以下のようなカスタムバリデーションを記述

```LetterOrDigit.java : java
package jp.ijufumi.validation.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

import jp.ijufumi.validation.validator.LetterOrDigitsConstraintValidator;

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = LetterOrDigitsConstraintValidator.class)
public @interface LetterOrDigits {

    String message() default "";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}
```

``` LetterOrDigitsConstraintValidator.java : java
package jp.ijufumi.validation.validator;

import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import jp.cotoco.frontend.asp.validation.annotation.LetterOrDigits;

import org.apache.commons.lang3.StringUtils;

/**
 * 英数字用バリデーションクラス。
 */
public class LetterOrDigitsConstraintValidator implements ConstraintValidator<LetterOrDigits, String> {

    private static final Pattern REGEX_PATTERN = Pattern.compile("^\\w+$");

    private String message;

    /**
     * Initializes the validator in preparation for
     * {@link #isValid(Object, ConstraintValidatorContext)} calls.
     * The constraint annotation for a given constraint declaration
     * is passed.
     * <p>
     * This method is guaranteed to be called before any use of this instance for
     * validation.
     *
     * @param constraint annotation instance for a given constraint declaration
     */
    @Override
    public void initialize(LetterOrDigits constraint) {
        message = constraint.message();
    }

    /**
     * Implements the validation logic.
     * The state of {@code value} must not be altered.
     * <p>
     * This method can be accessed concurrently, thread-safety must be ensured
     * by the implementation.
     *
     * @param value   object to validate
     * @param context context in which the constraint is evaluated
     * @return {@code false} if {@code value} does not pass the constraint
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (StringUtils.isEmpty(value)) {
            return true;
        }

        if (REGEX_PATTERN.matcher(value).matches()) {
            return true;
        }

        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(message);

        return false;
    }
}
```

### 起こった事象
以下のようなエラーが発生。

```
javax.validation.ValidationException: HV000033: At least one custom message must be created if the default error message gets disabled.
	at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorContextImpl.getConstraintViolationCreationContexts(ConstraintValidatorContextImpl.java:103) ~[hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
	at org.hibernate.validator.internal.engine.ValidationContext.createConstraintViolations(ValidationContext.java:260) ~[hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
	at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:456) ~[hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
	at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:127) ~[hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
	at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:87) ~[hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
	at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:73) ~[hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:592) ~[hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraint(ValidatorImpl.java:555) ~[hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:490) ~[hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:454) ~[hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:406) ~[hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
	at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:204) ~[hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
	at org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:92) ~[spring-context-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.validation.DataBinder.validate(DataBinder.java:869) ~[spring-context-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.validateIfApplicable(ModelAttributeMethodProcessor.java:164) ~[spring-web-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:111) ~[spring-web-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:78) ~[spring-web-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:162) ~[spring-web-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:129) ~[spring-web-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110) ~[spring-webmvc-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:806) ~[spring-webmvc-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:729) ~[spring-webmvc-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:959) ~[spring-webmvc-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893) ~[spring-webmvc-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970) [spring-webmvc-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872) [spring-webmvc-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:648) [servlet-api.jar:na]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846) [spring-webmvc-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:729) [servlet-api.jar:na]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:291) [catalina.jar:8.0.23]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) [catalina.jar:8.0.23]
	at org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter.doFilterInternal(OpenEntityManagerInViewFilter.java:178) [spring-orm-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.2.RELEASE.jar:4.2.2.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239) [catalina.jar:8.0.23]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) [catalina.jar:8.0.23]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:316) [spring-security-we
```

### 原因
```context.disableDefaultConstraintViolation();```を実行しているのが原因で、もしこれをやる場合は、```context.buildConstraintViolationWithTemplate(message).addPropertyNode(field).addConstraintViolation();```などを呼び出してあげる必要があった。

### 対策
```ConstraintValidator```の中の```context.disableDefaultConstraintViolation();```を削除するだけ。

``` LetterOrDigitsConstraintValidator.java : java
package jp.ijufumi.validation.validator;

import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import jp.cotoco.frontend.asp.validation.annotation.LetterOrDigits;

import org.apache.commons.lang3.StringUtils;

/**
 * 英数字用バリデーションクラス。
 */
public class LetterOrDigitsConstraintValidator implements ConstraintValidator<LetterOrDigits, String> {

    private static final Pattern REGEX_PATTERN = Pattern.compile("^\\w+$");

    private String message;

    /**
     * Initializes the validator in preparation for
     * {@link #isValid(Object, ConstraintValidatorContext)} calls.
     * The constraint annotation for a given constraint declaration
     * is passed.
     * <p>
     * This method is guaranteed to be called before any use of this instance for
     * validation.
     *
     * @param constraint annotation instance for a given constraint declaration
     */
    @Override
    public void initialize(LetterOrDigits constraint) {
        message = constraint.message();
    }

    /**
     * Implements the validation logic.
     * The state of {@code value} must not be altered.
     * <p>
     * This method can be accessed concurrently, thread-safety must be ensured
     * by the implementation.
     *
     * @param value   object to validate
     * @param context context in which the constraint is evaluated
     * @return {@code false} if {@code value} does not pass the constraint
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (StringUtils.isEmpty(value)) {
            return true;
        }

        if (REGEX_PATTERN.matcher(value).matches()) {
            return true;
        }

        context.buildConstraintViolationWithTemplate(message);

        return false;
    }
}
```