そろそろ区切りにしようかなと思う、Bean Validationネタ。今回は、@ConvertGroupを試してみました。
Group conversion
http://docs.jboss.org/hibernate/validator/5.1/reference/en-US/html_single/#section-group-conversion
これを使うと、バリデーション時にGroupの変換ができる(違うGroupを指定したことにできる)みたいです。@Validと一緒に使うのだとか。
試してみましょう。
準備
ビルド定義。
build.sbt
name := "bean-validation-group-conversion" version := "0.0.1-SNAPSHOT" scalaVersion := "2.11.7" organization := "org.littlewings" scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature") updateOptions := updateOptions.value.withCachedResolution(true) fork in Test := true val tomcatVersion = "8.0.23" val resteasyVersion = "3.0.11.Final" val weldServletVersion = "2.2.13.Final" val scalaTestVersion = "2.2.5" libraryDependencies ++= Seq( "org.apache.tomcat.embed" % "tomcat-embed-core" % tomcatVersion, "org.apache.tomcat.embed" % "tomcat-embed-jasper" % tomcatVersion, "org.apache.tomcat.embed" % "tomcat-embed-logging-juli" % tomcatVersion, "org.jboss.resteasy" % "resteasy-servlet-initializer" % resteasyVersion, "org.jboss.resteasy" % "resteasy-cdi" % resteasyVersion, "org.jboss.resteasy" % "resteasy-validator-provider-11" % resteasyVersion, "org.jboss.resteasy" % "resteasy-jackson2-provider" % resteasyVersion, "org.jboss.resteasy" % "resteasy-client" % resteasyVersion % "test", "org.jboss.weld.servlet" % "weld-servlet" % weldServletVersion, "org.scalatest" %% "scalatest" % scalaTestVersion % "test" )
今回は、Java SEでBean Validationを使うのではなく、JAX-RS(RESTEasy)と合わせてみました。実行は、組み込みTomcatで行います。…ムダにCDI入り。
beans.xmlも置いておきます。
src/main/resources/META-INF/beans.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" bean-discovery-mode="annotated"> </beans>
テストコードとJAX-RSで使うコードの用意
テストに、組み込みTomcatとJAX-RSを使います。まずはJAX-RSの有効化。
src/main/scala/org/littlewings/javaee7/rest/JaxrsApplication.scala
package org.littlewings.javaee7.rest import javax.ws.rs.ApplicationPath import javax.ws.rs.core.Application @ApplicationPath("rest") class JaxrsApplication extends Application
JAX-RSリソースクラス。
src/main/scala/org/littlewings/javaee7/rest/ValidationResource.scala
package org.littlewings.javaee7.rest import javax.enterprise.context.RequestScoped import javax.validation.Valid import javax.validation.groups.{ConvertGroup, Default} import javax.ws.rs.core.MediaType import javax.ws.rs.{POST, Path, Produces} import org.littlewings.javaee7.beanvalidation.{GroupA, User, User2} @Path("validation") @RequestScoped class ValidationResource { // 後で }
リソースクラスの中身は後で書きます。
テスト時に、組み込みTomcatを起動するTrait。
src/test/scala/org/littlewings/javaee7/beanvalidation/EmbeddedTomcatCdiSupport.scala
package org.littlewings.javaee7.beanvalidation import java.io.File import org.apache.catalina.startup.Tomcat import org.scalatest.{BeforeAndAfterAll, Suite} trait EmbeddedTomcatCdiSupport extends Suite with BeforeAndAfterAll { protected val port: Int = 8080 protected val tomcat: Tomcat = new Tomcat protected val baseDir: File = createTempDir("tomcat", port) protected val docBaseDir: File = createTempDir("tomcat-docbase", port) override def beforeAll(): Unit = { tomcat.setPort(port) tomcat.setBaseDir(baseDir.getAbsolutePath) val context = tomcat.addWebapp("", docBaseDir.getAbsolutePath) context.addParameter("org.jboss.weld.environment.servlet.archive.isolation", "false") context.addParameter("resteasy.injector.factory", "org.jboss.resteasy.cdi.CdiInjectorFactory") tomcat.start() } override def afterAll(): Unit = { tomcat.stop() tomcat.destroy() deleteDirs(baseDir) deleteDirs(docBaseDir) } private def createTempDir(prefix: String, port: Int): File = { val tempDir = File.createTempFile(s"${prefix}.", s".${port}") tempDir.delete() tempDir.mkdir() tempDir.deleteOnExit() tempDir } private def deleteDirs(file: File): Unit = { file .listFiles .withFilter(f => f.getName != "." && f.getName != "..") .foreach { case d if d.isDirectory => deleteDirs(d) case f => f.delete() } file.delete() } }
テストコードの雛形。
src/test/scala/org/littlewings/javaee7/beanvalidation/ConvertGroupSpec.scala
package org.littlewings.javaee7.beanvalidation import javax.ws.rs.client.{ClientBuilder, Entity} import javax.ws.rs.core.Response import org.scalatest.FunSpec import org.scalatest.Matchers._ class ConvertGroupSpec extends FunSpec with EmbeddedTomcatCdiSupport { describe("ConvertGroup Spec") { // ここに、テストを書く! } }
@ConvertGroupを使ったコードを書く
それでは、@ConvertGroupを使ったコードを書いてみます。
今回は、ネストしたJavaBeansに対してGroupを切り替えていくことを考えます。
ここでは、以下のようなGroupおよびDefaultを使っていきます。
src/main/scala/org/littlewings/javaee7/beanvalidation/Groups.scala
package org.littlewings.javaee7.beanvalidation trait GroupA trait GroupB trait GroupC
普通にJavaBeansを定義してみる
まずは、JavaBeans内では@ConvertGroupを使わないパターンで書いてみます。
まあ、単純なケースですね。
src/main/scala/org/littlewings/javaee7/beanvalidation/User.scala
package org.littlewings.javaee7.beanvalidation import javax.validation.Valid import javax.validation.constraints.{Max, Pattern} import javax.validation.groups.{ConvertGroup, Default} import scala.beans.BeanProperty class User { @BeanProperty @Valid var name: Name = _ @BeanProperty @Max.List( Array( new Max(value = 12), new Max(value = 32, groups = Array(classOf[GroupA])) ) ) var age: Int = _ } class Name { @BeanProperty @Pattern.List( Array( new Pattern(regexp = "^(カツオ|ワカメ)$"), new Pattern(regexp = "^(サザエ|マスオ|タラオ)$", groups = Array(classOf[GroupA])) ) ) var first: String = _ @BeanProperty @Pattern.List( Array( new Pattern(regexp = "^磯野$"), new Pattern(regexp = "^フグ田$", groups = Array(classOf[GroupA])) ) ) var last: String = _ }
それぞれ、DefaultとGroupAに対して、バリデーションを定義しています。
ちなみに、こういうのは
@Max.List( Array( new Max(value = 12), new Max(value = 32, groups = Array(classOf[GroupA])) ) )
Javaで書くとこういう意味になります。
@Max.List({@Max(value = 12), @Max(value = 32, groups = {GroupA.class})})
Scalaでアノテーションがネストする場合、書き方がちょっと…。
では、これに対するJAX-RSリソースクラス側の定義。
@Path("default") @POST @Produces(Array(MediaType.APPLICATION_JSON)) def defaultGroup(@Valid user: User): User = user
@Valid付き。
この場合、ネストしたJavaBeansも含めてDefaultとしてバリデーションされます。
it("Default Group, valid") { val user = new User user.name = new Name user.name.first = "カツオ" user.name.last = "磯野" user.age = 12 val client = ClientBuilder.newBuilder.build try { val response = client .target("http://localhost:8080/rest/validation/default") .request .post(Entity.json(user)) response.getStatus should be(Response.Status.OK.getStatusCode) val responseEntity = response.readEntity(classOf[User]) responseEntity.name.first = "カツオ" responseEntity.name.last = "磯野" response.close() } finally { client.close() } }
これはDefaultとしてはNG(lastnameが「磯野」じゃない)。
val user = new User user.name = new Name user.name.first = "カツオ" user.name.last = "磯野?" user.age = 12 val client = ClientBuilder.newBuilder.build try { val response = client .target("http://localhost:8080/rest/validation/default") .request .post(Entity.json(user)) response.getStatus should be(Response.Status.BAD_REQUEST.getStatusCode)
これもDefaultとしてはNG(ageが大きい)。
val user = new User user.name = new Name user.name.first = "カツオ" user.name.last = "磯野" user.age = 20 val client = ClientBuilder.newBuilder.build try { val response = client .target("http://localhost:8080/rest/validation/default") .request .post(Entity.json(user)) response.getStatus should be(Response.Status.BAD_REQUEST.getStatusCode)
では、JAX-RSリソース側で@ConvertGroupを付与するようにしてみます。
@Path("convertGroupA") @POST @Produces(Array(MediaType.APPLICATION_JSON)) def convertGroupA(@Valid @ConvertGroup(from = classOf[Default], to = classOf[GroupA]) user: User): User = user
@Validは、JAX-RSでの利用に限らず、合わせて使うことになるみたいです。
すると、バリデーションの内容がGroupAに切り替わります。
val user = new User user.name = new Name user.name.first = "サザエ" user.name.last = "フグ田" user.age = 24 val client = ClientBuilder.newBuilder.build try { val response = client .target("http://localhost:8080/rest/validation/convertGroupA") .request .post(Entity.json(user)) response.getStatus should be(Response.Status.OK.getStatusCode) val responseEntity = response.readEntity(classOf[User]) responseEntity.name.first = "サザエ" responseEntity.name.last = "フグ田" response.close() } finally { client.close() }
全部書くと長くなるので、NGのケースは省略します…。
ネストしたJavaBeansにも、@ConvertGroupをつけてみる
続いて、ネストしたJavaBeansにも@ConvertGroupをつけてみます。ネストしたJavaBeansに付与されたGroupが変えられないとか、呼び出し元に意識して欲しくないとかそういう時に使うといいのかな?
class User2 { @BeanProperty @Valid @ConvertGroup.List( Array( new ConvertGroup(from = classOf[Default], to = classOf[GroupB]), new ConvertGroup(from = classOf[GroupA], to = classOf[GroupC]) ) ) var name: Name2 = _ @BeanProperty @Max.List( Array( new Max(value = 12), new Max(value = 32, groups = Array(classOf[GroupA])) ) ) var age: Int = _ } class Name2 { @BeanProperty @Pattern.List( Array( new Pattern(regexp = "^(カツオ|ワカメ)$", groups = Array(classOf[GroupB])), new Pattern(regexp = "^(サザエ|マスオ|タラオ)$", groups = Array(classOf[GroupC])) ) ) var first: String = _ @BeanProperty @Pattern.List( Array( new Pattern(regexp = "^磯野$", groups = Array(classOf[GroupB])), new Pattern(regexp = "^フグ田$", groups = Array(classOf[GroupC])) ) ) var last: String = _ }
ネストしたName2クラスには、GroupB、GroupCのGroupが付与されています。で、外側のUser2はDefaultとGroupAを使うようになっています。
そこで、以下のように@ConvertGroupを付与して、Default時にName2はGroupBとしてバリデーションされるように、GroupAの時はGroupCとしてバリデーションされるようにしてみます。
@BeanProperty @Valid @ConvertGroup.List( Array( new ConvertGroup(from = classOf[Default], to = classOf[GroupB]), new ConvertGroup(from = classOf[GroupA], to = classOf[GroupC]) ) ) var name: Name2 = _
JAX-RSリソース側。まずはDefaultから。
@Path("default2") @POST @Produces(Array(MediaType.APPLICATION_JSON)) def default2(@Valid user: User2): User2 = user
テストがOKになるのは、User2がDefault、Name2がGroupBの時だけです。
it("Default Group 2, valid") { val user = new User2 user.name = new Name2 user.name.first = "カツオ" user.name.last = "磯野" user.age = 12 val client = ClientBuilder.newBuilder.build try { val response = client .target("http://localhost:8080/rest/validation/default2") .request .post(Entity.json(user)) response.getStatus should be(Response.Status.OK.getStatusCode) val responseEntity = response.readEntity(classOf[User2]) responseEntity.name.first = "カツオ" responseEntity.name.last = "磯野" response.close() } finally { client.close() } }
Name2側が、GroupBとして扱われていることになります。
では、DefaultをGroupAとして扱うようなメソッドを、JAX-RS側に追加します。
@Path("convertGroupA2") @POST @Produces(Array(MediaType.APPLICATION_JSON)) def convertGroupA2(@Valid @ConvertGroup(from = classOf[Default], to = classOf[GroupA]) user: User2): User2 = user
今度は、User2がGroupAとして、Name2がGroupCとしてバリデーションされるようになります。
it("GroupA 2, valid") { val user = new User2 user.name = new Name2 user.name.first = "サザエ" user.name.last = "フグ田" user.age = 24 val client = ClientBuilder.newBuilder.build try { val response = client .target("http://localhost:8080/rest/validation/convertGroupA2") .request .post(Entity.json(user)) response.getStatus should be(Response.Status.OK.getStatusCode) val responseEntity = response.readEntity(classOf[User2]) responseEntity.name.first = "サザエ" responseEntity.name.last = "フグ田" response.close() } finally { client.close() } }
動きましたね!
おわりに
今回は、@ConvertGroupで@Valid使用時のGroupの読み替えを試してみました。Groupまわりの話が、またひとつわかった気がします。
今回作成したソースコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/bean-validation-group-conversion
省略した、バリデーションがNGになるケースも入っています。