CLOVER🍀

That was when it all began.

Bean Validationのアノテーションに付いてるListについて

確認する順番が前後しているような気がしますが、Bean Validationで使用するアノテーションを定義する時は一緒にListも定義するのを慣例として覚えておいた方が良いみたいです。

Listって何かというと、例えば@Size.Listみたいなので、バリデーション対象に複数の同じ種類のバリデーションルールを付与するのに使います。これを使うと、(こちらはあまりないかもしれませんが)バリデーション対象に対して同じルールを複数摘要したり、グループを指定することで同じルールでも内容を切り替えることができるみたいです。

参考)
JSR 303 Bean Validationで遊んでみるよ!
http://yamkazu.hatenablog.com/entry/20110206/1296985545

はじめてのBean Validation その4
http://d.hatena.ne.jp/shin/20100115/p4

確認のために、自分でアノテーションを定義しつつ、テストをして確認してみましょう。

準備

ビルド定義は、こちら。
build.sbt

name := "bean-validation-list"

version := "0.0.1-SNAPSHOT"

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"
)

バリデーションを作る

すでに標準のバリデーション用のアノテーションに、Listも一緒に定義されていますが、それをそのまま使ったのでは面白くないので、自分で作ってみます。

今回作ったのは、自作のSize。MySizeとしましょう。
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();

    @Documented
    @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface List {
        MySize[] value();
    }
}

ここでのポイントは、アノテーション内部に定義されたアノテーション。

    @Documented
    @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface List {
        MySize[] value();
    }

外側に定義したアノテーションを、配列で持つように宣言するみたいです。

で、これに体操するValidatorも一応用意。簡単のため、Stringのみを対象にしました。
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
  }
}

グループを用意

グループを使った確認のために、グループ用のインターフェースを用意しておきます。
src/test/java/org/littlewings/javaee7/beanvalidation/MyGroup.java

package org.littlewings.javaee7.beanvalidation;

public interface MyGroup {
}

アノテーションを使ったクラスを定義

それでは、ここまで用意したアノテーションおよびグループを使ったクラスを定義します。
src/test/java/org/littlewings/javaee7/beanvalidation/Person.java

package org.littlewings.javaee7.beanvalidation;

import javax.validation.constraints.Pattern;

public class Person {
    @MySize.List({
            @MySize(min = 3, max = 3),
            @MySize(min = 4, max = 6, groups = MyGroup.class)
    })
    public String firstName;

    @Pattern.List({
            @Pattern(regexp = "^磯.*"),
            @Pattern(regexp = ".*野$")
    })
    public String lastName;
}

これをScalaで書くと、ちょっとわかりにくいのでJavaにしました…。

ここは、Defaultグループの場合はmin = 3、max = 3となり、MyGroupグループの場合はmin = 4, max = 6という意味になります。

    @MySize.List({
            @MySize(min = 3, max = 3),
            @MySize(min = 4, max = 6, groups = MyGroup.class)
    })
    public String firstName;

ここの場合、「磯」で始まり、「野」で終わらなければなりません。対象のグループは、Defaultグループです。

    @Pattern.List({
            @Pattern(regexp = "^磯.*"),
            @Pattern(regexp = ".*野$")
    })
    public String lastName;

意味ないですけど!!

テストしてみる

それでは、テストコードで動作確認。
src/test/scala/org/littlewings/javaee7/beanvalidation/AnnotationListSpec.scala

package org.littlewings.javaee7.beanvalidation

import javax.validation.{ConstraintViolation, Validation}

import org.scalatest.FunSpec
import org.scalatest.Matchers._

class AnnotationListSpec extends FunSpec {
  describe("AnnotationList Spec") {
    // ここに、テストを書く!
  }
}

まずは、DefaultグループでバリデーションOKのパターン。

    it("Default Group, valid") {
      val person = new Person
      person.firstName = "カツオ"
      person.lastName = "磯野"

      val factory = Validation.buildDefaultValidatorFactory
      val validator = factory.getValidator

      val constraintViolations =
        validator
          .validate(person)
          .toArray(Array.empty[ConstraintViolation[Any]])
          .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString)

      constraintViolations should be(empty)
    }

MyGroupに指定したバリデーションは、ここでは動作していませんね。

続いて、DefaultグループでNGとなるパターン、その1。

    it("Default Group, invalid case 1") {
      val person = new Person
      person.firstName = "カツオ"
      person.lastName = "磯の"

      val factory = Validation.buildDefaultValidatorFactory
      val validator = factory.getValidator

      val constraintViolations =
        validator
          .validate(person)
          .toArray(Array.empty[ConstraintViolation[Any]])
          .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString)

      constraintViolations should have size (1)
      constraintViolations(0).getPropertyPath.toString should be("lastName")
      constraintViolations(0).getMessage should be("must match \".*野$\"")
    }

その2。こちらは、firstNameもNGとなるようにしました。

    it("Default Group, invalid case 2") {
      val person = new Person
      person.firstName = "カツオ?"
      person.lastName = "いそ野"

      val factory = Validation.buildDefaultValidatorFactory
      val validator = factory.getValidator

      val constraintViolations =
        validator
          .validate(person)
          .toArray(Array.empty[ConstraintViolation[Any]])
          .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString)

      constraintViolations should have size (2)
      constraintViolations(0).getPropertyPath.toString should be("firstName")
      constraintViolations(0).getMessage should be("size must be between 3 and 3")
      constraintViolations(1).getPropertyPath.toString should be("lastName")
      constraintViolations(1).getMessage should be("must match \"^磯.*\"")
    }

@Pattern.Listに指定した内容がANDでバリデーションされていること、そしてfirstNameに指定した@MySize.Listもデフォルトのものしか動作していないことがわかります。

続いて、MyGroupグループを指定してバリデーションOKの場合。

    it("MyGroup, valid") {
      val person = new Person
      person.firstName = "katsuo"
      person.lastName = "isono"

      val factory = Validation.buildDefaultValidatorFactory
      val validator = factory.getValidator

      val constraintViolations =
        validator
          .validate(person, classOf[MyGroup])
          .toArray(Array.empty[ConstraintViolation[Any]])
          .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString)

      constraintViolations should be(empty)
    }

ここでは、@Pattern.Listはまったく見られていないことがわかります(両方ともDefaultグループなので)。

MyGroupグループでNGとなるパターン。

    it("MyGroup, invalid") {
      val person = new Person
      person.firstName = "isono katsuo"
      person.lastName = "isono"

      val factory = Validation.buildDefaultValidatorFactory
      val validator = factory.getValidator

      val constraintViolations =
        validator
          .validate(person, classOf[MyGroup])
          .toArray(Array.empty[ConstraintViolation[Any]])
          .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString)

      constraintViolations should have size(1)
      constraintViolations(0).getPropertyPath.toString should be("firstName")
      constraintViolations(0).getMessage should be("size must be between 4 and 6")
    }

MyGroupで指定したバリデーションが、ちゃんと動作しているようです。

というわけで

Listを使ったアノテーション定義と、その利用について確認してみました。今度から、Bean Validationを使ったアノテーションを定義する時は、一緒にListも付けることにしましょう…。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/bean-validation-list