JSFのバリデーション

この記事は「Java EE Advent Calendar 2014」の22日目のエントリーです。
昨日は@suke_masaさんの「続・JPQLでハマった話」でした。明日は@kikutaro_さんです。


今日はJSFのバリデーションについて整理したいと思います。

JSF1.*時代のバリデーション

JSF1.*の時代にはFaceletsのValidateタグを使用してバリデーションを行っていました。例えば5文字以上10文字以下の文字数のチェックを行うプログラムは以下です。

・facelets

<h:form>
  <div>
    <h:inputText id="msg" value="#{hogeBean.msg}">
        <f:validateLength minimum="5" maximum="10" />
    </h:inputText>
    <h:message for="msg" />
  </div>
  <h:commandButton action="#{hogeBean.exec}" value="Exec" />
</h:form>

初期のJSFは画面の作成をGUIで行っていました。そのためバリデーションの設定も画面に持つ事でプロパティで簡単に設定を行う事ができました。

JSF2.0以降のバリデーション

Java EE 6に導入されたJSF2.0からはバリデーションにBean Validationが使えるようになりました。Bean Validationを使用することでJPA等のバックグランドのチェック処理とJSFのチェック処理を共通化出来ます。Bean Validationのチェックは画面側では行わずEL式で紐づくバッキングビーン側のフィールドにアノテーションで設定します。

・バッキングビーン

@Named
@RequestScoped
public class HogeBean {
    @Size(min = 5, max = 10)
    private String msg = "Hello!";
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    public void exec() {
    }
}

JSF2.0からは設定より規約が重視されるようになり、GUIに頼らずコードベースで簡単に開発出来るようになりました。そのため、特に画面側にバリデーションを持つ必要性は無くなりました。

Bean Validationでnullチェック出来ない問題

JSFのチェックをBean Validationで行うとすると最初に@NotNullが使用出来ないという問題に遭遇します。具体的には以下のようなソースでは未入力チェックが出来ません。

・バッキングビーン

public class HogeBean {
    @NotNull //未入力チェック出来ない
    private String msg = "Hello!";

これは画面の入力項目がnullではなく空文字として設定されるため発生します。

対処法1 空文字対応のチェックアノテーションを使用する

対処法の1つ目は@NotNullではなく空文字にも対応したアノテーションを使用する事です。たとえばHibernate Validatorで提供されている@NotEmptyを使用すると空文字の場合もエラーにする事が出来ます。

・バッキングビーン

public class HogeBean {
    @NotEmpty //未入力チェック出来できる!
    private String msg = "Hello!";

ただし、@NotEmptyを使用するとJava EE標準ではない実装アノテーションにソースが依存する事になります。

対処法2 INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULLを指定する

web.xmlのコンテキストパラメータにINTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULLを設定すると、サブミットされた値が空文字であった場合に,JSF内でnullに変換してくれます。そうする事で@NotNullで未入力のチェックを行う事が出来ます。

・web.xml

<context-param>
    <param-name>javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL</param-name>
    <param-value>true</param-value>
</context-param>

・バッキングビーン(@NotNullのまま)

public class HogeBean {
    @NotNull //未入力チェック出来きる!
    private String msg = "Hello!";

相関チェック

JPAではエンティティを引数に渡すと設定してあるチェックは一通り実施してくれますが、JSFではEL式でフィールド単位に紐付けを行うため単項目チェックしか実施されません。対処法はいくつかありますが、今回はBean Validationで2つの項目が同じ値でないとエラーとなるバリデーションを作成してみます。

・Bean Validation用アノテーション

@Documented
@Constraint(validatedBy = {EqualsValidator.class})
@Target({FIELD})
@Retention(RUNTIME)
public @interface Equals {
  String message() default "同じ値ではありません";
 
  Class<?>[] groups() default {};
 
  Class<? extends Payload>[] payload() default {};
 
  String value();
 
  @Target({FIELD})
  @Retention(RUNTIME)
  @Documented
  public @interface List {
    Equals[] value();
  }
}

・Bean Validation実装

public class EqualsValidator implements ConstraintValidator<Equals, String> {
 
    private String target;

    @Override
    public void initialize(Equals equals) {
      target = equals.value();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        UIComponent currentComponent = UIComponent.getCurrentComponent(FacesContext.getCurrentInstance());
        if (currentComponent != null) {
            //同階層にあるUIComponentを検索
            UIComponent component = currentComponent.getParent().findComponent(target);
            if (component != null && component instanceof UIInput) {
                UIInput uiInput = (UIInput) component;
                Object targetValue = uiInput.getSubmittedValue();
                //SubmittedValueがnullの場合は検証後のデータを取得
                if(targetValue == null) {
                    targetValue = uiInput.getValue();
                }
                //指定された項目と同じ値かをチェック
                if (value == null) {
                    return targetValue == null || targetValue.equals("");
                } else {
                    return value.equals(targetValue);
                }
            }
        }
        return true;
    }
 
}

・利用例(サーバ側)

public class TestData implements Serializable {
    private String val1;
    @Equals("val1")
    private String val2;

・利用例(画面)

<h:form id="form">
    <h:inputText id="val1" value="#{checkBean.testData.val1}" /><h:message for="val1" /><br />
    <h:inputText id="val2" value="#{checkBean.testData.val2}" /><h:message for="val2" /><br />
    <h:commandButton action="#{checkBean.check()}" value="送信" />
</h:form>

・実行結果
f:id:den2sn:20141221102913p:plain

val1とval2が同じでないとエラーになります。
なんとか作成できましたが、JSF依存の実装で、かつコンポーネントの配置やIDを意識する必要があるので微妙な作りとなってしまいました。JSFの正式対応が望まれます。*1

*1:Java EE 8で<f:validateWholeBean>タグが導入されました