型に汎用的なアノテーションを作成したい

### 開発環境
- Java 1.8.0
- Spring Boot 2.0.2.RELEASE
 - Spring core 5.0.6.RELEASE
- Bean Validation 2.0

### 導入
入力された値がコード定義されているリストに含まれるかをチェックするアノテーションを`Bean Validation`を使って作成します。
使い方としては↓のような感じです。

```java:Sample.java
public class Sample{

  @CodeExists(StringCode.class)
  private String code;

  :
```
※コード定義は`String`型とします。


#### コードクラスを作る
コードは`Map`（コード値，コード名）で定義します。入力された値がコード定義されているかをチェックするメソッド`exists`を作成しておきます。
またコードクラスは`new`されないようにシングルトンで作成します。シングルトンの実装方法はいくつかありますが、[デザインパターン「Singleton」](https://qiita.com/shoheiyokoyama/items/c16fd547a77773c0ccc1)を参考に`Holder`クラスを利用しました。

```java:StringCode.java

public class StringCode {

  public static final StringCode INSTANCE = StringCodeHolder.INSTANCE;

  private final Map<String, String> codeMap = createCodeMap();

  private StringCode() {}

  public boolean exists(String code) {
    return codeMap.containsKey(code);
  }

  private Map<String, String> createCodeMap() {
    Map<String, String> map = new LinkedHashMap<>();
    map.put("1", "foo");
    map.put("2", "bar");
    map.put("3", "hage");
    return Collections.unmodifiableMap(map);
  }

  private static class StringCodeHolder {
    private static final StringCode INSTANCE = new StringCode();
  }
}
```

#### 入力チェック処理を実装する
アノテーションの引数のクラスからインスタンスを生成するところが雑ですが、`exists`メソッドの結果を返します。

```CodeExistsValidator.java
public class CodeExistsValidator implements ConstraintValidator<CodeExists, String> {

  StringCode target;

  @Override
  public void initialize(CodeExists constraintAnnotation) {
    try {
      // 対象の定数クラスのインスタンスを取得する
      target = (StringCode) (constraintAnnotation.value().getDeclaredField("INSTANCE").get(this));
    } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException
        | SecurityException e) {
      // 適当
      e.printStackTrace();
    }
  }

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    // existsメソッドの結果を返す
    return target.exists(value);
  }
}
```

#### アノテーションを作成する
ここは特に書くことはないですね。

```java:CodeExists.java
@Documented
@Constraint(validatedBy = {CodeExistsValidator.class})
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface CodeExists {
  String message() default "";

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

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

  Class<?> value();

  @Target({METHOD, FIELD})
  @Retention(RUNTIME)
  @Documented
  public @interface List {
    CodeExists[] value();
  }
}
```

#### アノテーションのテスト
では、さっそくテストしてみましょう。

```java:ValidationSampleApplicationTests.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidationSampleApplicationTests {

  private static final String EXIST_CODE = "1";
  private static final String NOT_EXIST_CODE = "5";

  private ValidatorFactory validatorFactory;
  private Validator validator;

  @Before
  public void setup() {
    validatorFactory = Validation.buildDefaultValidatorFactory();
    validator = validatorFactory.getValidator();
  }

  @Test
  public void existStringCode() throws Exception {
    StringCodeSample code = new StringCodeSample(EXIST_CODE);
    Set<ConstraintViolation<StringCodeSample>> result = validator.validate(code);
    assertThat(result.isEmpty(), is(true));
  }

  @Test
  public void notExistStringCode() throws Exception {
    StringCodeSample code = new StringCodeSample(NOT_EXIST_CODE);
    Set<ConstraintViolation<StringCodeSample>> result = validator.validate(code);
    assertThat(result.size(), is(1));
    assertThat(result.iterator().next().getInvalidValue(), is(NOT_EXIST_CODE));
  }

  /**
   * String型のコードを持つクラス
   */
  private static class StringCodeSample {
    @CodeExists(StringCode.class)
    private String code;

    public StringCodeSample(String code) {
      this.code = code;
    }
  }
}
```
`StringCode.java`でコード定義されている`"1"`を設定すると入力チェックエラーは0、コード定義されていない`"5"`を設定すると入力チェックエラーになることが確認できます。

### 本題
前置きがすごく長くなってしまいましたが……
↑では、コード定義を`String`に限定していましたが、`int`や`boolean`型でもコード定義できるようにしたいという要件が追加されたとします。

#### コード定義にインタフェースを適用
汎用的なコード定義クラスを作成するために、インタフェースを作成します。コード存在チェックは型によらず共通になるのでデフォルトメソッドで定義しておきます。

※コード名は重要ではないので`String`型で固定します。

```java:Code.java
public interface Code<T> {

  /**
   * 指定したコードがコードリスト上に存在するかチェックする.
   */
  default boolean exists(T code) {
    return asMap().containsKey(code);
  }

  /**
   * コード値とコード名を紐づけたマップを返す.
   */
  Map<T, String> asMap();
}
```

#### コード定義の修正
↑で作成した`StringCode.java`を修正します。

- `Code<String>`を実装
- `exists`メソッドを削除
- `asMap`で`codeMap`を返す

```java:StringCode.java
public class StringCode implements Code<String>{

  public static final StringCode INSTANCE = StringCodeHolder.INSTANCE;

  private final Map<String, String> codeMap = createCodeMap();

  private StringCode() {}

  @Override
  public Map<String, String> asMap() {
    return codeMap;
  }

  private Map<String, String> createCodeMap() {
    Map<String, String> map = new LinkedHashMap<>();
    map.put("1", "foo");
    map.put("2", "bar");
    map.put("3", "hage");
    return Collections.unmodifiableMap(map);
  }

  private static class StringCodeHolder {
    private static final StringCode INSTANCE = new StringCode();
  }
}
```

#### 入力チェック処理の修正
↑で作成した`CodeExistsValidator.java`を修正します。

- `ConstraintValidator`のパラメータを`String`から`T`に変更
- `target`の型を`Code<T>`に変更
- コード定義クラスからインスタンス取得をメソッドに抜き出し

```java:CodeExistsValidator.java
public class CodeExistsValidator<T> implements ConstraintValidator<CodeExists, T> {

  Code<T> target;

  @Override
  public void initialize(CodeExists constraintAnnotation) {
    try {
      // 対象の定数クラスのインスタンスを取得する
      target = convert(constraintAnnotation.value().getDeclaredField("INSTANCE").get(this));
    } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException
        | SecurityException e) {
      // 適当
      e.printStackTrace();
    }
  }

  @Override
  public boolean isValid(T value, ConstraintValidatorContext context) {
    // existsメソッドの結果を返す
    return target.exists(value);
  }

  @SuppressWarnings("unchecked")
  private Code<T> convert(Object o) {
    return (Code<T>) o;
  }
}
```

#### アノテーションを修正
コード定義と入力チェック処理を修正したので、後はアノテーションを修正するだけです。……が、`@Constraint`に指定するパラメータは`ConstraintValidator`を実装した実クラスなので、仮型引数を含めることはできないようです。
まぁ総称型を指定されても困りますよね。

```java:CodeExists.java
@Documented
@Constraint(validatedBy = {CodeExistsValidator.class}) // NG
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface CodeExists {
  String message() default "";

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

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

  Class<? extends Code<?>> value();

  @Target({METHOD, FIELD})
  @Retention(RUNTIME)
  @Documented
  public @interface List {
    CodeExists[] value();
  }
}
```

出力されるエラー

```txt
Type mismatch: cannot convert from Class<CodeExistsValidator> to Class<? extends ConstraintValidator<?,?>>
```

### `@Constraint`に型ごとの入力チェッククラスを指定できる
`@Constraint(validatedBy = {CodeExistsValidator.class})`のような書き方が出来れば楽で良かったんですが、無理っぽいので……`Bean Validation`のドキュメント[5.7.4. ConstraintValidator resolution algorithm](http://beanvalidation.org/latest-draft/spec/#constraintdeclarationvalidationprocess-validationroutine-typevalidatorresolution)を読んでいて気づいたのですが、`ConstraintValidator<A, T>`の`T`に対してマッチする入力チェックが自動的に実行される仕組みがあるようです。
つまり、`String`用の入力チェック、`int`用の入力チェックを指定すればOKってことですね。


#### String用の入力チェック処理を実装する
というわけで、`CodeExistsValidator`を継承した`String`用の入力チェック処理を実装します。と言っても処理内容は`CodeExistsValidator`で実装済みなので中身は空です。

```java:StringCodeExistsValidator.java
public class StringCodeExistsValidator extends CodeExistsValidator<String> {
}
```

アノテーションには、実装クラスを指定します。

```java:CodeExists.java
@Documented
@Constraint(validatedBy = {StringCodeExistsValidator.class}) // OK
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface CodeExists {
  String message() default "";

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

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

  Class<? extends Code<?>> value(); // Codeを実装したクラスのみ

  @Target({METHOD, FIELD})
  @Retention(RUNTIME)
  @Documented
  public @interface List {
    CodeExists[] value();
  }
}
```

#### int用の入力チェック処理を実装する
`int`型をキーに持つ`Map`を返すクラスを作成します。型以外は`String`と同じなので一部のみ抜粋します。

```java:IntCode.java
public class IntCode implements Code<Integer> {

 :

  @Override
  public Map<Integer, String> asMap() {
    return codeMap;
  }

  private Map<Integer, String> createCodeMap() {
    Map<Integer, String> map = new LinkedHashMap<>();
    map.put(4, "foo");
    map.put(5, "bar");
    map.put(6, "hage");
    return Collections.unmodifiableMap(map);
  }

 :
}
```

入力チェック処理は`String`と同様にクラスを作るだけで実装は空です。
また、元となる`CodeExistsValidator`は抽象クラスにします。

```java:IntCodeExistsValidator.java
public class IntCodeExistsValidator extends CodeExistsValidator<Integer> {
}
```

最後にアノテーションに実装クラスを追加します。

```java:CodeExists.java
@Documented
@Constraint(validatedBy = {StringCodeExistsValidator.class, IntCodeExistsValidator.class}) // ここ
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface CodeExists {

 :
}
```

この状態でテストを追加します。

```java:ValidationSampleApplicationTests.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidationSampleApplicationTests {

  private static final String STRING_EXIST_CODE = "1";
  private static final String STRING_NOT_EXIST_CODE = "5";
  private static final int INT_EXIST_CODE = 4;
  private static final int INT_NOT_EXIST_CODE = 1;

  private ValidatorFactory validatorFactory;
  private Validator validator;

  @Before
  public void setup() {
    validatorFactory = Validation.buildDefaultValidatorFactory();
    validator = validatorFactory.getValidator();
  }

  @Test
  public void existStringCode() throws Exception {
    StringCodeSample code = new StringCodeSample(STRING_EXIST_CODE);
    Set<ConstraintViolation<StringCodeSample>> result = validator.validate(code);
    assertThat(result.isEmpty(), is(true));
  }

  @Test
  public void notExistStringCode() throws Exception {
    StringCodeSample code = new StringCodeSample(STRING_NOT_EXIST_CODE);
    Set<ConstraintViolation<StringCodeSample>> result = validator.validate(code);
    assertThat(result.size(), is(1));
    assertThat(result.iterator().next().getInvalidValue(), is(STRING_NOT_EXIST_CODE));
  }

  @Test
  public void existIntCode() throws Exception {
    IntCodeSample code = new IntCodeSample(INT_EXIST_CODE);
    Set<ConstraintViolation<IntCodeSample>> result = validator.validate(code);
    assertThat(result.isEmpty(), is(true));
  }

  @Test
  public void notExistIntCode() throws Exception {
    IntCodeSample code = new IntCodeSample(INT_NOT_EXIST_CODE);
    Set<ConstraintViolation<IntCodeSample>> result = validator.validate(code);
    assertThat(result.size(), is(1));
    assertThat(result.iterator().next().getInvalidValue(), is(INT_NOT_EXIST_CODE));
  }

  /**
   * String型のコードを持つクラス
   */
  private static class StringCodeSample {
    @CodeExists(StringCode.class)
    private String code;

    public StringCodeSample(String code) {
      this.code = code;
    }
  }

  /**
   * int型のコードを持つクラス
   */
  private static class IntCodeSample {
    @CodeExists(IntCode.class)
    private int code;

    public IntCodeSample(int code) {
      this.code = code;
    }
  }
}
```

`@CodeExists`に指定するクラスによって入力チェックするコード定義が変わっていることが確認できました。
ただ`boolean`とか`char`とか型が増えていくとクラスが増えていくのはしょうがないですかねぇ。

Stack OverFlowに投稿されていた質問：
[Hibernate Validator - Add a Dynamic ConstraintValidator
](https://stackoverflow.com/questions/28360017/hibernate-validator-add-a-dynamic-constraintvalidator)に対する回答も参考になりました。

この記事に記載したコードは[GitHub上で公開](https://github.com/neriudon/validation_sample)しております