JSFでエラーのある項目の背景色を変える

さて今回はJSFで「エラーのある項目の背景色を変える」をやってみます。
Strutsにある機能だし、これくらい簡単にできるだろう、と思ってたら・・・けっこうめんどくさいことになっています。

基本的なアイデア

標準で提供されているh:inputTextを使って試してみます。
本来エラー項目の見た目を変えるにはclass属性を指定するんでしょうけど、ここではコード量を減らすためにstyleでいきます。

バッキングビーン(とはもう言わないのかな?)のメソッドで入力値が妥当かどうかを判定して、style属性値を変えればいいでしょう。

XHTMLは以下のように記述しました。

<h:inputText id="value" value="#{hogePage.value}" style="#{hogePage.valueStyle}" required="true" />

style属性値でバッキングビーンのプロパティを参照しているのがポイントです。

「入力値が妥当かどうか」

さてここで考えなければいけないことは入力値が妥当かどうかをいかにして判定するか、ということです。
入力値の妥当性はUIInput#isValid()で得られるので、XHTML上でid="value"という属性を付けた箇所に相当するUIInputをなんとか探し出さなくてはなりません。

これがなかなか素直にはいきません。
当初はこんなコードでいけると思ってました。

UIComponent cp = FacesContext.getCurrentInstance().getViewRoot().findComponent("value");

しかしこれではダメです。
どうもUIComponent#findComponent()には、UIComponentのidではなくてclientIdを渡す必要があるようなんです。

・・・なんじゃそりゃ?!
clientIdなんて自分が書くコードのどこにも出てきません。UIComponent#getClientId()から取得するしかない。
つまりこれから探し出すUIComponentオブジェクトのgetClientId()を呼ばないと得られないわけです。パラドックス

これは明らかにおかしい。きっと私が大きな勘違いをしているはず。
でも、勘違いしてるところが分かりませんでした。
仕方がないのでUIComponentのツリーをたどることにします。

UIComponentツリーを走査する

UIComponentツリーをたどるのですが、走査は時間がかかるので最小限に留めておきたいところです。
そこで検証フェイズ(Process Validate Phase)が終わったタイミングで、検証NGとなった場合のみ走査します。
また走査結果はUIComponent#getId()をキーにインデックス化しておきましょう。
これで、1リクエストに付き最大1回の走査で済むはずです。
XHTMLには以下のタグを書いておきます。

<f:event type="postValidate" listener="#{hogePage.onPostValidate}" />

これで検証フェイズが終わったタイミングをフックできます。

で、バッキングビーン側はこんな↓感じ。

    /**
     * 入力項目一欄. <br>
     * {@link #onPostValidate()}が呼び出されたら内容が変化します. <br>
     * 入力値検証がOKの場合は空になります. <br>
     */
    private final Map<String, UIInput> inputComponents  = new HashMap<String, UIInput>();

    /**
     * 入力値検証フェイズが終了したら呼び出されるメソッド. <br>
     * 検証NGの項目がある場合、入力項目のID値を収集して{@link #inputComponents}に格納します. <br>
     * 収集したIDは{@link #getStyleCore(String)}で参照します. <br>
     */
    public void onPostValidate() {
        final Map<String, UIInput> comps = new HashMap<String, UIInput>();
        if (FacesContext.getCurrentInstance().isValidationFailed()) {
            traversal(this.context.getViewRoot(), comps);
        }
        this.inputComponents.clear(); // リクエストスコープならこの行は不要だと思われるが、念のため.
        this.inputComponents.putAll(comps);
    }

    private static void traversal(final UIComponent pComponent, final Map<String, UIInput> pComponents) {
        if (pComponent instanceof UIInput) {
            // 入力項目の存在チェックのためにあえて全てのUIInputを収集する
            pComponents.put(pComponent.getId(), (UIInput) pComponent);
        }
        for (final UIComponent child : pComponent.getChildren()) {
            traversal(child, pComponents);
        }
    }

    /**
     * @return 入力項目valueのstyle属性値.
     */
    public String getValueStyle() {
        return getStyleCore("value"); // ここを文字列で指定するのは、仕方がない・・・
    }

    private String getStyleCore(final String pComponentId) {
        if (this.inputComponents.isEmpty()) { // 初期表示時、あるいは検証NGの項目がない
            return "";
        }
        final UIInput input = this.inputComponents.get(pComponentId);
        if (input == null) {
            throw new IllegalStateException("入力項目 '" + pComponentId + "' が見付かりませんでした。");
        }
        return input.isValid() ? "" : "color: white; background-color: red;";
    }

なかなかたいへんです。それに、JSFにべったりな実装です。

これを全画面について書くわけにはいかないので、Java側は基底クラスを用意することになるでしょう。
XHTMLにはタグが必要なわけですが、これくらいなら全画面に書いてもいいかと思います。
「Faceletsのテンプレート機能を使ったら全画面に書かなくて済むかも」とも思いましたが、PostValidateで呼び出すべきメソッドを持っているクラスは画面毎に異なるわけで、ちょっと難しい気がしています。

あと、この方法はAjax入れ子になったUIComponentがあるときにも正常に動作するかが不安です。
実戦投入には、もっと調査が必要ですね。

それから、この方法の最大の欠点は、getValueStyle()の実装です。

    return getStyleCore("value");

getStyleCore()に処理を委譲していますが、入力項目を文字列で指定しています。
これは、XHTMLの入力項目のid属性値を変えたときに、忘れずにgetValueStyle()も修正しなければならないということを意味します。
うーん、めちゃ修正もれが発生しそうです。

そこでせめてもの安全弁として、getStyleCore()メソッドの中で入力項目の存在チェックを行っています。



(2011/3/29追記)
やはり、この方法はUIComponentが入れ子になっている場合に安全でないことが分かりました。
階層の異なるUIComponentは、クライアントIDが異なっていても、単体で見たときのIDは重複する場合があるからです。



別解

別解があります。
UIComponentをバッキングビーンに持たせて、XHTMLにバインドする方法です。

XHTML側はこう↓なります。

<h:inputText id="value" binding="#{hogePage.valueComponent}" value="#{hogePage.value}" required="true" style="#{hogePage.valueStyle}" />

binding属性がポイントです。
バッキングビーン側はbinding属性に合わせてプロパティを追加します。ここではisValidメソッドを持っているUIInput型のプロパティにします。

    private UIInput valueComponent;

    public UIInput getValueComponent() {
        return this.valueComponent;
    }

    public void setValueComponent(final UIInput pValueComponent) {
        this.valueComponent = pValueComponent;
    }

    public String getValueStyle() {
        return this.valueComponent.isValid() ? "" : "color: white; background-color: red;";
    }

この方法の方が素直なんですが「バッキングビーンのコード量が増える」「XHTMLにbinding属性が必要」という欠点があります。
バッキングビーンのコード量は自動生成を考慮に入れればあまり問題にならない可能性があります。
しかしXHTMLは自動生成が難しく、手動でbinding属性を書く局面が多いと思います。いかにも書き漏れが生じそう。。。

しかし最初の方法も「getStyleCore()に文字列で入力項目を指定する」という、いかにも修正漏れが発生しそうな箇所があります。

XHTMLJava、どっちが漏れやすいかというと・・・どっちもどっちですね。
どちらの方法であっても100点は取れないわけです。

100点に向けてのアイデア

今回は手が回りませんでしたが、今後はカスタムUIやRendererの入れ替えなどを試してみて、なんとか100点な方法を見付けたいと思います。

まとめ

エラー項目の背景赤、というのは非常によくある要求だと思っていたので、こんなにコードを書かないと実現出来ないことに驚きました。
みんなどうやってるんでしょ?