CLOVER🍀

That was when it all began.

Bean Validationで、既存のアノテーションをまとめるアノテーションを作成する

このあたりを見て、こんなことができるんだなーと知りまして。

はじめてのBean Validation その2
http://d.hatena.ne.jp/shin/20100113/p1

5.5.3.1. 既存ルールを組み合わせたBean Validationアノテーションの作成
https://terasolunaorg.github.io/guideline/public_review/ArchitectureInDetail/Validation.html#id10

Validatorを作成する際に、完全に新しく作成するのではなく、すでに存在するアノテーションを組み合わせるだけで実現できるものについては、組み合わせるアノテーションをまとめるアノテーションを作成すればよい、という話みたいです。

試してみます。

準備

ビルド定義。
build.sbt

name := "bean-validation-aggregate-annotation"

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

アノテーションを用意する

それでは、既存のアノテーションをまとめるアノテーションを作成してみます。

今回は、@UserIdというアノテーションで、@Sizeと@Patternを組み合わせたものにしてみましょう。このような形になります。
src/main/java/org/littlewings/javaee7/beanvalidation/UserId.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;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

@Documented
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
@Size(min = 3, max = 5)
@Pattern(regexp = "[A-Z0-9]+")
public @interface UserId {
    String message() default "{message.UserId}";  // ここは使われない

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

このアノテーション自体は、アノテーション特有の引数は持ちません。なにせ、集約されているし…。メッセージも定義できますが、実際には使用されず、まとめられた個々のメッセージがバリデーションエラー時に使用されます。また、@ConstraintアノテーションのvalidateByには「{}」を指定します。

では、メッセージをまとめることはできないのか?ということで、作成したのがこちらになります。
src/main/java/org/littlewings/javaee7/beanvalidation/UserIdAggregate.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;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

@Documented
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
@Size(min = 3, max = 5)
@Pattern(regexp = "[A-Z0-9]+")
@ReportAsSingleViolation
public @interface UserIdAggregate {
    String message() default "{message.UserIdAsAggregate}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

先ほどの@UserIdアノテーションとの違いは、@ReportAsSingleViolationアノテーションが付与されていることですね。

@ReportAsSingleViolation
public @interface UserIdAggregate {

これを使用すると、message()で定義された内容が使われるようになるそうです。

対応するメッセージファイルも用意しましょう。
※実際には、native2asciiによるUnicodeエスケープが必要です
src/main/resources/ValidationMessages.properties

message.UserIdAsAggregate=ユーザーIDの形式が間違ってます

使ってみる

それでは、作成したアノテーションを使ってみます。

アノテーションを付与したクラスを作成。
src/test/scala/org/littlewings/javaee7/beanvalidation/User.scala

package org.littlewings.javaee7.beanvalidation

import javax.validation.constraints.NotNull

class User {
  @NotNull
  @UserId
  var id: String = _

  @NotNull
  @UserIdAggregate
  var idValidAggregate: String = _
}

なんとなく、作成したものとは別に@NotNullも付与。

こちらをテストコードを使用して確認してみます。
src/test/scala/org/littlewings/javaee7/beanvalidation/AggregateAnnotationSpec.scala

package org.littlewings.javaee7.beanvalidation

import javax.validation.{ConstraintViolation, Validation}

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

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

まずは、nullのケース。

    it("null") {
      val user = new User

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

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

      constraintViolations should have size (2)
      constraintViolations(0).getPropertyPath.toString should be("id")
      constraintViolations(0).getMessage should be("may not be null")
      constraintViolations(1).getPropertyPath.toString should be("idValidAggregate")
      constraintViolations(1).getMessage should be("may not be null")
    }

ここは、作ったアノテーションは関係ないですね。とりあえずの動作確認です。

続いて、今回作成したアノテーションでエラーとなるようなパターン。

    it("invalid user id") {
      val user = new User
      user.id = "ab"
      user.idValidAggregate = "ab"

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

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

      constraintViolations should have size (3)
      constraintViolations(0).getPropertyPath.toString should be("id")
      constraintViolations(0).getMessage should be("must match \"[A-Z0-9]+\"")
      constraintViolations(1).getPropertyPath.toString should be("id")
      constraintViolations(1).getMessage should be("size must be between 3 and 5")

      constraintViolations(2).getPropertyPath.toString should be("idValidAggregate")
      constraintViolations(2).getMessage should be("ユーザーIDの形式が間違ってます")
    }

先に作成したアノテーションを付与した方では個々のバリデーションのエラーメッセージが得られますが、@ReportAsSingleViolationアノテーションを付与した方ではエラーメッセージがまとめて得られています。

あと、バリデーションでエラーにならないケースも確認しておきましょう。

    it("user id") {
      val user = new User
      user.id = "AB001"
      user.idValidAggregate = "AB001"

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

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

      constraintViolations should be(empty)
    }

OKそうですね。

Bean Validationを全然知らないなーということを、改めて認識した気分でした。勉強しないと…。

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