シームレスな JSF、第 3 回: JSF と相性のいい Ajax

Seam Remoting と Ajax4jsf によるシームレスなクライアントとサーバーの結合

JSF のコンポーネント・ベースの手法は抽象化を促進しますが、大抵の Ajax 実装は基礎となる HTTP 交換を公開するため、抽象化の妨げとなります。連載「シームレスな JSF」の最終回となるこの記事では、Dan Allen が Seam Remoting API と Ajax4jsf コンポーネントを使って、ブラウザー・サイドの Bean と通信しているかのようにサーバー上の管理対象 Bean と通信する方法を説明します。JSF イベント駆動型アーキテクチャーにふさわしい改善として Ajax を利用するのがどれほど簡単か、そして JSF コンポーネント・モデルに影響を与えずにこの改善を行う方法を学んでください。

Dan Allen (dan.allen@mojavelinux.com), Senior Java engineer, CodeRyte, Inc.

Dan Allen現在、CodeRyte, Inc. のシニア Java エンジニアとして活躍する Dan Allen は、熱烈なオープン・ソース擁護者でもあり、ペンギンを目にすると決まって興奮してしまいます。Linux とオープン・ソース・ソフトウェアの世界に魅了されたのは、材料工学の学位で Cornell University を卒業した後です。それ以来、Web アプリケーションの虜になっていますが、この数年はとくに、Spring、Hibernate、Maven 2、そして豊富な JSF スタックなどの Java 関連の技術に重点を置いています。彼の開発経験を辿るには、http://www.mojavelinux.com でブログにサブスクライブしてください。



2007年 6月 12日

近頃ほとんどの Java™ 開発者が優れたマッシュアップを利用しているなか、Seam が Web 2.0 の異名を取る技術 (特に Ajax) とどれほど上手く統合するかという点が気になるところです。Seam を使って JSF において部分的なページの更新を可能にしたり、JSF アプリケーションと Google マップとのマッシュアップを簡単にすることができたら素晴しいことだと思いませんか? 実は、いずれも可能です。しかも至って簡単に実現できます。

連載「シームレスな JSF」の最終回となるこの記事では、Seam Remoting API と Ajax4jsf コンポーネントを使って、JSF ベースのアプリケーションで Ajax スタイルの対話動作を簡単に実現する方法を説明します。記事を読むとわかるように、Seam を Ajax と組み合わせた場合の最大の利点は、JavaScript の XMLHttpRequest オブジェクトに四苦八苦することなく、Web 2.0 ならではのあらゆる機能を実現できることです。Seam Remoting と Ajax4jsf を使えば、ブラウザー・サイドの Bean と通信しているかのようにサーバー上の管理対象 Bean と通信することができます。ブラウザーとサーバーは同期した状態を保つので、ブラウザーとサーバー間の通信を円滑に行うための下位レベルの API を扱う必要はまったくありません。

記事ではまず初めに、Seam が新しいコンポーネント・ベースの Ajax プログラミング手法を手助けする仕組みを知ってもらうために、Seam Remoting API を使って Ajax を介した JavaScript とサーバー・サイド・オブジェクト間の通信を行う方法を説明します。この新しい (そして簡単な) Ajax 手法を理解できたところで、この手法を使って以下の作業を行い、Open 18 アプリケーションを拡張します。

  • Open 18 のコースの一覧と Google マップとのマッシュアップを作成する。
  • Ajax4jsf を使ってアプリケーションのコースの一覧のページとコースの詳細ページを統合する。
  • アプリケーションの Spring 統合に立ち返って、Spring の Bean を Seam Remoting ライフ・サイクル期間中に使用できるようにする。

Seam 1.2.1.GA へのアップグレード

何事も素早く運ぶ Seam の世界では、前回の記事の後に早速、フレームワークの新しいバージョンが登場しています。今回の記事のサンプルに必要なのは、Seam 1.2.1 以降です。これまでの Seam バージョンでは、サーバー・サイドのリモーティング機能は SeamRemotingServlet が一貫して処理していましたが、最新のリリースではこの機能が Seam Remoting パッケージにリファクタリングされ、汎用の ResourceServlet を使って JSF 以外のリクエスト (リモーティング呼び出しなど) の処理を代行しています。また、リモーティング・ライブラリーも個別の JAR、jboss-seam-remoting.jar としてパッケージ化されています。この JAR を、「シームレスな JSF、第 1 回: JSF 用にあつらえたアプリケーション・フレームワーク」のサンプルを実行するために特定した必須の JAR と併せて、アプリケーションのクラスパスに含めてください。

Open 18 と Google マップのマッシュアップにより、ユーザーがゴルフ・コース一覧に示されているゴルフ・コースの場所をマップ上にプロットできるようにします。さらにコースの一覧のページとコースの詳細ページを統合して (そして基礎となるコードを Aax 化して)、新しいページをロードせずにコースの詳細を表示できるようにします。そして最後に Spring Bean を Seam Remoting に統合することで、Google マップの位置マーカーの変更後の位置を取得し、対応するコースの緯度と経度をデータベースに保管できるようにしています。その結果できあがるのは、ゴルフ・プレーヤーなら誰もが使いたがる素晴しい Web 2.0スタイルのアプリケーションです。

複雑すぎる JavaScript だらけの Ajax プログラミングでひどい目に遭った経験を持っている方や、Ajax の複雑さを理由に今まで Ajax を扱うのを避けていた方がこの記事を読めば、恐怖心は消えてなくなるはずです。アプリケーションのリファクタリングでは多少の JavaScript のコーディングを行いますが、大抵の Ajax 実装とは異なり、JavaScript がコードの大部分を占めることはありません。JavaScript はサーバー・サイドの Java オブジェクトを拡張するだけに過ぎません。

Ajax に対するもう 1 つの手法

Jアプリケーションでは明示的なメモリー管理を避けようとするのと同じで、下位レベルの Ajax リクエスト・プロトコルは扱わなくても済むようにしたいものですが、それを実現するのは大きな頭痛の種です。あるいは複数の頭痛の種と言ったほうがいいかもしれません。マルチブラウザーのサポート、データ・マーシャリング、並行性違反、サーバー負荷、それにカスタム・サーブレットにサーブレット・フィルターなどといったさまざまな問題があるからです。なかでも最大の頭痛の種で、かつ絶対に避けたいと思う問題は、JSF などのコンポーネント・ベースのフレームワークでは公開してはならないステートレスなリクエスト・レスポンス・パラダイムを不用意に公開してしまうことです。

JSF ライフ・サイクルでは、アプリケーション・コードを基礎となるサーブレット・モデルと切り離しておくことによって、コンポーネント指向の設計を強力に推し進めています。Ajax と連動する際にこの抽象化を維持するには、下位レベルの単調でつらい作業を Seam Remoting または Ajax4jsf に引き渡します。この 2 つのライブラリーはいずれも、Ajax による対話動作を介して JSF コンポーネントをブラウザーに結合するためのパイプ役を引き受けてくれます。図 1 に示す Seam Remoting の動作を見てください。ユーザーがボタンをクリックするなどしてイベントがトリガーされると、サーバー・サイドのコンポーネントに非同期でメッセージが送信されます。それに対する応答を受信すると、その受信した内容を使ってページがインクリメンタルに更新されます。ブラウザーとサーバー・サイド・コンポーネントとの対話を行う際に使用される下位レベルの通信プロトコルは、API の背後に隠されています。

この連載について

「シームレスな JSF」 で紹介する Seam は、JSF に申し分なく適合する初のアプリケーション・フレームワークとして、他のどの拡張フレームワークでも補えないような JSF の主要な欠点を補います。この連載の記事を読んで、Seam が JSF を補完するものとして十分かどうか、皆さん自身で判断してください。

図 1 のユース・ケースでは、ボタンをクリックした後に実行されたメソッド呼び出しの結果がユーザーに示されます。このユース・ケースを検討するときには、重要な 2 つの点を念頭に置いておかなければなりません。まず、(1) ページは決してリフレッシュされないということ、そして (2) クライアント・コードは明示的に URL を作成してリクエストを送るのではなく、コンポーネントのメソッドと透過的に通信するということです。裏ではもちろん標準的な HTTP リクエストが使用されていますが、クライアント・コードが HTTP プロトコルと直接やり取りする必要はありません。

図 1. JSF のコンポーネントとブラウザーを結合する Seam Remoting
A use-case diagram for Seam Remoting

Seam Remoting と Ajax4jsf

Seam Remoting と Ajax4jsf は異なるライブラリーであり、それぞれが JSF の「Ajax 化」に貢献します。この 2 つのライブラリーが Ajax を使用して導入する対話モデルでは、ブラウザーとサーバー間の非同期通信をユーザーには見えないようにしてバックグラウンドで行うことが可能です。そもそも、サーバー・サイドでメソッドを実行するためだけにページをリロードして、ユーザーの時間を無駄にする必要はありません。これらのライブラリーが実行する Ajax リクエストによってサーバーから取得した情報を使えば、ページの状態を「リアルタイム」でインクリメンタルに更新することができます。いずれのライブラリーも、ブラウザーが必要とするときにコンポーネントの状態をリストアできるライフ・サイクルを装備しています。このような Ajax による対話動作は、リクエストというよりは「リストアと実行」と呼んだほうがいいかもしれません。ブラウザーはサーバーの肩をたたいてサーバー・サイドの管理対象 Bean のいずれかでメソッドを実行するようにお願いをし、その結果を返しているようなものです。

2 つのライブラリーの動作方法は異なりますが、互いに排他的というわけではありません。どちらも JSF コンポーネント・モデルに従っているため、簡単に組み合わせることができます。その方法については後で説明することにして、ここでは Ajax スタイルの対話動作を JSF アプリケーションに組み込むためのそれぞれの手法を簡単に説明しておきます。

  • Seam Remoting が提供する JavaScript API を使用すると、メソッド呼び出しでデータを送信するにしても取得するにしても、ローカル・オブジェクトにアクセスするかのように JavaScript でサーバー・サイド・コンポーネントにアクセスすることができます。Seam Remoting は JSF とは別のカスタム・ライフ・サイクルを使用してブラウザーがサーバー・サイド・コンポーネントと通信できるようにします。リクエスト中にリストアされるのは、Seam コンテナーとそのコンポーネントだけです。トランスポート・プロトコルは Ajax ですが、パケット転送方法の詳細を心配する必要はありません。
  • Ajax4jsf は、使っている JavaScript を完全に隠すことで抽象化をさらに進め、すべてのロジックを基本 UI コンポーネント内にラップします。Ajax4jsf は JSF ライフ・サイクル全体をとおして Ajax リクエストを使用します。そのため、Ajax 対応のコンポーネントはブラウザー・ナビゲーション・イベントを一切トリガーすることなく、サーバー上でアクション・ハンドラーを実行し、JSF コンポーネント・ツリーを更新してページを部分的に再度レンダリングすることが可能です。この場合も同じく、通信は Ajax によって行われますが、すべては隠されているのでページ開発者には見えません。Ajax4jsf のこのようなコンポーネント指向の手法は、Ajax 機能を非準拠の部外者としてではなく、JSF の一部としてすんなり取り込みます。

上記の 2 つの手法について掘り下げる前に、Ajax の基本にちょっと寄り道してみましょう。


2 つの世界の架け橋

アプリケーションを Ajax/Web 2.0 感覚の「リッチ」アプリケーションに変身させるには、Web ブラウザー (またの名をクライアント) がサーバー上のコンポーネントに直接アクセスできなければなりません。この構想を現実にするのは難問です。クライアントとサーバーの間にはかなり大きな隔たりがあるためです。この隔たり (またの名をネットワーク) の片側にはクライアント・ブラウザーがあり、もう片側にはサーバーとそのコンポーネントがあります。この 2 つを対話させることが、Ajax アプリケーションの最終目標です。

実際、従来の Web アプリケーションのほとんどではクライアントとサーバーの対話は実現していますが、ただそれは完全に一方通行で、話し手はサーバー、聞き手はブラウザーと決まっています。この一方通行の対話に身動きが取れなくなった経験は誰にでもあるはずです。Ajax 通信を使わない世界では、ブラウザーはどの URL にも同期リクエストを送信できるとしても、その後はサーバーが送り返してくる HTML が何であれ、それをレンダリングせざるを得ません。このような対話動作でのもう 1 つの欠点は、多くの場合、長い待ち時間を伴うことです。

HTTP の初歩的言語しか使えないブラウザー・クライアントは、サーバーが HTML を生成する方法にはまったく関与しないので、そのコンポーネントについての知識はまるでありません。ブラウザーの側からすると、ページの生成プロセスはまさにブラック・ボックスです。ブラウザーは URL という形式でサーバーに、さまざまな質問をし、リクエスト・パラメーターと POST データとしてパッケージ化したヒントをサーバーに渡すことができますが、それでもサーバーの言語で話しているとは言えません。ブラウザーにアプリケーションのサーバー・サイドのアクティビティーがわかるようにするには、一層高度な通信手段を確立する必要があります。決まりきったページ指向の手法ではとても太刀打ちできないからです。

Ajax クライアントとしての Web ブラウザー

クライアント・コンポーネントとブラウザー・コンポーネント間の壁を打ち砕く手法は、Seam Remoting と Ajax4jsf とでは異なるため、それぞれをどのように利用するかを理解することが肝心です。まず、Seam Remoting はブラウザー固有の言語である JavaScript で API を提供し、この API からサーバー・サイド・コンポーネントのメソッドにアクセスできるようにしています。これらのメソッドへのアクセスを可能にするには、メソッドに @Remote 注釈で明示的に「リモート」とマークを付ける必要があります。

Seam Remoting での呼び出しメカニズムは、ローカル・プロキシー・オブジェクトつまり「スタブ」を使って、リモート・サーバー上のコンポーネントのメソッドを JavaScript から呼び出せるようにしているという点で、Java RMI と似ています。クライアントに関する限り、このスタブ・オブジェクトがリモート・オブジェクトです。スタブは、実際のリモート・オブジェクトでメソッドを実行するという役目を持ちます。リモート・メソッドが呼び出されると、応答ではメソッド呼び出しの戻り値をカプセル化します。この戻り値がネットワークを経由して返ってくると、受け取り側で JavaScript オブジェクトとしてアンマーシャル (整列解除) されます。このように、Seam Remoting はブラウザーがサーバー固有の言語で対話できるようにすることによって、Java コードと JavaScript の世界を 1 つにします。最初からこの 2 つが同じ言語だと信じ込んでいた人にとっては、驚きのことかもしれません。

Ajax による対話

Ajax に対する Seam の手法によって、ブラウザーはサーバーの状態に関する込み入った詳細情報 (どの対話がアクティブになっているかなど) にまで関与します。後でリモート呼び出しを対話スコープのオブジェクトで実行するときには、Seam Remoting を使って対話コンテキストを制御できるかどうかが重要になってきます。

一方の Ajax4jsf は、JSF コンポーネント・タグを使って、UI イベントをサーバー・サイドの管理対象 Bean のアクション・ハンドラーに宣言によって関連付けます。これらのアクション・ハンドラーのメソッドには「リモート」とマークを付ける必要はありません。アクション・ハンドラーは従来の JSF アクション・ハンドラーだからです。つまり、管理対象 Bean の引数を持たない公開メソッドか、ActionEvent を受け入れる管理対象 Bean の公開メソッドのいずれかです。

Seam Remoting とは異なり、Ajax4jsf は JSF コンポーネント・ツリー内の変更をブラウザーに返すことができます。変更は、XHTML フラグメントという形で返されます。このフラグメントはページ上の個別の JSF コンポーネントに該当し、部分的なページ更新として表されるので、ブラウザーは新しいマークアップを使ってページの個別の領域を再レンダリングすることができます。これらのフラグメントは、Ajax4jsf コンポーネント・タグの reRender 属性を使用するという手段、あるいは属性 ajaxRendered="true" でマークされた Ajax 出力パネル内のテンプレートの領域をラップするという手段で具体的に要求されます。reRender 属性が示すのは再レンダリングする特定のコンポーネント一式で、コンポーネントはそれぞれのコンポーネント ID で参照されます。それとは対象的に、ajaxRendered="true" の使用は包括的な手法で、Ajax4jsf管理対象の Ajax リクエストが完了したときには常に「Ajax でレンダリングされた」すべての領域を更新するように指示します。

Ajax4jsf と Seam Remoting はいずれも、ブラウザーを基本的な HTML レンダラーから完全な Ajax クライアントに成熟させます。この 2 つのフレームワークを統合すれば、アプリケーションは実に見事なものになります。実際 (今から言うことはショッキングなことなので、落ち着いて聞いてください)、Seam Remoting と Ajax4jsf の機能を組み合わせれば、独自のカスタム Ajax JSF コンポーネントを開発する必要もなくなります。Ajax に対応しない既存の JSF コンポーネントでも、その宣言の内側に a4j:support タグをネストさせるだけで Ajax 通信に参加させることができるのです。UI コンポーネントの外側で操作しているときに (後で Google マップのマッシュアップで実行します)、サーバー・サイド・コンポーネントに対して情報の照会や更新を行ったり、操作を実行するように命令しなければならない場合は、Seam Remoting を使用して対話を管理することができます。

Seam Remoting と Ajax4jsf には機能的に重複するところもありますが、Ajax スタイルの対話動作をアプリケーションに追加するには両方が役立ちます。さらに、これから説明するように、この 2 つのライブラリーは JSF アプリケーションのシームレスな Ajax ソリューションを実現します。


Seam Remoting のクイック・スタート

Seam Remoting は理想的なソリューションに思えるけれども、実装するのが厄介だったら話は別だとお考えでしょう。心配は要りません。Seam Remoting をあの恐ろしいリモート EJB オブジェクトのようなものだと想像しているとしたら、それはまったく見当違いです。Seam Remoting API を使って JavaScript コードとサーバー・サイド・コンポーネントとが対話できるようにする最大の利点は、そのプロセスが信じられないくらいに単純であるということです。厄介な仕事はすべて Seam が引き受けてくれるので、XML を 1 行も編集しなくても使い始めることができます (最近は Java プログラミングより XML プログラミングの作業のほうが多いという方には肩の荷が下りたことでしょう)。

それでは、Seam Remoting で JSF アプリケーションを「Ajax 化」するために必要なステップを簡単に案内しましょう。

Bean を公開する

サーバー・サイドのオブジェクト・メソッドをリモート Ajax クライアントに公開するための要件はたった 2 つしかありません。まず、メソッドが Seam コンポーネントの公開メンバーであること、そしてメソッドに @WebRemote 注釈が付いていることだけです。

実際にどれほど単純なのかは、リスト 1 を見ればわかります。このリストでは Seam コンポーネントの ReasonPotActiondrawReason() メソッドをリモートで実行する Ajax クライアントに公開しています。このメソッドがスタブで呼び出されるたびに、呼び出しがインターネットを介してサーバーに渡されます。サーバーは、サーバー・サイド・コンポーネントの対応するメソッドを使用して「次のプロジェクトに Seam を使用する 10 の理由」リストから 1 つの理由を無作為に選択し、その値をクライアントに返します (この 10 の理由についての詳細は、「参考文献」を参照してください)。

リスト 1. リモート・メソッド呼び出しの公開
@Name("reasonPot")
@Scope(ScopeType.SESSION)
public class ReasonPotAction {
private static String[] reasons = new String[] {
"It's the quickest way to get \"rich\".",
"It's the easiest way to get started with EJB 3.0.",
"It's the best way to leverage JSF.",
"It's the easiest way to do BPM.",
"But CRUD is easy too!",
"It makes persistence a breeze.",
"Use annotations (instead of XML).",
"Get hip to automated integration testing.",
"Marry open source with open standards.",
"It just works!"
};

private Random randomIndexSelector = new Random();

@WebRemote
public String drawReason() {
return reasons[randomIndexSelector.nextInt(reasons.length)];
}
}

リソースを準備する

サーバー・サイド・コンポーネントをセットアップしたら、次は、ブラウザーから @WebRemote メソッドを呼び出すように準備する必要があります。Seam は、リモート・メソッドを実行してその結果を返す HTTP リクエストを処理するためにカスタム・サーブレットを使用します。でも、心配要りません。そのサーブレットを直接操作する必要はないのです。Seam Remoting JavaScript ライブラリーが、XMLHttpRequest オブジェクトのあらゆる処理を行ってくれるのと同じように Seam サーブレットとの対話動作もすべて処理してくれます。

このサーブレットを意識する必要があるのは、アプリケーションで Seam Remoting をセットアップするときだけです。さらに嬉しいことに、カスタム・サーブレットのサービスを必要とする Seam 機能がいくつあったとしても、Seam のカスタム・サーブレットは 1 度構成するだけで済みます。アセットをブラウザーに提供する機能や、JSF 以外のリクエスト (Ajax リモーティング呼び出しなど) を処理する機能などの、各機能ごとに専用のサーブレットを使用する代わりに、Seam はこれらの作業を単一のコントローラー、Resource Servlet にバンドルしています。このサーブレットは代行チェーン・モデルを使用して、登録されたハンドラーに作業を代行させます。例えば、Remoting オブジェクト (後で説明) は Seam Remoting JavaScript ライブラリーから送信されたすべての Ajax リクエストを受け取るように自らを登録します。

Resource Servlet の XML 定義 (リスト 2 を参照) をインストールする場所はアプリケーションの web.xml ファイルです。通常は Faces Servlet の下にインストールします。

リスト 2. Seam Resource Servlet の XML 定義
<servlet>
<servlet-name>Seam Resource Servlet</servlet-name>
<servlet-class>org.jboss.seam.servlet.ResourceServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Seam Resource Servlet</servlet-name>
<url-pattern>/seam/resource/*</url-pattern>
</servlet-mapping>

API をブートストラップする

Seam Remoting メカニズムの背後にある真の魔法の威力は、JavaScript ライブラリーで発揮されます。Resource Servlet はこれらのライブラリーのための作業を Remoting オブジェクトに委任します。対象となる JavaScript ライブラリー (2 つ) は、該当するフックをブラウザーから利用できるようにして、JavaScript を介してリモート・メソッドを呼び出せるようにします。

ページには、リスト 3 に示す 2 つの JavaScript ライブラリーを両方ともインポートしてください。最初の remote.js は静的なライブラリーで、Seam のクライアント・サイドのリモーティング・フレームワークをブラウザーに提供します。2 番目の interface.js はリクエストごとに動的に作成されるライブラリーです。このライブラリーには、サーバー・サイド・コンポーネントを操作するのに必要なリモート・スタブと複合型が含まれます。Seam はメソッドをリモートとして指定しているコンポーネントすべてに対してスタブと複合型を生成するわけではありません。代わりに、interface.js URL の ? 記号に続くクエリー・ストリングを解析して、公開するコンポーネントの名前を収集します。各コンポーネント名は &amp; 記号で区切られます。このようにコンポーネントを連ねていく手法で、外部 JavaScript ファイルに対するブラウザーからのリクエスト数を最小限に抑えた上で、Seam が収集されたコンポーネントにのみスタブと複合型を生成します。リスト 3 はその一例です。この 2 つの JavaScript は Seam Remoting ライブラリーをロードし、Seam に reasonPot コンポーネントと anotherName コンポーネントを作成するように指示しています。

リスト 3. クライアント・サイド・フレームワークと API のインポート
<script type="text/javascript"
src="seam/resource/remoting/resource/remote.js"></script>
<script type="text/javascript"
src="seam/resource/remoting/interface.js?reasonPot&amp;anotherName"></script>

以上で、準備は完了です。Seam Remoting のセットアップが完了したので、今度はリモート呼び出しを作成する番です。


リモート呼び出しの実行

Seam Remoting ライブラリーのためのクライアント・サイド・フレームワークのコードはすべて、Seam JavaScript オブジェクトにカプセル化されます。この最上位レベルのオブジェクトは実際には単なる名前空間で、一連の機能を固有の名前でバンドルすることを可能にするコンテナーです。JavaScriptでは、名前空間は静的プロパティー、静的メソッド、そしてその他のネストされた名前空間オブジェクトが含まれるオブジェクトの 1 つでしかありません。重要な点は、Seam Remoting ライブラリーの JavaScript は他の JavaScript ライブラリーとも有効に機能するということです。つまり、Prototype や Dojo などのライブラリーを利用して、JSF UI をサーバー・サイド・コンポーネントと対話をさせることができます。これまで JSF 開発者にとって、JSF UI でサードパーティーの JavaScript ライブラリーを使用できるようにするという柔軟性が大きな課題だったので、Seam JavaScript オブジェクトは願ってもない追加です。

Seam Remoting ライブラリーの機能は、Seam.ComponentSeam.Remoting の名前空間オブジェクトに分けられます。Seam.Component の静的メソッドはリモート Seam コンポーネントへのアクセスを提供する一方、Seam.Remoting の静的メソッドはリモーティング設定の制御とカスタム・データ型の作成に使用されます。この後、サンプルで説明するように、カスタム・データ型はリモート・メソッドを呼び出すためにローカル・サイドで作成する必要がある非プリミティブ・オブジェクトです。

実行の詳細

リモート・メソッドを実行するには、まずメソッドをホストするコンポーネントのインスタンスを取得する必要があります。既存の Seam コンポーネント・インスタンス (サーバー・サイドに存在するという意味) を使う場合は Seam.Component.getInstance()、新しいインスタンスをインスタンス化してから呼び出しを行う場合はSeam.Component.getInstance() を使用します。これらのインスタンスは、実際には Seam が動的に作成するリモート・スタブです。このスタブはすべてのメソッド呼び出しのプロキシーとなってコンポーネント名、メソッド、引数を XML にマーシャルし (整理して配置し)、この XML ペイロードを Ajax リクエストによってサーバー・サイド・コンポーネントに渡します。このリクエストはサーバー上のリモーティング・フレームワーク・コードが受け取り、XML をアンマーシャルしてコンポーネント名、メソッド、引数を抽出してからサーバー・サイド・コンポーネントのインスタンスでメソッドを呼び出します。最後にサーバーが Ajax 呼び出しの応答で戻り値をクライアントに送信します。ここでもすべての作業はスタブによって隠されるため、JavaScript コードは大幅に単純化されます。

背景の説明はこれくらいで十分として、セッション・スコープの reasonPot コンポーネントで drawReason() メソッドを実行する方法をリスト 4 に記載します。このコンポーネントは、リスト 1 でリモート・サービスとしてフラグが付けられたものです。コンポーネント・スタブが取得された後、このメソッドが他のすべての JavaScript メソッド呼び出しと同じように実行されるかどうかに注目してください。

リスト 4. リモート・メソッドの呼び出し
<script type="text/javascript">
Seam.Component.getInstance("reasonPot").drawReason(displayReason);

function displayReason(reason) {
alert(reason);
}
</script>

リスト 4 には、スタブでのメソッドとサーバー・サイド・コンポーネントでの「実際の」メソッドとの重要な違いが示されています。何が違うがおわかりですか? コンポーネント・スタブでのすべての呼び出しは非同期で行われるということを考慮してください。つまり、リモート・メソッド呼び出しの結果は、実行中のスレッドではすぐに使用できないということです。そのためスタブでのメソッドには、対応するサーバー・サイドのメソッドが返す値を持っているかどうかにかかわらず、戻り値がありません。実行されたリモート・スタブのメソッドは、事実上こう言っているのと同じことです。「そうだな... それについては後で報告するよ。連絡先の番号は?」。ここで提供される番号が、「コールバック」JavaScript 関数です。この関数が実際の戻り値を受け取ります。

このコールバック関数は、非同期 API の標準構成の 1 つで、Ajax リクエストの応答がブラウザーに返ってきたときに Seam Remoting JavaScript フレームワークによって実行されます。サーバー・サイド・コンポーネントのメソッドに void 以外の戻り値がある場合、その値がコールバック関数の唯一の引数として渡されます。一方、サーバー・サイド・コンポーネントのメソッドの戻り値の型が void の場合は、このオプション・パラメーターを破棄することができます。

対話スコープ Bean

リモーティングのニーズの大部分は、今まで説明した API で満たすことができます。サーバー・サイド・コンポーネントを取得するためのリクエストや、その公開メソッドのいずれかを実行するためのリクエストを行うときには、そのコンポーネントがリモーティング・ライフ・サイクルのスコープ内になければなりません。セッション・スコープ Bean は同じブラウザー・セッション内で行われる HTTP リクエストからは常に使用可能であるため、セッション・スコープ Bean を参照する際には追加の作業は要りません。

ただし対話スコープ Bean となるとそれより厄介になります。対話スコープ Bean は、対話トークンの有無に応じて HTTP リクエストと関連付けられるからです。対話スコープ Bean を操作するときには、リモート呼び出しの実行期間中に正しい対話コンテキストを確立できなければなりません。対話を再びアクティブにするのは、リモート・リクエストと併せて送信される対話トークンです。送信する対話トークンを指定さえすれば、Seam Remoting フレームワークがこの対話の詳細を処理してくれます。

同じウィンドウで実行する Ajax リクエストが、サーバーと複数の別々の対話を行うことは、全く可能であるという点を忘れないでください。Seam がそれぞれの対話を区別できるようにするには、コンポーネント・インスタンス・スタブを取得する前に対話 ID を指定します。そのために使うのが、Seam.Remoting.getContext().setConversationId("#{conversation.id} ") です。

Seam は常に、#{conversation.id} 値バインティング表現の下に現行の対話 ID を公開します。JavaScript が実際に見るのは、式というプレースホルダーではなく、解決された値 (通常は番号) です。この値をリモーティング・コンテキストに正しく登録すると、以降のリクエストと併せて値が伝搬されます。逆に言うと、リモート呼び出しが行われた後に Seam.Remoting.getContext().getConversationId() を呼び出せば、前のリクエストで割り当てられた対話 ID を読み出すことができます。リモーティング呼び出しはこのように対話スコープのメリットを利用することで、ステートフルな振る舞いに参加できるというわけです。


Google による Open 18 アプリケーションのマッピング

リスト 1 の Reason Pot の例 (「Seam を使用する 10 の理由」) も面白いものでしたが、そろそろ Seam Remoting ライブラリーに本格的な作業を行わせる段階に来ました。「Seamless JSF, Part 2: Conversations with Seam」で紹介した Open 18 アプリケーションを思い出してください。Open 18 はゴルフ・コースの一覧です。ユーザーはコースのリストをブラウズして、特定のコースまでドリルダウンしてその詳細を表示することができます。Open 18 ではユーザーがコースを作成、更新、削除することもできます。Web 1.0 という当初の形では、ユーザーがこのアプリケーションと対話するたびに、ページがリロードされていました。

Open 18 アプリケーションを Ajax の魔法で拡張する方法はさまざまなので、この記事では複数の方法を試してみます。最初にできることは、Open 18 と Google マップとの間にマッシュアップを作成することです。最近のインターネットでは至るところでマッピング実装の実行に突き当たるので、Open 18 にもマッピング機能を追加すればユーザーに喜ばれるに違いありません。Seam Remoting API と Google マップ Geocoder API を組み合わせると、ゴルフ・コース一覧の各コースの位置を Google マップ上にプロットすることができます。これは、一覧をリストしたその直下に表示されます。

Google マップ API の使用方法

Google マップ API を使用するには、API キーの使用にサインアップする必要があります。これは完全に無料です。Google では、API の使用に関する統計を取ってサービスの乱用を抑制するという目的のためだけにキーを使用しているので、今後も無料登録が維持されるはずです。キーを要求するには Google アカウントが必要になりますが、キーの共有は利用規約により禁止されています。この条件に従うため、サンプルでは偽のキーを使っています。サンプル・アプリケーションをご使用のコンピューターで実行する際は、GOOGLE_KEY を自分のキーに置き換えてください。

Google が持つ世界のマップを拝借する

地理空間マップの作成と言うと難しく聞こえるかもしれませんが、Google マップ JavaScript API がほとんどの作業を代わりに行ってくれるとあれば、難しいことでも何でもありません。GMap2 クラスはマップを描画し、表示域でのスクロール・イベントとズーム・イベントに従って反応します。別の Google マップのクラス、GClientGeocoder が住所ストリングに基づいて地理空間座標を解決してくれます。私たちに必要な作業と言えば、マップを初期化して各コースのマーカーを追加するくらいのことです。

マーカーをマップ上に配置するには、まずリモート・メソッド呼び出しでサーバー・サイド・コンポーネントからコースのコレクションをフェッチします。次に、GClientGeocoder を使用して各コースの住所を地理空間の地点 (緯度と経度) に変換します。最後にそのポイントを使って、マップ上の対応する座標にマーカーを配置します。おまけの機能として、コースの一覧をリストしてあるそれぞれの行にコンパス・アイコンを設けて、編集アイコンの隣に表示させます。一覧の行でコンパス・アイコンをクリックすると、選択したコースが表示域に現れるまでマップがパンおよびズームされるという仕組みです。同時にマーカーの上にバルーンをレンダリングして、そのコースの名前、住所、電話番号、そして Web サイトを表示します。このバルーンは、マップ上のマーカーを直接クリックしたときにも表示されるようにします。このすべての機能を追加すると、アプリケーションは図 2 のように表示されることになります。

図 2. Google マップのマッシュアップのスクリーン・ショット
A screenshot of the Google Maps mashup.

マップ・コンポーネントとロケショーン指向のデータとの統合は概して興味深いものですが、このマッシュアップにはとりわけ大きな意義があります。ユーザーが実際に、マップ上でそれぞれのコースを見分けられるようになるからです。地図モードでは、ゴルフ・コースのプロパティーが薄い緑色でレンダリングされます。ズームインしていくと、透明なラベルがコース・エリアに表示され、そこにコースの名前が示されます。さらに面白いのは航空写真モードです。このモードではコースの地形が明らかになります。ズーム率が十分であれば、ティー・グラウンド、フェアウェイ、グリーンといったホールそれぞれの特徴まで示すことも可能です。このようなゴルフ・コースの位置と対話型マップ表示のマッシュアップは、作業するだけの見返りが十分にあります。


Google マップの統合

Google マップは、いとも簡単に Web アプリケーションに統合して組み込めます。前にも述べたように、Google マップはマップのレンダリングに関するすべての詳細を処理すると同時に、マップ上で地理空間の位置をプロットするための API を提供します。地理空間の位置を解決して必要な緯度と経度を返すという厄介な作業は、GClientGeocoder オブジェクトが引き受けてくれます。

住所を地理空間の地点に変換するメソッド・スタブは、Seam Remoting メソッド・スタブと同じように機能します。このメソッド・スタブが呼び出されると、戻り値をキャプチャーするためのコールバック関数がメソッドに渡されます。この時点で Ajax リクエストが Google HQ に送信され、ブラウザーに応答が返されるとコールバック関数が実行されます。郵便の宛先に応じて自由自在に位置をプロットすることを可能にしているのは、Google マップ API のインテリジェントなインターフェースです。コースごとに地理空間の座標を維持するとなると実に厄介なので、その心配から解放されるのは嬉しいことです。この API の追加ルーチンによって、マップ上で緯度と経度のデータによって位置のマーカーが作成されて、レンダリングされます。

カスタマイズしてみましょう!

Google マップの表示を構成するには、数え切れないほどのオプションがあります。マップ自体の構成はこの記事の焦点としているところではありませんが、「参考文献」に優れた資料を紹介しているので、ぜひご自分で構成してみてください。オブジェクト指向の構造を使ってロジックをカプセル化する代わりに最上位レベルの JavaScript 関数を使用することには反対だという読者も、お気に召すままにコードをカスタマイズしてください。

マップの統合に関連する API メソッドは、Geocoder.getLatLng()GMap.addOverlay() の 2 つです。まず、Geocoder.getLatLng() メソッドが住所ストリングを GLatLng ポイントにします。このデータ・オブジェクトはただ単に緯度と経度の値のペアをラップするに過ぎません。このメソッドへの 2 番目の引数はコールバック JavaScript 関数で、Google HQ との通信が完了すると実行されます。続いてこの関数が GMap.addOverlay() を使用してマーカー・オーバーレイをマップ上に追加します。デフォルトでは、マーカーは赤いピンとしてレンダリングされます。このピンのヒントが、マップ上で住所の位置を示します。

リスト 5 は、Google マップをセットアップしてマーカーを追加する JavaScript コードです。関数は実行順にリストされています。このリストでは、リスト 3 で使用した Seam Remoting スクリプトに新しい Google マップ API スクリプトのインポートが追加されています。

リスト 5. Open 18 と Geocoder とのマッピング
<script type="text/javascript"
src="http://maps.google.com/maps?file=api&amp;v=2.x&amp;key=GOOGLE_KEY"></script>
<script type="text/javascript"
src="seam/resource/remoting/resource/remote.js"></script>
<script type="text/javascript"
src="seam/resource/remoting/interface.js?courseAction"></script>
<script type="text/javascript">
// <![CDATA[
var gmap = null;
var geocoder = null;
var markers = {};
var mapIsInitialized = false;

GEvent.addDomListener(window, 'load', initializeMap);

/**
* Create a new GMap2 Google map and add markers (pins) for each of the
* courses.
*/
function initializeMap() {
if (!GBrowserIsCompatible()) return;
gmap = new GMap2(document.getElementById('map'));
gmap.addControl(new GLargeMapControl());
gmap.addControl(new GMapTypeControl());
// center on the U.S. (Lebanon, Kansas)
gmap.setCenter(new GLatLng(38.2, -95), 4);
geocoder = new GClientGeocoder();
GEvent.addDomListener(window, 'unload', GUnload);
addCourseMarkers();
}

/**
* Retrieve the collection of courses from the server and add corresponding
* markers to the map.
*/
function addCourseMarkers() {
function onResult(courses) {
for (var i = 0, len = courses.length; i < len; i++) {
addCourseMarker(courses[i]);
}

mapIsInitialized = true;
}

Seam.Remoting.getContext().setConversationId("#{conversation.id}");
Seam.Component.getInstance("courseAction").getCourses(onResult);
}

/**
* Resolve the coordinates of the course to a GLatLng point and adds a marker
* at that location.
*/
function addCourseMarker(course) {
var address = course.getAddress();
var addressAsString = [
address.getStreet(),
address.getCity(),
address.getState(),
address.getPostalCode()
].join(" ");
geocoder.getLatLng(addressAsString, function(latlng) {
createAndPlaceMarker(course, latlng);
});
}

/**
* Instantiate a new GMarker, add it to the map as an overlay, and register
* events.
*/
function createAndPlaceMarker(course, latlng) {
// skip adding marker if no address is found
if (!latlng) return;
var marker = new GMarker(latlng);
// hide the course directly on the marker
marker.courseBean = course;
markers[course.getId()] = marker;
gmap.addOverlay(marker);

function showDetailBalloon() {
showCourseInfoBalloon(this);
}

GEvent.addListener(marker, 'click', showDetailBalloon);
}

/**
* Display the details of the course in a balloon caption for the specified
* marker.  You should definitely escape the data to prevent XSS!
*/
function showCourseInfoBalloon(marker) {
var course = marker.courseBean;
var address = course.getAddress();
var content = '<strong>' + course.getName() + '</strong>';
content += '<br />';
content += address.getStreet();
content += '<br />';
content += address.getCity() + ', ' +
address.getState() + ' ' +
address.getPostalCode();
content += '<br />';
content += course.getPhoneNumber();
if (course.getUri() != null) {
content += '<br />';
content += '<a href="' + course.getUri() + '" target="_blank">' +
course.getUri().replace('http://', '') + '</a></div>';
}

marker.openInfoWindowHtml(content);
}

// ]]>
</script>

リスト 5 は第一印象では多少手ごわそうに見えますが、実際にはそれほどのものではありません。ページがロードされると、GMap2 コンストラクターによって Google マップがインスタンス化され、ターゲットの DOM 要素に挿入されて米国を基準にセンタリングされます。これで、Google マップの表示は完成です。思ったより簡単だと思いませんか? マップがインスタンス化されると、コース・マーカーが追加されます。このコードの最も重要な点はコース・マーカーを作成することなので、addCourseMarkers() 関数に的を絞って説明します。ここで関わってくるのが、Seam Remoting API です。

コースのピンポイント

JSF によってページがレンダリングされている間に対話スコープにロードされたコース・リストと同じリストをリモート呼び出しで取得すれば、サーバーのリソースが節約されることになります。このコースのコレクションを保持する対話をリモート呼び出し中に再度アクティブにするには、前に説明したように対話 ID をリモーティング・コンテキストで確立する必要があります。対話 ID を調べるため、現行の対話 ID を参照する値バインディング式 #{conversation.id} がページのレンダリングの際に解決され、その値が setConversationId() メソッドによってリモーティング・コンテキストに渡されます。コンポーネント・スタブを使用した以降のリモート・メソッド呼び出しでは、この値を渡して該当する対話をアクティブにします。対話がアクティブになれば、同じコース・リストが使用可能になります。

Aアクティビティー・インジケーター

サンプルを実行すると、呼び出しの送信中に 「Loading...」メッセージがページ右上隅に表示されます。Seam はこのメッセージによって、アクティビティーがバッググラウンドで行われていることを親切にユーザーに知らせます。メッセージを表示しないようにする場合、またはカスタマイズしたい場合は、Seam.Remoting JavaScript オブジェクトにそのためのメソッドがあります。

次のステップでは、コースを取得するために Seam.Remoting.getInstance() を使って courseAction という名前のコンポーネントへの参照を取得し、そのスタブで getCourses() メソッドを実行します。前にも説明したように、スタブ・メソッドはコールバック関数を最後の引数として使用し、そのコールバック関数で応答に含まれるコース・リストを受け取ります。戻り値は以前のようにプリミティブ型ではなく、ユーザーが定義した一連の JavaBean になることに注意してください。Seam は、サーバー・サイド・コンポーネントのメソッド・シグニチャーで使用されるあらゆる JavaBean クラスをエミュレートする JavaScript 構造を作成します。このサンプルの場合は、Seam は応答をすべて同じゲッターおよびセッターで Courseエンティティーを表す JavaScript オブジェクトにアンマーシャルします。リスト 5 では、コース・オブジェクトのゲッター・メソッドがコース・データを読み取るためにさまざまな場所で使用されています。

コースをマップ上に配置する最後のステップは、GClientGeocoderを使用して、それぞれのコースの住所を GLatLng ポイントに変換することです。その値を使って作成される GMarker ウィジェットが、オーバーレイとしてマップ上に追加されます。

マップでのコンパスの活躍

これでマップは見映えのいいマーカーで修飾されましたが、コース一覧の行をマップ上のマーカーと関連付ける手段がありません。そこで登場するのがコンパス・アイコンです。これから、Google マップ API を使用するちょっとした JavaScript を追加して、コンパス・アイコンがクリックされると、そのアイコンに対応するコースにズームしてマップがセンタリングされるようにします。リスト 6 は、コンパス・アイコンのコンポーネント・タグです。このコンポーネントは、コース一覧で各行の編集アイコンの直前に挿入されます (コースの編集機能については、「Seamless JSF, Part 2: Conversations with Seam」で説明しています)。

リスト 6. コンパス・アイコンをレンダリングするコンポーネント・タグ
<h:graphicImage value="/images/compass.png" alt="[ Zoom ]" title="Zoom to course on map"
onclick="focusMarker(#{_course.id}, true);" />

上記では、コンパス・アイコンの onclick イベントを処理するために JavaScript イベント・ハンドラー focusMarker() を登録しています。リスト 7 に示す focusMarker() メソッドがグローバル・レジストリー内の該当コースに以前登録されたGMarker を検索して、リスト 5showCourseInfoBalloon() 関数に作業を委任します。

リスト 7. マップを選択されたコースにフォーカスさせる focusMarker() イベント・ハンドラー
/**
* Bring the marker for the given course into view and display the
* details in a balloon.  This method is registered in an onclick
* handler on the compass icons in each row in the course directory.
*/
function focusMarker(courseId, zoom) {
if (!GBrowserIsCompatible()) return;
if (!mapIsInitialized) {
alert("The map is still being initialized. Please wait a moment and try again.");
return;
}
var marker = markers[courseId];
if (!marker) {
alert("There is no corresponding marker for the course selected.");
return;
}

showCourseInfoBalloon(marker);
if (zoom) {
gmap.setZoom(13);
}
}

コース・コレクションのリストア

リスト 8 に、以前フェッチされたコースを公開する役目を持つサーバー・サイド・コンポーネントのメソッドを示します (リストを簡潔にするため、「Seamless JSF, Part 2: Conversations with Seam」で Open 18 実装のために作成した CRUD アクション・ハンドラーは省略しています)。

リスト 8. getCourses メソッドを公開する CourseAction コンポーネント
@Name("courseAction")
@Scope(ScopeType.CONVERSATION)
public class CourseAction implements Serializable {
/**
* During a remote call, the FacesContext is <code>null</code>.
* Therefore, you cannot resolve this Spring bean using the
* delegating variable resolver. Hence, the required flag tells
* Seam not to complain.
*/
@In(value="#{courseManager}", required=false)
private GenericManager<Course, Long> courseManager;

@DataModel
private List<Course> courses;

@DataModelSelection
@In(value="course", required=false)
@Out(value="course", required=false)
private Course selectedCourse;

@WebRemote
public List<Course> getCourses() {
return courses;
}

@Begin(join=true)
@Factory("courses")
public void findCourses() {
System.out.println("Retrieving courses...");
courses = courseManager.getAll();
}

// .. additional CRUD action handlers ..
}

前にも説明したように、メソッド・スタブにはオプションのコールバック JavaScript 関数を最後の引数として組み込むことができます。クライアント・サイドのスタブの getCourses() オブジェクトが引数を 1 つ取る一方、それに対応するサーバー・サイド・コンポーネントのメソッドには引数がないのはそのためです。getCourses() メソッドは、ページがレンダリングされる時点で存在しているコースのコレクションを返します。


FacesContext のない人生

そろそろ、今まで説明したすべての Seam Remoting リクエストが JSF ライフ・サイクルではどのような役割を持つか気になってきたことでしょう。実は、JSF ライフ・サイクルではこれらのリクエストに出番はありません。少なくとも、通常のようには登場してこないと言えます。JSF ライフ・サイクルを呼び出していないことが、リクエストをこれほど軽量にしている理由です。JavaScript からメソッドがコンポーネント・スタブ上で呼び出されると、呼び出しは Seam Resource Servlet によって導かれ、Remoting 代行オブジェクトによって処理されます。Resource Servlet は JSF ライフ・サイクルを介してリクエストを取得することはしないので、リモート呼び出しの間は FacesContext を使用することはできません。代わりに Resource Servlet が使用するのは、Seam コンポーネント・コンテナーをただ単に利用するだけの独自のライフ・サイクルです。その結果、Seam Remoting API によってメソッドが実行される場合、Seam が管理するオブジェクトに帰着することができない値バインディング式はnullとなります。さらに、バッキング Bean が使用する JSF コンポーネントのバインディングも使用可能になりません。

Open 18 アプリケーション初期構成での CourseAction Bean は、値バインティング式 #{courseManager} によって CourseManager オブジェクトをバッキング Bean に注入する Spring framework の JSF 変数リゾルバーに依存しています。問題は、Spring 変数リゾルバーは Spring コンテナーの位置特定を FacesContext に依存することです。FacesContext が使用可能でないと、変数リゾルバーはどの Spring Bean にもアクセスできません。そのため、Seam Remoting API を介してコンポーネントにアクセスすると、値バインディング式 #{courseManager}nullになります。

このような状況は、リモート・メソッド呼び出し中にあらゆるものに対して CourseManager オブジェクトを使用しなければならない場合には問題になります。Seam のデフォルト動作は依存関係の存在を強制するようになっているので、@In 注釈には属性 required=false でマークを付け、これがオプションであることを示します。このようにすれば、リモート・スタブを使用して呼び出しが行われても Seam がきちんと動作してくれます。

以降のセクションでは、Ajax4jsf を使ってアプリケーションのコースの一覧のページとコース詳細ページを統合する方法を説明します。この拡張では、サービス層オブジェクトがコースへの変更を維持する必要があります。ただし、Ajax4jsf はアクション・ハンドラーを呼び出すときに完全なライフ・サイクルを使うので、#{courseManager} 式の解決方法は通常と同じです。


Ajax4jsf による再レンダリング

Google マップのマッシュアップは非常にエキサイティングなものですが、Open 18 アプリケーションに追加すると、ちょっとした設計上の問題を引き起こします。ユーザーがコースの詳細情報を表示するためにコース一覧のコース名をクリックすると、ページの更新が行われます。そのため、マップが頻繁にリロードされることになるのです。こうしたリロードはブラウザーにとってもサーバーにとってもオーバーヘッドを大幅に増やし、マップを初期位置にリストアしてしまいます。

Ajax4jsf プロジェクト

当初、Ajax4jsf プロジェクトは Exadel によって開始され、Java.net でホストされていましたが、最近の JBoss による Exadel 製品買収の一環として、Ajax4jsf ライブラリーも JBoss に吸収されています。Ajax4jsf を JSF と組み合わせると、イベント駆動型インターフェースという構想が見事に実現されます。ページのリロードという犠牲を招くことなく、サーバー上のアクションをイベントによってトリガーすることが可能になり、しかも UI は引き続きページ構造の変更を反映して更新されるのです。Ajax4jsf は、JSF をベースにした「リッチ」な Web プラットフォームを提供するという Seam の目的にはぴったりであることが実証されています。

マップがリロードされないようにするには、すべてのアクティビティーを単一のページ・ロードに制限しなければなりませんが、残念ながらフォームをサブミットするプロセスはページのリクエストに結合されています。この密結合は、Web ブラウザーのデフォルト動作です。ここで必要となるのはブラウザーにページを再度リクエストさせずにデータがサーバーに送信されるようにすることですが、Ajax4jsf コンポーネント・ライブラリーはまさにこの問題を解決するように設計されています。コースのリンクがクリックされるたびに Ajax4jsf にその魔法を発揮させるには、コース一覧のリストの Name 列 でh:commandLinkタグを a4j:commandLink タグに置き換えます。Ajax4jsf バージョンのリンクがアクティブになると、アクション属性 #{courseAction.selectCourseNoNav} のメソッド・バインディング式で指定されたメソッドが実行され、コース一覧の下に挿入されるコース詳細を含む置換マークアップが返されます。

selectCourseNoNav() メソッドは selectCourse() メソッドと同じロジックを実行しますが、このメソッドには JSF がナビゲーション・イベントを追跡することを確実にするための戻り値はありません。結局のところ、Ajax リクエストの要点はブラウザーがページを再度リクエストしないようにすることです。返された XHTML マークアップは、マップの状態を混乱させることなく、マップの下の領域に挿入されます。リスト 9 に示すコンポーネントは、「Seamless JSF, Part 2: Conversations with Seam」に記載した元のバージョンの courses.jspx で使用されている h:commandLinkに置き換わります。

リスト 9. Ajax4jsf が管理するコース選択用リンク
<a4j:commandLink id="selectCourse"
action="#{courseAction.selectCourseNoNav}" value="#{_course.name}" />

前にも述べたように、インクリメンタルに更新するページの領域を示す方法は 2 つあります。1 つ目の方法は、Ajax4jsf アクション・コンポーネントの reRender 属性にコンポーネント ID を指定して個々のコンポーネントを対象にする方法です。もう 1 つの方法では、ページの領域を a4j:outputPanel タグでラップし、そのタグの ajaxRendered 属性で領域に「Ajax によるレンダリング」のマークを付けます。リスト 10 に、出力パネルの手法で選択したコースの詳細を出力するビュー・テンプレートの領域を示します。

リスト 10. Ajaxでレンダリングされたコースの詳細パネル
<a4j:outputPanel id="detailPanel" ajaxRendered="true">
<h:panelGroup id="detail" rendered="#{course.id gt 0}">
<h3>Course Detail</h3>
<!-- table excluded for brevity -->
</h:panelGroup>
</a4j:outputPanel>

Ajax4jsf の構成

Ajax4jsf のカスタム・ビュー・ハンドラー

JSF 仕様のビュー・ハンドラー代行メカニズムはかなり制約されているため、ビュー・ハンドラー・チェーンの処理にはそれ程向いていません。Ajax4jsf では次善策として、カスタム・ビュー・ハンドラーを使用して実際のビュー・ハンドラーを修飾します。

更新したコードを実行するには、アプリケーションで Ajax4jsf を構成しておく必要があります。それにはまず、jboss-ajax4jsf.jar ライブラリーとその依存関係である oscache.jar をアプリケーションのクラスパスに追加します。次に、Ajax4jsf フィルターを web.xml に追加します。リスト 11 は、このフィルターの定義です。Ajax4jsf は web.xml ファイルの最初のフィルターとして定義しなければならないことに注意してください。このフィルターは、Seam の Resource Servlet とほとんど同じ方法で、Ajax4jsf コンポーネントが開始したリモート呼び出しを処理します。ただし Seam のリモート呼び出しとは異なり、非同期 Ajax4jsf リクエストの間は JSF ライフ・サイクルがアクティブになるので、通常のサーブレットではなくサーブレット・フィルターが必要になります。また、Facelets ビュー・ハンドラーの構成を faces-config.xml ファイルから web.xml に移し、サーブレット・コンテキスト・パラメーターに定義する必要もあります。

リスト 11. Ajax4jsf の構成
<context-param>
<param-name>org.ajax4jsf.VIEW_HANDLERS</param-name>
<param-value>org.jboss.seam.ui.facelet.SeamFaceletViewHandler</param-value>
</context-param>

<filter>
<filter-name>Ajax4jsf Filter</filter-name>
<filter-class>org.ajax4jsf.Filter</filter-class>
</filter>

<filter-mapping>
<filter-name>Ajax4jsf Filter</filter-name>
<url-pattern>*.action</url-pattern>
</filter-mapping>

リスト 11 のライブラリーを web.xml ファイルに組み込んでインストールしたら、Ajax4jsf を使用するための準備は完了です。Ajax4jsf タグ・ライブラリー定義、xmlns:a4j="https://ajax4jsf.dev.java.net/ajax" は必ず、Ajax4jsf タグを使用するビュー・テンプレートの先頭に追加してください。当然のことながら、このライブラリーで実現可能なことは 1 つの記事では説明しきれません。Ajax4jsf の使用方法に関する詳細と例については、「参考文献」を参照してください。


さらなる感動を追加!

ここまでのところ、Open 18 と Google マップのマッシュアップは完全に読み取り専用になっていますが、Ajax にまつわる素晴しさの大部分は、ページをリロードせずにバックエンドのデータを変更できることにあります。そこで、これからこの Google マップにちょっとした工夫を加えてデータを送り返せるようにしてみます。説明のため、Google がどんな郵便の宛先でも緯度と経度に解決できるとしても、ユーザーがピンのカスタム位置を指定することができるという前提にします。正確に言えば、ユーザーが特定のコースのクラブハウスや最初のティーを指定するにはピンが必要になるということです。嬉しいことに、Open 18 アプリケーションのこの最後の拡張は、Google マップ API を使えば至って簡単に実現できます。

まず、マーカーをドラッグできるようにするところから始めます。この機能を有効にするには、ただ単に、GMarker がインスタンス化されるときにドラッグ可能フラグを追加するだけです。次に、ユーザーがマーカーをドラッグし終わったときに起動される「ドラッグ終了」イベントをリッスンする JavaScript 関数を登録します。このコールバック関数のなかでは、courseAction スタブの新しいメソッド setCoursePoint() を実行してこの新しいポイントを保管します。

リスト 12 に、変更後のcreateAndPlaceMarker 関数を記載します。addCourseMarker() 関数も同じく変更して、マーカーを配置する際の Course エンティティーのカスタム・ポイントを考慮するようにしています。

リスト 12. マーカーをドラッグ可能にした更新後の JavaScript
function addCourseMarker(course) {
var address = course.getAddress();
if (course.getPoint() != null) {
var point = course.getPoint();
var latlng = new GLatLng(point.getLatitude(), point.getLongitude());
createAndPlaceMarker(course, latlng);
}
else {
var addressAsString = [
address.getStreet(),
address.getCity(),
address.getState(),
address.getPostalCode()
].join(" ");
geocoder.getLatLng(addressAsString, function(latlng) {
createAndPlaceMarker(course, latlng);
});
}
}

function createAndPlaceMarker(course, latlng) {
// skip adding marker if no address is found
if (!latlng) return;
var marker = new GMarker(latlng, { draggable: true });
// hide the course directly on the marker
marker.courseBean = course;
markers[course.getId()] = marker;
gmap.addOverlay(marker);

function showDetailBalloon() {
showCourseInfoBalloon(this);
}

function assignPoint() {
var point = Seam.Remoting.createType("com.ibm.dw.open18.Point");
point.setLatitude(this.getPoint().lat());
point.setLongitude(this.getPoint().lng());
var courseActionStub = Seam.Component.getInstance("courseAction");
courseActionStub.setCoursePoint(this.courseBean.getId(), point);
}

GEvent.addListener(marker, 'click', showDetailBalloon);
GEvent.addListener(marker, 'dragstart', closeInfoBalloon);
GEvent.addListener(marker, 'dragend', assignPoint);
}

まだ完了ではありませんが、完了まではあと一息です。残る作業はこの GLatLngポイントを保管するサーバー・サイド・コンポーネントにメソッドを追加することだけですが、もう少々我慢して説明を聞いてください。マッシュアップのこの最後の機能を完成するには、Open 18 アプリケーションの Spring 統合に立ち返らなければなりません (Spring 統合の詳細については、「Seamless JSF, Part 2: Conversations with Seam」を参照してください)。


Spring 統合の再登場

前にも説明したように、Spring コンテナーを Seam に統合するために最初に使った変数リゾルバーの手法には制約があります。正直言って、この手法では手に負えないところまで来ているので、今が別れを告げる時です。Seam の機能の大部分は JSF を必要としますが、一部は JSF ライフ・サイクルとは別のところで機能します。Spring との真の統合を実現するには、カスタム変数リゾルバーよりも優れたソリューションが必要です。幸い、Seam の開発者たちはこのニーズに対処するため、Spring を対象とした Seam 拡張機能を追加してくれています。Spring 統合パッケージでは、Spring 2.0 の新規機能を利用してスキーマ・ベースの拡張ポイントを作成します。Spring コンテナーの起動プロセス中にこれらのタグで動作する Bean 定義ファイルと名前空間のハンドラーでカスタム XML 名前空間を使用できるようにするのが、追加された Seam 拡張機能です。

Spring 対応 Seam 名前空間ハンドラー (何度か声に出して繰り替えさないと理解できないかもしれません) には、Spring コンテナーと対話する方法が複数あります。カスタム変数リゾルバーを排除するには、何らかの方法で Spring Bean を Seam コンポーネントとして公開する必要がありますが、そのために使うのがseam:componentタグです。このタグを Spring Bean 宣言に含めると、Spring Bean が Seam コンポーネントとしてプロキシー化 (つまり、ラップ) されることが Seam に通知されます。この時点で、Seam のバイジェクション・メカニズムが Bean を @Name で注釈が付けられたかのように扱うようになります。これでやっと、Seam と Spring との間のギャップを FacesContext で埋める必要がなくなるというわけです。

Seam と Spring の統合を構成するのは驚くほど簡単です。最初のステップではコードを変更する必要さえありません。必要な作業は、Spring 2.0 と Seam 1.2.0 (またはそれ以降のリリース) の両方を使用していることを確認するだけです (この連載の第 1 回が掲載されてから Seam は急速な変更過程を辿っているので、そろそろアップグレードに備えておく必要があります)。IOC 統合は jboss-seam-ioc.jar という個別の JAR としてパッケージ化されているので、前述の JAR の他にこの JAR をアプリケーションのクラスパスに含めてください。

次のステップにも Seam の構成は伴いません。ここで注目するのは、Spring の構成です。まず、Seam XML スキーマ宣言を、使用する Bean が定義されている Spring 構成 XML に追加します。このファイルの標準名は applicationContext.xml ですが、サンプル・アプリケーションではそれよりもふさわしい名前として spring-beans.xml というファイル名を付けています。次に、JSF に対して公開する Spring Bean の定義に seam:componentタグを含めます。Open 18 アプリケーションでは、このタグを courseManager Bean 定義のなかにネストします。完全な Bean リストを調べるには、この記事のサンプル・アプリケーションの spring-beans.xml ファイルを参照してください。

このように改善した Spring 統合を配置すると、courseManager プロパティーの上の @In 注釈に値バインディングを指定する必要も、required=false 属性を使う必要もなくなります。代わりに create=true 属性で、Seam に Spring コンテナーから Bean をフェッチして、それを Seam コンポーネントとして修飾するように指定します。リスト 13 のコードに記載する CourseAction クラスの抜粋は、コースの地理空間の地点を更新できるようにするために必要な部分です。

リスト 13. CourseAction の新規 setPoint メソッド
@WebRemote
public void setCoursePoint(Long id, Point point) {
System.out.println("Saving new point: " + point + " for course id: " + id);
Course course = courseManager.get(id);
course.setPoint(point);
courseManager.save(course);
}

さあ、サングラスをかけてふんぞり返ってください。そうです。あなたは見事にアプリケーションの「Ajax 化」に成功したのです。


まとめ

この記事では、Open 18 アプリケーションをかなり単純な CRUD 実装から、Ajax スタイルのページ・レンダリングと Google マップを統合する極めて洗練されたマッシュアップに拡張する方法を案内してきました。また、Seam Remoting API と Ajax4jsf で実現できる機能、そしてさらに高度な Seam と Spring フレームワークとの統合方法も説明しました。

Seam Remoting と Ajax4jsf はいずれも、その抽象化を拡張して Ajax 通信を組み込むことによって、JSF のコンポーネント・モデルを補完します。この記事に記載したコードのどれをとっても、XMLHttpRequest JavaScript オブジェクトを直接操作しなければならない箇所はありません。代わりにすべての作業を隠す高位レベルの API を使って、サーバー・サイド・コンポーネントを直接照会して操作したり、特定のページの領域でアクションを呼び出して更新することができます。さらに Ajax の魔法のおかげで、すべての操作は非同期で行われます。

この 3 回連載の記事をとおして、Seam による JSF 開発についてとても多くのことを理解してもらえたと思いますが、この連載で Seam の機能をすべて網羅したとは言えません。「シームレスな JSF」での説明やサンプルを出発点として、JSF プロジェクトの作成を楽しいものにする Seam の機能をさらに学んでください。Seam を使えば使うほど、その良さがわかるはずです。


ダウンロード

内容ファイル名サイズ
Open 18 sample application - Phase 21j-seam3.zip277K

  1. The sample application is organized as a Maven 2 project. All dependencies will be fetched on demand when the build is executed. The application uses several libraries from the Appfuse project to implement the service and DAO layers.

参考文献

学ぶために

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

  • JBoss Seam: バンドルされたサンプル・アプリケーションを含め、配布一式を入手してください。
  • Ajax4jsf: 既存の JSF コンポーネントに Ajax 機能を追加してください。
  • RichFaces: Ajax4jsf をベースとして、JSF Web アプリケーションにそのまま使える「リッチ」な機能を追加します。
  • Firebug: この Firefox のプラグインは、Ajax アプリケーションに取り組む際に欠かせないツールです。
  • Maven 2: ソース・コードのサンプルで使用した、ソフトウェア・プロジェクト総合管理ツールです。Maven はビルド・プロセス中に自動的に依存関係をダウンロードします。
  • Facelets: Seam アプリケーションに推奨される JSF ビュー・ハンドラーです。
  • Appfuse: 極めて包括的な Maven 2 ベースのプロジェクト・スケルトンを提供する Appfuse は、アプリケーション作成の基礎として使用できます。

議論するために

コメント

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=Java technology, Web development, Open source
ArticleID=240804
ArticleTitle=シームレスな JSF、第 3 回: JSF と相性のいい Ajax
publish-date=06122007