JSFのValidatorにおいて2回目のgetSubmittedValue()で値がnullになった時の対応方法


環境
OS : macOS Hight Sierra Version 10.13.2
Eclipse : Neon.3 Release (4.6.3)
Java : JDK8
GlassFish : 4.1.2

事象 : JSFのValidatorにおいて2回目のgetSubmittedValue()で値がnullになる

inputPassword.xhtml(画面)
<省略>
  <h:outputLabel>パスワードを入力して下さい。</h:outputLabel>
  <br />
  <h:inputSecret id="password" value="#{passwordBean.password}">
    <f:validateRequired />
    <f:validateLength minimum="3" maximum="10" />
    <f:validator validatorId="passwordValidator" />
    <f:attribute name="target" value="password" />
  </h:inputSecret>
  <h:message for="password" errorClass="error" />
  <br />
  <h:outputLabel>確認のためにもう&nbsp;&nbsp;1度入力して下さい。</h:outputLabel>
  <br />
  <h:inputSecret id="rePassword" value="#{passwordBean.kakuninPassword}">
    <f:validateRequired />
    <f:validator validatorId="passwordValidator" />
    <f:attribute name="target" value="rePassword" />
    <f:attribute name="passwordId" value="password" />
  </h:inputSecret>
  <h:message for="rePassword" errorClass="error" />
<省略>
PasswordValidator.java(カスタムバリデータ)
<省略>
@FacesValidator(value = "passwordValidator")
public class PasswordValidator implements Validator {
    /** パスワードに使っちゃダメな文字列. */
    private static final String KINSHI = "password";
    /** チェック対象を指定するname属性の名前. */
    private static final String TARGET_KEY = "target";
    /** パスワードをチェックする場合に指定するname属性の値. */
    private static final String TARGET_PASSWORD = "password";
    /** 確認パスワードをチェックする場合に指定するname属性の値. */
    private static final String TARGET_RE_PASSWORD = "rePassword";
    /** パスワード入力欄のid. */
    private static final String PASSWORD_ID_KEY = "passwordId";

    /*
     * (non-Javadoc)
     * @see javax.faces.validator.Validator#validate(javax.faces.context.
     * FacesContext, javax.faces.component.UIComponent, java.lang.Object)
     */
    @Override
    public void validate(FacesContext context, UIComponent component, Object value)
            throws ValidatorException {
        String target = (String) component.getAttributes().get(TARGET_KEY);
        if (TARGET_PASSWORD.equals(target)) {
            validatePassword(value);
        }
        if (TARGET_RE_PASSWORD.equals(target)) {
            validateRePassword(component, value);
        }
    }

    /**
     * 入力されたパスワードを検証する.
     * @param value 入力値.
     */
    private void validatePassword(Object value) {
        String inputedValue = (String) value;
        if (inputedValue.equals(KINSHI)) {
            FacesMessage errorMessage = new FacesMessage(KINSHI + "は使えません。");
            errorMessage.setSeverity(FacesMessage.SEVERITY_ERROR);
            throw new ValidatorException(errorMessage);
        }
    }

    /**
     * 確認用に再入力されたパスワードを検証する.
     * @param component {@link UIComponent}
     * @param value 入力値.
     */
    private void validateRePassword(UIComponent component, Object value) {
        String rePassword = (String) value;
        String passwordId = (String) component.getAttributes().get(PASSWORD_ID_KEY);
        if (StringUtils.isNotBlank(passwordId)) {
            String password = getInputedPasswordValue(component, passwordId);
            if (StringUtils.isNotEmpty(rePassword)) {
                if (!rePassword.equals(password)) {
                    FacesMessage errorMessage = new FacesMessage("入力されたパスワードと一致しません。");
                    errorMessage.setSeverity(FacesMessage.SEVERITY_ERROR);
                    throw new ValidatorException(errorMessage);
                }
            }
        }
    }

    /**
     * inputタグのid属性値からパスワードの入力値を取得する.
     * @param component {@link UIComponent}
     * @param id inputタグのid属性値.
     * @return パスワードの入力値.
     */
    private String getPasswordValueById(UIComponent component, String id) {
        HtmlInputSecret inputTextConf = (HtmlInputSecret) component.findComponent(id);
        Object submittedValue = inputTextConf.getSubmittedValue();
        return submittedValue.toString();
    }
}

2回目に値がnullにならないパターン : 1回目の検証でエラー

2回目に値がnullになるパターン : 1回目の検証で正常

理由 : 検証が終わってsetSubmittedValue(null)が実行されているから


JSFのライフサイクル
1. Viewの復元
2. 入力値の適用
3. 入力値の変換と検証
4. バッキングビーンの変数に値をバインド
5. 処理の実行
6. レスポンスのレンダリング

今回は、このライフサイクルの3で1回目の検証においてエラーになるとgetSubmittedValue()で値がnullになりました。

JSF 2.0 の詳細について | 寺田 佳央 - Yoshio Terada
2回目以降のリクエスト(必要項目に記入してボタンを押下したような場合)では、UIView のツリーが復元(ライフサイクルの1番目)された後、リクエスト値の適用フェーズ(ライフサイクルの2番目)で UIComponent に対して入力値を適用します。(この例では HtmlInput#setSubmittedValue()が実行される。) この、setSubmittedValue() された値は、入力値の検証フェーズ(ライフサイクルの3番目) が完了するまで、getSubmittedValue() で値を取り出す事ができます。入力値の検証フェーズが終わると、入力値は、setSubmittedValue(null)が実行され、HtmlInput#setValue(“foo”) が呼び出され、以降のフェーズでは入力値は HtmlInput#getValue() を利用して取得するようになります。

対応 : 2回目のgetSubmittedValue()で値がnullになったらgetValue()で値を取得する

<他の箇所は修正なし>
    private String getPasswordValueById(UIComponent component, String id) {
        HtmlInputSecret inputTextConf = (HtmlInputSecret) component.findComponent(id);
        Object submittedValue = inputTextConf.getSubmittedValue();
        if (submittedValue == null) {
            // getSubmittedValue()で値がnullになったらgetValue()で値を取得する
            submittedValue = inputTextConf.getValue();
        }
        return submittedValue.toString();
    }
<他の箇所は修正なし>

参考サイト:アノテーションでValidatorを仕掛ける時はこちらがさんこうになります