Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

Bean Validation(Hibernate Validator)で相関バリデーションを使う

ここでは相関バリデーションを、「2つの値の関係性をバリデーションする」とする。 たとえば、日付で開始日と終了日があって開始日 < 終了日とならなくてはならない、など。

Bean Validation 1.1の仕様には、相関バリデーションはなさそう。ただ、Hibernate Validator自体には相関バリデーション用の実装があったので、それが使える。

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>5.2.2.Final</version>
        </dependency>

サンプルとして、Spring MVCでMethod Validationするものを使う。SpringでMethod Validationする設定ははこちら。

jyukutyo.hatenablog.com

相関バリデーションは、Hibernate Validatorにある@ParameterScriptAssertアノテーションを使うと実現できる。

package com.jyukutyo.sample.controller;

import java.util.Date;

import org.hibernate.validator.constraints.ParameterScriptAssert;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@Validated
public class HogeController {

    @RequestMapping(path = "hoge")
    @ResponseBody
    @ParameterScriptAssert(script = "start.before(end)", lang = "javascript")
    public String index(@RequestParam @DateTimeFormat(pattern="yyyyMMdd") Date start, @RequestParam @DateTimeFormat(pattern="yyyyMMdd") Date end) {
        return "hoge";
    }
}

@ParameterScriptAssertはJSR 223でのスクリプト言語を使ってバリデーションを実行する。ここではJavaScriptを使ってバリデーションするのでlangの値"javascript"とし、バリデーションする内容はscriptの値に記述する。で@ParameterScriptAssertのドキュメンテーションコメントにはこう書いてある。

To refer to a parameter within the scripting expression, use its name as obtained by the active {@link javax.validation.ParameterNameProvider}. By default, {@code arg0}, {@code arg1} etc. will be used as parameter names.

スクリプトの記述でパラメータを参照したいとき、ParameterNameProviderから取得したものを名前として使うよ。デフォルトではarg0とかarg1という感じでパラメータ名になるよ。」とある。でもscript = "arg0.before(arg1)"として動かしてみると、例外出た。

Caused by: javax.validation.ConstraintDeclarationException: HV000023: Error during execution of script "arg0.before(arg1)" occurred.
        at org.hibernate.validator.internal.constraintvalidators.hv.ScriptAssertContext.evaluateScriptAssertExpression(ScriptAssertContext.java:51) [hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
        at org.hibernate.validator.internal.constraintvalidators.hv.ParameterScriptAssertValidator.isValid(ParameterScriptAssertValidator.java:46) [hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
        at org.hibernate.validator.internal.constraintvalidators.hv.ParameterScriptAssertValidator.isValid(ParameterScriptAssertValidator.java:28) [hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
        at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:448) [hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
        ... 75 common frames omitted
Caused by: javax.script.ScriptException: ReferenceError: "arg0" is not defined in <eval> at line number 1
        at jdk.nashorn.api.scripting.NashornScriptEngine.throwAsScriptException(NashornScriptEngine.java:455) [nashorn.jar:na]
        at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:439) [nashorn.jar:na]
        at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:401) [nashorn.jar:na]
        at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:397) [nashorn.jar:na]
        at jdk.nashorn.api.scripting.NashornScriptEngine.eval(NashornScriptEngine.java:152) [nashorn.jar:na]
        at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:233) [na:1.8.0_40]
        at org.hibernate.validator.internal.util.scriptengine.ScriptEvaluator.doEvaluate(ScriptEvaluator.java:56) [hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
        at org.hibernate.validator.internal.util.scriptengine.ScriptEvaluator.evaluate(ScriptEvaluator.java:50) [hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
        at org.hibernate.validator.internal.constraintvalidators.hv.ScriptAssertContext.evaluateScriptAssertExpression(ScriptAssertContext.java:48) [hibernate-validator-5.2.2.Final.jar:5.2.2.Final]
        ... 78 common frames omitted
Caused by: jdk.nashorn.internal.runtime.ECMAException: ReferenceError: "arg0" is not defined

@ParameterScriptAssertのバリデーション処理の実装であるParameterScriptAssertValidatorを読んでみた。

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ParameterScriptAssertValidator implements ConstraintValidator<ParameterScriptAssert, Object[]> {

    @Override
    public boolean isValid(Object[] arguments, ConstraintValidatorContext constraintValidatorContext) {
        List<String> parameterNames = ( (ConstraintValidatorContextImpl) constraintValidatorContext )
                .getMethodParameterNames();

        Map<String, Object> bindings = getBindings( arguments, parameterNames );

        return scriptAssertContext.evaluateScriptAssertExpression( bindings );
    }

    private Map<String, Object> getBindings(Object[] arguments, List<String> parameterNames) {
        Map<String, Object> bindings = newHashMap();

        for ( int i = 0; i < arguments.length; i++ ) {
            bindings.put( parameterNames.get( i ), arguments[i] );
        }

        return bindings;
    }

Mapであるbindingsで、引数名とバリデーション対象の値がマッピングできていて、それをevaluateScriptAssertExpression()に渡しているのだから、arg0とかではなくstartやendという引数名で参照できた。

で、@SupportedValidationTargetって何だろう?と。

A ConstraintValidator can target the (returned) element annotated by the constraint, the array of parameters of a method or constructor (aka cross-parameter) or both.

If @SupportedValidationTarget is not present, the ConstraintValidator targets the (returned) element annotated by the constraint.

A ConstraintValidator targeting cross-parameter must accept Object (or Object) as the type of object it validates.

ConstraintValidatorはアノテーションをつけた要素やメソッドの引数やコンストラクタのパラメータ(クロスパラメータ)を対象にできる。もし@SupportedValidationがなければ、ConstraintValidatorはアノテーションをつけた要素を対象にする。クロスパラメータを対象にしたConstraintValidatorはObject配列(またはObject)に対応しなければならない。

なるほど、だからParameterScriptAssertValidatorクラスはConstraintValidator<ParameterScriptAssert, Object>というObject配列の型で定義しているのか。