ここでは相関バリデーションを、「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する設定ははこちら。
相関バリデーションは、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配列の型で定義しているのか。