CLOVER🍀

That was when it all began.

Bean ValidationのMessage Interpolationを使ってみる

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