Bean Validationには、バリデーションのグループ化という機能があるそうです。
5.5.2.1.3. バリデーションのグループ化
https://terasolunaorg.github.io/guideline/public_review/ArchitectureInDetail/Validation.html#id6
JSR 303 Bean Validationで遊んでみるよ!
http://yamkazu.hatenablog.com/entry/20110206/1296985545
バリデーションのために付与したアノテーションのgroupsに指定することで、バリデーションをグループ化して実行する/しないを決めたりすることができるのだとか。
知っていた方がよさそうですね!
準備
まずは、ビルド定義。
build.sbt
name := "bean-validation-groups" version := "0.0.1" scalaVersion := "2.11.6" organization := "org.littlewings" scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature") updateOptions := updateOptions.value.withCachedResolution(true) libraryDependencies ++= Seq( "org.hibernate" % "hibernate-validator" % "5.1.3.Final", "javax.el" % "javax.el-api" % "2.2.5", "org.glassfish.web" % "javax.el" % "2.2.6", "org.scalatest" %% "scalatest" % "2.2.5" % "test" )
Validatorを作成する
グループを使うのに、Validatorを自作する必要はないのですが、ログを埋め込んで動きをみたいなと思い、今回はValidatorを作ることにしました。
アルファベットを許可するもの。
src/main/java/org/littlewings/javaee7/beanvalidation/Alphabetical.java
package org.littlewings.javaee7.beanvalidation; 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; @Documented @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = AlphabeticalValidator.class) public @interface Alphabetical { String message() default ""; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Validator。メッセージは、Validatorに直接書いています。
src/main/scala/org/littlewings/javaee7/beanvalidation/AlphabeticalValidator.scala
package org.littlewings.javaee7.beanvalidation import javax.validation.{ConstraintValidator, ConstraintValidatorContext} import org.jboss.logging.Logger import scala.util.matching.Regex class AlphabeticalValidator extends ConstraintValidator[Alphabetical, String] { override def initialize(constraintAnnotation: Alphabetical): Unit = () override def isValid(value: String, context: ConstraintValidatorContext): Boolean = { val logger = Logger.getLogger(getClass) logger.infof("Constraint[%s], property[%s]", classOf[Alphabetical].getSimpleName, value.asInstanceOf[Any]) val regex = """[a-zA-Z]*""".r value match { case null => true case regex(_) => true case _ => context.disableDefaultConstraintViolation() context .buildConstraintViolationWithTemplate("アルファベットで入力してください") .addConstraintViolation() false } } }
もうひとつ。@Sizeの自作版。メッセージは、オリジナルの@Sizeを引き継いでいます。
src/main/java/org/littlewings/javaee7/beanvalidation/MySize.java
package org.littlewings.javaee7.beanvalidation; 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; @Documented @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = MySizeValidator.class) public @interface MySize { String message() default "{javax.validation.constraints.Size.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int min(); int max(); }
自作版と言いつつ、バリデーションの内容は簡易版だったり…。
src/main/scala/org/littlewings/javaee7/beanvalidation/MySizeValidator.scala
package org.littlewings.javaee7.beanvalidation import javax.validation.{ConstraintValidator, ConstraintValidatorContext} import org.jboss.logging.Logger class MySizeValidator extends ConstraintValidator[MySize, String] { var min: Int = _ var max: Int = _ override def initialize(constraintAnnotation: MySize): Unit = { min = constraintAnnotation.min max = constraintAnnotation.max } override def isValid(value: String, context: ConstraintValidatorContext): Boolean = { val logger = Logger.getLogger(getClass) logger.infof("Constraint[%s], property[%s]", classOf[MySize].getSimpleName, value.asInstanceOf[Any]) value != null && value.size >= min && value.size <= max } }
これで、今回使用するValidatorは作成しました。両方のValidatorとも、バリデーション処理の最初にログ出力が入っています。
グループを使うためのインターフェースを用意
グループを使うためには、インターフェースを定義してそれをバリデーション用のアノテーションの、groupsに指定するようです。
では、インターフェースを作成する…前に、指定しなかったらそもそもどうなっているのかという話ですが、この場合は「javax.validation.groups.Default」というインターフェースが暗黙的に指定されているのと同じことになります。
明示的にgroupsに指定した場合は、Defaultを含めたい時は明示する必要があるようです。
では、グループで使うインターフェースを定義します。
src/test/scala/org/littlewings/javaee7/beanvalidation/Groups.scala
package org.littlewings.javaee7.beanvalidation import javax.validation.GroupSequence import javax.validation.groups.Default trait A trait B trait ExtendsDefault extends Default @GroupSequence(Array(classOf[Default], classOf[A])) trait DefaultWithA @GroupSequence(Array(classOf[A], classOf[B])) trait AwithB
Scalaなので、Traitで。
インターフェースは、継承して使うこともできるようです。
trait ExtendsDefault extends Default
この場合、ExtendsDefaultをバリデーション時に指定すると、DefaultのものとExtendsDefaultが指定されているものが動くようです。Defaultを指定すると、ExtendsDefaultも動く、ではないみたい。
また、@GroupSequenceというのが定義されていますが、
@GroupSequence(Array(classOf[Default], classOf[A])) trait DefaultWithA
これはDefaultの次にAのグループのバリデーションを行う、という意味になります。Defaultのバリデーションでエラーになった場合は、Aのバリデーションは実行されません。
グループを指定しただけだと、グループの実行順序は決まっていませんし、あるグループがエラーになっても引き続き実行されるようです。
アノテーションしたクラスを作成する
作成したアノテーション、グループを付与したクラスを作成します。
src/test/scala/org/littlewings/javaee7/beanvalidation/SomeClass.scala
package org.littlewings.javaee7.beanvalidation import javax.validation.groups.Default class SomeClass { @Alphabetical @MySize(min = 3, max = 5, groups = Array(classOf[A])) var field1: String = _ @Alphabetical(groups = Array(classOf[Default], classOf[A])) @MySize(min = 3, max = 5, groups = Array(classOf[A], classOf[B])) var field2: String = _ @Alphabetical(groups = Array(classOf[Default], classOf[B])) @MySize(min = 3, max = 5) var field3: String = _ @Alphabetical(groups = Array(classOf[ExtendsDefault])) @MySize(min = 3, max = 5, groups = Array(classOf[ExtendsDefault])) var field4: String = _ }
バリデーション自体の内容は、すべて同じです。グループの指定が異なります。
テストする
それでは、これらのコードをテストして確認してみます。
src/test/scala/org/littlewings/javaee7/beanvalidation/GroupsValidationSpec.scala
package org.littlewings.javaee7.beanvalidation import javax.validation.groups.Default import javax.validation.{ConstraintViolation, Validation} import org.scalatest.FunSpec import org.scalatest.Matchers._ class GroupsValidationSpec extends FunSpec { describe("GroupsValidation Spec") { // ここに、テストを書く!! } }
まずは、グループ指定なし。
it("implicit Default") { val target = new SomeClass target.field1 = "field1-ABC123" target.field2 = "field2-ABC123" target.field3 = "field3-ABC123" target.field4 = "field4-ABC123" val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator .validate(target) .toArray(Array.empty[ConstraintViolation[Any]]) .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) constraintViolations should have size (4) }
コンソールログ。
※不要な行は削っています
INFO: Constraint[MySize], property[field3-ABC123] INFO: Constraint[Alphabetical], property[field2-ABC123] INFO: Constraint[Alphabetical], property[field3-ABC123] INFO: Constraint[Alphabetical], property[field1-ABC123]
ここで動いているのは、groupsに何もしていないもの、明示的にDefaultを指定しているものですね。このケースでは、Defaultを継承したグループは動かないみたいです。
次、Defaultを明示的に指定してみます。
it("explicit Default") { val target = new SomeClass target.field1 = "field1-ABC123" target.field2 = "field2-ABC123" target.field3 = "field3-ABC123" target.field4 = "field4-ABC123" val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator .validate(target, classOf[Default]) .toArray(Array.empty[ConstraintViolation[Any]]) .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) constraintViolations should have size (4) }
Validator#validateの引数に、グループを指定します。可変長引数です。
結果。
INFO: Constraint[MySize], property[field3-ABC123] INFO: Constraint[Alphabetical], property[field2-ABC123] INFO: Constraint[Alphabetical], property[field3-ABC123] INFO: Constraint[Alphabetical], property[field1-ABC123]
ExtendsDefaultを指定。
it("Extends Default") { val target = new SomeClass target.field1 = "field1-ABC123" target.field2 = "field2-ABC123" target.field3 = "field3-ABC123" target.field4 = "field4-ABC123" val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator .validate(target, classOf[ExtendsDefault]) .toArray(Array.empty[ConstraintViolation[Any]]) .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) constraintViolations should have size (6) }
コンソールログ。
INFO: Constraint[Alphabetical], property[field4-ABC123] INFO: Constraint[MySize], property[field4-ABC123] INFO: Constraint[MySize], property[field3-ABC123] INFO: Constraint[Alphabetical], property[field2-ABC123] INFO: Constraint[Alphabetical], property[field3-ABC123] INFO: Constraint[Alphabetical], property[field1-ABC123]
この場合、ExtendsDefault、Defaultがgroupsに指定されたもの、そしてgroupsに何も指定されていないものが動きます。
Aを指定。
it("group A") { val target = new SomeClass target.field1 = "field1-ABC123" target.field2 = "field2-ABC123" target.field3 = "field3-ABC123" target.field4 = "field4-ABC123" val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator .validate(target, classOf[A]) .toArray(Array.empty[ConstraintViolation[Any]]) .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) constraintViolations should have size (3) }
コンソールログ。
INFO: Constraint[MySize], property[field2-ABC123] INFO: Constraint[Alphabetical], property[field2-ABC123] INFO: Constraint[MySize], property[field1-ABC123]
グループを2つ以上指定。
DefaultとA。
it("Default, group A") { val target = new SomeClass target.field1 = "field1-ABC123" target.field2 = "field2-ABC123" target.field3 = "field3-ABC123" target.field4 = "field4-ABC123" val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator .validate(target, classOf[Default], classOf[A]) .toArray(Array.empty[ConstraintViolation[Any]]) .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) constraintViolations should have size (6) }
コンソールログ。
INFO: Constraint[MySize], property[field3-ABC123] INFO: Constraint[Alphabetical], property[field2-ABC123] INFO: Constraint[Alphabetical], property[field3-ABC123] INFO: Constraint[Alphabetical], property[field1-ABC123] INFO: Constraint[MySize], property[field2-ABC123] INFO: Constraint[MySize], property[field1-ABC123]
この場合、Default+Aです。
ここからは、しばらく淡々と。
DefaultとB。
it("Default, group B") { val target = new SomeClass target.field1 = "field1-ABC123" target.field2 = "field2-ABC123" target.field3 = "field3-ABC123" target.field4 = "field4-ABC123" val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator .validate(target, classOf[Default], classOf[B]) .toArray(Array.empty[ConstraintViolation[Any]]) .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) constraintViolations should have size (5) }
コンソールログ。
INFO: Constraint[MySize], property[field3-ABC123] INFO: Constraint[Alphabetical], property[field1-ABC123] INFO: Constraint[Alphabetical], property[field3-ABC123] INFO: Constraint[Alphabetical], property[field2-ABC123] INFO: Constraint[MySize], property[field2-ABC123]
AとB。
it("group A, B") { val target = new SomeClass target.field1 = "field1-ABC123" target.field2 = "field2-ABC123" target.field3 = "field3-ABC123" target.field4 = "field4-ABC123" val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator .validate(target, classOf[A], classOf[B]) .toArray(Array.empty[ConstraintViolation[Any]]) .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) constraintViolations should have size (4) }
コンソールログ。
INFO: Constraint[MySize], property[field2-ABC123] INFO: Constraint[Alphabetical], property[field2-ABC123] INFO: Constraint[MySize], property[field1-ABC123] INFO: Constraint[Alphabetical], property[field3-ABC123]
B、A。
it("group B, A") { val target = new SomeClass target.field1 = "field1-ABC123" target.field2 = "field2-ABC123" target.field3 = "field3-ABC123" target.field4 = "field4-ABC123" val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator .validate(target, classOf[B], classOf[A]) .toArray(Array.empty[ConstraintViolation[Any]]) .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) constraintViolations should have size (4) }
コンソールログ。
INFO: Constraint[MySize], property[field2-ABC123] INFO: Constraint[Alphabetical], property[field3-ABC123] INFO: Constraint[Alphabetical], property[field2-ABC123] INFO: Constraint[MySize], property[field1-ABC123]
実行順ですが、指定グループ順のように見えなくもないですが、JSRで決まっていないというのでそう覚えます。
それでは、GroupSequenceを使ってみます。
DefaultWithA。
it("group sequence Default, A") { val target = new SomeClass target.field1 = "field1-ABC123" target.field2 = "field2-ABC123" target.field3 = "field3-ABC123" target.field4 = "field4-ABC123" val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator .validate(target, classOf[DefaultWithA]) .toArray(Array.empty[ConstraintViolation[Any]]) .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) constraintViolations should have size (4) }
コンソールログ。
INFO: Constraint[Alphabetical], property[field3-ABC123] INFO: Constraint[Alphabetical], property[field2-ABC123] INFO: Constraint[MySize], property[field3-ABC123] INFO: Constraint[Alphabetical], property[field1-ABC123]
単純に、Default、Aと指定した時よりも、明らかに数が減りました。Defaultでエラーになったので、Aが実行されないからですね。
DefaultWithB。
it("group sequence A, B") { val target = new SomeClass target.field1 = "field1-ABC123" target.field2 = "field2-ABC123" target.field3 = "field3-ABC123" target.field4 = "field4-ABC123" val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator .validate(target, classOf[AwithB]) .toArray(Array.empty[ConstraintViolation[Any]]) .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) constraintViolations should have size (3) }
コンソールログ。
INFO: Constraint[MySize], property[field1-ABC123] INFO: Constraint[Alphabetical], property[field2-ABC123] INFO: Constraint[MySize], property[field2-ABC123]
こちらは、Bが実行されていません。
まとめ
確認にけっこう手間がかかりましたが、グループおよびGroupSequenceを使った時の挙動を確認することができました。悩みの多いバリデーションですが、知識のひとつとして覚えておいた方が良さそうですね。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/bean-validation-groups