サーブレットを使用した JavaServer Faces テクノロジへの AJAX の組み込み

Mark Basler
ステータス: Early Access

課題

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

対処法

開発者が JSF コンポーネントの一部として AJAX サポートを組み込む方法は複数あります。その 1 つとして、サーブレットを使用して、カスタムコンポーネントの AJAX 要求を遮断し、処理する方法があります。JSF カスタムコンポーネントに必要なリソースは、アプリケーションバンドルにパッケージ化し、直接アクセスされるようにします。

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

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

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

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

このコンポーネント機能の簡単な例については、standalone.jsp を参照してください。1 つの JSP ページにすべてのスクリプトと機能が含まれています。


AJAX 要求の流れ



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

1) mouseover イベントハンドラで、XMLHttpRequest を生成して送信する JavaScript 関数がトリガーされます。URL には、コンテキストルート (bp-jsf-example)、web.xml 配備記述子で構成されているサーブレットマッピング (CompAServlet)、および要求に関連付けられている生成済みのクエリー文字列が含まれます。次に例を示します。
    /bp-jsf-example/CompAServlet?itemId=test1A
2) サーブレットで、要求が受信され、要求の itemId パラメータが取得され、適切な応答が作成および送信されます。

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

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


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

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

サーバー側のコード:

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

共通のアーティファクトのマークアップを生成するときは、要求マップにフラグを付けることで、生成済みかどうかを追跡できます。フラグが存在するかどうかはレンダリングのコードで確認します。フラグが存在しない場合は、フラグを設定し、共通のアーティファクトを生成します。フラグが設定されている場合はアーティファクトを再生成しません。

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 + "/compA.js", null);
writer.endElement("script");
writer.write("\n");

このコンポーネントは、詳細情報を表示するポップアップなので、表示される部分を実際に構成するマークアップを生成する必要があります。これには複数の方法があります。1 つは、レンダリング内でマークアップをハードコードし、名前空間の規則に従うために必要な値を設定する方法です。ここでは、コンポーネントの ID を前に付けて、複数のインスタンスが衝突しないようにします。コンポーネント開発者は、ResponseWriter の startElement メソッドと endElement メソッドを使用して、レンダリング内でテンプレートを直接コーディングすることもできます。コンポーネントをツール対応にし、IDE で視覚的に利用できるようにする場合は、これがもっとも良い方法です。startElement 呼び出しがそれぞれコンポーネントに関連付けられるからです。これにより、ツールで個々のコンポーネントに属するマークアップを追跡しやすくなります。
 
writer.write("<div class=\"bpui_compA_popTop\">");
writer.write(" <div class=\"bpui_compA_cornerTL\"><div class=\"bpui_compA_cornerTR\">");
writer.write(" <center><font color=\"white\"><b><span id=\"" + clientId + "_title\">title</span></b></font></center>");
writer.write(" </div></div>");
writer.write("</div>");
writer.write("<div class=\"bpui_compA_popMid\">");
writer.write(" <table border=\"0\" style=\"width: 100%\">");
writer.write(" <tr>");
writer.write(" <td>");
writer.write(" <table border=\"0\" bgcolor=\"#ffffff\" cellpadding=\"5\" cellspacing=\"5\">");
writer.write(" <tr>");
writer.write(" <td><span id=\"" + clientId + "_message\">Value</span></td>");
writer.write(" </tr>");
writer.write(" </table>");
writer.write(" </td>");
writer.write(" </tr>");
writer.write(" </table>");
writer.write("</div>");
writer.write("<div class=\"bpui_compA_popBot\">");
writer.write(" <div class=\"bpui_compA_cornerBL\"><div class=\"bpui_compA_cornerBR\">");
writer.write(" </div></div>");
writer.write("</div>");

Java サーブレット (com.sun.javaee.blueprints.components.ui.example.CompAServlet)
HttpServlet は、HttpServletResponse を引数として受け取り、XML 応答を生成して返します。この例では XML が返されることを想定しているので、使用する内容の種類を text/xml に設定しています。AJAX コールバック関数は、詳細データを表示する前に、応答状態の 200 を確認するので、状態コードも設定します。また、この要求と応答のセットをキャッシュしないように応答ヘッダーを設定しています。これらのヘッダー値を設定しないと、一部のブラウザでは結果がキャッシュされます。その結果、そのあとの要求の URL が同じだった場合に、ブラウザで自動的に同じ応答が返されます。多くの場合、新しいクエリーも実行されません。このような動作は便利な場合もありますが、AJAX コンポーネントのポーリングでは、キャッシュの使用は望ましくありません。

 protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/xml;charset=UTF-8");
PrintWriter out = response.getWriter();
String itemId=request.getParameter("itemId");

response.setHeader("Pragma", "No-Cache");
response.setHeader("Cache-Control", "no-cache,no-store,max-age=0");
response.setDateHeader("Expires", 1);
if(itemId != null) {
out.println("<response>");
// カスタム応答コードがここに入る
...

out.println("</response>");
} else {
out.println("<response>");
out.println("<title><![CDATA[REQUEST ERROR]]></title>");
out.println("<message><![CDATA[The query parameter 'itemId' required]]></message>");
out.println("</response>");
}
out.flush();
out.close();
}


コンポーネントの faces-config.xml ファイル
faces-config.xml に登録されたアーティファクトで CompA コンポーネントに関連するのは、標準コンポーネントファミリを使用するカスタムレンダリングだけです。
 <!-- 標準コンポーネントファミリのカスタムレンダリングを登録する -->
<render-kit>
<renderer>
<component-family>javax.faces.Output</component-family>
<renderer-type>CompA</renderer-type>
<renderer-class>com.sun.javaee.blueprints.components.ui.example.CompARenderer</renderer-class>
</renderer>
</render-kit>



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

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

 <ui:compA id="pop1" url="CompAServlet?itemId="/>

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

挿入されたカスタムタグが FacesServlet を使用して生成されると、HTML マークアップと JavaScript コードは次のようになります。
 <div id="pop1" class="bpui_compA_popup">
<div class="bpui_compA_popTop">
<div class="bpui_compA_cornerTL"><div class="bpui_compA_cornerTR">
<center>
<font color="white">
<b><span id="pop1_title">title</span></b></font>
</center>
</div></div>
</div>
<div class="bpui_compA_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_compA_popBot">
<div class="bpui_compA_cornerBL"><div class="bpui_compA_cornerBR">
</div></div>
</div>
</div>

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


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

this.ajaxReturnFunction=function() {
// 応答の準備ができていることを確認する
if (bpui.compA.req.readyState == 4) {
// 応答が有効であることを確認する
if (bpui.compA.req.status == 200) {
// 応答の情報をポップアップに挿入する
var resultx=bpui.compA.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.compA.req.status == 204){
// エラーの場合は警告を表示する
alert("204 returned from AJAX call");
}
}
}
}



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

 bpui.compA.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.compA.timeout=setTimeout("bpui.compA.showPopupInternal('" + popupx + "', '" + itemId + "')", 1000);
}


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

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


参考資料


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