このあたりを見て、こんなことができるんだなーと知りまして。
はじめての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