SeamFramework.orgCommunity Documentation

第10章 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 検証アーキテクチャの限界によるものです。

This approach defines constraints on the model, and presents constraint violations in the view — a significantly better design.

しかし、 例と比べてそれほど冗長性が軽減されているわけではないので、 <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> をフォーム内のすべての入力に追加します。 フォームが大きくなる場合は、 入力の手間をかなり省くことができることになります。

ここで、 検証が失敗した場合にユーザーに対してフィードバックを表示させるために何らかの手を打たなければなりません。 現在、 すべてのメッセージはフォームの冒頭で表示しています。 ユーザーがメッセージと入力を関連付けられるようにするため、 入力コンポーネントで標準の label 属性を使いラベルを定義する必要があります。


<h:inputText value="#{location.zip}" required="true" label="Zip:">
    <s:validate/>
</h:inputText
>

次にプレースホルダーの {0} を使ってこの値をメッセージ文字列にインジェクトすることができます (Hiberate Validator の制約用に JSF メッセージに渡される最初で唯一のパラメータ)。 これらのメッセージを定義する場所については国際化 (Internationalization) のセクションをご覧ください。

validator.length={0} 長さは {min} と {max} の間でなければなりません

実際に行いたいのは、 エラーを付けてフィールドのとなりにメッセージを表示 (プレーン 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 value="/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
>

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


<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" bypassUpdates="true"/>
        </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" bypassUpdates="true"/>
        </h:inputText>
    </s:decorate>

    <h:commandButton/>

</h:form
>

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


<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" bypassUpdates="true"/>
        </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" bypassUpdates="true"/>
        </h:inputText>
    </s:decorate>

    <h:commandButton/>

</h:form
>

検証が失敗したときに表示させるメッセージを変えたい場合はどうでしょう。 Seam メッセージバンドル (およびメッセージ中の EL 式やビューごとのメッセージバンドルなど優れたものすべて) を Hibernate Validator で使用することができます。

public class Location {

    private String name;
    private String zip;
    
    // Getters and setters for name
    @NotNull
    @Length(max=6)
    @ZipCode(message="#{messages['location.zipCode.invalid']}")
    public String getZip() { return zip; }
    public void setZip(String z) { zip = z; }
}
location.zipCode.invalid = #{location.name} に対する有効な郵便番号ではありません