CLOVER🍀

That was when it all began.

Bean ValidationのGroupを使う

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