第9章 Seam での JSF 形式検証

プレーンの JSF では、 検証はビューで定義されます。

<h:form>
    <h:messages/>

    <div>
        Country:
        <h:inputText value="#{location.country}" required="true">
            <my:validateCountry/>
        </h:inputText>
    </div>
    
    <div>
        Zip code:
        <h:inputText value="#{location.zip}" required="true">
            <my:validateZip/>
        </h:inputText>
    </div>

    <h:commandButton/>
</h:form>

実際には、 データモデルの一部であり、 またデータベーススキーマの定義全体にわたって存在する制約をほとんどの「検証」が強制実行するため、 この方法は通常、 DRY に違反してしまいます。 Seam は Hibernate Validator を使って定義されるモデルベースの制約に対するサポートを提供しています。

Location クラスで制約を定義するところから始めてみます。

public class Location {
    private String country;
    private String zip;
    
    @NotNull
    @Length(max=30)
    public String getCountry() { return country; }
    public void setCountry(String c) { country = c; }

    @NotNull
    @Length(max=6)
    @Pattern("^\d*$")
    public String getZip() { return zip; }
    public void setZip(String z) { zip = z; }
}

たしかに上記が正当ですが、 実際には Hibernate Validator にビルトインのものを使わずにカスタムな制約を使う方がスマートかもしれません。

public class Location {
    private String country;
    private String zip;
    
    @NotNull
    @Country
    public String getCountry() { return country; }
    public void setCountry(String c) { country = c; }

    @NotNull
    @ZipCode
    public String getZip() { return zip; }
    public void setZip(String z) { zip = z; }
}

いずれの方法をとるにしても、 JSF ページ内で使用される検証のタイプを指定する必要がなくなります。 かわりに、 <s:validate> を使ってモデルオブジェクトで定義される制約に対して検証を行うことができます。

<h:form>
    <h:messages/>

    <div>
        Country:
        <h:inputText value="#{location.country}" required="true">
            <s:validate/>
        </h:inputText>
    </div>
    
    <div>
        Zip code:
        <h:inputText value="#{location.zip}" required="true">
            <s:validate/>
        </h:inputText>
    </div>
    
    <h:commandButton/>

</h:form>

注記: このモデルで @NotNull を指定してもコントロールに出現させるのに required="true" が必要なくなるというわけではありません。これは JSF 検証アーキテクチャの限界によるものです。

この方法はモデル上の制約を定義し、 ビューで制約違反を 表示 します — デザイン性に優れている。。

しかし、 最初の例と比べてそれほど冗長性が軽減されているわけではないので、 <s:validateAll> を使ってみます。

<h:form>
    
    <h:messages/>

    <s:validateAll>

        <div>
            Country:
            <h:inputText value="#{location.country}" required="true"/>
        </div>

        <div>
            Zip code:
            <h:inputText value="#{location.zip}" required="true"/>
        </div>

        <h:commandButton/>

    </s:validateAll>

</h:form>

このタグは単純に <s:validate> をフォーム内での各入力に追加します。 フォームが大きくなる場合は、 入力の手間をかなり省くことができることになります。

ここで、 検証が失敗した場合にユーザーに対してフィードバックを表示させるために何らか手を打たなければなりません。 現在、 すべてのメッセージはフォームの冒頭で表示しています。 実際に行いたいのは、 エラーを付けてフィールドのとなりにメッセージを表示 (プレーン JSF で可能)、 フィールドとラベルをハイライトさせて (これは不可能)、 ついでにフィールドのとなりに何かイメージを表示させる (これも不可能) ことです。 また、 必須事項の各フィールドにはラベルのとなりに色の付いたアスタリスクを表示させたいとします。

フォームの各フィールドに対してかなり多くの機能を必要としています。 フォームにあるすべてのフィールドそれぞれに対してイメージ、メッセージ、入力フィールドのレイアウトやハイライトを指定したいとは思わないでしょうから、 代わりに facelets テンプレートで一般的なレイアウトを指定します。

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                xmlns:h="http://java.sun.com/jsf/html"
                xmlns:f="http://java.sun.com/jsf/core"
                xmlns:s="http://jboss.com/products/seam/taglib">
                 
    <div>
    
        <s:label styleClass="#{invalid?'error':''}">
            <ui:insert name="label"/>
            <s:span styleClass="required" rendered="#{required}">*</s:span>
        </s:label>
        
        <span class="#{invalid?'error':''}">
            <h:graphicImage src="img/error.gif" rendered="#{invalid}"/>
            <s:validateAll>
                <ui:insert/>
            </s:validateAll>
        </span>
        
        <s:message styleClass="error"/>
        
    </div>
    
</ui:composition>

<s:decorate> を使って各フォームフィールドにこのテンプレートを含ませることができます。

<h:form>

    <h:messages globalOnly="true"/>

    <s:decorate template="edit.xhtml">
        <ui:define name="label">Country:</ui:define>
        <h:inputText value="#{location.country}" required="true"/>
    </s:decorate>
    
    <s:decorate template="edit.xhtml">
        <ui:define name="label">Zip code:</ui:define>
        <h:inputText value="#{location.zip}" required="true"/>
    </s:decorate>

    <h:commandButton/>

</h:form>

最後に、 ユーザーがフォーム内を行ったり来たりするのに応じて Ajax4JSF を使って検証メッセージを表示させることができます。

<h:form>

    <h:messages globalOnly="true"/>

    <s:decorate id="countryDecoration" template="edit.xhtml">
        <ui:define name="label">Country:</ui:define>
        <h:inputText value="#{location.country}" required="true">
            <a:support event="onblur" reRender="countryDecoration"/>
        </h:inputText>
    </s:decorate>
    
    <s:decorate id="zipDecoration" template="edit.xhtml">
        <ui:define name="label">Zip code:</ui:define>
        <h:inputText value="#{location.zip}" required="true">
            <a:support event="onblur" reRender="zipDecoration"/>
        </h:inputText>
    </s:decorate>

    <h:commandButton/>

</h:form>

ページ上の重要なコントロールに対して、 特に Selenium のようなツールキットを使って UI の自動化テストを行いたい場合などは、 明示的な ID を定義するスタイルの方がよいでしょう。 明示的な ID を与えないと JSF によって ID が生成されますが、 ページに何らかの変更を加えるとこの生成された値も変更されます。

<h:form id="form">

    <h:messages globalOnly="true"/>

    <s:decorate id="countryDecoration" template="edit.xhtml">
        <ui:define name="label">Country:</ui:define>
        <h:inputText id="country" value="#{location.country}" required="true">
            <a:support event="onblur" reRender="countryDecoration"/>
        </h:inputText>
    </s:decorate>
    
    <s:decorate id="zipDecoration" template="edit.xhtml">
        <ui:define name="label">Zip code:</ui:define>
        <h:inputText id="zip" value="#{location.zip}" required="true">
            <a:support event="onblur" reRender="zipDecoration"/>
        </h:inputText>
    </s:decorate>

    <h:commandButton/>

</h:form>