Bean Validationで簡単入力チェック！

## 1. はじめに

今回は`Bean Validation`を利用した入力チェックの方法について説明したいと思います。

* Bean Validationの使い方
 * 入力チェックの対象クラスにチェックに応じたアノテーションを付与する
 * チェック対象となるインスタンスを生成する
 * `Validator`の`validate`メソッドにチェック対象のインスタンスを指定し、入力チェックを実行する

## 2. ライブラリの用意

必要となるライブラリを依存関係に追加します。
`hibernate-validator`のメッセージ解決で必要となるため`javax.el`も一緒に追加しています。
なお、Bean Validationのライブラリ（`groupId:javax.validation、artifactId:validation-api`）を記述していませんが、依存関係があるため自動で依存関係に追加されます。

```xml:pom.xml
  <dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.9.Final</version>
  </dependency>
  <dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.el</artifactId>
    <version>3.0.1-b09</version>
  </dependency>
```

## 3. モデルの定義

チェック対象となるモデルクラスを定義します。この際、各チェックに応じたアノテーションを付与します。付与可能なアノテーションは以下の３種類です。

* [Bean Validation(javax.validation.*)](http://beanvalidation.org/)
* [Hibernate Validator(org.hibernate.validator.constraints.*)](http://docs.jboss.org/hibernate/validator/5.2/reference/en-US/html/ch02.html#validator-defineconstraints-hv-constraints)
* 独自のカスタムValidator

数値の最大値を制限する`@Max`のように、チェックによっては制限値が必要になりますが、これはアノテーションの属性として設定します。設定可能な属性はアノテーションごとに異なります。
チェックでエラーになった際に表示するメッセージを変更したい場合、`message`属性でオーバーライドします。`message`属性で設定したメッセージが最優先されます。

```java:Book.java
package com.example.beanvalidation.app;

import java.io.Serializable;
import java.util.Date;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

public class Book implements Serializable {

    private static final long serialVersionUID = 1L;

    @NotNull
    // @Pattern(regexp = "[0-9,-]+")
    @Pattern(regexp = "[0-9,-]+", message = "入力された値  ${validatedValue} は正しくありません。ISBMの形式で入力してください。")
    private String isbn;

    @NotNull
    @Size(max = 200)
    private String title;

    @Max(9999)
    @Min(1)
    private int pageCount;

    @NotNull
    @Past
    private Date publishedDate;

    // constructor, setter, getter omitted
}
```

## 4. エラーメッセージの変更

`Bean Validation`は仕様のためのエラーメッセージは実装ライブラリに依存します。今回であれば`hibernate-validator`が定義したメッセージがデフォルトになります。
このデフォルトメッセージを変更したい場合、クラスパス直下に配置した`ValidationMessages.properties`でオーバライドすることが可能です。

* プロパティのキー　：　アノテーションのFQCN.message
* プロパティの値　：　デフォルトメッセージの内容

今回は`@Past`のデフォルトメッセージを変更してみたいと思います。

```properties:クラスパス直下のValidationMessages_ja.properties（日本語用）
# javax.validation.constraints.Past.message=入力された日付 ${validatedValue} は正しくありません。過去日を入力してください。
javax.validation.constraints.Past.message=\u5165\u529B\u3055\u308C\u305F\u65E5\u4ED8 ${validatedValue} \u306F\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093\u3002\u904E\u53BB\u65E5\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002
```
**（注意）**

* STS(Spring Tool Suite)のプロパティエディタでは自動で変換してくれるため日本語がunicode変換されています。
* メッセージ内で入力された値は`${validatedValue}`で参照可能です。
* `ValidationMessages.properties`は多言語対応可能で日本語の場合は`ValidationMessages_ja.properties`になります。

## 5. 基本的な使い方

前述で定義した`Book`クラスにいろいろ値を設定して、実際に入力チェックを実施してみたいと思います。
DIコンテナを利用すると`Validator`をインジェクションするのが一般的ですが、今回は`main`メソッドのある普通のJavaアプリとします。
といっても`Validator`の使い方は同じです。

```java:ValidationDemo.java
package com.example.beanvalidation.app;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

public class ValidationDemo {

    public static void main(String[] args) {
        // 1. create validator
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        // 2. create target object
        Book book = createBook();

        // 3. validate
        Set<ConstraintViolation<Book>> constraintViolations = validator
                .validate(book);

        // 4. check result
        int errorCount = constraintViolations.size();
        System.out.println("validate error count : " + errorCount);
        if (errorCount > 0) {
            showErrorDetails(constraintViolations);
        }

        System.out.println("=== demo : validate error ===");
        
        // 2. create target object
        book = createNgBook();

        // 3. validate
        constraintViolations = validator.validate(book);

        // 4. check result
        errorCount = constraintViolations.size();
        System.out.println("validate error count : " + errorCount);
        if (errorCount > 0) {
            showErrorDetails(constraintViolations);
        }
    }

    private static <T> void showErrorDetails(
            Set<ConstraintViolation<T>> constraintViolations) {
        for (ConstraintViolation<T> violation : constraintViolations) {
            System.out.println("----------");
            System.out.println(
                    "MessageTemplate : " + violation.getMessageTemplate());
            System.out.println("Message : " + violation.getMessage());
            System.out.println("InvalidValue : " + violation.getInvalidValue());
            System.out.println("PropertyPath : " + violation.getPropertyPath());
            System.out.println("RootBeanClass : " + violation.getRootBeanClass());
            System.out.println("RootBean : " + violation.getRootBean());
        }
    }

    private static Book createBook() {
        Date publishedDate = Date.from(LocalDateTime.of(2017, 7, 21, 0, 0, 0)
                .toInstant(ZoneOffset.ofHours(9)));
        return new Book("978-4798142470",
                "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 744,
                publishedDate);
    }

    private static Book createNgBook() {
        Date publishedDate = Date.from(LocalDateTime.of(2999, 7, 21, 0, 0, 0)
                .toInstant(ZoneOffset.ofHours(9)));
        return new Book("ERROR-ISBN-9999", null, 0, publishedDate);
    }
}
```

## 6. 実行結果（おまけ）

参考までに、前述のプログラムを実行した際の結果を以下に示します。

```console:実行結果
validate error count : 0
=== demo : validate error ===
validate error count : 4
----------
MessageTemplate : {javax.validation.constraints.Min.message}
Message : must be greater than or equal to 1
InvalidValue : 0
PropertyPath : pageCount
RootBeanClass : class com.example.beanvalidation.app.Book
RootBean : Book [isbn=ERROR-ISBN-9999, title=null, pageCount=0, publishedDate=Sun Jul 21 00:00:00 JST 2999]
----------
MessageTemplate : {javax.validation.constraints.Past.message}
Message : 入力された日付 Sun Jul 21 00:00:00 JST 2999 は正しくありません。過去日を入力してください。
InvalidValue : Sun Jul 21 00:00:00 JST 2999
PropertyPath : publishedDate
RootBeanClass : class com.example.beanvalidation.app.Book
RootBean : Book [isbn=ERROR-ISBN-9999, title=null, pageCount=0, publishedDate=Sun Jul 21 00:00:00 JST 2999]
----------
MessageTemplate : {javax.validation.constraints.NotNull.message}
Message : must not be null
InvalidValue : null
PropertyPath : title
RootBeanClass : class com.example.beanvalidation.app.Book
RootBean : Book [isbn=ERROR-ISBN-9999, title=null, pageCount=0, publishedDate=Sun Jul 21 00:00:00 JST 2999]
----------
MessageTemplate : 入力された値  ${validatedValue} は正しくありません。ISBMの形式で入力してください。
Message : 入力された値  ERROR-ISBN-9999 は正しくありません。ISBMの形式で入力してください。
InvalidValue : ERROR-ISBN-9999
PropertyPath : isbn
RootBeanClass : class com.example.beanvalidation.app.Book
RootBean : Book [isbn=ERROR-ISBN-9999, title=null, pageCount=0, publishedDate=Sun Jul 21 00:00:00 JST 2999]
```

## 7. さいごに

今回は`Bean Validation`による入力チェックの方法について説明しました。
チェック用のアノテーションを付与するだけで、入力チェックの仕様を定義できるのでシンプルで分かりやすいかと思います。