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 要求を処理するサーバー側コンポーネントを作成します。
この方法は、次の場合に使用できます。
この方法には、次の問題があります。
前述の理由により、現時点では、サードパーティーのライブラリを使用する方法が推奨されます。この方法については、「静的リソースおよび動的リソースへのアクセス」で詳しく説明しています。新しいテクノロジについてはまだ調査中なので、推奨する方法に変更があった場合はエントリを更新します。
図 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) 応答が解析され、ポップアップの値が挿入されて、ポップアップが表示されます。
このコンポーネントは詳細情報を表示するポップアップなので、表示される部分を実際に構成するマークアップを生成する必要があります。これには複数の方法があります。1 つは、テンプレートファイルを使用し、そのファイルを読み込み、名前空間の規則に従うために必要な値のトークン置換を行う方法です。ここでは、テンプレートテキストの "%%%ID%%%" トークンをコンポーネントの ID に置き換えて、コンポーネントの複数のインスタンスが衝突しないようにします。コンポーネント開発者は、ResponseWriter の startElement メソッドと endElement メソッドを使用して、レンダリング内でテンプレートを直接コーディングすることもできます。コンポーネントをツール対応にし、IDE で視覚的に利用できるようにする場合は、これがもっとも良い方法です。startElement 呼び出しがそれぞれコンポーネントに関連付けられるからです。これにより、ツールで個々のコンポーネントに属するマークアップを追跡しやすくなります。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");
<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>
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) 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();
}
}
<!-- 標準コンポーネントファミリのカスタムレンダリングを登録する -->
<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>
<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/>
<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 関数によってポップアップがトリガーされるときに取得されます。この関数については、次の項で説明します。
// コンポーネントの 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");
}
}
}
}
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);
}
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);
}