いずれか１つのフィールドへの入力だけを許可するバリデータの作り方

### はじめに
ぶっちゃけBean Validationの拡張方法です:thermometer_face:

### 確認環境
- 開発環境
 - JDK 1.8
 - Spring Framework 4.3
 - BeanValidation 1.1
- 動作環境
 - JBoss EAP 7.1

※フレームワークとしてTERASOLUNA Server Framework for Java (5.3.0.RELEASE)を使用しています。

### そもそもバリデータを作るには？
#### まずはアノテーションを作る

ほとんど[Constraint composition](http://beanvalidation.org/1.1/spec/#constraintsdefinitionimplementation-constraintcomposition)にあるコードのコピペですが:sweat:

```java:AllowSingleInput.java
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

@Documented
@Constraint(validatedBy = {AllowSingleInputValidator.class}) // ↓で作るクラス
@Target({TYPE, ANNOTATION_TYPE}) // クラスまたはアノテーションにのみ付加できるようにする
@Retention(RUNTIME)
public @interface AllowSingleInput {
  String message() default "{com.neriudon.example.validator.SingleInput.message}";

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

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

  String[] fields(); // 配列にする

  @Target({TYPE, ANNOTATION_TYPE})
  @Retention(RUNTIME)
  @Documented
  @interface List {
    AllowSingleInput[] value();
  }
}
```
ポイントはアノテーションのターゲットをクラスまたはアノテーションのみにすることと、`String`の配列をフィールドに持たせることです。
メッセージはべた書きしてもいいのですが、プロパティで`Please input to only one text filed`を設定しています。

#### バリデータを作る
さて、実際の入力チェックをするクラスを作ります。
まぁ基本は[Example](http://beanvalidation.org/1.1/spec/#constraintsdefinitionimplementation-validationimplementation-example)のコピペなのですが:sweat:

```java:AllowSingleInputValidator.java
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.util.StringUtils;

public class AllowSingleInputValidator implements ConstraintValidator<AllowSingleInput, Object> {
  private String[] fields;

  private String message;

  @Override
  public void initialize(AllowSingleInput constraintAnnotation) {
    fields = constraintAnnotation.fields(); // フィールド名を取得
    message = constraintAnnotation.message();
  }

  @Override
  public boolean isValid(Object value, ConstraintValidatorContext context) {
    BeanWrapper beanWrapper = new BeanWrapperImpl(value);
    // いずれか１つのフィールドに入力されたかチェックする処理
    boolean isSingleInput = false;
    for (String field : fields) {
      if (!StringUtils.isEmpty(beanWrapper.getPropertyValue(field))) {
        if (isSingleInput) {
          isSingleInput = false;
          break;
        } else {
          isSingleInput = true;
        }
      }
    }

    if (isSingleInput) {
      return true;
    } else {
      context.disableDefaultConstraintViolation();
      // 条件を満たさない場合はメッセージ出力
      context.buildConstraintViolationWithTemplate(message).addPropertyNode(fields[0])
          .addConstraintViolation();
      return false;
    }
  }
}
```
`initialize`で対象のフィールド名を取得し、`isValid`でいずれか１つのフィールドだけに入力されているかをチェックしています（もしかしたら他にいい書き方があるかも:frowning2:）。

### 使い方
`Form`クラスにアノテーションを付与し、`fields`に入力チェックをするフィールド名を設定します。

```java
@AllowSingleInput(fields = {"todoTitle1", "todoTitle2", "todoTitle3"})
public class TodoForm implements Serializable {

  private static final long serialVersionUID = 1L;

  private String todoTitle1;

  private String todoTitle2;

  private String todoTitle3;

  // getter/setterは省略します
}
```
jspは特に変更する必要はありません。

```jsp
<!-- Formのところだけ抜き出します -->
<form:form action="${pageContext.request.contextPath}/todo/create"
	method="post" modelAttribute="todoForm">
    <form:label path="todoTitle1"/>
    <form:input path="todoTitle1" />
    <form:errors path="todoTitle1" cssClass="text-error" /><br>
    <form:label path="todoTitle2"/>
    <form:input path="todoTitle2" />
    <form:errors path="todoTitle2" cssClass="text-error" /><br>
    <form:label path="todoTitle3"/>
    <form:input path="todoTitle3" />
    <form:errors path="todoTitle3" cssClass="text-error" /><br>
    <form:button>Create Todo</form:button>
</form:form>
```

### 実行結果

![実行結果](https://qiita-image-store.s3.amazonaws.com/0/162142/18bd88ce-a0c8-0085-6d17-5a6d8305cc39.png)

複数のフィールドに入力してボタンを押すとエラーメッセージが表示されるようになりました:v:。

### この投稿に至った経緯
携わっている案件で、このバリデータが必要になったためメモとして。