Apache Derby、Apache MyFaces、および Facelets による…

強力なトリオで Java ベースの MVC Web アプリケーションを作成する

この記事では、Apache Derby、Apache MyFaces、および Facelets で JSF (Java™Server™ Faces) アプリケーションを開発する方法を説明します。この記事でダウンロード用に準備されたサンプル・アプリケーションでは MVC (Model-View-Controller) アーキテクチャーを使用して、MyFaces コンポーネントの威力、そして Apache Derby と最新のビュー技術、Facelets による開発の容易さを説明します。

JSF、Facelets、Apache Derby とは

この記事のデモ用 Web アプリケーションで使用する技術は、JSF、Facelets、そしてリレーショナル・データベース Apache Derby に保管されたデータにアクセスするための JDBC™ (Java™ Database Connectivity) の 3 つです。JSF はユーザー・インターフェース (UI) をビルドするための Web アプリケーション・フレームワーク、Facelets は JSF 専用に設計されたプレゼンテーション技術、そして Apache Derby は Java JDBC に 100% 準拠したデータベースです。この 3 つのコンポーネントを組み合わせることで、Java ベースの MVC Web アプリケーション開発に完璧な環境を実現できます。

JSF は当初、MVC Web アプリケーションのコントローラー層とビュー層をより明確に区分するために導入されました。また、JSF はイベントをサーバー・サイドに関連付けたため、イベント処理をクライアント・サイドの JavaScript™ に完全に依存する必要がなくなりました。ですが JSF の第一の貢献は、再利用性と拡張性を向上させる、そのコンポーネント・ベースのモデルにあります。ただし、JSF の使用には 1 つの欠点がありました。それは、デフォルトでレンダリング層に使用する技術が JSP™ (JavaServer Pages™) によって提供されていたという点です。JSP はコンポーネント・ベースのシステムではないため、JSF のモデルが持つ機能をすべて活用できません。JSF アプリケーションの JSP タグは、ビューをレンダリングしてコンポーネントを表示しますが、JSF コンポーネントの状態を変更することはできないのです。

Facelets

そこで登場するのが Facelets です。特に JSF のコンポーネント・ベースの技術を考慮して設計された Facelets は、独自のコンポーネント・ツリーを作成して Web アプリケーションのビューで使用します。コンパイルされてサーブレットを作成する JSP は動的コンテンツのレンダリングに使用されますが、このコンテンツは常に JSF が作成するコンポーネント・ツリーと同期しているわけではありません。Facelets は JSF コンポーネント・ツリーと連動して一体化するため、レンダリングされた出力には、JSF で JSP を使ってレンダリングするときに見られるような不測の事態が発生することがありません。

この記事で説明するサンプル・アプリケーション (記事の終わりにある「ダウンロード」セクションで入手可能) では、Facelets のテンプレート作成機能を利用し、Faceletsで開発した場合に改善できるエラー・メッセージの例を示します。この記事では触れていませんが、Facelets には他にも多くの機能があります (これらの機能の詳細については、記事の終わりにある「参考文献」セクションを参照してください)。

Apache MyFaces を使用した JSF

Apache の MyFaces プロジェクトは、JSF の Web アプリケーション・フレームワーク仕様 JSR 127 のオープン・ソース実装を提供します (リンクについては、「参考文献」を参照)。MyFaces には、この仕様に必要なすべてのクラスが用意されている他、Tomahawk という JSF コンポーネントが追加されています。これらのコンポーネントには、仕様の要件以上の新規機能を提供するものや、機能を拡張するものがあります。

この記事を最大限活用するには、JSF の概念を十分理解していなければなりません。「参考文献」セクションにリストした Rick Hightower による優れた連載は最適な入門編になります。

Apache Derby

このサンプル・アプリケーションは架空のフライト予約システムで、モデル層では Apache Derby を使用しています。Apache Derby は管理が一切必要ない完全な Java リレーショナル・データベースです。組み込み可能な機能を持ち、JDBC 標準に準拠していることから、Java ベースの Web アプリケーション開発には申し分のない組み合わせとなります。

この記事の焦点は、MyFaces、Facelets、そして Derby を同時に使用して Web アプリケーションをビルドする方法です。そのため、Web アプリケーション開発、JSF、および JDBC によるデータベース・アクセスについての基本知識があることを前提としています。


Web アプリケーションのコンポーネント

フライト予約アプリケーションで使用するソフトウェア・コンポーネントと技術を以下にリストします。このリストには、このアプリケーションで使用している特定の機能も記載しています。

  • Apache MyFaces JSF Implementation 1.1.4 Core および Tomahawk 1.1.3
    • バリデーター -- 正規表現、Equals (文字列比較)、クレジット・カード検証が含まれます。
    • updateActionListener -- このリスナーを ActionSource UIComponent (リンクまたはボタン) と関連付けることによって、値をプロパティーに関連付けることができます。
    • 拡張 DataTable -- 標準 JSF データ・テーブルをヘッダーで拡張し、列を基準としたソートを可能にします。
    • JavaScript メニュー -- JSCookMenu コンポーネントが CSS および JavaScript を使用してメニュー項目を作成し、動的メニューを作成します。
  • Apache Derby データベース・エンジン 10.1.3.1
    • Derby の EmbeddedDataSource を使用します。
    • ServletContextListener によって Derby を起動および停止します。
    • JDBC Callable Statement でストアード・プロシージャーを実行して、SQL ステートメントを Derby のメッセージ・ログ・ファイルに書き込みます。
    • JDBC Prepared Statement で Derby のレコードを挿入および削除します。
  • Facelets -- JSF View Definition Framework 1.1.11
    • テンプレート作成 -- ページ・コードを再利用および置換するためのテンプレートを作成する機能です。
    • 改善されたエラー・メッセージ -- デバッグを容易にします。
  • Apache Tomcat サーブレット・エンジン 5.0.28
    • XHTML (Extensible Hypertext Markup Language) ページ、サーブレットのフィルターとリスナー、および JSF コンポーネントで構成された Web アプリケーションを実行します。

ソフトウェア要件

このセクションに記載しているソフトウェアは無料でダウンロードできます。これらのソフトウェアは、サンプル Web アプリケーションを実行する前にインストールしてください。

  1. 以下のいずれかの Java 開発キット:
    • IBM SDK 1.5 以降
    • Sun JDK 1.5 以降
  2. Apache Tomcat。バージョン 5.0.28 をダウンロードします (「参考文献」を参照)。
  3. Facelets (JSF View Definition Framework)。バージョン 1.1.11 をダウンロードします (「参考文献」を参照)。
  4. tagHandlers-0.9.jar。Facelets で Tomahawk コンポーネント <t:updateActionListener> を使用するために必要なタグ・ライブラリー・クラスが含まれています (「参考文献」を参照)。
  5. サンプル・アプリケーションのソース・コードおよび Web アプリケーションの .zip ファイル:
    • Apache_Derby_MyFaces_Demo.zip をファイル・システムにダウンロードしてください (「ダウンロード」セクションを参照)。このファイルには、src ファイルと Web アプリケーション・ファイルがすべて含まれています。上記でダウンロードした追加ライブラリーもアプリケーションを実行するために必要です。

ソフトウェアのインストール

必要なすべてのコンポーネントをダウンロードしたら、以下の手順に従ってインストールしてください (まだインストールしていない場合)。構成および各コンポーネントについての詳細は、後で説明します。

  1. JDK をインストールします。バージョン 1.5 以降の JDK がない場合、インストールします。JDK は Tomcat を実行するために必要です。
  2. Apache Tomcat をインストールします。Apache Tomcat を解凍またはインストールします。
  3. facelets-1.1.11.zip をディレクトリーに解凍します。後で、Facelets JAR ファイルのうちの 3 つを WEB-INF/lib ディレクトリーにコピーすることになります。
  4. Apache_Derby_MyFaces_Demo.zip を解凍します。解凍すると、最上位ディレクトリー Flight_Reservation と以下のサブディレクトリーが作成されます。
    • src -- アプリケーションで使用されるすべての Java ソース・ファイルのパッケージです。
    • org.apache.derby.demo.beans.model および org.apache.derby.demo.beans.view -- 基礎となるモデル (データベース・テーブル) を表す JSF 管理対象 Bean クラスとその他の Java Bean クラスです。
      • org.apache.derby.demo.filters -- javax.Servlet.Filter インターフェースを実装する LoginFilter クラスです。
      • org.apache.derby.demo.persistence -- DataFactory ServletContextListener、DatasourceObject インターフェース、および DatasourceObject インターフェースを実装する DerbyDatabase クラスです。
      • org.apache.derby.demo.resource -- ErrorMessages クラスはリソース・バンドル (プロパティー・ファイル) を介してアプリケーションがエラー・メッセージを使用できるようにします。
      • org.apache.derby.demo.validators -- ForwardDates はカスタム JSF バリデーター・クラスで、フライトの予約日が当日または後日であることを確実にします。
  5. Derby_MyFaces -- Web アプリケーションのすべてのライブラリー (上記でダウンロードしたものを除く)、クラス、構成ファイル、および XHTML ファイルが含まれるディレクトリーです。
  6. Licenses -- Web アプリケーションに含まれるサード・パーティーのライブラリー用です。

アプリケーションの詳細を掘り下げる前に、全体がどのように組み合わされているのかを理解できるようにアーキテクチャーについて簡単に説明します。まず、このフライト予約アプリケーション・デモは、記事「Build Web applications with Eclipse, WTP, and Derby」(developerWorks、2005 年 9 月) のために以前作成したアプリケーションに基づいています。ただしアーキテクチャーは変更して、コントローラーとビュー層にはサーブレットと JSP ではなく JSF と Facelets を使用しています。また、Tomcat がデータ・ソースとして Derby を使用するように構成する作業も軽減されています。この記事で用いる JSF および Facelets コンポーネントをベースとした手法は従来の Servlet と JSP ベースの手法に比べ、開発を一層容易にし、完成品も改善することになります。


フライト予約システムのアーキテクチャー

Apache Derby と MyFaces によるフライト予約システムは簡易化されたフライト予約アプリケーションで、ユーザーは以下のことを行えます。

  • アプリケーションへのログイン
  • アカウントの作成
  • 日付と出発地に基づいた予約可能なフライトの表示
  • 出発地と日付に基づいた到着空港の選択
  • クレジット・カード情報の入力によるフライトの予約
  • ユーザーごとの予約フライトの表示
  • アプリケーションからのログアウト

設計と使用する技術

図 1 は、使用する技術とコンポーネントの点から見たフライト予約アプリケーションの設計概要です。この図には、実装固有のファイル名とライブラリーも併せて示しています。

図 1. フライト予約アプリケーションの技術、コンポーネントおよび実装
図 1. フライト予約アプリケーションの技術、コンポーネントおよび実装

ビュー -- Facelets

ビュー層は、Facelets 技術および XHTML Web ページによって実装されます。Faceletsを使用するには、Web アプリケーションにjsf-facelets.jar を組み込む必要があります。JSP の代わりに XHTML をプレゼンテーション層に使うことができるとページを XML ツールで検証できるため、開発プロセスに役立ちます。さらに、JSP 構文よりも XHTML 構文のほうが一般的には簡単です。

コントローラー

コントローラーは、サーブレット・リスナーとサーブレット・フィルターを備えた JSF 層で構成されます。これらのリスナーとフィルターは JSF とは無関係ですが、Web アプリケーションの機能を拡張します。

JSF -- MyFaces

図 1 には、JSF の管理対象 Bean 機能によって制御される Java クラスの一部がリストされています。このアプリケーションでは、Tomahawk バリデーターのいくつかと、カスタム・バリデーター・クラスを使用します。JSF アプリケーションでの常として、ナビゲーションは faces-config.xml ファイルでマッピングされます。XHTML ページで使用されるコンポーネントには、html form、html PanelGroup などの標準 JSF コンポーネントと、inputCalendar (月のなかの 1 日を選択するためのカレンダーを表示) や selectOneListbox などの Tomahawk コンポーネントの両方があります。

Web アプリケーションのライフ・サイクル・イベントの活用

フライト予約システムはサーブレット・コンテナー (この例では Apache Tomcat 5.0) が使用可能な Web アプリケーションのライフ・サイクル・イベントを利用して、データ・ソースと JSF 環境を初期化し、ユーザーにアプリケーションを使用する前にログインするよう要求します。

使用するサーブレット・フィルターは 2 つです。1 つは LoginFilter クラスで、ログイン・ページを介してアプリケーションに対するすべての要求をルーティングします。もう 1 つは MyFaces の ExtensionsFilter で、一部の Tomahawk コンポーネントに必要な外部リソース (画像、JavaScript ファイルなど) をロードするために使用します。

アプリケーションで使用されるサーブレット・コンテキスト・リスナーは、JSF 環境の初期化に使用する MyFaces の StartupServletContextListener、そしてプロパティー・ファイルを読み取って、使用するデータ・ソースを動的に構成するためのDataFactory アプリケーション・クラスです。このアプリケーションでは、javax.sql.DataSource インターフェースの Derby EmbeddedDataSource 実装により、Apache Derby がデータ・ソースとして使用されます。ただし、このアプリケーションの設計には柔軟性があるため、別のデータ・ソースを接続することも可能です。例えば、JDBC でデータベースに直接アクセスする代わりに、Derby で JDO (Java Data Object) 仕様を実装する Java 永続オブジェクト (JPOX) をバックエンドとして使用することもできます。

DataFactory クラスは ServletContextListener インターフェースを実装するため、サーブレット・コンテキストが最初に作成されるときには、このクラスの init メソッドが必ず呼び出されることになります。このクラスの詳細については後で説明しますが、ServletContextListener インターフェースを実装するクラスでデータベース・リソースを初期化すると、Web アプリケーションのライフ・サイクル全体を通して、これらのリソースを確実に使用できるようになります。

モデル

モデル層には、derby.jar という単一の .jar ファイルで構成された Derby データベース・エンジンが組み込まれます。Derby に対する JDBC 呼び出しを行うために使用される POJO クラスは DerbyDatabase と呼ばれ、このクラスが別のアプリケーション・クラスである DatasourceObject インターフェースを実装します。


アプリケーションの実行

Web アプリケーションを Tomcat の webapps ディレクトリーにコピーする

アプリケーションの実行に問題がある場合

  1. 4 つの追加ライブラリー (sf-facelets.jar、el-api.jar、el-ri.jar、tagHandlers-0.9.jar) が %TOMCAT_HOME%/webapps/Derby_MyFaces/WEB-INF/lib ディレクトリーにコピーされていることを確認してください。
  2. Tomcat を停止してから再起動しましたか。
  3. Tomcat のコンソールとログを調べて、トラブルシューティングの参考にしてください。
  4. アプリケーションの最初のページを表示する際にエラー・メッセージ HTTP 500 javax.servlet.ServletException: net/sf/jsfcomp/facelets/taghandlers/ tomahawk/UpdateActionListenerHandler (Unsupported major.minor version 49.0) が表示された場合は、起動した Tomcat で 1.5 JDK が使用されていないことを示します。JDK 1.5 をまだインストールしていない場合はこれをインストールし、JAVA_HOME 環境変数がこのインストールを指定していることを確認してから Tomcat を起動してください。

.zip ファイルを解凍したときに作成された Flight_Reservation ディレクトリーから、Derby_MyFaces ディレクトリー全体を %TOMCAT_HOME%/webapps ディレクトリーにコピーします。

アプリケーションには Apache 1.1 および 2.0 のライセンス交付済み JAR ファイルがすでにバンドルされていますが、ダウンロードした 4 つの JAR ファイルをさらに追加する必要があります。

以下のすべての JAR ファイルを %TOMCAT_HOME%/webapps/Derby_MyFaces/WEB-INF/lib ディレクトリーにコピーしてください。

  • facelets-1.1.11 ディレクトリーにある jsf-facelets.jar
  • facelets-1.1.11/lib ディレクトリーにある el-api.jar および el-ri.jar
  • 「ソフトウェア要件」セクションでダウンロードした場所にある tagHandlers-0.9.jar

これで、アプリケーションを立ち上げて、すべてのソフトウェアが正しくインストールされていること、そしてすべてのファイルが正しい場所にコピーされていることを確認できます。Tomcat を起動して、ブラウザーで URL、http://<hostname>:<port>/Derby_MyFaces/ (ご使用の環境に応じた hostname と port を代入) を開きます。

このアプリケーション・デモでは Tomcat をインストールするときに標準構成を使用したので、私の環境では、この URL は http://localhost:8080/Derby_MyFaces/ となります。

アプリケーションの最初のページを図 2 に示します。この図では、ユーザー名とパスワードの両方に apacheu という値が入力されています。

図 2. フライト予約アプリケーション・デモの最初のページ
図 2. フライト予約アプリケーション・デモの最初のページ

ご使用のシステムでも、ユーザー名とパスワードに apacheu と入力して、Submit ボタンをクリックしてみてください。これによって、Derby データベースへの接続テストが行われます。apacheu という値は USERS テーブルに含まれる 1 つの行を表すため、このテーブルが選択されてユーザー名とパスワードの値が正しいかどうかが検証されます。Web アプリケーションの一部として、airlinesDB という Derby データベースがすでにインストールされています。Derby では、データベースはディレクトリーとしてディスク上に表示されます。構成が正しい場合、アプリケーションの次のページ (図 3) で出発日と出発空港を選択できます。

図 3. 出発日と出発空港を選択する SelectDateAirport
図 3. 出発日と出発空港を選択する SelectDateAirport

アプリケーションを進める前に、モデル層でこれまでに使用されていたクラスについて説明しておきます。


モデル -- Derby データ・ソース

データ・ソースで使用されているアプリケーション・クラスは、以下の 3 つです。

  • DatasourceObject.java
  • DerbyDatabase.java
  • DataFactory.java

: この記事では、データ・ソースという用語はデータのソース全般を指します。javax.sql.DataSource インターフェースを実装するクラス、またはインターフェース自体を指すときには、インターフェース名を使用します。

上記の Java ファイルの詳細を検討するには、Flight_Reservation.zip ファイルを解凍したディレクトリーまでブラウズして、Flight_Reservation/src ディレクトリーの下にある org/apache/derby/demo/persistence ディレクトリー内でファイルを見つけてください。

別のデータ・ソースを使用できるというアプリケーションの設計自体が拡張性のあるアーキテクチャーに役立っていると前に述べましたが、別のデータ・ソースを使用するには、そのデータ・ソースがリスト 1 に示す DatasourceObject インターフェースを実装する必要があります。

以下に示す initialize メソッドはデータ・ソースを初期化して起動し、shutdown メソッドはデータ・ソースを停止またはシャットダウンするためのものです。

リスト 1. DatasourceObject インターフェース
package org.apache.derby.demo.persistence;

public interface DatasourceObject {

  public void initialize(String pathtoResource);

  public void shutdown();

  public boolean checkUserName(String userName);

  public UserBean getUserPassword(String userName);

  public int insertUser(String firstName, String lastName, String userName,
			String email, String password);

  public int insertUserCreditCard(String lastName, String userName,
			String creditCardType, String creditCardNum,
			String creditCardDisplay);

  public CityBean[] destAirports(String origAirport);

  public CityBean[] cityList();

  public FlightsBean[] origDestFlightList(String origAirport,
			String destAirport, Date beginDate);

  public FlightHistoryBean[] fetchFlightHistory(String userName);

  public int insertUserFlightHistory(String userName,
     FlightsBean flightsBean, String creditCardType, String creditCardNum);

}

リスト 2 に、DatasourceObject インターフェースを実装するために必要な DerbyDatabase クラスの initialize メソッドを示します。

リスト 2. DerbyDatabase クラスの initialize メソッド
public class DerbyDatabase implements DatasourceObject {

  private static EmbeddedDataSource ds = null;

  ...

  private static final String databaseName = "airlinesDB";

  public void initialize(String filePathToWebApp) {
    if (isInitialized) {
      return;
    }

    try {
      if (ds == null) {
        ds = new EmbeddedDataSource();
        ds.setDatabaseName(filePathToWebApp + "/" + databaseName);

        // Call this method only during development
	logStatements();
      }
    } catch (Exception except) {
     except.printStackTrace();
    }

    isInitialized = true;
  }
  ...
}

上記の initialize メソッドは、まず org.apache.derby.jdbc.EmbeddedDataSource を作成し、データベース名を Web アプリケーションの完全パスに追加して、使用する javax.sql.DataSource のデータベース名を設定した後、logStatements メソッドを呼び出します。このメソッドについては後で説明します。

それでは何が DerbyDatabase initialize メソッドを呼び出して、実際に DataSource を作成するのでしょうか。ここで登場するのが、Web アプリケーションのライフ・サイクル・イベントです。リスト 3 に出てくる DataFactory クラスは ServletContextListener インターフェースを実装するため、このクラスが contextInitialized メソッドを実装します。Web アプリケーションが最初にロードされるときは、このメソッドが Web コンテナーによって呼び出されます。

contextInitialized メソッドは、DataFactory クラスの init メソッドを呼び出します。次に呼び出される createDatasourceObject メソッドが、プロパティー・ファイルを読み取って datasource-type というストリングを検索し、このタイプのインスタンスを作成します。このアプリケーションでは、プロパティー・ファイルには以下に示す 1 つのエントリーしか含まれませんが、DatasourceObject インターフェースを実装すれば、これ以外の実装も可能です。

datasource-type=org.apache.derby.demo.persistence.DerbyDatabase

DatasourceObject インターフェースの実装が作成されると、initializeDatasourceObject メソッドで示しているように、この実装の initialize メソッドが呼び出されます (リスト 3 を参照) 。これで、リスト 2の DerbyDatabase initialize メソッドに戻るというわけです。

リスト 3.DataFactory クラスによるデータ・ソースの作成
public class DataFactory implements ServletContextListener {

  public static final String DATASOURCEOBJECT_TYPE = "datasource-type";

  private static final String FACTORY_CONFIG = "factory.properties";

  private ServletContext servletContext;

  private static DatasourceObject datasourceObject;
  
  ...

  private synchronized void init() {

    if (isInitialized)
      return;

    createDatasourceObject();

    initializeDatasourceObject(servletContext.getRealPath(""));

    isInitialized = true;
  }

  private void createDatasourceObject() {

    InputStream is = this.getClass().getResourceAsStream(FACTORY_CONFIG);

    prop = new Properties();
    try {
      prop.load(is);
    } catch (IOException exception) {

    throw new RuntimeException(Messages
     .getString("FACTORY_CONFIG_NOT_FOUND"), exception);

    }

    String datasourceType = prop.getProperty(DATASOURCEOBJECT_TYPE);

    if (datasourceType == null)
      throw new RuntimeException(Messages
        .getString("DATASOURCEOBJECT_TYPE_NOT_FOUND"));

      datasourceObject = (DatasourceObject) createInstance(datasourceType,
				DatasourceObject.class);

  } 

  ...

  private void initializeDatasourceObject(String pathToResource) {
    datasourceObject.initialize(pathToResource);
  }
  ...

  public void contextInitialized(ServletContextEvent sce) {
    servletContext = sce.getServletContext();
    System.out.println("contextInitialized called in 
     org.apache.derby.demo.persistence.DataFactory");
    init();
  }

  ...

  public void contextDestroyed(ServletContextEvent sce) {
    datasourceObject.shutdown();
  }
}

ServletContextListener インターフェースの contextInitialized メソッドを実装することによって、DatasourceObject インターフェースの実装 (この場合は、DerbyDatabase クラス) が作成されます。この実装は、Web アプリケーションが最初にロードされるときに初期化され、この Web アプリケーションに対する最初の要求に備えます。

リスト 2 では、DerbyDatabase クラスの initialize メソッドで logStatements メソッドが呼び出されていますが、このメソッドは開発環境で、アプリケーション内で行われたデータベース照会を Derby のメッセージ・ファイル derby.log に記録する場合にのみ使用してください。リスト 4 では、SQL の CALL 構文を使用して SQL の CallableStatement を呼び出す場合を説明するために、logStatements メソッドを含めています。

リスト 4. DerbyDatabase クラスの logStatements および getUserPassword メソッド
/* call this method only during development
   * logs all sql statements to derby.log for debugging purposes
   */

  public void logStatements() {
    String query = "CALL 
     SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.language.logStatementText', 'true')";
    Connection conn;

    try {
      conn = ds.getConnection();
      CallableStatement cs = conn.prepareCall(query);
      cs.execute();
      cs.close();
      conn.close();
    } catch (SQLException sqlExcept) {
        sqlExcept.printStackTrace();
      }
  }

  public UserBean getUserPassword(String userName) {
    String query = "select username, password from APP.USERS where username = ?";
    UserBean userBean = new UserBean();
    Connection conn;

    try {
      conn = ds.getConnection();
      PreparedStatement prepStmt = conn.prepareStatement(query);
      prepStmt.setString(1, userName);
      ResultSet results = prepStmt.executeQuery();

      while (results.next()) {
        String username = results.getString(1);
	String password = results.getString(2);
	userBean.setUserName(username);
	userBean.setPassword(password);
	}

	results.close();
        prepStmt.close();
        conn.close();
    } catch (SQLException sqlExcept) {
        System.out.println("Exception in getUserPassword");
	sqlExcept.printStackTrace();
      }

    return userBean;

  }

  ...
}

リスト 5 は、Deby のシステム・プロシージャー、 SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY が呼び出された場合に derby.log ファイルに含まれる出力のスニペットです。このスニペットで、apacheu という 1 つのパラメーターによって USERS テーブルが選択されていることが分かります。

リスト 5. システム・プロシージャー SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY を呼び出した場合の derby.log の出力
...
2006-08-25 00:04:42.139 GMT Thread[http-8080-Processor24,5,main] (XID = 1439), 
(SESSIONID = 1), 
(DATABASE = C:/jakarta-tomcat-5.0.28/webapps/Derby_MyFaces/airlinesDB), 
(DRDAID = null), 
Begin compiling prepared statement: select username, password from APP.USERS 
where username = ? 
:End prepared statement
2006-08-25 00:04:42.369 GMT Thread[http-8080-Processor24,5,main] (XID = 1439), 
(SESSIONID = 1), 
(DATABASE = C:/jakarta-tomcat-5.0.28/webapps/Derby_MyFaces/airlinesDB), 
(DRDAID = null), 
End compiling prepared statement: select username, password from APP.USERS 
where username = ? 
:End prepared statement
2006-08-25 00:04:42.399 GMT Thread[http-8080-Processor24,5,main] (XID = 1439), 
(SESSIONID = 1), 
(DATABASE = C:/jakarta-tomcat-5.0.28/webapps/Derby_MyFaces/airlinesDB), 
(DRDAID = null), 
Executing prepared statement: select username, password from APP.USERS where username = ? 
:End prepared statement 
with 1 parameters begin parameter #1: apacheu :end parameter 
2006-08-25 00:04:42.439 GMT Thread[http-8080-Processor24,5,main] (XID = 1439), 
(SESSIONID = 1), 
(DATABASE = C:/jakarta-tomcat-5.0.28/webapps/Derby_MyFaces/airlinesDB), 
(DRDAID = null), Committing
...

リスト 4 に示した getUserPassword メソッドは、DatasourceObject インターフェースに必要なメソッドの 1 つです。これは、SQL の PreparedStatement を使用して Derby データベースを照会する標準的な方法で、すでにお気付きかもしれませんが、最初のページにユーザー名とパスワードを入力したときには、このメソッドが呼び出されて derby.log ファイルに出力が作成されました。

リスト 6 に示す DerbyDatabase クラスの shutdown メソッドを見ると、Tomcat が起動した airlinesDB をシャットダウンする方法が分かります。setShutdownDatabase メソッドがストリング "shutdown" によって呼び出され、getConnection の呼び出しによって実際にシャットダウンが実行されます。Derby はデータベースをシャットダウンするときに、SQLState を 08006 に設定した SQLException をスローします。そのため、キャッチ・ブロックはこの例外を無視します。

リスト 6. airlinesDB Derby データベースのシャットダウン
public void shutdown() {
  if (isShutdown) {
    return;
  }
  try {
    ds.setShutdownDatabase("shutdown");
    // necessary to actually shut down the derby database
    ds.getConnection();
  } catch (SQLException except) {
    if (except.getSQLState().equals("08006")) {
      // ignore, this is the SQLState derby throws when shutting down the database
      System.out.println("Derby database shut down.");
      isShutdown = true;
    }
    else {
      except.printStackTrace();
    }
  }
}

DerbyDatabase クラスの initialize メソッドが contextInitialized という Web ライフ・サイクル・イベントによって呼び出されるように、リスト 6 の shutdown メソッドも以下に示す DataFactory クラスの contextDestroyed ライフ・サイクル・イベントによって呼び出されます。そのため、Tomcat が Web アプリケーションをシャットダウンしてアンロードする際には、以下に示す DatasourceObject インターフェースを実装するクラスの shutdown メソッドが呼び出されます。

public void contextDestroyed(ServletContextEvent sce) {
datasourceObject.shutdown();
}

これで、モデル層の注目すべき詳細のほとんどを説明したことになります。次に取り上げるのは、コントローラーです。


コントローラー -- ライフ・サイクル・イベントとサーブレット・フィルター

前のセクションでは、モデル層の初期化を設定する際に、Web アプリケーションの作成および破棄プロセスの一環として呼び出される contextInitialized および contextDestroyed メソッドの使用方法について説明しました。これらのメソッドはコントローラー層の一部だとは思いますが、この記事ではモデルのセクションで取り上げました。

サーブレット・フィルターも、Web アプリケーションのコントローラー層の一部と考えることができます。サーブレット・フィルターは一般的に、ユーザー・タイプ (あるいは認証と許可) またはページの機能に基づいてアプリケーションのフローを制御するために使用します。チェーンの次のフィルターを呼び出すために使用可能な javax.servlet.FilterChain オブジェクトは、サーブレット・コンテナーが提供します。フィルターを Web アプリケーションの一部として登録するには (つまり、FilterChain の一部として doFilter メソッドが呼び出されるようにするには)、フィルターをアプリケーションの web.xml ファイルに追加する必要があります。

web.xml ファイルには、javax.servlet.Filter インターフェースを実装する LoginFilter クラスに対してこのエントリーがあります。リスト 7 のエントリーは、URL パターンが *.jsf または /Derby_MyFaces/* であればすべて LoginFilter クラスの対象になることを意味します。

リスト 7. web.xmlファイルの LoginFilter エントリー
<filter>
  <filter-name>login</filter-name>
  <filter-class>org.apache.derby.demo.filters.LoginFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>login</filter-name>
  <url-pattern>*.jsf</url-pattern>
</filter-mapping>

<filter-mapping>
  <filter-name>login</filter-name>
  <url-pattern>/Derby_MyFaces/*</url-pattern>
</filter-mapping>

リスト 8 に示す LoginFilter クラスは、以下のことを行います。

  • 登録ページの Register.jsf に対して直接要求を行う場合、FilterChain オブジェクトのその他のサーブレット・フィルターを呼び出さずに、要求をこのページに送信します。
  • セッション属性 "login-status" が true の場合、チェーンの次のフィルターを呼び出して要求を続行させます。
  • セッション属性 "login-status" が null または true でない場合、チェーンの次のフィルターの doFilter メソッドを呼び出さずに、要求をログイン・ページの Welcome.jsf に転送します。
リスト 8. LoginFilter クラス
public class LoginFilter implements Filter {

  private FilterConfig config;

  private RequestDispatcher dispatcherLogin;

  private RequestDispatcher dispatcherRegister;

  private static final String LOGIN_PAGE = "/login/Welcome.jsf";

  private static final String REGISTER_PAGE = "/login/Register.jsf";

  public static final String AUTH_STATUS = "login-status";

  public void init(FilterConfig filterConfig) throws ServletException {

    config = filterConfig;

    dispatcherLogin = config.getServletContext().getRequestDispatcher(LOGIN_PAGE);

    dispatcherRegister = config.getServletContext().getRequestDispatcher(REGISTER_PAGE);

    } 

  public void doFilter(ServletRequest req, ServletResponse res,
      FilterChain chain) throws IOException, ServletException {

    HttpServletRequest request = (HttpServletRequest) req;

    // if a request is made directly for Register.jsf send them to it
    if (request.getServletPath().equals(REGISTER_PAGE)) {
      dispatcherRegister.forward(req, res);
    }
    // otherwise if their login-status is not null
    else if (request.getSession(true).getAttribute(AUTH_STATUS) != null) {
      // if it is true, send them on to the next filter in the chain
      if (request.getSession(true).getAttribute(AUTH_STATUS) == Boolean.TRUE) {
        chain.doFilter(req, res);
        // otherwise send them to the login page
      } else {
        dispatcherLogin.forward(req, res);
      }
    }
    // if login-status is not set at all send them to the login page
    else {
      dispatcherLogin.forward(req, res);
    }

}

コントローラー --Apache MyFaces を使用した JSF

ページのフローとナビゲーション

図 1 をもう一度見てみると、JSF がこのアプリケーションのコントローラー層の中核となっています。そこで、アプリケーションのフローを理解する第一歩として、JSF 構成ファイル faces-config.xmlについて説明します (JSF を使い慣れていないため faces-config.xml ファイルの目的が分からないという方は、「参考文献」セクションに記載されている JSF の基礎知識を学ぶためのリンクを参照してください)。

ここでは、特定のアクションまたは結果をマッピングしてページのナビゲーションを可能にする、このファイルのナビゲーション・ディレクティブについて簡単に説明します。まず、このアプリケーションのページのフローと各ページの機能を以下にリストします。

  1. Welcome.xhtml: 既存のユーザーをログインさせます。
  2. Register.xhtml: 新規ユーザーを登録します。
  3. SelectDateAirport.xhtml: 出発日と出発空港を選択します。
  4. DestinationAirport.xhtml: 出発空港に基づいて到着空港を表示し、到着空港を選択できるようにします。
  5. FlightList.xhtml: 選択された到着空港と出発空港に基づいて、予約可能なフライトを表示します。
  6. CreditCard.xhtml: ユーザーが選択したフライトに基づいてクレジット・カード情報を入力できるようにします。
  7. FlightHistory.xhtml: 該当ユーザーが予約したすべてのフライトを表示します。
  8. LoggedOut.xhtml: アプリケーションからログアウトします。

図 4 に、faces-config.xml ファイルのナビゲーション部分をグラフィックで描写します。

図 4 の上半分に位置するページは、登録、ログイン、ログアウトを処理します。フライトの選択、予約、支払いに関するページのフローを辿るには、図の左下半分の上から下、次に右下半分の上から下を見てください。

この図の解釈方法として、例えば /flights/SelectDateAirport.xhtml ページを表すオレンジ色のブロックを見てください。このブロックを指している 2 つの矢印はそれぞれ受け入れ可能な結果、つまり logged_in と back_to_flights を表し、そのどちらかが発生しないとこのページは表示されません。この 2 つの必要な結果に対応する faces-config.xml のテキスト表現については、リスト 9 を参照してください。必要な結果のいずれかを得る方法については、次のセクションで説明します。

図 4. faces-config.xml ファイルのページ・ナビゲーション
図 4. faces-config.xml ファイルのページ・ナビゲーション
リスト 9. SelectDateAirport ページの faces-config.xml から抜粋した 2 つのナビゲーション事例
<navigation-case>
    <from-outcome>logged_in</from-outcome>
    <to-view-id>/flights/SelectDateAirport.xhtml</to-view-id>
</navigation-case>

<navigation-case>
    <from-outcome>back_to_flights</from-outcome>
    <to-view-id>/flights/SelectDateAirport.xhtml</to-view-id>
</navigation-case>

管理対象 Bean、アクション、および結果

JSF の管理対象 Bean 作成機能は、自動的に Bean を作成して初期化し、参照時の適切な適用範囲にこの Bean を保管します。作成された Bean が使用されなくなると、この機能に戻されます。

管理対象 Bean 機能の大きな利点の一つは、JSF の式言語構文を使って Bean を参照できることです。この構文はほとんどの XHTML ページで使用されています。

それでは、リスト 9 の <from-outcome> の作成に関連するアプリケーションの管理対象 Bean の 1 つを取り上げてみましょう。リスト 10 は、loginBean を示す faces-config.xml ファイルの別のスニペットです。ここには、JSF アプリケーションで使用されるすべての管理対象 Bean を記述する必要があります。

リスト 10. JSF アプリケーションで使用されるすべての管理対象 Bean を記述する loginBean
<managed-bean>
  <managed-bean-name>loginBean</managed-bean-name>
  <managed-bean-class>org.apache.derby.demo.beans.model.LoginBean</managed-bean-class>
  <managed-bean-scope>session</managed-bean-scope>
</managed-bean>

Bean には、ユーザーがログインまたはログアウトしたかどうかを追跡するために使用するものとして、session スコープがあります。ユーザーの (ログアウト、またはブラウザーのウィンドウを閉じることによる) セッション終了時には、該当ユーザーに対して Bean が再作成される必要があります。

リスト 11 に、loginBean の authenticate メソッドを示します。このメソッドは、図 2 の Submit のボタンがクリックされると呼び出されます。

リスト 11. LoginBean クラスの部分的リスト
public class LoginBean implements Serializable {

  private String username;

  private String password;

  private String loggedIn = "false";

  public LoginBean() {
    username = "";
    password = "";
  }

  public LoginBean(String userName, String passWord) {
    username = userName;
    password = passWord;
  }

  // verify if the username already exists
  // verify if the username and password match what is in the data source
  public String authenticate() {
    String result = "failure";
    FacesContext context = FacesContext.getCurrentInstance();
    DatasourceObject datasourceObject = DataFactory.getDatasourceObject();

    // create a UserBean by retrieving the username and password from
    // the database if the username entered on the page is in the db
    UserBean userBean = datasourceObject.getUserPassword(username);
    String userName = userBean.getUserName();

    // verify the name entered on the page and the one in the db
    // are the same
    if (userName != null && userName.equals(username)) {

      String actualPassword = userBean.getPassword();
      // verify the passwords are the same
      if (actualPassword != null && actualPassword.equals(password)) {
        // set the session attribute LoginFilter.AUTH_STATUS to true
        ((HttpSession) context.getExternalContext().getSession(true))
          .setAttribute(org.apache.derby.demo.filters.LoginFilter.AUTH_STATUS,
				Boolean.TRUE);
        setLoggedIn("true");
	// set 'result' to "logged_in" which maps to the faces-config navigation case
	// for the /flights/SelectDateAirport.xhtml <from-outcome> value.
        result = "logged_in";
      }
      // the userName and the username match ... but the passwords don't
      else {
        String failureMsg = ErrorMessages.getString("LoginBean.LOGIN_FAILURE");
	FacesMessage message = new FacesMessage(
	    FacesMessage.SEVERITY_ERROR, failureMsg, failureMsg);
	context.addMessage("login_form:loginButton", message);
      }
      // the userName is not null but it does not match the username
    } else {
      String failureMsg = ErrorMessages.getString("LoginBean.LOGIN_FAILURE");
      FacesMessage message = new FacesMessage(
        FacesMessage.SEVERITY_ERROR, failureMsg, failureMsg);
      context.addMessage("login_form:loginButton", message);
    }
    return result;
    }  
 ...

getUserPassword メソッドはDatasourceObject (この場合は、UserBean を作成する DerbyDatabase クラス) で呼び出されます。この UserBean は、フォームに入力されたユーザー名がデータベース内のユーザー名と一致するかどうかによって、Derby データベースから戻された単一の行を表したり、あるいは行を表さない場合があります。データベースから行が戻された場合は、UserBean にヌル以外のユーザー名とパスワード・メンバーの変数が入力されます。

データベースから行が取得されるということは、userName 変数と actualPassword 変数はヌルではないということです。この両方の変数が、フォームからの値と比較されます。一致する場合、ユーザーのセッションにユーザーがログインしたことを示す属性が設定されます。このセッション属性 (リスト 8) が doFilter メソッドでどのように検討されたかを覚えていますか。この属性が設定される場所は authenticate メソッドです。認証が成功すると、ストリング変数 result が logged_in に設定されます (成功しなかった場合、値は failure に設定されたままになります)。

この logged_in という値はリスト 9 に示した必須の値で、これによって次のページ /flights/SelectDateAirport.xhtml (図 3) にナビゲートできます。

MyFaces バリデーター

Web アプリケーションのフォーム・エントリーを検証することによって、例えば値をデータベースに挿入するなどのデータ処理を行う前に、データが正しいフォーマットまたは範囲であることを確実します。JSF による検証では、検証中のコンポーネントにエラー・メッセージを柔軟に関連付けることができるという点が理想的な特性となっています。JSF の MyFaces 実装 (具体的には Tomahawk コンポーネント) にはすぐに使用できるバリデーターが組み込まれているため、多くの共通データ・エントリー・タイプにカスタム・バリデーターを作成するという作業が軽減されます。

図 2 に示したアプリケーションの最初のページには、新規ユーザーを登録するオプションがあります。Register ボタンをクリックすると、Register.xhtml ページが表示されます。図 5 に、テキスト・フィールドに誤った値を入力して Submit ボタンを押した場合の結果を示します。このページで使用されているのは、validateRegExpr、validateEmail、および validateEqual という Tomahawk コンポーネントです。

図 5. MyFaces の Tomahawk バリデーターによる検証
図 5. MyFaces の Tomahawk バリデーターによる検証

正規表現バリデーターの使用方法と、Last Name テキスト・フィールドに対して表示された関連エラー・メッセージを理解するには、リスト 12 に示す Register.xhtml のソース・コードのスニペットを見てください。

リスト 12. Register.xhtml から抜粋したソース・コード
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<t:document 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:t="http://myfaces.apache.org/tomahawk">
...

<h:form id="register_form">
 ...

<h:outputLabel for="lastname" styleClass="standard" 
   value="#{messages['register_lastname']}" />
<h:inputText id="lastname" value="#{userBean.lastName}" required="true" maxlength="40">
  <t:validateRegExpr pattern='[a-zA-Z]+' />
</h:inputText>
<t:message id="lastNameError" for="lastname" styleClass="error" />
...

</h:form>

JSF の HTML タグ <h:outputLabel> によって、関連するコンポーネント、ラベルに使用する CSS スタイル、そして message というメッセージ・バンドルから取得したラベルの値が指定されています。

HTML inputText フィールドには lastname の ID があり、このテキスト・フィールドの値が UserBean の lastName プロパティーに割り当てられています。次の行を見ると、Tomahawk の validateRegExpr バリデーターの使用方法と、検証を実行するために使用する正規表現パターンが分かります。上記の例では、テキスト・フィールドのストリングは英字で構成されること、そして inputText の maxlength 属性に指定されているように、ストリングの長さは 1 文字から最大 40 文字の範囲であることが条件となっています。

Tomahawk メッセージ・コンポーネントは、エラーのレポート対象となる別のコンポーネントに関連付ける必要があります。この関連付けには、対応するコンポーネントの id 属性と一致する for 属性が使用されます。これで、テキスト・フィールドには値 !Cline が使用できない理由、そしてページに表示されたエラー・メッセージが CSS エラー・スタイルの色である赤でレポートされた理由が明らかになったはずです。

正規表現の利点は、使用する正規表現をカスタマイズできることです。例えば上記のリストでは、ユーザー名とパスワードのテキスト・フィールドには正規表現として pattern='[a-zA-Z0-9]+' というパターンを指定して、複数のケースに対するこのコンポーネントの柔軟性と再利用性を向上させています。

リスト 13 の passwordVerify inputSecret タグのように、1 つのコンポーネントに複数のバリデーターを使用することもできます (読みやすいように改行しています)。

リスト 13. 同じコンポーネントに対する複数バリデーターの使用
<h:outputLabel for="password" styleClass="standard" 
   value="#{messages['register_password']}" />

<h:inputSecret id="password" value="#{userBean.password}" required="true" maxlength="20">
  <t:validateRegExpr pattern='[a-zA-Z0-9]+'>
</h:inputSecret>

<t:message id="passwordError" for="password" styleClass="error" />

<h:outputLabel for="passwordVerify" styleClass="standard" 
  value="#{messages['register_password_verify']}" />
<h:inputSecret id="passwordVerify" value="#{userBean.passwordVerify}" required="true" 
    maxlength="20">
  <t:validateRegExpr pattern='[a-zA-Z0-9]+' />
  <t:validateEqual for="password" />
</h:inputSecret>

<t:message id="passwordVerifyError" for="passwordVerify" styleClass="error" />

その他の Tomahawk

その他の Tomahawk コンポーネントについて説明する前に、アプリケーションをもう少し詳しく見てみましょう。apacheu ユーザーとしてアプリケーションにログインしたところから続けます。図 3 では 9 月 28 日のトロント発のフライトを選択するところでした。これを選択すると、図 6 の結果になります。

図 6. トロント発の予約可能なフライト
図 6. トロント発の予約可能なフライト

このページにはとりたてて注目する点はありません。標準 JSF コンポーネントが使用されているためです。アプリケーションがインストールされていれば、このページのソースを調べることができます。各ページの右下隅にある Show Source というリンクを選択すると、現在表示しているページの XHTML ソースが表示されます。

このページの Go ボタンをクリックして、FlightList.xhtml に進んでください。このページは、Tomahawk の dataTable、commandLink、および updateActionListener コンポーネントを利用しています。

図 7. トロント発ロンドン着のフライト・リスト -- Tomahawk の追加コンポーネント
図 7. トロント発ロンドン着のフライト・リスト -- Tomahawk の追加コンポーネント

リスト 14 の dataTable コンポーネントはとりあえず無視してください。このコンポーネントは基本的に、いくつかの機能が追加された HTML テーブルを表すものですが、これについては後で説明します。ここでは、commandLink コンポーネント内にネストされた updateActionListener コンポーネントに注目してください。

リスト 14. commandLink および updateActionListener コンポーネント
...

<t:dataTable id="data" styleClass="scrollerTable" headerClass="standardTable_Header" 
 footerClass="standardTable_Header" rowClasses="standardTable_Row1,standardTable_Row2" 
 columnClasses="standardTable_ColumnCentered" var="flight"
 value="#{flightConfig.availableFlights}" preserveDataModel="true" rows="5">

 <h:column>
  <f:facet name="header">
   <h:outputText value="#{messages['flight_id']}" />
  </f:facet>
  <t:commandLink id="command_link" action="#{flight.selectedFlight}" immediate="true">
   <h:outputText value="#{flight.flightId}" />
   <t:updateActionListener property="#{flightBean}" value="#{flight}" />
  </t:commandLink>
</h:column>

...

updateActionListener コンポーネントは、ActionSource (commandLink または commandButton) を親コンポーネントとして持つ必要があります。flight 変数に対する flightId の出力テキスト値に関連付けられた commandLink をクリックすると、以下の結果になります。

  • commandLink アクションの値がメソッド・バインディングとして指定されます。この場合、これは FlightsBean クラスの selectedFlight() メソッドに対応します。このメソッドが行うのは、次のページのナビゲーション結果として使用されるストリングを戻すことだけです。図 4 のストリング get_selected_flight を調べて、ナビゲート先のページを確認してください。
  • updateActionListener コンポーネントの value 属性の値が property 属性に割り当てられます。つまり、flight 変数に含まれる値が、管理対象 Bean である flightBean に割り当てられます。flight 変数は、flightConfig Bean の availableFlights 変数によって表される FlightsBeans の java.util.List に含まれる単一の FlightsBean オブジェクトを表します。これを可能にするのは、dataTable コンポーネントです。このコンポーネントは、availableFlights リストに含まれる FlightsBeans を繰り返して、変数 flight に FlightsBean の各インスタンスを配置します。

ここで updateActionListener コンポーネントが重要になる理由が分かりますか。次のページではユーザーがクレジット・カード情報を入力してフライトを予約しますが、特定のフライト (FlightsBean) を渡すことができなければ、この情報が失われてしまうからです。

図 8 に、トロント発ロンドン着のフライト番号 US1592 を選択し、クレジット・カード情報を入力して Charge My Credit Card ボタンをクリックした結果を示します。

図 8. フライトの予約
図 8. フライトの予約

Tomahawk の CreditCard バリデーター

図 8 に示した Credit Card フィールドにバリデーターがあるのは明らかです。リスト 15 に、CreditCard.xhtml ページの一部のソースを示します。

リスト 15. CreditCard.xhtml のTomahawk タグ、validateCreditCard
...
<h:panelGrid columns="3">
 <h:outputLabel for="creditCardNumber" styleClass="standard" 
   value="#{messages['validate_credit']}" />
  <h:inputText id="creditCardNumber" value="#{creditBean.creditCardNumber}" 
    required="true" size="16" maxlength="16" immediate="true">
   <t:validateCreditCard amex="true" visa="true" mastercard="true" discover="false" />
  </h:inputText>
  <t:message id="creditCardNumberError" for="creditCardNumber" styleClass="error" />
...

<h:form id="validate_cc">
 <h:panelGrid columns="1">
  <h:selectBooleanCheckbox id="r2" value="#{creditBean.creditCardValidate}"  
   immediate="true" 
    onclick="submit()" valueChangeListener="#{creditBean.handleValidation}">
   <h:outputText value="#{messages['validate_cc']}" styleClass="standard"/>
  </h:selectBooleanCheckbox>
  <br/>
 </h:panelGrid>
</h:form>

validateCreditCard コンポーネントは、5 つの属性を受け入れます。そのうちの 4 つはリスト 15 に示されているもので、もう 1 つは none と呼ばれます。上記のリストでは、Amex、Visa、および MasterCard のクレジット・カードは受け付け、Discover は受け付けないことを指定しています。このコンポーネントで使用しているのは、Jakarta Common 検証です。

注: この検証は非常に効果的に機能します。検証を無効にするか、有効なクレジット・カード番号を入力しない限り、次のページには進めません。

リスト 15 の 2 番目のコード・スニペットでは、HTML の selectBooleanCheckbox を利用して動的に検証の無効化と有効化を処理しています。onclick 属性は、immediate 属性の値 true によってイベントが起動されると同時に、チェック・ボックス #{creditBean.creditCardValidate} のブール値をサブミットします。このイベントを受信するために呼び出されるメソッドは、valueChangeListener 属性で指定された、UserCreditCardBean の handleValidation メソッドです。

これをテストするには、Enable Validation of Credit Card チェック・ボックスのチェック・マークを外してクレジット・カード検証を無効にしてから、Charge My Credit Card ボタンをクリックします。検証の切り替えがどのように機能するかを理解するには、リスト 16 に示す UserCreditCardBean クラスの handleValidation メソッドを見てください。このメソッドは、ボックスにチェック・マークが付けられたとき、またはチェック・マークが外されたときに呼び出されます。

リスト 16. UserCreditCardBean による動的検証の処理
public void handleValidation(ValueChangeEvent event) {
  Object value = event.getNewValue();
		
  if (value == Boolean.TRUE) {
    enableValidateCC();
  }
  else {
    disableValidateCC();
  }
		
}
...
public String enableValidateCC() {

  FacesContext facesContext = FacesContext.getCurrentInstance();

  UIInput creditCardNumber = (UIInput) facesContext.getViewRoot()
   .findComponent("credit_card_form:creditCardNumber");
  Validator[] validators = creditCardNumber.getValidators();
  if (validators == null || validators.length == 0) {
    creditCardNumber.addValidator(new CreditCardValidator());
  }
  return "ok";
}
...
public String disableValidateCC() {

  FacesContext facesContext = FacesContext.getCurrentInstance();
		
  UIInput creditCardNumber = (UIInput) facesContext.getViewRoot()
   .findComponent("credit_card_form:creditCardNumber");
  Validator[] validators = creditCardNumber.getValidators();
  if (validators != null) {
    for (int i = 0; i < validators.length; i++) {
      Validator validator = validators[i];
      creditCardNumber.removeValidator(validator);
    }
  }

  return "disabled";
}

クレジット・カード検証を有効または無効にするかどうかを決定するには、handleValidation メソッドに渡されたイベントに関連付けられた新しい値が使用されます。この値は、チェック・ボックス・コンポーネントのブール値です。enableValidateCC と disableValidateCC の 2 つのメソッドが FacesContext の currentInstance を取得します。次に、creditCardNumber 入力テキスト・フィールド (リスト 15 を参照) に対応する UIInput コンポーネントを取得して、呼び出しの目的が検証の有効化または無効化のどちらであるかに応じてバリデーターを追加、あるいは除去します。

Tomahawk の拡張コンポーネント、DataTable および JSCookMenu

クレジット・カード検証を無効にした後、トロント発ロンドン着のフライトを予約し、いくつかの旅行区間を追加で予約することにしました。図 9 に、その結果を示します。このページで強調表示されているのは、Tomahawk の拡張コンポーネント、DataTable および JSCookMenu を使用している部分です。

図 9. フライト履歴 -- JSCookMenu とソート可能な DataTable
図 9. フライト履歴 -- JSCookMenu とソート可能な DataTable

Tomahawk の DataTable コンポーネントは、標準 JSF DataTable に以下の 2 つの機能を追加します。

  • DataModel の状態を保管する機能。データベース接続によってデータがバックアップされる場合、データベース内のデータは前回の要求以降に変更されている可能性があるため、この機能が重要になります。
  • クリックすることによってソートできるヘッダーのサポート。

リスト 17 に示すのは、FlightList.xhtml ページの Tomahawk dataTable コンポーネントの XHTML の一部です。このリストでは、preserveDataModel 属性を true に設定して、sortColumn 属性と sortAscending 属性の FlightsHistory 管理対象 Bean への値のバンディングを指定しています。また、dataTable タグ内で Tomahawk の commandSortHeader タグを使用していることにも注意してください。このコンポーネントは、commandLink actionSource コンポーネントから派生しています。

リスト 17. FlightList.xhtml ページにある Tomahawk の dataTable コンポーネント用 XHTML の抜粋
<t:dataTable id="data" styleClass="scrollerTable" headerClass="standardTable_Header" 
  footerClass="standardTable_Header" rowClasses="standardTable_Row1,standardTable_Row2" 
  columnClasses="standardTable_ColumnCentered" var="bookedFlight"
  value="#{flightHistory.bookedFlights}" preserveDataModel="true" 
  rows="8" sortColumn="#{flightHistory.sort}" 
  sortAscending="#{flightHistory.ascending}" preserveSort="true">

  <h:column>
   <f:facet name="header"> 
    <t:commandSortHeader columnName="flight_id" arrow="true"> 
     <h:outputText value="#{messages['flight_id']}" /> 
    </t:commandSortHeader> 
   </f:facet>
   <h:outputText value="#{bookedFlight.flightId}" />
  </h:column>
...

テーブルのソートがどのように機能するか分かりますか。Charge My Credit Card ボタン (図 8 を参照) をクリックしたときには、UserCreditCardBean クラスの submitCC() メソッドが呼び出され、これによって FlightsHistory クラスの findBookedFlights メソッドが呼び出されました。このメソッドは、java.util.List に該当ユーザーが予約したすべてのフライトを入力します。commandSortHeader リンクのいずれかをクリックすると、この列の値に基づいて行がソートされます。この場合、bookedFlights が flightHistory Bean から取得されなければなりません。これを取得する際に、FlightsHistory クラスの getSort() メソッドが呼び出されます。getSort() メソッドは、このクラスに実装されている java.util.Comparator を使用して、選択されたヘッダーに応じてリストをソートします。このコードを自分の目で確かめるには、FlightsHistory.java ファイルを開いてください。

ヘッダーをクリックしてみて、どのようにソートが機能するかを試してみてください。

最後に注目する Tomahawk コンポーネントは、図 9 の下部に示された JSCookMenu です。強調表示されたメニュー項目、Logout を見てください。JSCookMenu はコンポーネント内の JavaScript と CSS を使用して、見映えのいいメニュー項目を作成します。

リスト 18 は、JSCookMenu タグが使用されている部分の FlightHistory.xhtml のソースです。

リスト 18. JSCookMenu タグ
<h:form id="dummy2">
 <h:panelGrid columns="2">
  <t:jscookMenu layout="hbr" theme="ThemeOffice">
   <t:navigationMenuItem id="nav_1" itemLabel="#{messages['nav_back_to_flights']}" 
     action="back_to_flights" />
    <t:navigationMenuItem id="nav_2" itemLabel="#{messages['nav_other_choices']}">
    <t:navigationMenuItem id="nav_2_1" itemLabel=""#{messages['nav_register_user']}" 
     action="register_new_user" />
    <t:navigationMenuItem id="nav_2_2" itemLabel="#{messages['nav_logout']}" 
    action="logout" split="true" />
   </t:navigationMenuItem>
  </t:jscookMenu>
 </h:panelGrid>
</h:form>

jscookMenu のレイアウト属性は hbr に設定されているため、メニューは水平に配置され、子は下部と右側に分類されます。値にはその他、hbl (水平、下部、左側)、hur (水平、上部、右側)、hul (水平、上部、左側)、vbr (垂直、下部、右側)、vbl (垂直、下部、左側)、vur (垂直、上部、右側)、そして vul (垂直、上部、左側) があります。CSS を使用したコンポーネントのルック・アンド・フィールは、テーマ属性によって指定されます。図 10 に、テーマが ThemeMiniBlack に切り替えられた場合に、ページ上で JSCookMenu がどのように表示されるかを示します。

図 10. ThemeMiniBlack を使用した JSCookMenu
図 10. ThemeMiniBlack を使用した JSCookMenu

ビュー -- Facelets および XHTML

これまでのところで、アプリケーションのアーキテクチャー、モデル、コントローラー、MyFaces コンポーネントについて説明しました。また、JSF コンポーネントはタグ・ライブラリーとして公開されていることから、XHTML ページの一部のソースも記載しました。ここからは、Facelets の使用方法と、これが JSF アプリケーション開発を容易にする理由について説明していきます。

前にも言ったように、このアプリケーションは、サーブレットと JSP だけを使用するものから、MyFaces と Facelets を使用するものに変更されています。この変更を行うため、まず、JSF 以外の JSP を JSF ベースの JSP にマイグレーションした後、Facelets について調べて、JSP から XHTML ファイルに変更しました。この作業は難なく行えました。Facelets はJSP タグと同じ方法ですべての JSF UIComponents をサポートしているためです。JSP ページのほんの数行を変更しただけで、JSP を使用した場合より簡単にページをモジュール化できるテンプレートを作成することができました。

Facelets に対応した JSF アプリケーションの構成

JSF のデフォルト・ビュー・ハンドラーは JSP であるため、faces-config.xml ファイルをリスト 19 のように変更する必要があります。<view-handler> タグで、ビューには FaceletsViewHandler クラスを使用するように指定します。このクラスは、前述の手順でダウンロードした Tomcat の webapps/Derby_MyFaces/WEB-INF/lib ディレクトリーにコピーした jsf-facelets.jar ファイルにあります。

リスト 19. faces-config.xml ファイルの変更
<application>
  <!-- Use Facelets instead of JSP-->
  <view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
</application>

faces-config.xml ファイルに別のエントリーが必要なだけでなく、アプリケーションの web.xml ファイルにも追加が必要です。javax.faces.DEFAULT_SUFFIX パラメーターを追加して、ビューで使用するページ・タイプの接尾部を JSP から XHTML に変更しなければなりません。Facelets でタグ・ライブラリーを使用するために必要なもう一つのパラメーターは、facelets.LIBRARIES パラメーターです。Facelets で MyFaces の Tomahawk コンポーネントに使用するタグ・ライブラリーは tomahawk.taglib.xml という名前で、これには追加 Tomahawk コンポーネントのマッピングが含まれます。タグ名は、コンポーネントを実装する Java クラス、そして表示されるコンポーネントの場合にはレンダリングされるクラスにマッピングされます。最後のパラメーター facelets.DEVELOPMENT は必須ではありませんが、これによって Facelets のエラー処理機能を使用できるようになります。リスト 20 に、web.xml ファイル内でのこの 3 つのパラメーターと値を示します。

リスト 20. Facelets と Tomahawk 用に web.xml ファイルに追加するエントリー
<context-param>
  <param-name>javax.faces.DEFAULT_SUFFIX</param-name>
  <param-value>.xhtml</param-value>
</context-param>

<context-param>
  <param-name>facelets.LIBRARIES</param-name>
  <param-value>/WEB-INF/tomahawk.taglib.xml</param-value>
</context-param>

<context-param>
  <param-name>facelets.DEVELOPMENT</param-name>
  <param-value>true</param-value>
</context-param>

Tomahawk アプリケーションが Facelets を使用できるようにする方法について詳しくは、「参考文献」セクションに記載されている MyFaces Wiki エントリーを参照してください。

Facelets を使用する準備が整ったところで、いくつかの XHTML ページと、これらのページで使用するために作成されたテンプレートについて説明します。

XHTML と Facelets テンプレートの作成

リスト 21 に、アプリケーションのすべての XHTML ファイルに共通の最初の数行を示します。参照される名前空間は、JSF の HTML タグとコア・タグ、Tomahawk タグ、そして Facelets タグのためのものです。HTML タグとコア・タグは Sun 名前空間を参照していますが、MyFaces ライブラリーが使用されているため、これらのタグは実際には Apache によって実装されます。

リスト 21. XHTML DOCTYPE および XML 名前空間
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "
   http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <t:document 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:t="http://myfaces.apache.org/tomahawk">
 ...
</t:document>

Facelet のテンプレート作成機能を使ってタイトル、ヘッダー、およびフッター・エリアの JSP で重複するテキストを除去し、ページの本文に異なるコンテンツを使用できるようにしています。Facelets でテンプレートを使用するには、まず、アプリケーションのページ・コンテンツの構造となるページを作成する必要があります。つまり、テンプレート・ページを Web ページのタイトル、本文、フッターなどの標準セクションに区分し、それぞれのセクションにデフォルト・コンテンツを提供して、このコンテンツをオーバーライドできるようにしておきます。

リスト 22 は、template.xhtml の簡素化バージョンです (名前空間宣言と JavaScript 関数を省いています)。

リスト 22. アプリケーションのテンプレート・ファイル
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
...
<t:documentHead>
  <link rel="stylesheet" type="text/css" href="../css/basic.css" />
  <title>
   The Apache Derby and MyFaces Flight Reservation Demo
  </title>
</t:documentHead>

<t:documentBody>

  <div id="header">
   <ui:insert name="page_title">
    <ui:include src="pageTitle.xhtml" />
   </ui:insert>
  </div>

<div id="body">
  <br />
   <ui:insert name="body">
   </ui:insert>
  <br />
</div>


<div id="footer">
  <ui:insert name="footer">
   <div class="pageFooter">
    <script type="text/javascript">
document.write
("<a href='" + getSourceUrl() + "#{mypage}.txt'>Show Source</a>");
    </script>
   </div>
  </ui:insert>
</div>

</t:documentBody>
</html>

このテンプレート・ページでは、ui 名前空間に関連付けられた Facelets ライブラリーの insert タグと include タグを使用しています。insert タグでは、insert の name 属性で参照されるページのエリアにコンテンツを注入できます。例えば、ページでこのテンプレート・ファイルを使用するには、そのページで使用する define タグの name 属性に、insert タグで参照される name 属性と同じ値を指定する必要があります。この仕組みを説明するため、define タグを使用したページの例をこれから紹介します。

リスト 22 で、id が header となっている div タグを見てください。このタグには insert タグと include タグがネストされています。insert タグの名前は page_title で、include タグの src は pageTitle.xhtml となっています。つまり、id が header の div には pageTitle.xhtml ページのコンテンツが含まれることになります。このコンテンツは The Apache Derby and MyFaces Flight Reservation Demo という単純なテキストですが、別のコンテンツに代えることができます。

それではここで、Register.xhtml を見てみましょう。このページはテンプレート・ページ template.xhtml を参照および利用して、define タグと composition タグを導入しています (リスト 23 を参照)。

リスト 23. composition、define、および param タグを使用した Register.xhtml
...
 <ui:composition template="../format/template.xhtml">
  <ui:define name="page_title">
   <div class="pageHeader">
    Register to use the Apache Derby and MyFaces Flight Reservation Demo
   </div>
   </ui:define>

  <ui:define name="body"">
   <f:view>

   ...

   </f:view>
  </ui:define>

  <ui:param name="mypage" value="Register.jsf" />
 </ui:composition>
</t:document>

リスト 23 に示した composition タグの template 属性は、リスト 22 のテンプレート・ページを指定しています。また、2 箇所で使用されているui:define タグにも注目してください。名前が page_title の最初のui:define タグでは、ページのヘッダーの値をオーバーライドできます。リスト 22 では、ページのヘッダーは The Apache Derby and MyFaces Flight Reservation Demo でしたが、上記のリストで Register to use the Apache Derby and MyFaces Flight Reservation Demo に変更しています。2 番目のui:define タグではページの本文を指定できます。テンプレート・ページでは、本文にはデフォルト・コンテンツがありません。つまり、composition タグで template.xhtml を参照しているページでは、body という名前の ui:define がページから省略されている場合、本文セクションには出力がないということです。

リスト 23 で最後に注目してほしいタグは、ui:param タグです。このタグには name と value の両方の属性が必要です。上記の例では、param タグで現行ページの名前をテンプレート・ページに渡し、各ページの下部にある Show Source 機能のリンクを動的に出力しています。

パラメーターがどこで使用されているかを理解するには、リスト 22 の footer div を見てください。JavaScript の小さなセクションを使って、document.write 関数で出力を動的に作成しています。URL は、Web アプリケーションの実際の URL を取得する getSourceUrl() 関数 (リストには記載されていません) を呼び出して作成されます。ui:param タグによって渡されたページ名を出力するには、JSF EL 式 #{mypage} が使用されます。これに .txt 拡張子が追加され、HTML の <a href> タグの適切なフォーマットが作成されて Show Source リンクが出力されます。

Facelets のエラー・メッセージ処理

この記事で最後に取り上げるのは、Facelets の改善されたエラー・メッセージ処理機能です。この機能は、facelets.DEVELOPMENT パラメーターをリスト 20 の web.xml ファイルに追加すると使用可能になります。Facelets を使ったブラウザーでどのようにエラーが表示されるかを説明するため、Welcome.xhtml ファイルでわざと、LoginBean のプロパティー名を username という正しい値から usersname という誤った値に変更しています。このように変更した Welcome.xhtml のコード行は以下のようになりますが、これは誤ったものです。

<h:inputText id="username" value="#{loginBean.usersname}" required="true" maxlength="20">

図 11 に、エラー処理機能を有効にしてアプリケーションの最初のページ、Welcome.xhtml を呼び出した場合の出力を示します。この図を、機能を無効にした従来の HTTP Status 500 エラーとスタック・トレースを示した図 12 と比較してください。

図 11.Facelets のエラー・メッセージ表示
図 11.Facelets のエラー・メッセージ表示
図 12.Facelets のエラー・メッセージ機能を無効にした場合
Facelets error display

スタック・トレースではエラーの要因 (root cause) がレポートされていますが、Facelets のエラー・レポートの方が改善されているのは明らかです。Facelets では、エラーの原因を直接的に通知するだけでなく、+ 記号を展開してスタック・トレースを表示できるようにもなっています。また、コンポーネント・ツリー、要求パラメーターおよび属性、そしてセッションとアプリケーションの属性まで調べることができます。さらに、ページがどのようにレンダリングを開始しているかに注目してください。ヘッダーと開始テキストにはエラーがないことが分かります。すなわち、追加の情報源として、ページのどこに問題があるのかを一目で判断することができます。


まとめ

この記事では、Apache MyFaces、Apache Derby、そして Faceletsを使用して JSF Web アプリケーションを開発する方法を紹介しました。ここで取り上げた Derby 機能には、EmbeddedDataSource を使用するもの、ServletContextListener によって Derby を起動および停止するもの、そして JDBC の Prepared Statement と Callable Statement を使用するものがあります。

Apache MyFaces の Tomahawk バリデーターとコンポーネントの使用方法は、ブラウザーの出力表示を例に用いて説明しました。また、JSF 構成ファイル、faces-config.xml の中でマッピングされた Web アプリケーションのナビゲーションについても見てきました。そして Facelets が持つ多数の機能のなかからテンプレート作成機能およびエラー処理機能などのいくつかの機能と、Tomahawk ベースの JSF アプリケーションを構成して Facelets を使用する方法を説明しました。

Apache Derby の開発の容易さ、組み込み可能な性質、および管理不要の特性、そして MyFaces コンポーネントの強力さと幅の広さ、Facelets の使いやすさと再利用性は、頑強で動的な JSF ベースの Web アプリケーションを開発するための強力なトリオになります。


ダウンロード

内容ファイル名サイズ
Source and binary files for the ApplicationApache_Derby_MyFaces_Demo.zip5377KB

参考文献

学ぶために

  • Apache Derby の Web サイトにアクセスしてください。Derby を使用したアプリケーション開発に関する豊富な記事が記載されています。この記事のサンプル・アプリケーションではバージョン 10.1.3.1 を使っていますが、現在バージョン 10.2 がリリースされてダウンロードできるようになっています。
  • Derby wiki は、Derby の初心者はもとより、コミュニティーに参加したいと思っている開発者やユーザーにとって非常に役立つ情報源になります。
  • developerWorks に記載されている Rick Hightower による一連の記事「JSFを信じない人のために」を読んで、JSF について学んでください。
  • サンプル Web アプリケーションで使用しているバージョン 1.1.4 の MyFaces Core およびバージョン 1.1.3 の Tomahawk コンポーネントの詳細を調べてください。Derby Wiki と同様、MyFaces wiki も初心者と経験豊富なユーザーの両方を対象にしています。
  • Wiki エントリーの「Use Facelets with Tomahawk」では、Facelets を Tomahawk コンポーネントと併用する際の要件を詳しく説明しています。
  • Facelets の拡張機能についての詳細を学んでください。
  • Facelets フレームワークの定義、環境の設定方法、API に関する情報を参照してください。
  • 「Build Web applications with Eclipse, WTP, and Derby」(developerWorks、2005年9月) を参照して、この記事に記載した JSF と Facelets でビルドしたアプリケーションと、サーブレットと JSP でビルドしたアプリケーションを比較してください。
  • オープン・ソース技術を使用して開発し、IBM の製品と併用するときに役立つ広範囲のハウツー情報、ツール、およびプロジェクト・アップデートについては、developerWorks Open source ゾーンを参照してください。
  • developerWorks の Open source ゾーンには、Apache に関するさまざまな記事無料の Apache チュートリアルが豊富に用意されています。
  • Safari bookstore には、この記事に関連した話題や他の技術を取り上げた技術書が豊富に取り揃えられています。
  • developerWorks の Apache Geronimo プロジェクト・エリアを調べてみてください。今日からでも Derby を使い始めるための様々な記事やチュートリアル、その他の資料が用意されています。
  • Apahce Derby コード・ベースを使って構築された IBM® Cloudscape™ データベースについて、より深く学んでください。

製品や技術を入手するために

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source, Information Management
ArticleID=232661
ArticleTitle=Apache Derby、Apache MyFaces、および Facelets による…
publish-date=10242006