PhaseListener を使用した JavaServer Faces テクノロジへの AJAX の組み込み

Mark Basler
ステータス: Early Access

課題

Java Enterprise Edition 5 プラットフォームには、拡張性があり、成熟したユーザーインタフェースコンポーネントモデルを提供する JavaServer Faces (JSF) テクノロジが含まれています。このモデルのデザインによって、アプリケーション開発者は JSF テクノロジに用意されている標準のコンポーネントを拡張して簡単にカスタムコンポーネントを作成し、アプリケーションにまたがってそれらコンポーネントを再利用できます。AJAX を JSF と組み合わせて使用する方法は数多くあります。このエントリでは、PhaseListener を使用して AJAX 機能を JSF アプリケーションに組み込む方法について重点的に説明します。静的リソースおよび動的リソースにアクセスするためのさまざまな方法については、「静的リソースおよび動的リソースへのアクセス」を参照してください。代替のソリューションおよび関連するソリューションについては、「JavaServer Faces テクノロジと AJAX の使用」を参照してください。

対処法

開発者が JSF コンポーネントの一部として AJAX サポートを組み込む方法は複数あります。その 1 つとして、PhaseListener を使用して、カスタムコンポーネントの AJAX 要求を遮断し、要求を処理し、必要に応じてコンポーネントのリソースを提供する方法があります。

ここでは、PhaseListener を使用して JSF アプリケーションに AJAX サポートを組み込む方法について説明します。この方法では、クライアント側で AJAX 対話を実行するために必要な JavaScript を生成するカスタムコンポーネントを作成し、PhaseListener の形式で AJAX 要求を処理するサーバー側コンポーネントを作成します。

この方法は、次の場合に使用できます。

この方法には、次の問題があります。

前述の理由により、現時点では、サードパーティーのライブラリを使用する方法が推奨されます。この方法については、「静的リソースおよび動的リソースへのアクセス」で詳しく説明しています。新しいテクノロジについてはまだ調査中なので、推奨する方法に変更があった場合はエントリを更新します。

AJAX 要求の流れ


図 1. AJAX 要求送信時の処理の流れ


1) mouseover イベントハンドラで、XMLHttpRequest を生成して送信する JavaScript 関数がトリガーされます。URL には、コンテキストルート (bp-jsf-example)、FacesServlet マッピング (faces)、CompBPhaseListener に要求の処理を指示する、名前空間に依存するフラグ (bpui_compb_action)、faces-config.xml に登録されている管理対象 Bean の名前 (CompBBean)、呼び出す管理対象 Bean のメソッド (processMethod)、および要求に関連付けられている生成済みのクエリー文字列が含まれます。次に例を示します。

    /bp-jsf-example/faces/bpui_compb_action/CompBBean/processMethod?itemId=test1A

2) CompBPhaseListener が faces-config.xml ファイルに登録されているので、要求に対して呼び出され、ルート ID (URL の /faces サーブレットマッピングのあとの部分) で bpui_compb_action フラグが検索されます。フラグが見つかった場合は、ルート ID が管理対象 Bean 名とメソッド名に解析され、javax.el.MethodExpression の形式に変更されます。次に、管理対象 Bean のメソッドが呼び出されます。次に例を示します。

    #{CompBBean.processMethod}

3) 管理対象 Bean は faces-config.xml で管理対象プロパティー (itemId) が設定されているので、その値は要求から管理対象 Bean インスタンスに自動的に設定されます。次に、管理対象 Bean によって FacesContext から HttpServletResponse が取得され、要求とともに送信された itemId に適切な応答が作成されます。

4) XML 応答がブラウザに返され、コールバック関数が呼び出されます。

5) 応答が解析され、ポップアップの値が挿入されて、ポップアップが表示されます。


AJAX カスタムコンポーネントの作成

アプリケーションで使用するカスタムコンポーネントのデザインを開始するには、『Java EE 5 チュートリアル』の「Creating Custom UI Components」(カスタム UI コンポーネントの作成) の内容を理解している必要があります。ほかの開発者が再利用できるようにカスタムコンポーネントをデザインするには、考慮すべき点がいくつかあります。これらの点について簡単に説明します。

JSF 1.2 カスタムコンポーネントを作成するときに必要なオブジェクトは、タグライブラリ定義ファイル、JSF 構成ファイル (通常は faces-config.xml)、タグハンドラクラス、およびレンダリングクラスです。通常はカスタムコンポーネントクラスも開発しますが、すべての場合に必要なわけではありません。次の各項では、コーディング例として、bp-jsf-example サンプルアプリケーションの CompB カスタムコンポーネントを使用します。

サーバー側のコード:

JSF カスタムレンダリング (com.sun.javaee.blueprints.components.ui.example.CompBRenderer)

共通のアーティファクトのマークアップを生成するときは、要求マップにフラグを付けることで、生成済みかどうかを追跡できます。フラグが存在するかどうかはレンダリングのコードで確認します。フラグが存在しない場合は、フラグを設定し、共通のアーティファクトを生成します。フラグが設定されている場合はアーティファクトを再生成しません。このコンポーネントでは、FacesServlet が "/faces/*" URI にマップされていると想定しています。この単純なコンポーネントを外部に配布する場合は、ユーザードキュメントでこの依存関係について説明する必要があります。

Map requestMap=context.getExternalContext().getRequestMap();
Boolean scriptRendered=(Boolean)requestMap.get(RENDERED_SCRIPT_KEY);

// リソースが生成済みかどうかを確認する
if (scriptRendered != null && scriptRendered.equals(Boolean.TRUE)) {
return;
}

// 生成済みであることを示すフラグを requestMap に付ける
requestMap.put(RENDERED_SCRIPT_KEY, Boolean.TRUE);

String contextRoot=context.getExternalContext().getRequestContextPath();

// javascript ファイルのマークアップタグを生成する
writer.startElement("script", component);
writer.writeAttribute("type", "text/javascript", null);
writer.writeAttribute("src", contextRoot + "/faces/jsf-example/compB/compB.js", null);
writer.endElement("script");
writer.write("\n");

このコンポーネントは詳細情報を表示するポップアップなので、表示される部分を実際に構成するマークアップを生成する必要があります。これには複数の方法があります。1 つは、テンプレートファイルを使用し、そのファイルを読み込み、名前空間の規則に従うために必要な値のトークン置換を行う方法です。ここでは、テンプレートテキストの "%%%ID%%%" トークンをコンポーネントの ID に置き換えて、コンポーネントの複数のインスタンスが衝突しないようにします。コンポーネント開発者は、ResponseWriter の startElement メソッドと endElement メソッドを使用して、レンダリング内でテンプレートを直接コーディングすることもできます。コンポーネントをツール対応にし、IDE で視覚的に利用できるようにする場合は、これがもっとも良い方法です。startElement 呼び出しがそれぞれコンポーネントに関連付けられるからです。これにより、ツールで個々のコンポーネントに属するマークアップを追跡しやすくなります。

 <div class="bpui_compB_popTop">
<div class="bpui_compB_cornerTL"><div class="bpui_compB_cornerTR">
<center><font color="white"><b><span id="%%%ID%%%_title">title</span></b></font></center>
</div></div>
</div>
<div class="bpui_compB_popMid">
<table border="0" style="width: 100%">
<tr>
<td>
<table border="0" bgcolor="#ffffff" cellpadding="5" cellspacing="5">
<tr>
<td><span id="%%%ID%%%_message">Value</span></td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<div class="bpui_compB_popBot">
<div class="bpui_compB_cornerBL"><div class="bpui_compB_cornerBR">
</div></div>
</div>


JSF カスタム PhaseListener (com.sun.javaee.blueprints.components.ui.example.CompBPhaseListener)
この例の PhaseListener は、さまざまな種類の要求を処理します。ルート ID に APP_KEY があり、この PhaseListener が対象とする種類に一致する接尾辞が付いている場合は、静的リソースを提供します。また、適切な ACTION_KEY を含む AJAX 要求を、ルート ID で指定されている管理対象 Bean のメソッドに転送します。

public void afterPhase(PhaseEvent event) {
FacesContext context=event.getFacesContext();
String rootId=context.getViewRoot().getViewId();

int iPos=rootId.indexOf(APP_KEY);
int iPosx=rootId.indexOf(ACTION_KEY);

// 内容の種類へのマップに使用されている接尾辞を確認する
if (rootId.endsWith(SCRIPT_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "text/javascript");
} else if (rootId.endsWith(CSS_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "text/css");
} else if (rootId.endsWith(GIF_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "image/gif");
} else if (rootId.endsWith(JPG_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "image/jpeg");
} else if (rootId.endsWith(PNG_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "image/x-png");
} else if (iPosx > -1) {
// 延期メソッド式を使用して呼び出すアクション
String methodx="#{" + rootId.substring(iPos + ACTION_KEY.length() + 1) + "}";
methodx=methodx.replace('/','.');

try {
Class[] argTypes = { PhaseEvent.class};
// メソッド式を作成する
MethodExpression mex=context.getApplication().getExpressionFactory().createMethodExpression(context.getELContext(),
methodx, null, argTypes);
Object[] args = { event };
// 管理対象 Bean のメソッドを呼び出す
mex.invoke(context.getELContext(), args);
} catch (Exception e) {
// 例外をログに記録する
e.printStackTrace();
}
}
}


JSF 管理対象 Bean (com.sun.javaee.blueprints.components.ui.example.CompBBean)
管理対象 Bean では、PhaseEvent から HttpServletReponse を抽出し、XML 応答を作成して返します。この例では XML が返されることを想定しているので、使用する内容の種類を text/xml に設定しています。AJAX コールバック関数は、詳細データを表示する前に、応答状態の 200 を確認するので、状態コードも設定します。また、この要求と応答のセットをキャッシュしないように応答ヘッダーを設定しています。これらのヘッダー値を設定しないと、一部のブラウザでは結果がキャッシュされます。その結果、そのあとの要求の URL が同じだった場合に、ブラウザで自動的に同じ応答が返されます。多くの場合、新しいクエリーも実行されません。このような動作は便利な場合もありますが、AJAX コンポーネントのポーリングでは、キャッシュの使用は望ましくありません。また、FacesContext の responseComplete メソッドを呼び出して、残りの JSF ライフサイクルを省略することを指定しています。

 public void processMethod(PhaseEvent event) {
HttpServletResponse response=null;
try {
FacesContext context=event.getFacesContext();
response=(HttpServletResponse)context.getExternalContext().getResponse();
StringBuffer sb=new StringBuffer();
response.setContentType("text/xml;charset=UTF-8");
response.setStatus(200);
// 同じ URL を使用したときに IE が要求を実行するようにキャッシュなしを設定する必要がある
response.setHeader("Pragma", "No-Cache");
response.setHeader("Cache-Control", "no-cache,no-store,max-age=0");
response.setDateHeader("Expires", 1);
sb.append("<response>");
// カスタム応答コードがここに入る
...
sb.append("</response>");
response.getWriter().write(sb.toString());
} catch (IOException iox) {
iox.printStackTrace();
} finally {
try {
response.getWriter().flush();
} catch (Exception ee) {}
// JSF ライフサイクルが継続されないように応答を完了に設定する
event.getFacesContext().responseComplete();
}
}


コンポーネントの faces-config.xml ファイル
faces-config.xml に登録されている、CompB コンポーネントに関連するアーティファクトを次に示します。標準コンポーネントファミリを使用するカスタムレンダリング、静的要求を処理して動的要求を転送する PhaseListener、および AJAX 要求に対する応答を作成する管理対象 Bean があります。管理対象 Bean には、ミュテータメソッドを通じて各要求のあとに事前設定される管理対象プロパティーがあります。要求に itemId パラメータが含まれない場合、itemId は null に設定されます。
 <!-- 標準コンポーネントファミリのカスタムレンダリングを登録する -->
<render-kit>
<renderer>
<component-family>javax.faces.Output</component-family>
<renderer-type>CompB</renderer-type>
<renderer-class>com.sun.javaee.blueprints.components.ui.example.CompBRenderer</renderer-class>
</renderer>
</render-kit>

<!-- コンポーネントのカスタム PhaseListener を登録する -->
<lifecycle>
<phase-listener>com.sun.javaee.blueprints.components.ui.example.CompBPhaseListener</phase-listener>
</lifecycle>

<!-- カスタム管理対象 Bean、管理対象プロパティー (要求の itemId パラメータ) を登録する -->
<managed-bean>
<managed-bean-name>CompBBean</managed-bean-name>
<managed-bean-class>com.sun.javaee.blueprints.components.ui.example.CompBBean</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>itemId</property-name>
<value>#{param.itemId}</value>
</managed-property>
</managed-bean>




クライアント側のコード:


CompB カスタムコンポーネントのタグは、JSP ページに配置します。次に詳細を示します。

 <ui:compB id="pop1" url="faces/bpui_compb_action/CompBBean/processMethod?itemId="/>

        <a href="#" onmouseover="bpui.compB.showPopup('pop1', event, 'test1A')" 
onmouseout="bpui.compB.hidePopup('pop1')" style="cursor: pointer"><b>Mouse over link to see popup (test1A)</b></a><br/>
<small><i>(With a JSF Managed Bean fulfilling the AJAX Request)</i></small><br/><br/>

挿入されたカスタムタグが FacesServlet を使用して生成されると、HTML マークアップと JavaScript コードは次のようになります。
 <div id="pop1" class="bpui_compB_popup">            
<div class="bpui_compB_popTop">
<div class="bpui_compB_cornerTL"><div class="bpui_compB_cornerTR">
<center><font color="white"><b><span id="pop1_title">title</span></b></font></center>
</div></div>
</div>
<div class="bpui_compB_popMid">
<table border="0" style="width: 100%">
<tr>
<td>
<table border="0" bgcolor="#ffffff" cellpadding="5" cellspacing="5">
<tr>
<td><span id="pop1_message">Value</span></td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<div class="bpui_compB_popBot">
<div class="bpui_compB_cornerBL"><div class="bpui_compB_cornerBR">
</div></div>
</div>
</div>

<script type="text/javascript">
bpui.compB['pop1']=new bpui.compB.createPopup('pop1', '/bp-jsf-example/faces/bpui_compb_action/CompBBean/processMethod?itemId=');
</script>
新しい bpui.compB.createPopup オブジェクトを作成し、その結果を bpui.compB 名前空間の pop1 プロパティーに格納する JavaScript コードがあります。このオブジェクトは JavaScript クロージャーであり、bpui.compB.showPopup 関数によってポップアップがトリガーされるときに取得されます。この関数については、次の項で説明します。


JavaScript の関数:
次の JavaScript 関数は、CompB コンポーネントのマークアップが生成されるときに呼び出され、コンポーネントの識別子と URL とともに AJAX コールバック関数を保持するポップアップオブジェクトクロージャーを開始します。このコールバック関数は、応答が有効であることを確認して返される XML データを解析し、挿入して、ポップアップを表示します。
 // コンポーネントの ID と AJAX URL とともにコールバック関数を維持するクロージャーを作成する
bpui.compB.createPopup=function(componentId, urlx) {
this.componentId=componentId;
this.urlx=urlx;

this.ajaxReturnFunction=function() {
// 応答の準備ができていることを確認する
if (bpui.compB.req.readyState == 4) {
// 応答が有効であることを確認する
if (bpui.compB.req.status == 200) {
// 応答の情報をポップアップに挿入する
var resultx=bpui.compB.req.responseXML.getElementsByTagName("response")[0];
document.getElementById(componentId + "_title").innerHTML=resultx.getElementsByTagName("title")[0].childNodes[0].nodeValue;
document.getElementById(componentId + "_message").innerHTML=resultx.getElementsByTagName("message")[0].childNodes[0].nodeValue;;
// 新しい情報が挿入されたポップアップを表示する
document.getElementById(componentId).style.visibility='visible';
} else if (bpui.compB.req.status == 204){
// エラーの場合は警告を表示する
alert("204 returned from AJAX call");
}
}
}
}



次の JavaScript 関数は、ポップアップトリガーオブジェクトによって使用され、ポップアップを配置し、AJAX 要求を送信する関数をトリガーするタイムアウト値を設定します。

 bpui.compB.showPopup=function(popupx, eventx, itemId) {
// イベントに基づいてポップアップを配置し、ポップアップが常にフラッシュしないようにタイムアウトを設定する
var xx=0;
var yy=0;
if (!eventx) var eventx=window.event;
if (eventx.pageX || eventx.pageY){
xx=eventx.pageX;
yy=eventx.pageY;
} else if (eventx.clientX || eventx.clientY) {
xx=eventx.clientX + document.body.scrollLeft;
yy=eventx.clientY + document.body.scrollTop;
}
document.getElementById(popupx).style.left= (xx + 3) + "px";
document.getElementById(popupx).style.top=yy + "px";
// ポップアップは常に表示するのではなく、マウスを 1 秒以上置いた場合にのみ表示する
bpui.compB.timeout=setTimeout("bpui.compB.showPopupInternal('" + popupx + "', '" + itemId + "')", 1000);
}


次の JavaScript 関数は、XMLHttpRequest を開始し、JavaScript ポップアップオブジェクトを識別子によって取得します。次に、AJAX URL と itemId を連結し、特定のポップアップの AJAX コールバック関数を設定して、要求を送信します。
 bpui.compB.showPopupInternal=function(popupx, itemId) {
// AJAX 要求を初期化する
bpui.compB.req=bpui.compB.initRequest();
// 表示されている正しいポップアップオブジェクトを取得する
popObject=bpui.compB[popupx];

// itemId 値と URI を連結する
url=popObject.urlx + escape(itemId);
// 正しいポップアップのコールバック関数を設定する
bpui.compB.req.onreadystatechange = popObject.ajaxReturnFunction;
bpui.compB.req.open("GET", url, true);
// 要求を送信する
bpui.compB.req.send(null);
}



ビュー全体に対する AJAX 要求の処理:

前述の例では、FacesServlet を使用して、静的リソース (JavaScript ファイルと CSS シート) の提供と、動的リソース (AJAX 要求) の委託を行いました。別の方法として、HTTP POST を使用して、実際のビュー状態を AJAX 要求とともにサーバーに返す方法があります。このためには、通常のページ処理を打ち切り、AJAX 要求に対する JSP ページ全体の描画が行われないようにする AJAX 対応のコンポーネントが必要です。そのためには、FacesContext に対する ResponseComplete メソッドを使用して、残りのフェーズを省略します。この方法のもう 1 つの相違点は、PhaseListener で静的リソースの生成だけを行い、レンダリングで AJAX 要求を処理して XML 応答を作成することです。

AJAX 要求量を基準に各要求に関連付けるビュー状態量を決定する際のパフォーマンスを考慮する必要があります。この方法は、JSF ライフサイクルと密に結合されているので、Faces オブジェクトにすぐにアクセスできますが、ビュー状態と密に結合されていない方法と比較して、パフォーマンスが低下する可能性があります。


参考資料


© Sun Microsystems 2006. Java BluePrints Solutions Catalog の内容はすべて著作権保護されており、サン・マイクロシステムズ社の書面による許可なしに他の著作物に発表することを禁止します。