Hibernate Validator(Bean Validation)のメッセージにアノテーション属性以外の値を渡す方法（埋め込む方法）

Bean Validationのエラーメッセージには、以下のように制約アノテーションに指定した属性値を埋め込むことができます。（なお、本エントリーで扱う内容は、Hibernate Validatorを使う前提になります）

```java:制約アノテーションの指定例
@Size(min = 4, max = 16)
private String foo;
```

```java:バリデーション実行例
// ...
Bean bean = new Bean();
bean.foo = "123";
Set<ConstraintViolation<Bean>> violations = vb.validate(bean);
// ...
```

上記のケースで生成されるエラーメッセージは、「size must be between 4 and 16」になります。

# アノテーション属性値以外を埋め込みたいと思った背景は・・・

今携わっている案件で、入力チェック時に参照する基準値を外部設定（実際にはプロパティファイル）から取得する必要性に迫られました。Bean Validationを使わずロジックでチェックするという選択肢もありましたが、入力チェックはできるだけBean Validationの仕組みを使って行いたかったので・・・Bean Validationで実現する方法を調査しました。

# 具体的にはどんな制約アノテーションを作ったの？

実際に作成したものではありませんが、「指定日数以降の日付であること」をチェックするアノテーションを作成し、「指定日数」の部分をプロパティファイルから取得するような制約アノテーションを作成することにしました。

```java:制約アノテーションの作成例
@DaysLater(days = "foo.daysLater", defaultValue = 5)
private LocalDate fooDate;
```

```properties:プロパティファイルの設定例
foo.daysLater=10
```

```properties:src/main/resources/ValidationMessages.propertiesの設定例
com.example.validation.DaysLater.message = must be {days} days later
```

# チェックはできたがメッセージが残念・・・

制約違反した際のエラーメッセージは「must be 10 days later」としたいところですが・・・残念ながらデフォルトの動作では「must be foo.daysLater days later」となってしまいます。ま〜当然の結果なのですが・・・

# Bean Validation仕様準拠で対応可能か？

ちゃんと調べていませんが・・・ざっと仕様書を見た感じだとBean Validationの仕様内で対応する方法はなさそうでした。ただ・・・メッセージを組み立てる際にEL式が使えるので・・・EL式を使いこなせば対応できるかもしれません。

# Hibernate Validatorの拡張機能を使えば対応できる！

[Hibernate Validationが提供している拡張機能](https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/?v=6.0#section-hibernateconstraintvalidatorcontext)を使うと、メッセージ定義から参照できる値を変更することができます。

```java

private int value; // アノテーションの属性値（プロパティキー）からプロパティ値を格納

// ...

public boolean isValid(LocalDate targetValue, ConstraintValidatorContext context) {

  boolean result = // チェック内容は省略...
  
　　　　// チェックNGの場合はデフォルト動作を無効化し、任意の制約違反情報を生成する
  if (!result) {
    // Hibernate Validatorの拡張機能を使うため、Hibernate Validator提供のインタフェースに変換
    HibernateConstraintValidatorContext hibernateContext =
        context.unwrap(HibernateConstraintValidatorContext.class);

    // デフォルトの制約違反情報の生成を無効化
    hibernateContext.disableDefaultConstraintViolation();

    // 任意の制約違反情報を生成
    hibernateContext
        // メッセージ定義で{value}で値を参照できるようにする
        .addMessageParameter("days", this.days)
        // メッセージ定義内のEL式でvalueという変数名で値を参照できるようにする
        .addExpressionVariable("days", this.days)
        // メッセージのテンプレートはアノテーションに指定しているテンプレートを使用する（デフォルト動作と同じ）
        .buildConstraintViolationWithTemplate(hibernateContext.getDefaultConstraintMessageTemplate())
        // 制約違反情報を追加する
        .addConstraintViolation();
  }
  return result;
}
```

上記のような実装にすることで、メッセージにプロパティキーではなく、プロパティキーに対応する値を埋め込むことができるようになります。この仕組みを活用すると、メッセージの中に「チェックがOKとなる日付」を埋め込むこともできるようになります。

```java
public boolean isValid(LocalDate targetValue, ConstraintValidatorContext context) {

  LocalDate validMinDate = LocalDate.now().plusDays(value);
  boolean result = // チェック内容は省略...
  
  if (!result) {
    HibernateConstraintValidatorContext hibernateContext =
        context.unwrap(HibernateConstraintValidatorContext.class);
    hibernateContext.disableDefaultConstraintViolation();
    hibernateContext
        .addMessageParameter("days", this.days)
        .addExpressionVariable("days", this.days)
        // チェックOKになる日付を追加
        .addMessageParameter("validMinDate", validMinDate)
        .addExpressionVariable("validMinDate", validMinDate)
        .buildConstraintViolationWithTemplate(hibernateContext.getDefaultConstraintMessageTemplate())
        .addConstraintViolation();
  }
  return result;
}
```

```properties:src/main/resources/ValidationMessages.properties
com.example.validation.DaysLater.message = must be {validMinDate} or later
```

制約違反した際のエラーメッセージは「must be 2018-09-26 or later」となり、より直感的なメッセージにすることができます。

# まとめ

Hibernate Validator上で動かすことが前提条件となっているのであれば、今回紹介した拡張機能を使うことでユーザビリティの高いメッセージを組み立てやすくなると思われます。今携わっている案件はHivernate Validatorでの実行を前提として良いので、この仕組みを有効に活用しようと思っています。