ちょっとBean Validationを試してみたくなりまして。
といっても、全然知らないので、この辺りを参考に試してみました。
JSR 303 Bean Validationで遊んでみるよ!
http://yamkazu.hatenablog.com/entry/20110206/1296985545
Hibernate Validatorについて
http://d.hatena.ne.jp/Yosuke_Taka/20110827/1314413434
Bean Validationの実装は、もちろんHibernate Validatorを使用します。
Hibernate Validator
http://hibernate.org/validator/
あとは、Getting Startedも合わせて見つつ、試してみましょう。
Getting started with Hibernate Validator
http://hibernate.org/validator/documentation/getting-started/
準備
相変わらずScalaでやるので、まずはsbtで依存関係の定義。
build.sbt
name := "bean-validation-getting-started" version := "0.0.1-SNAPSHOT" scalaVersion := "2.10.4" organization := "org.littlewings" scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked") fork in Test := true libraryDependencies ++= Seq( "org.hibernate" % "hibernate-validator" % "5.1.0.Final", "javax.el" % "javax.el-api" % "2.2.4", "org.glassfish.web" % "javax.el" % "2.2.4", "org.scalatest" %% "scalatest" % "2.1.2" % "test" )
今回は、スタンドアロンで使用するので、ELの実装も依存関係に含めています。
バリデーション対象のクラス
とりあえず、簡単なサンプルを用意。
src/main/scala/org/littlewings/javaee7/beanvalidation/Book.scala
package org.littlewings.javaee7.beanvalidation import javax.validation.constraints.{Min, NotNull, Size} object Book { def apply(isbn: String, title: String, price: Int): Book = { val book = new Book book.isbn = isbn book.title = title book.price = price book } } class Book { @NotNull @Size(min = 14, max = 14) var isbn: String = _ @NotNull var title: String = _ @Min(1) var price: Int = _ }
ISBNは、ハイフン入り前提。
では、これを動かすテストコードを用意。
src/test/scala/org/littlewings/javaee7/beanvalidation/BeanValidationGettingStartedSpec.scala
package org.littlewings.javaee7.beanvalidation import scala.collection.JavaConverters._ import javax.validation.{Validation, ValidatorFactory} import javax.validation.constraints.{Min, NotNull, Size} import org.scalatest.FunSpec import org.scalatest.Matchers._ class BeanValidationGettingStartedSpec extends FunSpec { describe("bean-validation simple spec") { // ここに、テストを書く! } }
…の前に、せっかくなのでメッセージを日本語にしてみましょう。
src/test/resources/ValidationMessages.properties.utf8
javax.validation.constraints.Digits.message=は境界以外の数値(予測:<{integer}digits>.<{fraction}digits>) javax.validation.constraints.Min.message=は{value}以上でなければなりません javax.validation.constraints.NotNull.message=は必須です javax.validation.constraints.Size.message=のサイズは、{min}以上{max}以下でなければなりません
エラーになった項目名自体は、含めることができなさそうなので、自分でくっつけることにしました…。
これ、もしかしてnative2asciiしないといけない?
$ native2ascii src/test/resources/ValidationMessages.properties.utf8 src/test/resources/ValidationMessages.properties
そうなのかな…。
ではでは、気を取り直して、まずはエラーが発生しないパターン。
it("normal") { val book = Book("978-4798124605", "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava", 4536) val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator.validate(book).asScala constraintViolations should be (empty) }
使い方は、Validation#buildDefaultValidatorFactoryでValidatorFactoryを取得し、さらにそこからValidatorを取得して使用します。
Validator#validateで実行するので、そこで結果のSetが返ってくるのでその結果を見るという感じみたいですね。中身は、ConstraintViolationとなります。
では、エラーパターン。
it("required, negative") { val book = Book(null, "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava", -1) val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator.validate(book).asScala constraintViolations should have size 2 val violations = constraintViolations .toArray .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) val v0 = violations(0) v0.getPropertyPath.toString should be ("isbn") val v1 = violations(1) v1.getPropertyPath.toString should be ("price") s"${v0.getPropertyPath}${v0.getMessage}" should be ("isbnは必須です") s"${v1.getPropertyPath}${v1.getMessage}" should be ("priceは1以上でなければなりません") v0.getConstraintDescriptor.getAnnotation should be (a [NotNull]) v1.getConstraintDescriptor.getAnnotation should be (a [Min]) }
もうひとつ。
it("size") { val book = Book("12345", "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava", 4536) val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator.validate(book).asScala constraintViolations should have size 1 val violations = constraintViolations.toArray val v0 = violations(0) v0.getPropertyPath.toString should be ("isbn") s"${v0.getPropertyPath}${v0.getMessage}" should be ("isbnのサイズは、14以上14以下でなければなりません") v0.getConstraintDescriptor.getAnnotation should be (a [Size]) }
どの制約に引っかかったかは、ConstraintViolation#getConstraintDescriptorで取得できるConstraintDescriptorから、getAnnotationで確認することができるようです。
case classで試す
せっかくScalaでやるのなら、case classで使ってみましょう。
普通にフィールドにアノテーションを付与するだけでは、コンストラクタ引数に対するアノテーションとして解釈されてしまうので、このような形になります。
src/main/scala/org/littlewings/javaee7/beanvalidation/CaseBook.scala
package org.littlewings.javaee7.beanvalidation import scala.annotation.meta.field import javax.validation.constraints.{Min, NotNull, Size} case class CaseBook( @(NotNull @field) @(Size @field)(min = 14, max = 14) isbn: String, @(NotNull @field) title: String, @(Min @field)(1) price: Int )
テストコード自体は、ほぼ変わりません。
describe("bean-validation case-class spec") { it("normal") { val book = CaseBook("978-4798124605", "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava", 4536) val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator.validate(book).asScala constraintViolations should be (empty) } it("required, negative") { val book = CaseBook(null, "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava", -1) val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator.validate(book).asScala constraintViolations should have size 2 val violations = constraintViolations .toArray .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) val v0 = violations(0) v0.getPropertyPath.toString should be ("isbn") val v1 = violations(1) v1.getPropertyPath.toString should be ("price") s"${v0.getPropertyPath}${v0.getMessage}" should be ("isbnは必須です") s"${v1.getPropertyPath}${v1.getMessage}" should be ("priceは1以上でなければなりません") v0.getConstraintDescriptor.getAnnotation should be (a [NotNull]) v1.getConstraintDescriptor.getAnnotation should be (a [Min]) } it("size") { val book = CaseBook("12345", "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava", 4536) val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator.validate(book).asScala constraintViolations should have size 1 val violations = constraintViolations.toArray val v0 = violations(0) v0.getPropertyPath.toString should be ("isbn") s"${v0.getPropertyPath}${v0.getMessage}" should be ("isbnのサイズは、14以上14以下でなければなりません") v0.getConstraintDescriptor.getAnnotation should be (a [Size]) } }
複数の制約に引っかかる場合は?
Commons Validatorみたいに、複数の制約に引っかかると全部入るのかなぁ?ということで。
src/main/scala/org/littlewings/javaee7/beanvalidation/Bean.scala
package org.littlewings.javaee7.beanvalidation import scala.annotation.meta.field import javax.validation.constraints.{Digits, Size} case class Bean( @(Digits @field)(integer = 2, fraction = 0) @(Size @field)(min = 1000, max = 9999) value: String )
まあ、これを満たす値って入らないですけどね…。動作確認ということで。
確認。
describe("complex constraint") { it("test") { val bean = Bean("100") val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator.validate(bean).asScala constraintViolations should have size 2 val violations = constraintViolations .toArray .sortWith(_.getMessageTemplate < _.getMessageTemplate) val v0 = violations(0) v0.getPropertyPath.toString should be ("value") val v1 = violations(1) v1.getPropertyPath.toString should be ("value") s"${v0.getPropertyPath}${v0.getMessage}" should be ("valueは境界以外の数値(予測:<2digits>.<0digits>)") s"${v1.getPropertyPath}${v1.getMessage}" should be ("valueのサイズは、1000以上9999以下でなければなりません") } }
同じように、複数入るみたいですね。
なんとなく、初歩の使い方はわかったかな?
今回作成したコードは、こちらにアップしています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/bean-validation-getting-started