概要
この記事ではPortletsとFaceletsを使ったリッチなWebアプリケーションを作成するためにどのようにJSF、DWR、DOJOを組み合わせることが可能か、そのアプローチを提示します。読者にはこれらのフレームワークとそれらが提供する機能について基礎的な知識があることを前提としています。
サンプルアプリケーション
この記事で議論するサンプルはプロダクト管理アプリケーションで、以下に挙げる機能をユーザに提供します。
- ユーザは名前を指定してプロダクトタイプを検索可能。
- ユーザがプロダクトを選択するとスプリットペインを使った新しい画面が開く。スプリットペインの左側には選択したプロダクトタイプに属するサブタイプがツリー形式(Windowsのエクスプローラのようなもの)でまとめて表示される。各プロダクトサブタイプはさらにそのサブタイプをもつことができる。右側のペインには次のようなラベルのタブをもったタブ付きペインが表示される。
- Products - 選択されたサブタイプに属するプロダクトの一覧を表示
- Add Product Subtype - 選択されたサブタイプに新しいプロダクトサブタイプを追加するための入力フォームを表示
- スプリットペインの下部にはプロダクトタイプ検索ページに戻るためのBackボタンがある
- サブタイプに紐付くプロダクトの一覧がスプリットペインの右側に表示される際、1ページに表示されるプロダクトは10件までとする。つまり画面上でページングと一覧のソートができなければならない。
- スプリットペインの左側に表示されたツリーは展開したり折りたたんだりできる。その際にページがリフレッシュされないようにしユーザエクスペリエンスを向上させる。
- ツリー上でプロダクトサブタイプを表している任意のノードを右クリックするとポップアップメニューが表示される。ポップアップメニューの選択肢には、新しいプロダクトサブタイプを選択されたサブタイプの子要素として追加、選択されたプロダクトサブタイプの削除、選択されたプロダクトタイプの子要素一覧をデータベースの最新データに更新、などがある。
- 選択されたプロダクトサブタイプに新しいプロダクトサブタイプが追加されたら、ページをリフレッシュしなくてもただちにツリー上に表示されなくてはならない。
図1はユーザがプロダクトタイプやサブタイプを選択したとき画面にどのように表示されるべきかを示したものです。
図 1. プロダクトサブタイプ詳細画面
DOJO
クロスブラウザなツリー構造、タブ付きペイン、スプリットペイン、ポップアップメニューなどを作成するにはかなりの時間が必要ですし、経験豊富な JavaScript/DHTMLプログラマであって初めて成し遂げられるものです。これらのクロスブラウザなUIウィジェットを提供するツールキットはたくさん存在しますが、リッチなイベントハンドリングモデルを提供しているものは多くありません。以下には、ユーザの要求があったときにサンプルアプリケーションが応答する必要のあるいくつかのユーザアクションを一覧しています。
ユーザアクション | アプリケーションの対応 |
ツリーノードを選択 | そのノードに属するプロダクトサブタイプの一覧を表示 |
ツリーノードを右クリック | 子要素となるプロダクトサブタイプの追加や選択したプロダクトサブタイプの削除などの選択肢をもったポップアップメニューの表示 |
タブ付きペインで 'Add Product Subtype' タブを選択 | 新規プロダクトサブタイプの情報の入力フォームを表示 |
ツリーを展開して子要素を表示させるために [+] ボタンをクリック | データベースから子要素の情報を読み込んでツリーに表示 |
DOJOはリッチなUIウィジェット(ツリーやタブ付きペインやポップアップメニューを含むが、それにとどまらない)を、サンプルアプリケーションでの利用に適したイベントハンドリングモデルと共に提供してくれるJavaScript/DHTMLツールキットです。
DWR
DWR(Direct Web Remoting)はJavaでAJAXアプリケーションを簡単に構築するためのAJAXフレームワークです。DWRは多くの機能を提供してくれます。以下にいくつか挙げますが、機能はこれら以外にもたくさんあります。
- JavaのクラスからJavaScriptを生成(JavaのクラスはAJAXリクエストを処理するために開発者が作成する)
- JSFのmanaged beanにAJAXリクエストを処理するJavaのクラスとしての役割をもたせることが可能
- コンバータを使ったJavaScriptの連想配列とJava beansとの間の相互変換
- コンバータを使ったJavaScriptの配列とJavaのコレクションとの間の相互変換
コンバータはDWRにおいて非常に重要な役割を果たし、きれいなプログラミングモデルを提供します。たとえば、ユーザが新しいプロダクトサブタイプの情報を入力し、それを保存するようにアプリケーションに要求したとき、Webレイヤでその情報を取り出す方法は2つあります。
- HttpServletRequestのgetParameterメソッドを使って新しいプロダクトサブタイプに関する全情報を取得する
- Crプロダクトサブタイプの全属性に対応するgetterとsetterをもつProductSubtypeというDTOを作成する。dwr.xmlファイルにProductSubtypeをbeanコンバータとして設定する。JavaScript側では単純に連想配列を生成して、AJAXリクエストを処理するJavaクラスに定義された、ProductSubtypeをパラメータとして受け取るメソッドに渡す。このケースではProductSubtypeがbeanコンバータを利用すると宣言されているので、DWRはJavaScriptの連想配列からProductSubtypeへの変換を行う。
後者のほうがきれいなプログラミングモデルを提供します。リクエストパラメータの取得を行いJavaで利用するDTOを自分で生成する必要はありません。
AJAXリクエスト処理では、ほとんどの時間がステータスコードの取得とJavaScriptコールバックメソッドでのメッセージやデータの取得、そしてユーザに何を見せて何を隠すかの決定に費やされます。そのようなシナリオではbeanコンバータはとても役に立ちます。
DOJOのリッチなイベントモデルを、JavaアプリケーションでAJAXリクエストを処理するためのDWRのクリーンなアプローチと組み合わせることで、DOJOのコンポーネントが生成したイベントをDWRに渡して処理するという高度にインタラクティブなWebアプリケーション(サンプルアプリケーションのような)を作成することができます。
問題点
- Portal環境において開発者はユーザインターフェースとなるHTMLの生成に関与しない。それはFaceletsに相当するXHTMLファイルを解析して生成される。JSF manged beanのプロパティが値としてHTML文字列をもっていたとしても、Portalはそれを解析せず、そのままでユーザインターフェース上に表示する。 UIウィジェットを生成するためにWebブラウザが実行するDOJO特有のHTMLを、どのようにして生成すればよいか?
- DOJOはリッチなイベントモデルを提供し、これらのイベントをJavaScript内でインターセプトすることができる。DWRフレームワークはサーバ側でこれらのイベントを受け取るために利用することができるが、対話状態はサーバ側のどこに保持すべきか?
- ツリーノードの数が数百を超えると、JavaScriptがIEやMozilla上でDOJOのツリーノードを生成するのに時間がかかる。ということは、ノード数が数百を超える場合、アプリケーションはDOJOのツリーウィジェットを使えないということなのだろうか?
- WebアプリケーションにおけるAJAXの使用について。画面上でユーザイベントが発生した際のHTMLまたはHTMLフラグメントの生成方法は? Javaコード内に書かれるべきか、外部ファイルから読み込むべきか? アプリケーションはAJAXリクエストへのレスポンス内で複雑なユーザインターフェースを表示することができるのか?
- AJAXを利用すると、コード内にHttpServletRequestのgetParameter("fieldName")メソッドが撒き散らかされメンテナンス性が低下するのではないか?
解決法
カスタムJSFコンポーネント
カスタムJSFコンポーネントはDOJOのTreeやSplitContainerに必要なHTMLを生成するために使用します。JSFコンポーネントに生成されたHTMLは常にブラウザによって解析されます。Portalによってテキストとして出力されるのではありません。次に示すXHTMLファイルの断片は、DOJOのツリーやタブ付きペインのようなウィジェットを生成するためにカスタムJSFコンポーネントがどのように使用されるのかを記しています。
<div xmlns="http://www.w3.org/1999/xhtml"
...
...
xmlns:dojo="http://dojotoolkit.org/"
xmlns:mytree="http://mytree.com/tree"
xmlns:mytab="http://mypane.com/tabPane">
<ui:composition>
<ui:define name="body">
<f:view>
<h:form styleClass="form" id="formId">
<div dojoType="SplitContainer" orientation="horizontal" sizerWidth="5" activeSizing="false" style="overflow:
auto; whitespace: nowrap; height: 550px; background: transparent; padding: 5px;" >
<div dojoType="ContentPane" sizeShare="20"
style="overflow: auto; whitespace: nowrap;">
<mytree:treeComponent backingBeanName="treeBackingBean"></mytree:treeComponent>
</div>
<div dojoType="ContentPane" sizeShare="80" style="overflow: auto; white-space nowrap;">
<mytab:tabPaneComponent/>
</div>
</div>
...
...
ツリーノードの数が大きい(200以上)場合、カスタムJSFコンポーネントは200を超えるノードのためのコードを生成すべきではありません。ノード数が200を超えると、IEはページのロード時にこれらのウィジェットを生成するのにかなりの時間を要します。サンプルアプリケーションのカスタムJSFコンポーネントは(ルートレベルにおいて)100個のツリーノードしか生成しません。そして最後に「もっと見る」というオプションを表示します。ユーザが「もっと見る」を選択したら、DWRがデータベースから残りのノードに関する情報を取得します。この情報は、プログラムでTreeNodeウィジェットを生成するためにJavaScriptのコールバックメソッドに渡されます。
MyFacesもDOJOのTreeを生成するコンポーネントを提供していますが、MyFacesのコンポーネントは一度に全てのノードを生成します。ツリーノードが1,000個に達するとこのコンポーネントは使い物にならなくなるので、これはよいアプローチではありません。
DWRとJSF
DWRではJavaのクラスを作成してdwr.xmlファイルにそのクラスを設定することが要求されます。DWRはdwr.xmlで設定された名前に基づいてJavaScriptファイル(.js拡張子をもつ)を生成します。
dwr.xmlから抜粋した次の設定情報は、どのようにJavaのクラスを設定すればよいかを示しています。
<create creator="jsf" javascript="AjaxBean" scope="request">
<param name="managedBeanName" value="ajaxBean" />
<param name="class"
value="com.somebean.AjaxBean" />
</create>
creator="jsf"
これはJavaのクラスがJSF managed beanとして設定されているということを表します。JavaのクラスにはJavaScriptによって実行されることになる全てのAJAXメソッドが定義されています。
<param name="managedBeanName" value="ajaxBean" />
これはmanaged beanの名前がfaces-config.xmlファイル内でajaxBeanと設定されていることを表します。
<param name="class"
value="com. somebean.AjaxBean" />
これは実際のJavaのクラスを参照しています。
javascript="AjaxBean"
これはJavaのクラスがJavaScriptコードによって参照されるときの名前です。
JavaScriptコード内でAjaxBeanを利用するには、このJavaScriptを<script>タグを使ってインポートする必要があります。
DWRで利用されるJavaのクラス(ただのプレーンなJavaのクラスです。これらのクラスはDWR独自のインターフェースやクラスを何も実装していません)は、AJAXリクエストを受け取るたびにインスタンス化されます。ということは、アプリケーションがAjaxBeanクラス内に状態を保持しなければならないとしても、新しいリクエストが来るとそれは消えてしまいます。AjaxBeanのメソッドは事実上ステートレスであることが要求されており、対話状態はどこか別の場所に保持されなくてはなりません。
AJAXリクエストはPortletRequestではありません。これは、DWRで利用されるJavaのクラスはPortletSessionオブジェクトにはアクセスできないということです。AJAXリクエストは単なるHTTPリクエストなのでDWRで利用されるJavaのクラスは HttpSessionオブジェクトにアクセスすることが可能です。Portlet仕様によると、HttpSessionとPortletSession は同期がとられていなくてはなりません。たとえば、PortletSessionオブジェクトに属性を追加したら、それをHttpSessionオブジェクトにも追加しなくてはなりません(同じ名前である必要はないです)。
たとえばJBoss ASの場合、managed beanがsomeManagedBeanという名前でPortalSessionに追加されると、同じオブジェクトがjavax.portlet.p.
サンプルアプリケーションでは対話状態をsomeManagedBeanに保持します。DWRで利用されるJavaのクラスはHttpSessionから someManagedBeanのインスタンスを取得し、そのプロパティを設定してユーザのその時点での対話状態を反映させます。
ビジネスサービス(たとえばSpringレイヤ内の)にアクセスし相互に作用を及ぼすのにAjaxBeanはService Locatorパターンを使うことができます。
注意: JSF managed beanはそれがfacesリクエストを受け取った後でのみインスタンス化されますので、対話状態を保存するのに利用しようとしているJSF managed beanがすでにfacesリクエストを受け取り済みであることを確認するようにしてください。そのmanaged beanのスコープはsessionでなくてはなりません。
HTMLテンプレート
AJAXを使って作業する際に直面する問題の一つは、ユーザアクションに基づく複雑なHTMLフラグメントの生成です。通常これはメンテナンスコストの高いアプリケーションを生む結果になり、時には、長期にわたってアプリケーションの要求に磨きをかける際のユーザインターフェースの変更さえも非常に困難にします。HTMLはtilesの概念をもちませんが、サンプルアプリケーションではtilesに似たものを実現可能です。
<table style="height: 80%; width: 100%; padding-bottom: 100px; visibility: {0};">
<tr valign="top">
<td>
<table align="left" valign="top">
<tr>
<td class="formLabel">Product Category:</td>
<td class="formField">{1}</td>
</tr>
<tr>
<td class="formLabel"><span class="required">*</span>Description:</td>
<td class="formField"><textarea rows="2" id="desc_field" cols="80"
name="desc_field"></textarea></td>
</tr>
<table>
<tr>
<td>{dataTable}</td>
</tr>
<tr>
<td>{dataScroller}</td>
</tr>
</table>
<tr valign="bottom">
<td align="right" style="padding-right: 50px;">
<table>
<tr>
<td><input type="button" class="inputButton" onclick="saveDetails('{2}');" value="Save"/></td>
</tr>
</table>
</td>
</tr>
...
...
上記のHTMLテンプレートファイルには2種類のプレースホルダが使われています。
- データプレースホルダ
- HTMLプレースホルダ
public static String getStringWithValues(String template, Object[] values) throws IncorrectNumberOfValues {
for(int i = 0; i < values.length; i++) {
int index = template.indexOf("{" + i + "}");
if(index == -1) {
throw new IncorrectNumberOfValues("The number of values passed is : " + values.length + "
which doens't match the number of placeholders in : " + template);
} else {
if(values[i] != null) {
template = StringUtils.replace(template, "{" + i + "}", values[i].toString());
} else {
template = StringUtils.replace(template, "{" + i + "}", "");
}
}
}
return template;
}
ここでtemplateは解析してプレースホルダをvalues配列の値で置き換える必要のあるHTMLテンプレートです。つまり、データプレースホルダの{0}はvalues配列の最初の要素で置き換えられ、{1}は二番目の要素で置き換えられます。以降も同様です。
{dataTable}と{dataScroller}はHTMLプレースホルダで、HTMLフラグメントによる置き換えを想定しています。 {dataTable}プレースホルダは、プロダクトカテゴリに含まれる各プロダクトに対応するレコードを表示するデータテーブルによって置き換えられることになっています。{dataTable}に対応するHTMLは別のHTMLテンプレート内にあります。HTMLプレースホルダを対応するHTMLで置き換えるアプローチには次の2つがあります。
- プログラムから行う: AJAXを処理するbeanの内部でプレースホルダをHTMLに置き換え可能。このアプローチはいかなる種類のフレームワークの作成も要求しない。
- プレースホルダとHTMLテンプレートの間のマッピングをpropertiesファイルかXMLドキュメント内に作成する。たとえば次のようなマッピングを行うためにpropertiesファイルが作成されるかもしれない。
{dataTable} = /WEB-INF/classes/templates/dataTableTemplate.html {dataScroller} = /WEB-INF/classes/templates/dataScroller.html
実行時にはこれらのプレースホルダは対応するテンプレートで置き換えられます。このアプローチでは、propertiesファイルを読みHTMLプレースホルダを対応するHTMLファイルで置き換えるためにHTMLテンプレートを解析する小さなフレームワークが必要です。
DWRのbeanコンバータ
Webアプリケーションは通常DTOを使って情報をWebレイヤからServiceレイヤに渡します。サンプルアプリケーションでは情報を JavaScriptからWebレイヤに渡すというレベルの異なったDTOを使います。たとえばプロダクト検索時、ユーザはプロダクト名・プロダクトコード・プロダクトIDという情報や検索方法(部分一致か完全一致か)を入力することができます。これらのパラメータはDTOを使ってサンプルアプリケーションのWebレイヤに渡されます。JavaScriptにおける連想配列の概念がこれらのパラメータをセットするために使われ、DWRはこれらの連想配列から対応するDTOへと変換を行います。連想配列内の名前はDTOのプロパティと一致しなくてはなりません。
以下はdwr.xmlにおけるDTOの設定方法を示しています。
<convert match="com.search.product.SearchCriteria" converter="bean"/>
このアプローチを利用するとサーバサイドのJavaコードが一段ときれいになります。なぜならAJAXリクエストに付いてくる各リクエストパラメータに対応する値を明示的に取得する必要がなくなるからです。
JSFのrendered属性
JSFのHTMLタグは全てrendered属性を持ち、そのHTMLウィジェットをユーザに見せるかどうかを決定するboolean型の値をとります。 HTMLテンプレートでこれと同じ機能性を実現可能です。これには、HTML内の式を解析してHTMLフラグメントを表示または隠すフレームワークの作成が求められます。
<exp:if value="someManagedBean.permissions.save">
<td><input type="button" class="inputButton" onclick="removeProduct('{0}');" value="Remove" /></td>
</exp:if>
ページ遷移にFacesRequestを使うか、HttpServletRequestを使うか
ユーザがプロダクト詳細ページでBackボタンをクリックしたら、プロダクト検索ページが表示されなければなりません。その際、もしBackボタンのクリックがAJAXリクエストを使って処理されているとしたら、アプリケーションはfaces-config.xmlで定義されたナビゲーションルールを活用していません。ですので、プロダクト詳細ページには、別のページに遷移する際にサーバにHttpServletRequest(やAJAXリクエスト)を投げるHTMLボタンよりもJSFのHTMLコンポーネントを使うほうが得策です。
検討した代案
Ajax4jsf
Ajax4jsfはJSFコンポーネントでAJAXを用いる方法を提供してくれますが、Portletのサポートはバージョン1.1.1で導入されました。サンプルアプリケーションの作成時点ではPortletはサポートされていませんでした。
HTMLとCSSによるツリー
HTMLの要素とCSSを連携させて用いてツリー構造を作成することが可能です。ですが、これらのツリー構造はイベントハンドリングモデルを全面的に欠いているため高度にインタラクティブなツリーウィジェットという要求には適しません。
RichFaces
RichFacesはAJAXの機能性をもつリッチなJSFコンポーネントを提供してくれます。RichFacesはAJAXの機能性を実現するのにAjax4jsfを使っています。
著者紹介
Ashish SarinはJavaによるWebアプリケーションの開発およびデザインに8年を超える経験をもつ。
References
DOJO
DOJOはJavaScriptで書かれたオープンソースのDHTMLツールキットです。更に詳しく知りたい方はhttp://www.dojotoolkit.org/aboutを参照してください。
DWR
DWR(Direct Web Reporting)はJavaアプリケーションでAjaxを簡単に扱えるようにしてくれます。更に詳しく知りたい方はhttp://getahead.org/dwr/documentationを参照してください。
RichFaces
http://labs.jboss.com/jbossrichfaces/
Ajax4jsf
http://labs.jboss.com/jbossajax4jsf/
Facelets
https://facelets.dev.java.net/