Bean ValidationのMessage Interpolationをカスタムできるらしいので、JSRと合わせてちょっと見てみました。
Interpolating constraint error messages
http://docs.jboss.org/hibernate/validator/5.1/reference/en-US/html_single/#chapter-message-interpolation
MessageInterpolator
http://docs.jboss.org/hibernate/validator/5.1/reference/en-US/html_single/#section-validator-factory-message-interpolator
これを使うと、Message Templateでもっと頑張れるようになるのかなーと思ったのですが、どうなんでしょう。ただ、これを調べていてメッセージの書き方をちゃんと見ることになったので、勉強にはなりましたね。
まあ、まずは試してみましょう。
準備
依存関係の定義。
build.sbt
name := "bean-validation-message-interpolation" version := "0.0.1-SNAPSHOT" scalaVersion := "2.11.7" 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" )
自作のMessageInterpolatorを書く
MessageInterpolatorを自分で作るには、MessageInterpolatorインターフェースを実装したクラスを定義します。
デフォルトのMessageInterpolatorに委譲しているだけですが、こんな感じで書いてみました。
src/main/scala/org/littlewings/javaee7/beanvalidation/MyCustomMessageInterpolator.scala
package org.littlewings.javaee7.beanvalidation import java.util.Locale import javax.validation.{MessageInterpolator, Validation} class MyCustomMessageInterpolator(delegate: MessageInterpolator) extends MessageInterpolator { def this() = this(Validation.byDefaultProvider.configure.getDefaultMessageInterpolator) override def interpolate(messageTemplate: String, context: MessageInterpolator.Context): String = { println(s"received messageTemplate = $messageTemplate") delegate.interpolate(messageTemplate, context) } override def interpolate(messageTemplate: String, context: MessageInterpolator.Context, locale: Locale): String = { println(s"received messageTemplate = $messageTemplate") delegate.interpolate(messageTemplate, context, locale) } }
JSR-349を読むと、最終的にはデフォルトのMessageInterpolatorに委譲するのことを勧めるとのことでしたので、それに習いコンストラクタで委譲先のMessageInterpolatorを受けとるか、そうでなければデフォルトのMessageInterpolatorを使うようにしました。
あと、なんとなくmessageTemplateをprintlnしています。
バリデーション対象のクラスを作る
バリデーションを適用する対象のクラスを作成します。今回は、2種類用意しました。
src/test/scala/org/littlewings/javaee7/beanvalidation/MyBean.scala
package org.littlewings.javaee7.beanvalidation import javax.validation.constraints.{Min, Pattern, Size} class MyBean { @Size(min = 3, max = 5) var a: String = _ @Min(5) var b: Int = _ @Pattern(regexp = "^[a-z]$") var c: String = _ } class MyBeanWithMessage { @Size(min = 3, max = 5, message = "size must be between {min} and {max}, input = ${validatedValue}") var a: String = _ @Min(value = 5, message = "must be greater than or equal to {value}," + " formatted input = ${formatter.format('%1$05d', validatedValue)}") var b: Int = _ @Pattern(regexp = "^[a-z]$", message = "must match \"{regexp}\", input = ${validatedValue}") var c: String = _ }
今回、messageTemplateに書ける部分をちょっと読んでみました。
- {}は、メッセージのパラメーターの開始・終了を表す
- $は、式の開始を表す
アノテーションを付与した際に指定した属性値を、{}で囲うと参照できる、という話ですね。また、$と合わせることで式も表現できると。
また、${}内でEL式が使えると。以下のような特徴があるようで。
- アノテーションを付与した際に指定した属性値を参照可能
- 「validatedValue」で、バリデーションした値を参照可能
- 「formatter.format」で、java.util.Formatter.formatが呼び出せる
一応、このあたりを使ったクラス定義にしています。
動作確認
それでは、動作確認をしてみます。テストコードを用意。
src/test/scala/org/littlewings/javaee7/beanvalidation/MessageInterpolatorSpec.scala
package org.littlewings.javaee7.beanvalidation import javax.validation.{Configuration, ConstraintViolation, Validation} import org.scalatest.FunSpec import org.scalatest.Matchers._ class MessageInterpolatorSpec extends FunSpec { describe("MessageInterpolator Spec") { // ここに、テストを書く! } }
自作のMessageInterpolatorの摘要
先ほど作成した、自作のMessageInterpolatorを使うコードを書いてみます。
it("use, my custom message interpolator") { val bean = new MyBean bean.a = "abcdef" bean.b = 3 bean.c = "ABC" val factory = Validation.buildDefaultValidatorFactory val validator = factory .usingContext .messageInterpolator(new MyCustomMessageInterpolator(factory.getMessageInterpolator)) .getValidator val constraintViolations = validator .validate(bean) .toArray(Array.empty[ConstraintViolation[Any]]) .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) constraintViolations(0).getPropertyPath.toString should be("a") constraintViolations(0).getMessage should be("size must be between 3 and 5") constraintViolations(1).getPropertyPath.toString should be("b") constraintViolations(1).getMessage should be("must be greater than or equal to 5") constraintViolations(2).getPropertyPath.toString should be("c") constraintViolations(2).getMessage should be("must match \"^[a-z]$\"") }
このようにすれば、ValidatorにMessageInterpolatorを設定できるようです。コンストラクタには、デフォルトのMessageInterpolatorを渡しています。
val factory = Validation.buildDefaultValidatorFactory val validator = factory .usingContext .messageInterpolator(new MyCustomMessageInterpolator(factory.getMessageInterpolator)) .getValidator
なお、このコードを実行すると、以下のようなログが出力され、自作のMessageInterpolatorが動作していることがわかります。
received messageTemplate = {javax.validation.constraints.Size.message} received messageTemplate = {javax.validation.constraints.Min.message} received messageTemplate = {javax.validation.constraints.Pattern.message}
やっぱり、messageTemplateがそのまま渡ってくるんですね。
自作のMessageInterpolatorの摘要、その2
自作のMessageInterpolatorの設定方法、その2。
it("use, my custom message interpolator, with contextual container") { val bean = new MyBean bean.a = "abcdef" bean.b = 3 bean.c = "ABC" val configuration = Validation.byDefaultProvider.configure val factory = configuration .messageInterpolator(new MyCustomMessageInterpolator(configuration.getDefaultMessageInterpolator)) .asInstanceOf[Configuration[_]] .buildValidatorFactory val validator = factory.getValidator val constraintViolations = validator .validate(bean) .toArray(Array.empty[ConstraintViolation[Any]]) .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) constraintViolations(0).getPropertyPath.toString should be("a") constraintViolations(0).getMessage should be("size must be between 3 and 5") constraintViolations(1).getPropertyPath.toString should be("b") constraintViolations(1).getMessage should be("must be greater than or equal to 5") constraintViolations(2).getPropertyPath.toString should be("c") constraintViolations(2).getMessage should be("must match \"^[a-z]$\"") }
先ほどは、Validation#buildDefaultValidatorFactoryを使用していましたが、今回はValidatorFactoryの取得方法を変更します。
val configuration = Validation.byDefaultProvider.configure val factory = configuration .messageInterpolator(new MyCustomMessageInterpolator(configuration.getDefaultMessageInterpolator)) .asInstanceOf[Configuration[_]] .buildValidatorFactory
デフォルトのMessageInterpolatorは、Configuration#getDefaultMessageInterpolatorで取得しています。
asInstanceOf(要はキャスト)が入っているのは、Scala都合です…。
この場合、作成したValidatorFactoryから取得できるValidatorに、MessageInterpolatorが適用されるようになります。
val validator = factory.getValidator
validatedValueやformatterを使ってみる
今度は、messageTemplateに着目して、以下のクラスをバリデーション対象にします。
class MyBeanWithMessage { @Size(min = 3, max = 5, message = "size must be between {min} and {max}, input = ${validatedValue}") var a: String = _ @Min(value = 5, message = "must be greater than or equal to {value}," + " formatted input = ${formatter.format('%1$05d', validatedValue)}") var b: Int = _ @Pattern(regexp = "^[a-z]$", message = "must match \"{regexp}\", input = ${validatedValue}") var c: String = _ }
${validatedValue}および${formatter}を使用しています。
確認コード。
it("use, my custom message interpolator, with MessageTemplate") { val bean = new MyBeanWithMessage bean.a = "abcdef" bean.b = 3 bean.c = "ABC" val configuration = Validation.byDefaultProvider.configure val factory = configuration .messageInterpolator(new MyCustomMessageInterpolator(configuration.getDefaultMessageInterpolator)) .asInstanceOf[Configuration[_]] .buildValidatorFactory val validator = factory.getValidator val constraintViolations = validator .validate(bean) .toArray(Array.empty[ConstraintViolation[Any]]) .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) constraintViolations(0).getPropertyPath.toString should be("a") constraintViolations(0).getMessage should be("size must be between 3 and 5, input = abcdef") constraintViolations(1).getPropertyPath.toString should be("b") constraintViolations(1).getMessage should be("must be greater than or equal to 5, formatted input = 00003") constraintViolations(2).getPropertyPath.toString should be("c") constraintViolations(2).getMessage should be("must match \"^[a-z]$\", input = ABC") }
バリデーション対象の値や、Formatterを使った効果がメッセージに現れています。
constraintViolations(0).getPropertyPath.toString should be("a") constraintViolations(0).getMessage should be("size must be between 3 and 5, input = abcdef") constraintViolations(1).getPropertyPath.toString should be("b") constraintViolations(1).getMessage should be("must be greater than or equal to 5, formatted input = 00003") constraintViolations(2).getPropertyPath.toString should be("c") constraintViolations(2).getMessage should be("must match \"^[a-z]$\", input = ABC")
validation.xmlでMessageInterpolatorを設定する
MessageInterpolatorを設定するには、ここまでのようにAPIを操作する以外に、validation.xmlを利用して設定することもできるようです。
src/main/resources/META-INF/validation.xml
<?xml version="1.0" encoding="UTF-8"?> <validation-config xmlns="http://jboss.org/xml/ns/javax/validation/configuration" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration validation-configuration-1.1.xsd" version="1.1"> <message-interpolator>org.littlewings.javaee7.beanvalidation.MyCustomMessageInterpolator</message-interpolator> </validation-config>
validation.xml、初めて書きました…。
確認コード。
it("using Validation.xml") { val bean = new MyBean bean.a = "abcdef" bean.b = 3 bean.c = "ABC" val factory = Validation.buildDefaultValidatorFactory val validator = factory.getValidator val constraintViolations = validator .validate(bean) .toArray(Array.empty[ConstraintViolation[Any]]) .sortWith(_.getPropertyPath.toString < _.getPropertyPath.toString) constraintViolations(0).getPropertyPath.toString should be("a") constraintViolations(0).getMessage should be("size must be between 3 and 5") constraintViolations(1).getPropertyPath.toString should be("b") constraintViolations(1).getMessage should be("must be greater than or equal to 5") constraintViolations(2).getPropertyPath.toString should be("c") constraintViolations(2).getMessage should be("must match \"^[a-z]$\"") }
デフォルトの使い方ですが、これでも実行すると以下のように自作のMessageInterpolatorが動いた結果が現れます。
received messageTemplate = {javax.validation.constraints.Min.message} received messageTemplate = {javax.validation.constraints.Pattern.message} received messageTemplate = {javax.validation.constraints.Size.message}
オマケと気になること
今回、messageTemplate自体を特にどうこうすることはありませんでしたが、メッセージファイルの読み方はHibernate Validatorの場合はResourceBundleLocatorを使うとカスタマイズできるみたいですね。
ResourceBundleLocator
http://docs.jboss.org/hibernate/validator/5.1/reference/en-US/html_single/#section-resource-bundle-locator
あと、propertyPathってMessageInterpolatorから見れないものなんですねぇ。見れたら嬉しいなーと思ったりしたのですが、そうでもないのかな。
ま、ここまでということで。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/bean-validation-message-interpolation