開発者:J2EE

JSFを使用したデータベース駆動型アプリケーションの構築
Andrei Cioroianu著

Oracle TopLinkとJSTLのSQLタグを使用して、リレーショナル・データベースの更新と問合せを実行するJSFベースのWebアプリケーションの開発について学習します。

 

関連するダウンロード・リンク
 サンプル・コード
 Oracle TopLink 10g
 Oracle Database 10g
 OC4J 10g
 Oracle ADF Components
 JavaServer Faces



JavaServer Faces(JSF)は、Webベースのユーザー・インタフェースを作成するための標準Javaテクノロジーです。 この記事では、JSFフォームの構築、JSFによるフォーム・データの検証、データベースにアクセスするJSFアクションの実装、およびJSFによるSQL結果セットの表示について説明します。 リレーショナル・データベースでは、低レベルAPI(JDBC)、オブジェクト・リレーショナル(O/R)マッピング・フレームワーク(Oracle TopLinkなど)、またはJSPタグ・ライブラリ(JSTLなど)を使用して更新と問合せを実行できます。 この記事では、これらのオプションと主要なJSF機能について説明します。

アプリケーションの概要

JSFは、単なるWeb開発のためのタグ・ライブラリではありません。 JSFは、MVCベースのフレームワークであり、Webフォームを制御し、JavaBeansを管理します。 JSFの標準UIコンポーネントは限られていますが、Oracle Application Development Framework(Oracle ADF)Facesなどのコンポーネント・ライブラリを使用したり、独自のカスタム・コンポーネントを構築したりすることができます。 これらのUIコンポーネントは標準JSF APIに基づいているため、すべてのコンポーネントを同じWebページ内で使用できます。

JSFページは、標準JSFタグ・ライブラリやそのほかのJSFベースのライブラリを使用した通常のJSPです。 JSFページが実行されると、JSFフレームワークにより"コンポーネント・ツリー"(または"コンポーネント・ビュー")の取得やリストアが試行されます。 ここには、Webページのコンポーネント、実行される必要があるデータ変換とデータ検証、UIの状態が格納されることが必要なBeanプロパティ、登録されたイベント・リスナーなどの情報が記載されています。コンポーネント・ツリーが存在しない場合は、新しく作成され、クライアント(非表示フォーム・フィールド使用)やサーバーに(セッション属性として)保存されて、後続リクエストで使用されます。

ここでは、ニュースレターのサブスクライブに使用するWebアプリケーションを例としています。 サブスクライバは、電子メール・アドレス、氏名、設定を入力して登録をおこないます。 また、あとでプロファイルを変更できるようにパスワードを選択する必要があります。

図1はサブスクリプション・フォームを示しています。 サンプル・アプリケーションではニュースレターは配信されませんが、実際のアプリケーションでは、マネージャー、開発者、管理者にそれぞれ別のニュースレターが送信されます(同一のサブスクライバが複数のニュースレターを受信することも可能です)。 サンプル・アプリケーションでは、ユーザー・プロファイルがリレーショナル・データベースに保存され、サブスクライバは、各自のプロファイルを変更できます。

図1:サブスクリプション・フォーム


アプリケーションの構成

JSFでWebフォームを制御するには、 web.xmlアプリケーション・ディスクリプタにFaces Servletを設定する必要があります。 Webページには .jsp拡張子が使用されますが、URLには .faces接尾辞または /faces/接頭辞のいずれかが使用されます。 JSFコントローラ・サーブレットは、 web.xmlファイルの *.facesまたは /faces/*にマッピングする必要があります。 ここで示すサンプル・アプリケーションでは、接尾辞マッピングを使用して、クライアントにJSFビューを保存します。

 

                                 
<web-app>
    ...
    <context-param>
        <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
        <param-value>client</param-value>
    </context-param>

    <servlet>
        <servlet-name>FacesServlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>FacesServlet</servlet-name>
        <url-pattern>*.faces</url-pattern>
    </servlet-mapping>
    ...
</web-app>
                              


モデル・ビューの分離

 

リソース

 

Oracle JDeveloper 10g(10.1.3)開発者プレビュー版を使用することで、 TopLinkマッピング定義する単一のIDEの取得、Javaコードの記述、 視覚的なJSFページとJSPページの作成をおこなうことができます。 さらに、Oracle JDeveloperには豊富なJSFコンポーネントのADF Faces一式が付属しており、アプリケーションで再利用することが可能です。

1)Oracle JDeveloper 10g(10.1.3)開発者プレビュー版の ダウンロード

2)Oracle JDeveloperを使用した JSF/TopLink アプリケーションの構築

3) Oracle ADF Faces Componentsのプレリリース版(豊富なJSFコンポーネントの一式)

そのほかのリソース

Oracle JDeveloper 10gでのADF Facesの使用方法

Facesの作成
JavaServer Facesは、Web GUIを作成する強力な新フレームワークを提供します。

ADF UIXからJSFまで
Oracle ADF Faces Componentsは、JavaServer Facesで再利用できるライブラリを提供します。

Oracle JDeveloperとJSF

Java Webアプリケーション(JSFベースのアプリケーションを含む)は、JSPとJavaBeansを使用してプレゼンテーションをアプリケーション・ロジックから分離します。 JSFタグの属性は、JSP 2.0 ELと類似した式言語(EL)を使用してBean属性(Beanメソッドの場合あり)にバインドできます。 ${...}構成を使用する代わりに、JSF ELでは #{...}構文を使用します。これは、JSP 1.2とJSP 2.0の両方で使用できるためです。 さらに、JSPコンテナで式を評価する代わりに、JSFで必要な時にいつでも式を評価(再評価)できます。 JSF ELバインディングは、必要に応じて双方向に機能します。 たとえば、UIコンポーネントでBeanプロパティの値を取得して、デフォルト値としてユーザーに提示できます。 ユーザーがフォーム・データを送信すると、アプリケーション・ロジックが新しい値を処理できるよう、UIコンポーネントがBeanプロパティを更新します。

JavaBeans(モデル)、JSFページ(ビュー)、Faces Servlet(コントローラ)のロールは明確に定義されており、オブジェクト・モデル、プレゼンテーション、リクエスト処理が分離されています。 もう1段階進んで、UI固有のJavaコードを、データの管理と処理を実行するインスタンスのあるクラスから分離できます。 この際に考えられる解決策としては、ユーザー・インタフェースと関係のないモデルBeanを作成し、アクションやバリデータなどのJSF固有のメソッドを使用してモデル・クラスを拡張する方法があります。これについては後ほど説明します。

サンプル・アプリケーションには2つのモデルBean( LoginInfoSubscriber)があり、JSFがインスタンスを管理する2つのビューBean( LoginInfoBeanSubscriberBean)で拡張されています。 各ビューBeanのインスタンスには、表1に示したような識別子とスコープがあります。JSF仕様では、ビューBeanは"マネージドBean"や"バッキングBean"と呼ばれます。 JSFでは、"モデルBean"と"ビューBean"の区別はありません。 ちなみに、アプリケーション・ロジックとUIコードを同一クラス内に配置することは可能ですが、クラスの再利用性が低下することに加え、メンテナンスが困難となるため、専門の開発者がアプリケーション・ロジックやWeb設計などの主要業務に注力できなくなるおそれがあります。

 

モデルBeanクラス ビューBeanクラス Bean識別子 JSFスコープ
LoginInfo LoginInfoBean loginInfo request
Subscriber SubscriberBean subscriber session
表1: サンプルWebアプリケーションのモデルBeanとビューBean


通常のJSPページでは、 <jsp:useBean>標準アクションを使用してJavaBeansがインスタンス化されます。 JSFフレームワークを使用すれば、Webページのクラス名を指定する必要はありません。 その代わりに、通常XMLファイルでは、Beanインスタンスを faces-config.xmlという名前に設定します(大規模なアプリケーションを開発する場合は、複数の構成ファイルを使用できます。 その場合は、 web.xmlファイルに javax.faces.CONFIG_FILESパラメータを追加する必要があります)。 各Beanインスタンスで、識別子(Bean名)、クラス名、有効なJSFスコープ( application、session、request、または none)を指定する必要があります。 JSF式( #{...})でBeanが参照されると、JSFフレームワークによりBeanインスタンスが特定のスコープに存在するかどうかが検証され、見つからない場合はデフォルト値で作成および初期化されます。このデフォルト値は、構成ファイルで次のように指定することもできます。

 

                                 
<faces-config>
    ...
    <managed-bean>
        <managed-bean-name>loginInfo</managed-bean-name>
        <managed-bean-class>jsfdb.view.LoginInfoBean</managed-bean-class>
        <managed-bean-scope>request</managed-bean-scope>
    </managed-bean>

    <managed-bean>
        <managed-bean-name>subscriber</managed-bean-name>
        <managed-bean-class>jsfdb.view.SubscriberBean</managed-bean-class>
        <managed-bean-scope>session</managed-bean-scope>
        <managed-property>
            <property-name>email</property-name>
            <null-value/>
        </managed-property>
        ...
        <managed-property>
            <property-name>subscriptionType</property-name>
            <value>1</value>
        </managed-property>
    </managed-bean>
    ...
 </faces-config>
                              


各BeanプロパティをJSF構成ファイルで管理プロパティとして宣言する必要はありません。これは、宣言されていないプロパティをJSFページのUIコンポーネントにバインドできるからです。 Beanプロパティを faces-config.xmlファイルで指定する必要があるのは、デフォルト値で初期化したい場合のみです。

Beanとページの関係

データ・アクセス・メソッド(次項で説明)は、ビューBeanのアクション・メソッドから呼び出すことができます。 各アクション・メソッドは、表2に示すようにJSPページのSubmitボタンにバインドされています。 JSFフレームワークによりフォーム・データが検証され、エラーのない場合はフォームがユーザーに返されます。 エラーがある場合は、有効なフォーム・データがマネージドBeanのプロパティに保存され、クリックしたボタンにバインドされたアクション・メソッドがJSFによって呼び出されます。 そのため、アクション・メソッドにそのBeanのプロパティ値を処理するアプリケーション・ロジックが実装されている必要があります。

 

データ・アクセス・メソッド マネージドBeanとJSFアクション JSFページ
SubscriberDAO.select() LoginInfoBean.loginAction() login.jsp
SubscriberDAO.insert() SubscriberBean.subscribeAction() subscribe.jsp
SubscriberDAO.update() SubscriberBean.profileAction() profile.jsp
SubscriberDAO.delete() SubscriberBean.unsubscribeAction() unsubscribe.jsp
表2: JSFページのSubmitボタンにバインドされたJSFアクションの使用するDAOメソッド


ページ間のナビゲーション

各アクション・メソッドは、"outcome"という文字列を返します。 JSFではナビゲーション・ハンドラを使用して、それぞれの結果に必要な処理を決定します。 アクション・メソッドが nullを返した場合は、同じページを再表示する必要があります。 それ以外の場合には、返された結果に応じて別のページを表示できます。 表3に、サンプルWebアプリケーションのJSFフォーム、結果の候補、各結果により表示されるページを示します。

 

JSFフォーム 結果の候補 表示されるページ
subscribe.jsp subscribed subscribed.jsp
login.jsp profile profile.jsp
login.jsp list list.jsp
profile.jsp login login.jsp
unsubscribe.jsp login login.jsp
unsubscribe.jsp unsubscribed unsubscribed.jsp
unsubscribe.jsp cancel profile.jsp
表3: ユーザーがフォームのSubmitボタンをクリックすると、JSFによりフォーム・データが検証され、
エラーがなければクリックしたボタンにバインドされたアクション・メソッドが呼び出されます。
その後、JSFはアクション・メソッドにより返された結果を使用して、次に表示されるページを決定します。


デフォルトのJSFナビゲーション・ハンドラでは、JSF構成ファイルに指定されたナビゲーション・ルール一式が使用されます。 たとえば、ユーザーがログイン・フォーム( login.jsp)に入力してLoginボタンをクリックすると、そのユーザーのロールがサブスクライバであるか、または管理者であるかによって、 loginAction()メソッドが profileまたは listを返します。 ユーザーが不明の場合や、パスワードが間違っている場合は、 loginAction()nullを返します。 この場合は、ログイン・フォームが再表示されます。 認証に成功すると、 loginAction()で返された結果に応じて profile.jspまたは list.jspにリクエストが転送されます。 faces-config.xmlのナビゲーション・ルールは、次のとおりです。

 

                                 
<faces-config>
    ...
    <navigation-rule>
        <from-view-id>/login.jsp</from-view-id>
        <navigation-case>
            <from-outcome>profile</from-outcome>
            <to-view-id>/profile.jsp</to-view-id>
        </navigation-case>
        <navigation-case>
            <from-outcome>list</from-outcome>
            <to-view-id>/list.jsp</to-view-id>
        </navigation-case>
    </navigation-rule>
    ...
 </faces-config>
                              


JavaBeansとデータ・アクセス・オブジェクト(DAO)

この項では、モデルBeanとビューBeanの作成方法と、Beanの永続性の実装方法について説明します。 モデルBeanは、データベースへの保存が必要なプロパティを定義します。 ビューBeanは、モデルBeanをUI固有のコード、アクション、バリデータなどで拡張します。 JSFは faces-config.xmlファイルで指定されたビューBeanのインスタンスを作成しますが、サンプル・アプリケーションの永続レイヤーはモデルBeanで機能します。 そのため、アプリケーションには ModelUtils.copy()などのユーティリティ・メソッドが必要です。このメソッドは、JSFがインスタンス化したビューBeanのプロパティを永続レイヤーが作成したモデル・オブジェクトにコピーします。また、その逆の動作も実行します。 ModelUtilsクラスを使用して、 ModelResourcesというリソース・バンドルからモデル・リソース(構成パラメータ、SQL文、エラー・メッセージなど)を取得することもできます。 さらに ModelUtilsには、 getSubscriberDAO()メソッドがあります。このメソッドは、リレーショナル・データベースでの Subscriberオブジェクトの選択、挿入、更新、削除方法を定義する SubscriberDAOインタフェースのインスタンスを返します。

サンプル・アプリケーションには、2種類の SubscriberDAOインタフェースが実装されています。1つはJDBC APIベース、もう1つはOracle TopLinkベースのインタフェースです。 この2種類の実装は、同じアプリケーション・インスタンスで併用することはできません。 ModelResourcesバンドルには DAOパラメータがあり、このパラメータで使用するDAO実装を指定できます。 JDBCベースのDAOを選択する唯一のメリットは、標準Java APIとSQLのみで構成されている点です。 TopLinkも内部でJDBCが使用されていますが、TopLinkの場合はさらに次のような多数のメリットがあります。

  • ビジュアル・ツール(Mapping WorkbenchとSession Editor)を使用してO/Rマッピングの定義とアプリケーションの構成をおこなうことにより、生産性を向上できます。 これにより手動で記述しなければならないコードが大幅に削減されます。
  • JDBCなどの低レベルAPIが不要になります。
  • データベースから読み出すオブジェクトをキャッシュに保存することができるため、アプリケーションの実行速度が向上します。 また、効率的なSQL文を内部的に作成できます。
  • SQLのほかにも、Query-by-Example(QBE)、Java式ベースの問合せ、EJB QLなどのさまざまな問合せメカニズムに対応しています。
  • 複数のサーバー・インスタンス(クラスタリング)に対応しているため、スケーラブルなアプリケーションを構築できます。


モデルBeanの作成:サンプルWebアプリケーションには、JSF APIを使用しない2つのモデルBean( LoginInfoSubscriber)があります。 LoginInfoクラスは2つのプロパティ( emailpassword)を定義し、 java.io.Serializableインタフェースを使用して、JavaBeanのようにシリアライズ可能なクラスとして宣言します。

 

                                 
package jsfdb.model;

public class LoginInfo implements java.io.Serializable {
    private String email;
    private String password;

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}
                              


Subscriberクラスは LoginInfoを拡張し、5つの追加プロパティ( name、manager、developer、administratorsubscriptionType)を定義します。このクラスには countNewsletters()というメソッドがあります。

 

                                 
package jsfdb.model;

public class Subscriber extends LoginInfo {
    public static final int TYPE_DAILY = 1;
    public static final int TYPE_WEEKLY = 2;
    public static final int TYPE_MONTHLY = 3;

    private String name;
    private boolean manager;
    private boolean developer;
    private boolean administrator;
    private int subscriptionType;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    ...

    public int countNewsletters() {
        int count = 0;
        if (isManager())
            count++;
        if (isDeveloper())
            count++;
        if (isAdministrator())
            count++;
        return count;
    }

}
                              


Subscriberのインスタンスは、次のSQL文で作成される表に保存されます。

 

CREATE TABLE subscribers (
       subscriberEmail VARCHAR2(80) PRIMARY KEY,
       subscriberPassword VARCHAR2(20),
       subscriberName VARCHAR2(80),
       managerFlag NUMBER(1),
       developerFlag NUMBER(1),
       administratorFlag NUMBER(1),
       subscriptionType NUMBER(1) )


通常、Beanのプロパティとそれに対応する表の列には、同じ名前をつける必要があります。 ここでは別の名前をつけていますが、これはプロパティの使用箇所と列の使用箇所をわかりやすくするためです。

データ・アクセス・オブジェクトの使用: SubscriberDAOインタフェースは、モデルBeanから継承するプロパティの保存と取得をおこなうビューBeanによって呼び出されるメソッドを定義します。

 

                                 
package jsfdb.model.dao;

import jsfdb.model.LoginInfo;
import jsfdb.model.Subscriber;
import jsfdb.model.err.IncorrectPasswordException;
import jsfdb.model.err.LoginException;
import jsfdb.model.err.ProfileException;
import jsfdb.model.err.SubscribeException;
import jsfdb.model.err.UnknownSubscriberException;
import jsfdb.model.err.UnsubscribeException;

public interface SubscriberDAO {
    public Subscriber select(LoginInfo loginInfo)
            throws LoginException,
            UnknownSubscriberException,
            IncorrectPasswordException;

    public void insert(Subscriber subscriber)
            throws SubscribeException;

    public void update(Subscriber subscriber)
            throws ProfileException;

    public void delete(Subscriber subscriber)
            throws UnsubscribeException;
}
                              


getSubscriberDAO()メソッドは、DAO実装( TopLinkSubscriberDAOまたは JDBCSubscriberDAO)をロードして、ロードされたクラスのインスタンスを返します。

 

                                 
package jsfdb.model;

import jsfdb.model.dao.SubscriberDAO;
...
public class ModelUtils {
    ...
    private static SubscriberDAO subscriberDAO;
    ...
    public synchronized static SubscriberDAO getSubscriberDAO() {
        if (subscriberDAO == null)
            try {
                Class daoClass = Class.forName(getResource("DAO"));
                subscriberDAO
                    = (SubscriberDAO) daoClass.newInstance();
            } catch (ClassNotFoundException x) {
                log(x);
                throw new InternalError(x.getMessage());
            } catch (IllegalAccessException x) {
                log(x);
                throw new InternalError(x.getMessage());
            } catch (InstantiationException x) {
                log(x);
                throw new InternalError(x.getMessage());
            }
        return subscriberDAO;
    }
    ...
}
                              


getSubscriberDAO()メソッドは、 getResource()によりDAO実装の名前を取得します。 getResources()は、モデル・リソースの取得に使用されます。 想定外のエラーが発生した場合は、 log()メソッドによりログが作成されます。

 

                                 
package jsfdb.model;
...
import java.util.ResourceBundle;
import java.util.MissingResourceException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ModelUtils {
    public static final String RESOURCES
        = ModelUtils.class.getPackage().getName()
        + ".res.ModelResources";
    private static ResourceBundle resources;

    public static void log(Throwable x) {
        Logger.global.log(Level.SEVERE, x.getMessage(), x);
    }

    public static synchronized ResourceBundle getResources() {
        if (resources == null)
            try {
                resources = ResourceBundle.getBundle(RESOURCES);
            } catch (MissingResourceException x) {
                log(x);
                throw new InternalError(x.getMessage());
            }
        return resources;
    }

    public static String getResource(String key) {
        return getResources().getString(key);
    }
    ...
}
                              


ModelResourcesバンドルには、DAOパラメータ、JDBCベースのDAOに使用されるSQL文、DAOメソッドがスローした例外メッセージが含まれています。

 

                                 
dao=jsfdb.model.dao.TopLinkSubscriberDAO
TopLinkSession=JSFDBSession

# dao=jsfdb.model.dao.JDBCSubscriberDAO
JavaCompEnv=java:comp/env
DataSource=jdbc/OracleDS

SelectStatement=...

InsertStatement=...

UpdateStatement=...

DeleteStatement=...

SubscribeException="Subscription" failed. \
    Please try another email address.
ProfileException=Couln't update your profile. \
    Please contact the Webmaster.
UnsubscribeException="Unsubscription" failed. \
    Please contact the Webmaster.
LoginException="Login" failed. \
    Please contact the Webmaster.
UnknownSubscriberException="Unknown" subscriber. \
    Please subscribe.
IncorrectPasswordException="Incorrect" password. \
    Please try to login again.
                              


例外クラスはすべて ModelExceptionを拡張します。

 

                                 
package jsfdb.model.err;

import jsfdb.model.ModelUtils;

public class ModelException extends Exception {
    public ModelException(String messageKey) {
        super(ModelUtils.getResource(messageKey));
    }
}
                              


各例外クラスには、クラス名を ModelExceptionスーパークラスのコンストラクタに渡すコンストラクタのみが含まれています。

 

                                 
package jsfdb.model.err;

public class LoginException extends ModelException {
    public LoginException() {
        super("LoginException");
    }

}
                              


JDBCSubscriberDAOクラスは、JNDIで DataSourceを取得し、 SubscriberDAOの定義したメソッドを実装します。 この記事のダウンロード・アーカイブで、 JDBCSubscriberDAO.javaのソース・コードを参照してください。

TopLinkSubscriberDAOクラスはまた、 SubscriberDAOインタフェースも実装します。 TopLinkSubscriberDAO()コンストラクタは、サーバー・セッションを取得して、JVM停止時にセッションを閉じる停止フックを追加します。 TopLinkベースのDAOには、 SubscriberDAOインタフェースの実装メソッドに使用されるクライアント・セッションと作業単位を取得するユーティリティ・メソッドがあります。

 

                                 
package jsfdb.model.dao;
...
import oracle.toplink.sessions.UnitOfWork;
import oracle.toplink.threetier.ClientSession;
import oracle.toplink.threetier.Server;
import oracle.toplink.tools.sessionmanagement.SessionManager;

public class TopLinkSubscriberDAO implements SubscriberDAO {
    private Server serverSession;

    public TopLinkSubscriberDAO() {
        SessionManager manager = SessionManager.getManager();
        String id = ModelUtils.getResource("TopLinkSession");
        ClassLoader loader = this.getClass().getClassLoader();
        serverSession = (Server) manager.getSession(id, loader);
        Runtime.getRuntime().addShutdownHook(new Thread() {
            public void run() {
                serverSession.logout();
                SessionManager.getManager().getSessions().remove(
                    ModelUtils.getResource("TopLinkSession"));
            }
        });
    }

    private ClientSession acquireClientSession() {
        return serverSession.acquireClientSession();
    }

    private UnitOfWork acquireUnitOfWork() {
        return acquireClientSession().acquireUnitOfWork();
    }
    ...
}
                              


ビューBeanの作成:先ほど述べたように、2つのビューBean( LoginInfoBeanとSubscriberBean)は、データ検証メソッド(バリデータ)やデータ処理メソッド(アクション)などのUI固有のコードでモデルBean( LoginInfoSubscriber)を拡張します。 LoginInfoBeanクラスには、 loginAction()という単一のアクション・メソッドがあります。 もう1つのビューBean( SubscriberBean)は、追加プロパティ( loggedIn)の定義、@が電子メール・アドレスに表示されているかどうかを検証する emailValidator()メソッドの実装、各種アクション・メソッドの提供をおこない、さらにELを使用して Subscriberからの継承定数を読取り専用プロパティとして公開し、JSFページでのアクセスを可能にします。

 

                                 
package jsfdb.view;

import jsfdb.model.Subscriber;
...
public class SubscriberBean extends Subscriber {
    ...
    private transient boolean loggedIn = false;

    public boolean isLoggedIn() {
        return loggedIn;
    }

    public void setLoggedIn(boolean loggedIn) {
        this.loggedIn = loggedIn;
    }

    public void emailValidator(FacesContext context,
            UIComponent comp, Object value) {
        ...
    }

    public String subscribeAction() {
        ...
    }

    public String profileAction() {
        ...
    }

    public String unsubscribeAction() {
        ...
    }

    public String cancelAction() {
        if (!loggedIn)
            return "login";
        else
            return "cancel";
    }

    public int getDailyConst() {
        return TYPE_DAILY;
    }

    public int getWeeklyConst() {
        return TYPE_WEEKLY;
    }

    public int getMonthlyConst() {
        return TYPE_MONTHLY;
    }

}
                              


2つのビューBeanのアクション・メソッドについては、本項以降で詳しく説明します。 具体的には、以下を実行する方法について説明します。

  • loginAction()による行の選択
  • subscribeAction()による新規の行の挿入
  • profileAction()による既存の行の更新
  • unsubscribeAction()による行の削除


次項( JSFビューとデータ検証

  • サンプル・アプリケーションのJSFページの表示
  • JSFによるデータの検証方法
  • UIコンポーネントとBeanプロパティのバインドを作成する方法
  • アクション・メソッドとJSFフォームのSubmitボタンをバインドする方法のデモンストレーション


行の選択(ログイン・アクション) JDBCSubscriberDAOselect()メソッドは、特定の電子メール・アドレスをもつサブスクライバを選択してパスワードを検証するSQL問合せを実行します。 JDBCベースのDAOで使用されるSELECT文は、次のとおりです。

 

SELECT subscriberPassword, 
       subscriberName, 
       managerFlag, 
       developerFlag, 
       administratorFlag, 
       subscriptionType 
    FROM subscribers 
    WHERE subscriberEmail=?


TopLinkではSQL文の作成は不要ですが、必要に応じてSQLを使用できます。 問合せAPIを read()プライベート・メソッドで使用する場合は、SQL問合せがTopLinkフレームワークによって生成されます。このメソッドは、 TopLinkSubscriberDAOの各種パブリック・メソッドから呼び出され、特定の電子メール・アドレスをもつサブスクライバを返す問合せを実行します。

 

                                 
package jsfdb.model.dao;
...
import oracle.toplink.expressions.ExpressionBuilder;
import oracle.toplink.queryframework.ReadObjectQuery;
import oracle.toplink.sessions.Session;

public class TopLinkSubscriberDAO implements SubscriberDAO {
    ...
    private Subscriber read(Session session, String email) {
        ReadObjectQuery query
            = new ReadObjectQuery(Subscriber.class);
        ExpressionBuilder builder = new ExpressionBuilder();
        query.setSelectionCriteria(
            builder.get("email").equal(email));
        return (Subscriber) session.executeQuery(query);
    }
    ...
}
                              


TopLinkSubscriberDAOselect()メソッドは、クライアント・セッションを取得して read()を呼び出します。

 

                                 
package jsfdb.model.dao;
...
public class TopLinkSubscriberDAO implements SubscriberDAO {
    ...
    public Subscriber select(LoginInfo loginInfo)
            throws LoginException,
            UnknownSubscriberException,
            IncorrectPasswordException {
        Subscriber s = null;
        try {
            ClientSession session = acquireClientSession();
            s = read(session, loginInfo.getEmail());
        } catch (Exception x) {
            ModelUtils.log(x);
            throw new LoginException();
        }
        if (s == null)
            throw new UnknownSubscriberException();
        if (!s.getPassword().equals(loginInfo.getPassword()))
            throw new IncorrectPasswordException();
        return s;
    }
    ...
}
                              


LoginInfoBeanクラスには、 loginAction()メソッドが含まれています。このメソッドは、 ViewUtils.eval()を使用して、JSFの管理する SubscriberBeanクラスのインスタンスと adminEmail初期化パラメータの値を取得します。 その後、 loginAction()select()を呼び出し、 ModelUtils.copy()を使用して選択されたサブスクライバのプロパティをJSFの管理するJavaBeanにコピーして、 loggedInフラグを trueに設定し、サブスクライバの電子メールに応じて listまたは profileの結果を返します。

 

                                 
package jsfdb.view;

import jsfdb.model.LoginInfo;
import jsfdb.model.Subscriber;
import jsfdb.model.ModelUtils;
import jsfdb.model.err.LoginException;
import jsfdb.model.err.IncorrectPasswordException;
import jsfdb.model.err.UnknownSubscriberException;

public class LoginInfoBean extends LoginInfo {
    public String loginAction() {
        SubscriberBean subscriber
            = (SubscriberBean) ViewUtils.eval("#{subscriber}");
        String adminEmail
            = (String) ViewUtils.eval("#{initParam.adminEmail}");
        try {
            Subscriber selectedSubscriber
                = ModelUtils.getSubscriberDAO().select(this);
            ModelUtils.copy(selectedSubscriber, subscriber);
            subscriber.setLoggedIn(true);
            if (subscriber.getEmail().equals(adminEmail))
                return "list";
            else
                return "profile";
        } catch (LoginException x) {
            ViewUtils.addExceptionMessage(x);
            return null;
        } catch (UnknownSubscriberException x) {
            ViewUtils.addExceptionMessage(x);
            return null;
        } catch (IncorrectPasswordException x) {
            ViewUtils.addExceptionMessage(x);
            return null;
        }
    }

}
                              


ViewUtilsクラスは、JSF式を評価してエラー・メッセージをJSFコンテキストに追加するユーティリティ・メソッドを提供します。

 

                                 
package jsfdb.view;

import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;

import java.util.ResourceBundle;

public class ViewUtils {
    public static Object eval(String expr) {
        FacesContext context
            = FacesContext.getCurrentInstance();
        ValueBinding binding
            = context.getApplication().createValueBinding(expr);
        return binding.getValue(context);
    }

    public static void addErrorMessage(FacesContext context,
            String compId, String messageId) {
        ResourceBundle bundle = ResourceBundle.getBundle(
            context.getApplication().getMessageBundle());
        FacesMessage message = new FacesMessage(
            bundle.getString(messageId));
        message.setSeverity(FacesMessage.SEVERITY_ERROR);
        context.addMessage(compId, message);
    }

    public static void addExceptionMessage(Exception x) {
        FacesContext context
            = FacesContext.getCurrentInstance();
        FacesMessage message
            = new FacesMessage(x.getMessage());
        message.setSeverity(FacesMessage.SEVERITY_FATAL);
        context.addMessage(null, message);
    }

}
                              


ModelUtilscopy()メソッドは、JavaBeans Introspection APIとJava Reflection APIを使用して、ソースBeanのプロパティを取得し、別のBeanのプロパティを設定します。

 

                                 
package jsfdb.model;
...
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.IntrospectionException;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
...
public class ModelUtils {
    ...
    public static void copy(Object source, Object dest) {
        try {
            Class sourceClass = source.getClass();
            Class destClass = dest.getClass();
            BeanInfo info = Introspector.getBeanInfo(sourceClass);
            PropertyDescriptor props[]
                = info.getPropertyDescriptors();
            Object noParams[] = new Object[0];
            Object oneParam[] = new Object[1];
            for (int i = 0; i < props.length; i++) {
                Method getter = props[i].getReadMethod();
                if (getter == null)
                    continue;
                Object value = getter.invoke(source, noParams);
                Method setter = props[i].getWriteMethod();
                if (setter != null && sourceClass != destClass)
                    try {
                        setter = destClass.getMethod(
                            setter.getName(),
                            setter.getParameterTypes());
                    } catch (NoSuchMethodException x) {
                        setter = null;
                    }
                if (setter != null) {
                    oneParam[0] = value;
                    setter.invoke(dest, oneParam);
                }
            }
        } catch (IntrospectionException x) {
            log(x);
            throw new InternalError(x.getMessage());
        } catch (IllegalAccessException x) {
            log(x);
            throw new InternalError(x.getMessage());
        } catch (IllegalArgumentException x) {
            log(x);
            throw new InternalError(x.getMessage());
        } catch (SecurityException x) {
            log(x);
            throw new InternalError(x.getMessage());
        } catch (InvocationTargetException x) {
            log(x.getTargetException());
            throw new InternalError(
                x.getTargetException().getMessage());
        }
    }

}
                              


copy()メソッドは、すべてのプロパティが Stringなどのプリミティブ・オブジェクトまたは不変オブジェクトである場合に機能します。 BeanにサブBeanや集合などの可変オブジェクトが含まれている場合は、それらのプロパティの値のクローンを作成して dest Beanに保存する必要があります。

新しい行の挿入(サブスクライブ・アクション): JDBCSubscriberDAOinsert()メソッドは、データベースに新規のサブスクライバを挿入するSQL文を実行します。

 

INSERT INTO subscribers ( 
       subscriberEmail, 
       subscriberPassword, 
       subscriberName, 
       managerFlag, 
       developerFlag, 
       administratorFlag, 
       subscriptionType ) 
    VALUES (?, ?, ?, ?, ?, ?, ?)


TopLinkでは、O/RマッピングからSQL文が自動生成されます。O/RマッピングはMapping Workbenchツールを使用して定義する必要があります。 TopLinkSubscriberDAOinsert()メソッドは、作業単位の取得、 Subscriberインスタンスの新規作成、プロパティの設定、新規オブジェクトの登録、および commit()の呼出しを実行します。

 

                                 
package jsfdb.model.dao;
...
public class TopLinkSubscriberDAO implements SubscriberDAO {
    ...
    public void insert(Subscriber subscriber)
            throws SubscribeException {
        try {
            UnitOfWork uow = acquireUnitOfWork();
            Subscriber s = new Subscriber();
            ModelUtils.copy(subscriber, s);
            uow.registerObject(s);
            uow.commit();
        } catch (Exception x) {
            ModelUtils.log(x);
            throw new SubscribeException();
        }
    }
    ...
}
                              


SubscriberBeansubscribeAction()メソッドは、 insert()を呼び出して loggedInフラグを trueに設定し、エラーが発生していない場合は subscribedの結果を返します。

 

                                 
package jsfdb.view;
...
public class SubscriberBean extends Subscriber {
    ...
    public final static String SELECT_NEWSLETTER_ID
        = "jsfdb.view.SubscriberBean.SELECT_NEWSLETTER";
    ...
    public String subscribeAction() {
        if (countNewsletters() == 0) {
            ViewUtils.addErrorMessage(
                FacesContext.getCurrentInstance(),
                null, SELECT_NEWSLETTER_ID);
            return null;
        }
        try {
            ModelUtils.getSubscriberDAO().insert(this);
            setLoggedIn(true);
            return "subscribed";
        } catch (SubscribeException x) {
            ViewUtils.addExceptionMessage(x);
            return null;
        }
    }
    ...
}
                              


既存の行の更新(プロファイル・アクション): JDBCSubscriberDAOupdate()メソッドは、既存のサブスクライバのプロファイルを更新するSQL文を実行します。

 

UPDATE subscribers SET 
       subscriberPassword="?," 
       subscriberName="?," 
       managerFlag="?," 
       developerFlag="?," 
       administratorFlag="?," 
       subscriptionType="?" 
    WHERE subscriberEmail=?


TopLinkSubscriberDAOupdate()メソッドは、作業単位の取得、 read()プライベート・メソッドの呼出しによる特定の電子メールをもつBeanの取得、Beanプロパティの更新、 commit()の呼出しを実行します。

 

                                 
package jsfdb.model.dao;
...
public class TopLinkSubscriberDAO implements SubscriberDAO {
    ...
    public void update(Subscriber subscriber)
            throws ProfileException {
        try {
            UnitOfWork uow = acquireUnitOfWork();
            Subscriber s = read(uow, subscriber.getEmail());
            ModelUtils.copy(subscriber, s);
            uow.commit();
        } catch (Exception x) {
            ModelUtils.log(x);
            throw new ProfileException();
        }
    }
    ...
}
                              


SubscriberBeanprofileAction()メソッドは、 update()を呼び出して nullの結果を返します。

 

                                 
package jsfdb.view;
...
public class SubscriberBean extends Subscriber {
    ...
    public final static String SELECT_NEWSLETTER_ID
        = "jsfdb.view.SubscriberBean.SELECT_NEWSLETTER";
    ...
    public String profileAction() {
        if (!loggedIn)
            return "login";
        if (countNewsletters() == 0) {
            ViewUtils.addErrorMessage(
                FacesContext.getCurrentInstance(),
                null, SELECT_NEWSLETTER_ID);
            return null;
        }
        try {
            ModelUtils.getSubscriberDAO().update(this);
            return null;
        } catch (ProfileException x) {
            ViewUtils.addExceptionMessage(x);
            return null;
        }
    }
    ...
}
                              


行の削除(サブスクライブ解除アクション): JDBCSubscriberDAOdelete()メソッドは、データベースからサブスクライバを削除するSQL文を実行します。

 

DELETE FROM subscribers WHERE subscriberEmail=?


TopLinkSubscriberDAOdelete()メソッドは、作業単位の取得、 read()プライベート・メソッドの呼出しによる特定の電子メールをもつBeanの取得、Beanの削除、 commit()の呼出しを実行します。

 

                                 
package jsfdb.model.dao;
...
public class TopLinkSubscriberDAO implements SubscriberDAO {
    ...
    public void delete(Subscriber subscriber)
            throws UnsubscribeException {
        try {
            UnitOfWork uow = acquireUnitOfWork();
            Subscriber s = read(uow, subscriber.getEmail());
            uow.deleteObject(s);
            uow.commit();
        } catch (Exception x) {
            ModelUtils.log(x);
            throw new UnsubscribeException();
        }
    }

}
                              


SubscriberBeanunsubscribeAction()メソッドは、 delete()を呼び出して、エラーが発生しない場合は unsubscribedという結果を返します。

 

                                 
package jsfdb.view;
...
public class SubscriberBean extends Subscriber {
    ...
    public String unsubscribeAction() {
        if (!loggedIn)
            return "login";
        try {
            ModelUtils.getSubscriberDAO().delete(this);
            return "unsubscribed";
        } catch (UnsubscribeException x) {
            ViewUtils.addExceptionMessage(x);
            return null;
        }
    }
    ...
}
                              


TopLinkプロジェクトの作成: Mapping Workbenchを使用してTopLinkプロジェクトを作成する手順は、次のとおりです。

手順1: OracleAS TopLink Mapping Workbenchを起動します。 次に、メイン・メニューで、「 File」→「 New Project...」を選択します。 Create New Projectダイアログで、既存のデータベースの名前( orclなど)を入力して、「 OK」をクリックします。

手順2: Mapping Workbenchに、プロジェクトを保存するウィンドウが表示されます。 ディレクトリを選択し、ファイル名( jsfdbなど)を入力して、「 Save」をクリックします。

手順3: 左側のNavigatorペインでデータベースを選択し、「 Add...」をクリックしてログイン・ラベル(orclLoginなど)を入力し、「 OK」をクリックします。

手順4: メイン・ウィンドウで、新しく作成されたログインを選択し、JDBCドライバ・クラス名、データベースのURL、ユーザー名、パスワードを入力します。 かならず「 Save Password」チェック・ボックスを選択してください。



手順5: Navigatorペインのデータベース名を右クリックし、「 Log In to Database」をクリックします。

手順6: Navigatorペインのデータベース名を右クリックし、「 Add or Update Existing Tables from Database」をクリックします。

手順7: Import Tables from DatabaseウィンドウのTable Name Patternフィールドに SUBSCRIBERSを入力します。次に、「 Get Table Names」をクリックして左側のAvailable Tablesで「 SUBSCRIBERS」を選択し、右側のSelected Tablesペインに追加して「 OK」をクリックします。



手順8: Navigatorペインでプロジェクト(jsfdb)を選択し、右ペインの現在のタブがGeneralであることを確認して、「 Add Entries...」をクリックします。表示されたディレクトリ・ツリーで、Webアプリケーションのコンパイル済みのクラスがあるディレクトリを選択し、「 OK」をクリックします。 選択したディレクトリは、メイン・ウィンドウのClass Pathリストに追加されます。

手順9: Navigatorペインのプロジェクトを右クリックし、「 Add or Refresh Classes...」をクリックします。

手順10: Select Classesウィンドウの左側のAvailable Packages/Classesペインで、「 LoginInfo 」と「 Subscriber 」クラスを選択します。

手順11: LoginInfo 」と「 Subscriber 」クラスを右側のSelected Classesペインに移動して、「 OK」をクリックします。 Subscriberクラスのみが表にマッピングされますが、 Subscriberのスーパークラスである LoginInfoも追加する必要があります。



手順12: メイン・ウィンドウのNavigatorペインで、「 Subscriber」クラスを選択します。 右側のEditorペインで、現在のタブがDescriptorであることを確認して、Associated Tableリストで「 SUBSCRIBERS 」を選択します。



手順13: Navigatorペインの「 Subscriber 」クラスを右クリックし、「 Map Inherited Attributes」を選択して「 To Superclass」をクリックします。

手順14: Subscriberクラスの各Beanプロパティ( administratorなど)を右クリックし、「 Map As...」を選択して「 Direct to Field」をクリックします。

手順15: Navigatorペインで Subscriberクラスの各Beanプロパティ( emailなど)を選択し、右側のEditorペインで対応するDatabase Field( SUBSCRIBEREMAILなど)を設定します。



手順16: LoginInfoクラスは表にマッピングされないため、プロジェクトから削除する必要があります。 Navigatorペインの「 LoginInfo 」を右クリックし、「 Remove Class」をクリックします。 Remove Descriptorsダイアログが表示されたら、「 Remove」をクリックします。

手順17: メイン・メニューで、「 File」→「 Export」→「 Project Deployment XML...」の順に選択するか、または[Ctrl]+[D]を押します。 JSFDBProject.xmlと入力し、「 OK」をクリックします。 次に、XMLファイルを保存するディレクトリを選択します。

 

 

手順18: File」→「 Save」を選択するか、または[Ctrl]+[S]を押して、プロジェクトを保存します。 jsfdb.mwpファイルにプロジェクト設定が保存され、 class, descriptortableサブディレクトリに各種XMLファイルが作成されます。 これで、OracleAS TopLink Mapping Workbenchを終了できます。

TopLinkセッションの構成:Sessions Editorを使用してTopLinkセッションを構成する手順は、次のとおりです。

手順1: OracleAS TopLink Sessions Editorを起動します。 メイン・メニューで、「 File」→「 New...」を選択します(または、[Ctrl]+[N]を押します)。 Newダイアログで、 sessions.xmlファイルの名前は変更せずに「 Browse」ボタンをクリックしてこのファイルのロケーションを選択します。次に、Session Nameフィールドに JSFDBSessionと入力して「 OK」をクリックします。

手順2: Navigatorペインで「 JSFDBSession 」を選択し、Editorペインの現在のタブがGeneralであることを確認して、Project TypeグループのXMLフィールドに JSFDBProject.xmlと入力します。

手順3: Editorペインの「 Logging」タブを選択し、Enable Loggingリストで「 True」を選択します。

手順4: Editorペインの「 Login」タブを選択し、「 Database Platform」チェック・ボックスを選択します。次に、対応するリストで「 Oracle」を選択し、Data Sourceフィールドに java:comp/env/jdbc/OracleDSと入力します。

手順5: メイン・メニューで「 File」→「 Save」を選択して(または、[Ctrl]+[S]を押して) sessions.xmlファイルを保存します。 OracleAS TopLink Sessions Editorを閉じます。

JSFビューとデータ検証

サンプルWebアプリケーションには、サブスクリプション・ページ( subscribe.jsp)、ログイン・ページ( login.jsp)、プロファイル編集ページ( profile.jsp)、サブスクリプション解除ページ( unsubscribe.jsp)、2つの確認ページ( subscribed.jspunsubscribed.jsp)、サブスクライバの一覧ページ( list.jsp)、およびログアウト・ページ( logout.jsp)が含まれています。 JSFページで作成されたフォームには、前項のDAOメソッドを使用して、基本的なデータベース操作( INSERT、SELECT、UPDATE、 DELETE)の実行アクションをトリガーするものもあります。

WebページでのJSFの使用:JSFは、2つの標準タグ・ライブラリ(CoreとHTML)を定義します。この2つのタグ・ライブラリは、 <%@taglib%>ディレクティブを使用してJSPページで宣言する必要があります。 JSF Coreライブラリにはマークアップ言語に依存しないタグが含まれており、JSF HTMLライブラリはWebブラウザで表示するページ向けに設計されています。 この2つのタグ・ライブラリの標準接頭辞は、JSF Coreが"f"でJSF HTMLが"h"となります。 JSFタグはすべて、 <f:view>要素内にネストする必要があります。 <f:view>タグを使用すれば、JSFフレームワークでUIコンポーネントの状態をHTTPリクエストのレスポンスの一部として保存できます。

サンプルWebアプリケーションのページでは、JSFとJSTLのCoreライブラリを使用してHTMLヘッダーを作成しています。

 

                                 
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<%@ taglib prefix="f" uri="http://java.sun.com/jsf/core" %>
<%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %>

<f:view>

<f:loadBundle var="labels" basename="jsfdb.view.res.Labels"/>
<c:set var="stylesheet"
    value="${pageContext.request.contextPath}/stylesheet.css"/>

<html>
<head>
    <title><h:outputText value="#{labels.subscribe}"/></title>
    <link rel="stylesheet" type="text/css"
        href="<c:out value='${stylesheet}'/>">
</head>
<body>

    ...

</body>
</html>

</f:view>
                              


JSFの <h:outputText>タグは、JSTLの <c:out>タグに非常に似ていますが、いくつかの相違点があります。 たとえば、 <h:outputText>ではJSF ELを使用しますが、 <c:out>では当初JSTL 1.0用に作成され、その後JSP 2.0に採用された式言語を使用します。 上記のコード断片では、 <f:loadBundle>タグにより、全JSFページのラベルとタイトルを含むリソース・バンドルがロードされます。

 

subscribe=Subscribe
subscribed=Subscribed
login=Login
logout=Logout
profile=Profile
update=Update
unsubscribe=Unsubscribe
unsubscribed=Unsubscribed
cancel=Cancel
email=Email
password=Password
passwordDetail="Useful" to change your profile
name=Name
newsletters=Newsletters
manager=Manager
developer=Developer
administrator=Administrator
subscriptionType="Subscription" Type
daily=Daily
weekly=Weekly
monthly=Monthly
list=List


JSFタグを使用したフォームの作成:アプリケーションのWebページの多くは、JSFを使用してフォームを作成しています。 たとえば、 login.jspでは、 <h:inputText><h:inputSecret>がそれぞれ textタイプと passwordタイプの2つのHTML <input>要素をレンダリングします。 各フォーム・フィールドには、 <h:outputLabel>でレンダリングされたラベルがあります。 エラー・メッセージは、 <h:message><h:messages>でレンダリングされます。 <f:validateLength>タグは、フォーム・フィールドに入力された文字列の長さを検証します。 <h:commandButton>は、HTMLのSubmitボタンをレンダリングします。 これらのタグはすべて、HTMLの <form>タグと </form>タグをレンダリングする <h:form>要素内に配置されています。 HTMLフォームに加え、 login.jspページにもサブスクリプション・フォームへのリンクがあり、 <h:outputLink><h:outputText>によりHTMLの <a>要素とそのコンテンツがレンダリングされます。

 

                                 
<!-- login.jsp -->

<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<%@ taglib prefix="f" uri="http://java.sun.com/jsf/core" %>
<%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %>

<c:remove var="subscriber" scope="session"/>

<f:view>
    ...
    <h1><h:outputText value="#{labels.login}"/></h1>

    <h:outputLink value="subscribe.faces">
        <h:outputText value="#{labels.subscribe}"/>
    </h:outputLink>

    <h:form id="login">

        <h:messages globalOnly="true" styleClass="message"/>

        <p><h:outputLabel for="email"
            value="#{labels.email}"/>
        <h:message for="email" styleClass="message"/><br>
        <h:inputText id="email" required="true"
                value="#{loginInfo.email}"
                size="40" maxlength="80">
            <f:validateLength minimum="1" maximum="80"/>
        </h:inputText>

        <p><h:outputLabel for="password"
            value="#{labels.password}"/>
        <h:message for="password" styleClass="message"/><br>
        <h:inputSecret id="password" required="true"
                value="#{loginInfo.password}"
                size="10" maxlength="20">
            <f:validateLength minimum="6" maximum="20"/>
        </h:inputSecret>

        <p><h:commandButton id="command"
            value="#{labels.login}"
            action="#{loginInfo.loginAction}"/>

    </h:form>
    ...
</f:view>
                              


login.jspページを実行すると、JSFにより LoginInfoBeanのインスタンスが作成され、プロパティがUIコンポーネントにバインドされます。 Loginボタンをクリックすると、Webブラウザによりユーザーの電子メール・アドレスとパスワードがサーバーに送信されます。 さらにJSFフレームワークにより、両方の必須フィールドに値が入力されたかどうかが検証されます。 文字列値の長さが <f:validateLength>による検証をパスすると、JSFによりBeanプロパティが設定され、ユーザーの認証をおこなう loginAction()メソッドが呼び出されます。

login.jspでは、JSTLの <c:remove>タグにより sessionスコープから subscriber Beanが削除されます。 ユーザーがWebアプリケーションに初めてアクセスした場合や、前回のセッションの有効期限が切れている場合は、そのようなBeanインスタンスは存在しないため、 <c:remove>は無効になります。 それ以外の場合は、このタグによりセッション・スコープがクリーンアップされます。 subscriber Beanは loginAction()が評価するEL式で参照されているため、 SubscriberBeanのインスタンスは faces-config.xmlの指定に従って作成されます。 loginAction()メソッドは、データベースからサブスクライバのプロファイルを読み出して、Beanのプロパティを設定します。 subscriber BeanはほかのJSFページで使用され、 logout.jspで削除されるまで sessionスコープに保存されます。

 

<!-- logout.jsp -->
...
<f:view>
    ...
</f:view>

<c:remove var="subscriber" scope="session"/>


subscribe.jspページと profile.jspページでは、JSFによりチェック・ボックスやリストなどの追加フォーム要素がレンダリングされます。 JSFの <h:panelGrid>タグと <h:panelGroup>タグは、セルにサブスクリプション・フォームの3つのチェック・ボックスがあるHTML表のレンダリングに使用されます。 <h:selectBooleanCheckbox>タグは、 checkboxタイプの <input>要素をレンダリングします。

 

                                 
<!-- subscribe.jsp -->
...
<f:view>
    ...
    <h:form id="subscribe">
        ...
        <p><h:outputText value="#{labels.newsletters}"/>
        <h:message for="newsletters" styleClass="message"/><br>
        <h:panelGrid id="newsletters"
                columns="3" border="0" cellspacing="5">
            <h:panelGroup>
                <h:selectBooleanCheckbox id="manager"
                    value="#{subscriber.manager}"/>
                <h:outputLabel for="manager"
                    value="#{labels.manager}"/>
            </h:panelGroup>
            <h:panelGroup>
                <h:selectBooleanCheckbox id="developer"
                    value="#{subscriber.developer}"/>
                <h:outputLabel for="developer"
                    value="#{labels.developer}"/>
            </h:panelGroup>
            <h:panelGroup>
                <h:selectBooleanCheckbox id="administrator"
                    value="#{subscriber.administrator}"/>
                <h:outputLabel for="administrator"
                    value="#{labels.administrator}"/>
            </h:panelGroup>
        </h:panelGrid>
        ...
    </h:form>
    ...
</f:view>
                              


<h:selectOneMenu>タグと <f:selectItem>タグは、ユーザーがサブスクリプション・タイプ(日次、週次、月次)を選択するドロップダウン・リストをレンダリングします。

 

                                 
<!-- subscribe.jsp -->
...
<f:view>
    ...
    <h:form id="subscribe">
        ...
        <p><h:outputLabel for="subscriptionType"
            value="#{labels.subscriptionType}"/>
        <h:message for="subscriptionType"
            styleClass="message"/><br>
        <h:selectOneMenu id="subscriptionType"
                value="#{subscriber.subscriptionType}"
                required="true">
            <f:validateLongRange minimum="1" maximum="3"/>
            <f:selectItem itemLabel="#{labels.daily}"
                itemValue="#{subscriber.dailyConst}"/>
            <f:selectItem itemLabel="#{labels.weekly}"
                itemValue="#{subscriber.weeklyConst}"/>
            <f:selectItem itemLabel="#{labels.monthly}"
                itemValue="#{subscriber.monthlyConst}"/>
        </h:selectOneMenu>
        ...
    </h:form>
    ...
</f:view>
                              


subscribeAction()メソッド( subscribe.jspで使用)は、データベースに新しいサブスクライバのプロファイルを保存します。 profile.jspには subscriber Beanで管理する設定が表示され、ユーザーが各自のプロファイルを変更できます( profileAction()メソッドは、データベースに保存された情報を更新します)。

unsubscribe.jspでレンダリングされたフォームには、別々のアクション・メソッドにバインドされたSubmitボタンが含まれています。

 

                                 
<!-- unsubscribe.jsp -->
...
<f:view>
    ...
    <h:form id="unsubscribe">
        ...
        <p><h:commandButton id="command"
            value="#{labels.unsubscribe}"
            action="#{subscriber.unsubscribeAction}"/>
        <h:commandButton id="cancel"
            value="#{labels.cancel}"
            action="#{subscriber.cancelAction}"/>

    </h:form>
    ...
</f:view>
                              


unsubscribeAction()メソッドは、データベースからサブスクライバのプロファイルを削除します。 すべてのアクション・メソッドのコードについては、前項(JavaBeansとデータ・アクセス・オブジェクト)で説明しています。

標準バリデータとカスタム・バリデータの使用:JSFには、サンプル・アプリケーションのJSFページで使用されている <f:validateLength><f:validateLongRange>などのバリデータ・タグがあります。 また、HTMLタグの多くが required属性をサポートしています。この属性を使用して、ユーザーにUIコンポーネントへ常に値を提供させるかどうかを指定できます。 ただし、多くの場合、アプリケーション固有の検証が必要です。 カスタム検証タグの作成も可能ですが、多くの標準JSFタグがサポートする validator属性を使用するほうが簡単です。 この属性を使用して、ユーザーが入力した値の検証メソッドを指定できます。

 

                                 
<!-- subscribe.jsp -->
...
<f:view>
    ...
    <h:form id="subscribe">
        ...
        <p><h:outputLabel for="email"
            value="#{labels.email}"/>
        <h:message for="email" styleClass="message"/><br>
        <h:inputText id="email" required="true"
                validator="#{subscriber.emailValidator}"
                value="#{subscriber.email}"
                size="40" maxlength="80">
            <f:validateLength minimum="1" maximum="80"/>
        </h:inputText>
        ...
    </h:form>
    ...
</f:view>
                              


SubscriberBeanemailValidator()メソッドは、電子メール・アドレスに@が含まれているかどうかを検証します。

 

                                 
package jsfdb.view;
...
import javax.faces.component.UIComponent;
import javax.faces.component.EditableValueHolder;
import javax.faces.context.FacesContext;

public class SubscriberBean extends Subscriber {
    public final static String INVALID_EMAIL_ID
        = "jsfdb.view.SubscriberBean.INVALID_EMAIL";
    ...
    public void emailValidator(FacesContext context,
            UIComponent comp, Object value) {
        String email = (String) value;
        if (email.indexOf("@") == -1) {
            String compId = comp.getClientId(context);
            ViewUtils.addErrorMessage(
                context, compId, INVALID_EMAIL_ID);
            ((EditableValueHolder) comp).setValid(false);
        }
    }
    ...
}
                              


エラー・メッセージは、 <h:message/>によりJSFページでレンダリングされ、アプリケーションのメッセージ・バンドルから取得されます。

 

jsfdb.view.SubscriberBean.INVALID_email=invalid
jsfdb.view.SubscriberBean.SELECT_newsletter=\
    You must subscribe to at least one newsletter.
javax.faces.component.UIInput.required=required
javax.faces.validator.LengthValidator.maximum=\
    may contain maximum {0} characters
javax.faces.validator.LengthValidator.minimum=\
    must contain minimum {0} characters


アプリケーション固有のメッセージのほかに、このリソース・バンドルには各種のデフォルトJSFメッセージの置換文字列が含まれています。 たとえば、 javax.faces.component.UIInput.REQUIREDキーのあとには、必要なフォーム要素の値が入力されていない場合にレンダリングされるエラー・メッセージが続きます。 アプリケーションのメッセージ・バンドルは、 faces-config.xmlで構成する必要があります。

 

                                 
<faces-config>

    <application>
        <locale-config>
            <default-locale>en</default-locale>
        </locale-config>
        <message-bundle>jsfdb.view.res.Messages</message-bundle>
    </application>
    ...
 </faces-config>
                              


バリデータ・タグとバリデータ・メソッドは、単一のUIコンポーネントの値を検証します。 ただし、グループ単位でコンポーネントの検証が必要になることもあります。 その場合は、アクション・メソッドを使用して検証できます。 たとえば、サブスクリプション・フォームのチェック・ボックスはいずれも非選択のままにすることができますが、ユーザーは1つ以上のニュースレターをサブスクライブする必要があります。 subscribeAction()メソッドは、 countNewsletters()0を返した場合にエラーを通知します。

 

                                 
package jsfdb.view;
...
public class SubscriberBean extends Subscriber {
    ...
    public final static String SELECT_NEWSLETTER_ID
        = "jsfdb.view.SubscriberBean.SELECT_NEWSLETTER";
    ...
    public String subscribeAction() {
        if (countNewsletters() == 0) {
            ViewUtils.addErrorMessage(
                FacesContext.getCurrentInstance(),
                null, SELECT_NEWSLETTER_ID);
            return null;
        }
        ...
    }
    ...
}
                              


特定のUIコンポーネントに関連づけられていないエラー・メッセージは、 <h:messages globalOnly="true"/>を使用してJSFページでレンダリングできます。

JSFとJSTLのSQLタグの併用:JSF APIは、表の抽象データ・モデル( javax.faces.model.DataModel)を定義して、配列、リスト、JDBC結果セット、JSTLの <sql:query>タグの結果、およびスカラーをラッピングする実装を提供します。 JSFデータ・モデルには行データへのアクセスを実行するメソッドがありますが、行の表示方法は具体的なモデルの実装によって異なります。 配列やリストの場合は、各要素が行になります。 結果セットの場合は、各行は java.util.Mapとして表示され、キーが表の列になります。 行モデルには制限がないため、Web開発者が <h:column>タグを使用してJSFページの列のレンダリング方法を定義する必要があります。また、このタグは <h:dataTable>要素内でネストする必要があります。

サンプル・アプリケーションの web.xmlファイルでは、 <resource-ref>によりデータソースを宣言し、 javax.servlet.jsp.jstl.sql.dataSourceというコンテキスト・パラメータによりデフォルトのJSTLデータソースとして設定します。 さらに別のパラメータ( adminEmail)で、ログインしたサブスクライバの emailプロパティと( list.jsp内で)比較する電子メール・アドレスを指定します。

 

                                 
<web-app>

    <context-param>
        <param-name>adminEmail</param-name>
        <param-value>admin@localhost</param-value>
    </context-param>

    <context-param>
        <param-name>javax.servlet.jsp.jstl.sql.dataSource</param-name>
        <param-value>jdbc/OracleDS</param-value>
    </context-param>
    ...
    <resource-ref>
        <res-ref-name>jdbc/OracleDS</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
    </resource-ref>
    ...
</web-app>
                              


web.xmlで電子メール・アドレスが指定されているユーザーだけが、 list.jspページを実行できます。 そのため、アプリケーションの導入後は、 admin@localhostとしてサブスクライブする必要があります。 adminEmailアドレスでログインすると、 login.jspページによりリクエストが list.jspに転送されます。 サンプル・アプリケーションでは、この認証メカニズムを、 loginAction()などのアクション・メソッドがプログラム可能な条件に応じて異なる結果( listまたは profile)を返す方法を表示するために使用しています。 実際のアプリケーションでは、標準HTTPベースの認証またはフォームベースの認証を使用して管理者のIDが検証されます。

list.jspページでは、JSTLタグによりSQL問合せが実行され、結果セットを保持する subscriberList変数が作成されます。 これが <h:dataTable>に渡されて、結果セットの行が繰り返され、各行のタグ・ボディが起動されます。 各 <h:column>タグには、現在のセルの値をレンダリングする <h:outputText value="#{row...}"/>タグと、表のヘッダーの一部としてレンダリングされるファセットが含まれています。

 

                                 
<!-- list.jsp -->

<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<%@ taglib prefix="sql" uri="http://java.sun.com/jstl/sql" %>
<%@ taglib prefix="f" uri="http://java.sun.com/jsf/core" %>
<%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %>

<c:if test="${subscriber == null || !subscriber.loggedIn}">
    <c:redirect url="/login.faces"/>
</c:if>

<c:if test="${subscriber.email != initParam.adminEmail}">
    <c:redirect url="/profile.faces"/>
</c:if>

<sql:query var="subscriberList" scope="request">
    SELECT * FROM subscribers ORDER BY subscriberEmail
</sql:query>

<f:view>
    ...
    <h:form id="list">

        <h:dataTable id="table" var="row"
                value="#{subscriberList}"
                border="1" cellpadding="5">

            <h:column>
                <f:facet name="header">
                    <h:outputText value="#{labels.email}"/>
                </f:facet>
                <h:outputText value="#{row.subscriberEmail}"/>
            </h:column>

            <h:column>
                <f:facet name="header">
                    <h:outputText value="#{labels.password}"/>
                </f:facet>
                <h:outputText value="#{row.subscriberPassword}"/>
            </h:column>

            ...

        </h:dataTable>

    </h:form>

</body>
</html>

</f:view>
                              


<sql:query>タグでは、 list.jspページの requestスコープが使用されます。これは、JSFがJSPの pageスコープをサポートしていないためです。 JSFがサポートするスコープを指定し忘れた場合、JSTLタグではデフォルトのページ・スコープが使用され、2つのタグ・ライブラリのタグが通信不可になります。 したがって、JSFのタグとJSTLのタグを併用する場合は、共通スコープ( request、session、または application)のいずれかをかならず使用するようにしてください。

まとめ

この記事では、DAOパターン、JDBC、SQL、TopLink、およびJSTLを使用してリレーショナル・データベースにアクセスするJSFベースのWebアプリケーションについて説明しました。 ここで説明した手法は実際のアプリケーションの開発で利用できます。また、ここで示した例は、Oracle Database、Oracle Application Server Containers for J2EE、TopLinkのO/Rマッピング・フレームワーク、およびJSFリファレンス実装でテストできます。


Andrei Cioroianu devtools@devsphere.com )は Devsphere の創立者で、Javaフレームワーク、XMLコンサルティング、Web開発サービスを提供しています。Oracle Technology Network、 ONJava JavaWorld Java Developer's Journal で多くの記事を執筆しています。 また、『 Java XML Programmer's Reference 』と『 Professional Java XML 』(いずれもWrox Press刊)の共著者でもあります。