CLOVER🍀

That was when it all began.

Bean Validationで、Validatorを自作する

少しずつ、Bean Validationの勉強をしていこうと思いまして。

以前、本当に少しだけやったのですが、その続きな感じですね。

Bean Validationを試してみる
http://d.hatena.ne.jp/Kazuhira/20140405/1396702413

今回のお題は、自分でValidatorを自作することです。

自分でValidatorを作るには、

  • バリデーション対象に付与するアノテーション
  • アノテーションに対するConstraintValidatorインターフェースの実装クラス
  • 使用するメッセージの作成(これはなくてもいい)

を作成する必要があるようです。では、作ってみましょう。

今回は、配列で指定されたいずれかの値を取るようなValidatorを作成してみます。

  @Select(Array("カツオ", "ワカメ", "波平", "フネ"))
  var name: String = _

準備

まずは、ビルド定義から。Java SE環境で動かすので、このような定義となりました。
build.sbt

name := "bean-validation-my-constraint"

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.4",
  "org.glassfish.web" % "javax.el" % "2.2.4",
  "org.scalatest" %% "scalatest" % "2.2.5" % "test"
)

アノテーションとConstraintValidatorの実装の作成

では、Validatorの定義に移ります。

まずはアノテーションですが、このような形になりました。
src/main/java/org/littlewings/javaee7/beanvalidation/Select.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;

@Documented
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SelectValidator.class)
public @interface Select {
    String message() default "{message.Select}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String[] value();
}

@Constraintアノテーションで、実際にバリデーションを行うクラスを指定します。また、メッセージの定義はmessage()で行います。

メッセージはこのように書くと設定ファイルから参照することになりますが、

    String message() default "{message.Select}";

message()に固定で書いてもよいらしいです。

    String message() default "NGですよ";

また、今回作成するアノテーション固有のパラメーターとしては、Stringの配列を取るようにしました。

    String[] value();

続いて、実際にバリデーションを行うクラス。
src/main/scala/org/littlewings/javaee7/beanvalidation/SelectValidator.scala

package org.littlewings.javaee7.beanvalidation

import javax.validation.{ConstraintValidator, ConstraintValidatorContext}

class SelectValidator extends ConstraintValidator[Select, String] {
  private var selectableValues: Array[String] = _

  override def initialize(constraintAnnotation: Select): Unit =
    selectableValues = constraintAnnotation.value()

  override def isValid(value: String, context: ConstraintValidatorContext): Boolean =
    value match {
      case null => true
      case _ => selectableValues.exists(_ == value)
    }
}

今回はシンプルな実装になりました。ConstraintValidatorインターフェースが型パラメータを取り、アノテーションとバリデーション対象の型を指定します。

class SelectValidator extends ConstraintValidator[Select, String] {

パラメーターを取る場合は、initializeで受け取ればよいみたいですね。

最後に、メッセージを定義します。クラスパス上に、「ValidationMessages.properties」という名前でファイルを作成します。
src/main/resources/ValidationMessages.properties

message.Select={value}のいずれかから選択してください

日本語で書いているように見えますが、実際にはこのファイルはnative2asciiによる変換が必要です…。

使ってみる

それでは、作成したValidatorを使ってみます。

Validatorを適用するクラスを定義。
src/test/scala/org/littlewings/javaee7/beanvalidation/Isono.scala

package org.littlewings.javaee7.beanvalidation

import javax.validation.constraints.{NotNull, Size}

class Isono {
  @NotNull
  @Size(min = 1, max = 3)
  @Select(Array("カツオ", "ワカメ", "波平", "フネ"))
  var name: String = _
}

他のバリデーション用のアノテーションも付けてみました。

テストコード。
src/test/scala/org/littlewings/javaee7/beanvalidation/MyConstraintSpec.scala

package org.littlewings.javaee7.beanvalidation

import javax.validation.{ConstraintViolation, Validation}

import org.scalatest.FunSpec
import org.scalatest.Matchers._

class MyConstraintSpec extends FunSpec {
  describe("MyConstraint Spec") {
    // ここに、テストを書く!
  }
}

まず、必須的な。

    it("not null") {
      val isono = new Isono

      val factory = Validation.buildDefaultValidatorFactory
      val validator = factory.getValidator

      val constraintViolations =
        validator
          .validate(isono)
          .toArray(Array.empty[ConstraintViolation[Any]])

      constraintViolations should have size (1)
      constraintViolations.map(_.getMessage) should contain only ("may not be null")
    }

@Sizeと自作のValidatorでNGになるようにしてみます。

    it("size & select") {
      val isono = new Isono
      isono.name = "フグ田サザエ"

      val factory = Validation.buildDefaultValidatorFactory
      val validator = factory.getValidator

      val constraintViolations =
        validator
          .validate(isono)
          .toArray(Array.empty[ConstraintViolation[Any]])

      constraintViolations should have size (2)
      constraintViolations
        .map(_.getMessage) should contain only(
        "[カツオ, ワカメ, 波平, フネ]のいずれかから選択してください",
        "size must be between 1 and 3"
        )
    }

自作のValidatorのみ。

    it("select") {
      val isono = new Isono
      isono.name = "サザエ"

      val factory = Validation.buildDefaultValidatorFactory
      val validator = factory.getValidator

      val constraintViolations =
        validator
          .validate(isono)
          .toArray(Array.empty[ConstraintViolation[Any]])

      constraintViolations should have size (1)
      constraintViolations.map(_.getMessage) should contain only ("[カツオ, ワカメ, 波平, フネ]のいずれかから選択してください")
    }

バリデーションOKの場合。

    it("valid") {
      val isono = new Isono
      isono.name = "カツオ"

      val factory = Validation.buildDefaultValidatorFactory
      val validator = factory.getValidator

      val constraintViolations =
        validator
          .validate(isono)
          .toArray(Array.empty[ConstraintViolation[Any]])

      constraintViolations should be(empty)
    }

大丈夫そうですね。

まずは、自作のValidatorということで。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/bean-validation-my-constraint