レベル: 中級 Andrei Cioroianu, Senior Java Developer and Consultant, Devsphere
2007年 9月 18日 連載「Ajax による JSF フォームの自動保存」の第 1 回では、Java™ 開発者である著者、Andrei Cioroianu が Ajax (Asynchronous JavaScript + XML) および JSF (JavaServer Faces) 技術を用いて、Web フォームを自動的に保存する Java アプリケーションを作成する手順を紹介しました。この手順に沿って、JavaScript と XMLHttpRequest を使ってフォーム・データを取得、エンコード、送信する方法、Ajax リクエストの処理に JSF リクエスト処理のライフ・サイクルを適応させる方法、JSF コンポーネント・ツリーから送信されたデータをサーバー側で取得する方法を理解できたはずです。3 回連載の 2 回目となるこの記事では、ブラウザー・セッション間で匿名ユーザーを識別する方法、複数のユーザーとページに対して自動保存されたフォーム・データを管理する方法、データ・リポジトリーを選択する方法、そしてスレッド・セーフの問題に対処する方法を説明します。
はじめに
この連載の第 1 回では、アプリケーションがサーバーでフォーム・データを自動的に保存し、ユーザーがブラウザーをいったん閉じてから開いたときにフォームがリストアされるようにするというシナリオを説明しました。このソリューションは、ユーザーのブラウザーが異常終了した場合にも、あるいはユーザーが Web フォームで Submit をクリックしないでアプリケーションを終了した場合にも効果があります。
この記事に付属のサンプル・アプリケーション (「ダウンロード」を参照) には、SupportForm.jsp という名前の典型的な JSF フォームが含まれています。このフォームのデータは、AutoSaveScript.js ファイルの JavaScript 関数によって定期的にサーバーに送信されます。SupportForm.jsp および AutoSaveScript.js の説明、そして JSF フェーズ・リスナーを使ってアプリケーション・ロジックに干渉することなく Ajax リクエストを処理する方法については、第 1 回を参照してください。
今回の記事で紹介するのは、自動保存されたフォーム・データを保持するスレッド・セーフなデータ・リポジトリーを作成する手順です。この手順では、データ構造の選択方法、そのデータ構造に JSF コンポーネント・ツリーから抽出したフォーム・データを入力する方法、JSF コンポーネントの状態をリストアする方法、データ・リポジトリーのメモリー・リソースを制限する方法、そしてデータ・リポジトリーのパーシスタンスを実装する方法について説明します。さらに、フィルターやブラウザー ID クッキーを使用するなど、いくつかの Web 手法も学べます。
セッション間でのユーザーの識別
ユーザーがブラウザーをいったん閉じて再び開いたときに Web フォームをリストアするには、アプリケーションがセッション間でユーザーを識別する必要があります。これは、ユーザーの認証が行われる場合は簡単です。アプリケーションで標準的なユーザー認証方法を使用するのであれば、HttpServletRequest インターフェースが定義する getUserPrincipal() メソッドを呼び出し、それから java.security.Principal の getName() メソッドを使用すればユーザー名を取得することができます。
アプリケーションが匿名ユーザーに対してもフォームの保存とリストアの機能をサポートしなければならない場合は、ブラウザー ID を設定するという手段を使えます。ブラウザー ID は、単一のセッション内でユーザーを追跡するセッション ID と非常によく似ています。実際、セッション ID クッキー の値を取得して、ユーザーがアプリケーションに初めてアクセスしたときに BROWSERID という別の クッキー を設定することができます。セッション ID とブラウザー ID との違いは、セッション ID はセッションの開始時に生成されて終了時に有効期限が切れる一方、BROWSERID クッキー は一度だけ設定され、その有効期限は長期間 (数年間など) にわたるという点です。
 |
ユーザーに感謝されること間違いありません
この記事を読んで、Ajax アプリケーションに自動保存機能を組み込むとユーザーの Web エクスペリエンスが快適かつ効率的になる仕組みを理解してください。記事を読んでいくうちに、フィルターやブラウザー ID クッキー を使用するなど、高度な Web 手法についても学べます。
|
|
サーブレット・フィルターを使用する
BROWSERID クッキー を設定するのにふさわしい場所は、サーブレット・フィルターです。このフィルターはすべての HTTP リクエストをインターセプトし、ユーザーがそのアプリケーションにアクセスするのが初めての場合には HTTP レスポンスに クッキー を追加できるからです。ブラウザーが最初のレスポンスでこの クッキー を受信すると、以降のすべてのリクエストには BROWSERID クッキー が組み込まれるため、アプリケーションがブラウザー ID から匿名ユーザーを識別できるようになります。この記事に付属のサンプル・コードには、javax.servlet.Filter を実装する BrowserIdFilter というクラスが含まれています。このクラスには getBrowserId() というメソッドもあり (リスト 1 を参照)、このメソッドがリクエスト・オブジェクトの クッキー を繰り返し処理して BROWSERID クッキー の値を返し、該当する クッキー が存在しない場合には null を返します。
リスト 1. ブラウザー ID クッキー の取得
package autosave;
...
import javax.servlet.http.Cookie;
...
public class BrowserIdFilter implements Filter {
public static String BROWSERID = "BROWSERID"; // cookie name
public static String getBrowserId(HttpServletRequest httpRequest) {
String browserId = null;
Cookie cookies[] = httpRequest.getCookies();
if (cookies != null)
for (int i = 0; i < cookies.length; i++) {
if (BROWSERID.equals(cookies[i].getName())) {
browserId = cookies[i].getValue();
break;
}
}
return browserId;
}
...
}
|
リスト 2 に記載するのは、BrowserIdFilter の doFilter() メソッドです。このメソッドは getBrowserId() を使用して クッキー が設定済みかどうかをテストします。getBrowserId() が null を返すと、doFilter() は getSession() によってセッション・オブジェクトを取得し、getId() によってセッション ID を取得します。次に、doFilter() は Cookie オブジェクトを作成して maxAge および path プロパティーを設定し、この Cookie オブジェクトを httpResponse オブジェクトに追加します。HTTP リクエストが適切に処理されるようにするため、chain パラメーターの doFilter() メソッドには request オブジェクトと response オブジェクトを渡しています。
リスト 2. Filter インターフェースの doFilter() メソッド実装
package autosave;
...
import javax.servlet.Filter;
import javax.servlet.FilterChain;
...
public class BrowserIdFilter implements Filter {
public static String BROWSERID = "BROWSERID"; // cookie name
public static int IDAGE = 3600 * 24 * 365 * 3; // three years
...
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
if (getBrowserId(httpRequest) == null) {
// The BROWSERID cookie has not been found. This must be
// the first time the user accesses the application.
// Use the current session's ID as the value for
// the BROWSERID cookie.
HttpServletResponse httpResponse
= (HttpServletResponse) response;
String browserId = httpRequest.getSession().getId();
Cookie browserCookie = new Cookie(BROWSERID, browserId);
browserCookie.setMaxAge(IDAGE);
browserCookie.setPath(httpRequest.getContextPath());
httpResponse.addCookie(browserCookie);
}
chain.doFilter(request, response);
}
...
}
|
サーブレット・フィルターを構成する
リスト 3 に示すのは、サーブレット・フィルターがすべての HTTP リクエストをインターセプトできるように web.xml で BrowserIdFilter クラスを構成する方法です。
リスト 3. フィルターの構成
<web-app ...>
...
<filter>
<filter-name>BrowserIdFilter</filter-name>
<filter-class>autosave.BrowserIdFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>BrowserIdFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>
|
 |
ブラウザー ID を設定してください
ブラウザー ID を認証済みユーザーに対しても使用すれば、ユーザーが毎回アプリケーションにアクセスするたびにログインする必要がなくなります。ログイン画面に「Remember me on this computer」というラベルが付いたチェック・ボックスのある Web サイトを見たことがあると思いますが、このようなチェック・ボックスは、ユーザーがサイトに戻ってきたときに再びログインしなくても済むようにサイトにブラウザー ID の設定を許可するためのものです。
|
|
ブラウザー ID のソリューションを使用するのは、フォームに機密情報が含まれない場合に限ります。なぜなら、共有コンピューターからアプリケーションにアクセスするすべてのユーザーは、実際には単一のユーザーとして扱われるためです。セッション間でユーザーを識別する方法として唯一安全なのは、ユーザー名とパスワードに基づく標準認証方式を使うことですが、パスワードに基づく認証にはユーザーがアプリケーションに登録しなければならないという不便があります。たいていの場合、セキュリティーは極めて重要ですが、その一方でユーザーが Web サイトに登録するよりも匿名のままでいるほうを選ぶこともあります。そのような場合、ブラウザー ID は匿名ユーザーを簡単に追跡する方法となります。
この記事で紹介するフォームの自動保存機能は、認証済みユーザーと匿名ユーザーの両方に有効です。以降のセクションでは、自動保存されたフォーム・データを保管し、取得する方法、そしてこのデータをマルチスレッド環境で操作する方法を説明します。
データ・リポジトリーの選択
まず始めに、フォーム・データを保持するためのデータ構造およびリポジトリーを選択する必要があります。リポジトリーには一時データを保管するために頻繁にアクセスすることになるため、リポジトリーの選択は重要な決定事項です。サンプル・アプリケーションでは、あらゆるフォーム・インスタンスに対して 10 秒ごとにデータが自動保存されるようになっていますが、実際のアプリケーションでは、ログオン・ユーザーが多い場合には自動保存間隔を最大 10 分まで延ばすのが妥当でしょう。
自動保存されるフォーム・データを保管する場所は当然、メモリーとなります。保管されたフォーム・データのほとんどは短時間のうちに新しいデータに置き換えられるためです。各フォーム・インスタンスは、ユーザーがフォームを送信するまで定期的にそのデータを保存します。フォームが送信されたら、そのフォームに関連する一時データはメモリーから消去しなければなりません。ユーザーがフォームを送信できない場合、あるいは Submit ボタンをクリックしないでページを中止した場合には、最後に保存したデータをできるだけ長くメモリーに保持します。ユーザーがそのフォームに戻ったときに保存されたデータがまだ有効であれば、フォームをリストアするというオプションを使用できます。
JSF コンポーネントの値を保存してリストアする
自動保存されたすべてのフォーム・インスタンスのデータは Map<String, Object> インスタンスに保持されます。このようなデータ・マップのエントリーと要素のそれぞれには JSF 入力コンポーネントの値が含まれ、このコンポーネントの ID も同じくデータ・マップのキーとなります。この構造は javax.servlet.ServletRequest のパラメーター・マップと似てはいるものの、まったく同じというわけではありません。リクエスト・パラメーターのマップにはストリング配列が含まれる一方、リポジトリーのデータ・マップには JSF ビューのすべての入力コンポーネントの値が変換および検証されてから保持されるからです。DataMapRepository クラスの saveValues() は再帰的メソッドで、JSF コンポーネント・ツリーをトラバースして、EditableValueHolder を実装する入力コンポーネントの値をデータ・マップに入力します (リスト 4 を参照)。
リスト 4. JSF 入力コンポーネントの値のデータ・マップへの保管
package autosave;
...
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
...
public class DataMapRepository ... {
...
public static void saveValues(UIComponent comp,
Map<String, Object> dataMap) {
if (comp == null)
return;
if (comp instanceof EditableValueHolder) {
// Input component. Put its value into the data map
EditableValueHolder evh = (EditableValueHolder) comp;
dataMap.put(comp.getId(), evh.getValue());
}
// Iterate over the children of the current component
Iterator children = comp.getChildren().iterator();
while (children.hasNext()) {
UIComponent child = (UIComponent) children.next();
// Recursive call
saveValues(child, dataMap);
}
}
...
}
|
JSF コンポーネント・ツリーをトラバースして入力コンポーネントの値をリストアするのは restoreValues() メソッド (リスト 5 を参照) です。このメソッドも各 EditableValueHolder コンポーネントの submittedValue プロパティーをクリアし、送信されたデータとは関係なくフォームのデータがマップのデータにリストアされるようにします。saveValues() メソッドは、後でこの記事のなかで使用します。restoreValues() については連載第 3 回で使用することにします。
リスト 5. JSF 入力コンポーネント値のリストア
package autosave;
...
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
...
public class DataMapRepository ... {
...
public static void restoreValues(UIComponent comp,
Map<String, Object> dataMap) {
if (comp == null || dataMap == null)
return;
if (comp instanceof EditableValueHolder) {
// Input component. Get its value from the data map
// and clear any submitted value
EditableValueHolder evh = (EditableValueHolder) comp;
evh.setValue(dataMap.get(comp.getId()));
evh.setSubmittedValue(null);
}
// Iterate over the children of the current component
Iterator children = comp.getChildren().iterator();
while (children.hasNext()) {
UIComponent child = (UIComponent) children.next();
// Recursive call
restoreValues(child, dataMap);
}
}
}
|
各データ・マップ・インスタンスはユーザー ID と JSF ビュー ID で構成された固有の ID を持っているため、リポジトリー・マップには当然、このすべてのデータ・マップが保持されます。java.util.LinkedHashMap を継承する DataMapRepository クラスには、データ・マップの ID を生成する getDataMapId() というメソッドがあります (リスト 6 を参照)。このメソッドは ID を生成する際に特定の faces コンテキストを使用し、そのコンテキストが持つメソッドによって必要なユーザーおよびビュー情報を収容したオブジェクトを返します。
ユーザーがログインすると、getDataMapId() が Principal オブジェクトからユーザー名を取得します。ユーザーが匿名であれば、getDataMapId() はブラウザー ID を使用します。返される ID に組み込まれた JSF ビュー ID は JSF ページごとに固有のものなので、生成されるデータ・マップ ID もユーザーとページの組み合わせごとに固有になるというわけです。
リスト 6. データ・マップに固有の ID の生成
package autosave;
...
import javax.faces.component.UIViewRoot;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
...
import java.security.Principal;
...
public class DataMapRepository ... {
...
public static String getDataMapId(FacesContext ctx) {
UIViewRoot root = ctx.getViewRoot();
if (root == null)
return null;
ExternalContext ectx = ctx.getExternalContext();
String userId = null;
Principal principal = ectx.getUserPrincipal();
if (principal != null) {
// Use the name of the authenticated user.
userId = principal.getName();
} else {
// Use the browser ID of the anonymous user.
userId = BrowserIdFilter.getBrowserId(
(HttpServletRequest) ectx.getRequest());
}
if (userId == null)
return null;
// Concatenate the user ID and the JSF view ID
return userId + root.getViewId();
}
...
}
|
データ・リポジトリーのメモリー・リソースを制限する
Java ヒープには限りがあるため、データ・リポジトリーがメモリーを使い過ぎないようにするための制限が必要です。サンプル・アプリケーションでは、リポジトリーが保管できるデータ・マップ・インスタンス数に上限を設けています。この上限に達すると、最も古いデータ・マップがリポジトリーから削除されてガーベッジ・コレクションの対象となるという仕組みです。このメカニズムはすでに java.util.LinkedHashMap クラスに組み込まれているので、リポジトリーのエントリーが最大許容数を超えたときに true を返すようにするには、removeEldestEntry() メソッドをオーバーライドするだけです。
リスト 7 は前述のとおり、maxDataMaps プロパティーを追加して removeEldestEntry() メソッドをオーバーライドするように LinkedHashMap を継承した DataMapRepository クラスです。さらに、DataMapRepository に含まれるコンストラクターを使ってリポジトリー・インスタンスのコピーを作成することもできるため、アプリケーションの実行中にリポジトリーのスナップショットを取りたい場合に便利です。元のリポジトリーとそのコピーには同じデータ・マップ・オブジェクトが含まれることになりますが、後で説明するようにデータ・マップが変更されることはないので、まったく問題ありません。
リスト 7. データ・リポジトリー・クラス
package autosave;
...
import java.util.LinkedHashMap;
import java.util.Map;
public class DataMapRepository
extends LinkedHashMap<String, Map<String, Object>> {
private static final int DEFAULT_MAX_DATA_MAPS = 1000;
private int maxDataMaps;
public DataMapRepository() {
maxDataMaps = DEFAULT_MAX_DATA_MAPS;
}
public DataMapRepository(DataMapRepository repository) {
maxDataMaps = repository.maxDataMaps;
putAll(repository);
}
public int getMaxDataMaps() {
return maxDataMaps;
}
public void setMaxDataMaps(int maxDataMaps) {
this.maxDataMaps = maxDataMaps;
}
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > maxDataMaps;
}
...
}
|
リポジトリーのデータ・マップにアクセスするには、getDataMap() および setDataMap() メソッドを使用できます (リスト 8 を参照)。この 2 つのメソッドは getDataMapId() を使って特定のコンテキストに ID を生成した上で、LinkedHashMap クラスから継承した get()、put()、remove() メソッドを呼び出します。
リスト 8. データ・マップを保管および取得するメソッド
package autosave;
...
public class DataMapRepository ... {
...
public Map<String, Object> getDataMap(FacesContext ctx) {
String id = getDataMapId(ctx);
if (id == null)
return null;
return get(id);
}
public void setDataMap(FacesContext ctx, Map<String, Object> dataMap) {
String id = getDataMapId(ctx);
if (id == null)
return;
if (dataMap != null)
put(id, dataMap);
else
remove(id);
}
...
}
|
マルチスレッド環境での操作
前のセクションで紹介した DataMapRepository クラスとその基本クラスである LinkedHashMap はスレッド・セーフではありません。これに対処する 1 つの方法は java.util.Collections の synchronizedMap() メソッドでスレッド・セーフなラッパー・マップを返すことですが、フォーム・データ・リポジトリーの場合にはカスタム・ラッパー・クラスを作成するほうが適しています。カスタム・ラッパー・クラスによって、マルチスレッド化されたサーバー環境でリポジトリーが安全に使用されることを確実にするとともに、リポジトリーへのアクセスを制御します。
データ・リポジトリーにスレッド・セーフなラッパーを使用する
リスト 9 に、サンプル・アプリケーションのラッパー・クラス、RepositoryWrapper を記載します。このラッパー・クラスが持つ唯一のフィールドは、repository という名前で、その型は DataMapRepository です。ここには専用 repository のコピーを返す getRepository() メソッドがある一方、新しいコピーを作成する setRepository() メソッドもあります。これらのコピーには元のリポジトリーと同じデータ・マップ・オブジェクトが含まれますが、いったん作成されてフォーム・データが入力されたデータ・マップが変更されることはないため、これで問題ありません。
リスト 9. データ・リポジトリーのラッパー・クラス
package autosave;
...
public class RepositoryWrapper implements java.io.Serializable {
private DataMapRepository repository;
public RepositoryWrapper() {
repository = new DataMapRepository();
}
public synchronized DataMapRepository getRepository() {
return new DataMapRepository(repository);
}
public synchronized void setRepository(
DataMapRepository repository) {
if (repository != null)
this.repository = new DataMapRepository(repository);
else
this.repository.clear();
}
...
}
|
RepositoryWrapper クラスに含まれるのは、データ・マップ、そしてラップされたリポジトリーの maxDataMaps プロパティーにアクセスするためのスレッド・セーフなメソッドです (リスト 10 を参照)。getDataMap() および setDataMap() メソッドは特定の FacesContext に対するデータ・マップの取得、保管を行います。hasDataMap() メソッドは ctx パラメーターにデータ・マップが存在する場合には true を返し、clearDataMap() メソッドはリポジトリーからデータ・マップを削除します。
リスト 10. データ・リポジトリーにアクセスするためのスレッド・セーフなメソッド
package autosave;
...
import javax.faces.context.FacesContext;
...
import java.util.Map;
public class RepositoryWrapper implements java.io.Serializable {
...
public synchronized Map<String, Object> getDataMap(
FacesContext ctx) {
return repository.getDataMap(ctx);
}
public synchronized void setDataMap(FacesContext ctx,
Map<String, Object> dataMap) {
repository.setDataMap(ctx, dataMap);
}
public synchronized boolean hasDataMap(FacesContext ctx) {
return getDataMap(ctx) != null;
}
public synchronized void clearDataMap(FacesContext ctx) {
setDataMap(ctx, null);
}
public synchronized int getMaxDataMaps() {
return repository.getMaxDataMaps();
}
public synchronized void setMaxDataMaps(int maxDataMaps) {
repository.setMaxDataMaps(maxDataMaps);
}
...
}
|
ラッパーを JSF 管理 Bean として構成する
フォーム・データが含まれるすべてのマップ・オブジェクトは、faces-config.xml ファイルで JSF 管理 Bean として構成されたラッパーを持つリポジトリー・インスタンスに保持されます (リスト 11 を参照)。この Bean に指定された名前は repositoryWrapper で、Bean スコープは applicationです。JSF 構成ファイルを使って、データ・リポジトリーの maxDataMaps プロパティーに値を指定することもできます。
リスト 11. JSF 管理 Bean として構成されたリポジトリー・ラッパー
<faces-config>
...
<managed-bean>
<managed-bean-name>repositoryWrapper</managed-bean-name>
<managed-bean-class>autosave.RepositoryWrapper</managed-bean-class>
<managed-bean-scope>application</managed-bean-scope>
...
<managed-property>
<property-name>maxDataMaps</property-name>
<value>100</value>
</managed-property>
</managed-bean>
...
</faces-config>
|
RepositoryWrapper クラスには管理 Bean インスタンスを返す 2 つの静的メソッドがあります。これらのメソッド (リスト 12 を参照) は、FacesContext あるいは ServletContext を使用して application スコープからラッパー Bean を取得できます。
リスト 12. 管理 Bean インスタンスの取得
package autosave;
import javax.faces.application.Application;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;
import javax.servlet.ServletContext;
...
public class RepositoryWrapper implements java.io.Serializable {
...
public static RepositoryWrapper getManagedBean(FacesContext ctx) {
Application app = ctx.getApplication();
ValueBinding vb = app.createValueBinding("#{repositoryWrapper}");
return (RepositoryWrapper) vb.getValue(ctx);
}
public static RepositoryWrapper getManagedBean(ServletContext ctx) {
return (RepositoryWrapper) ctx.getAttribute("repositoryWrapper");
}
}
|
JSF フェーズ・リスナーの変更
連載第 1 回では、AutoSaveListener というクラスを使用して Ajax リクエストを処理し、送信されたデータを JSF コンポーネント・ツリーから取得しました。今回は、フォーム・データを出力する代わりにデータ・リポジトリーに保管します。
現行ビューのデータをリポジトリーに保管する
リスト 13 に示す AutoSaveListener の saveCurrentView() は、現行 JSF ビューのフォーム・データをリポジトリーに保管するメソッドです。このメソッドの最初のステップは getCurrentInstance() を呼び出して faces コンテキストを返してから、getViewRoot() を使って JSF ビューのルート・コンポーネントを取得することです。次に saveCurrentView() は、新しいデータ・マップを作成し、このデータ・マップに JSF コンポーネントの値を保管するため DataMapRepository クラスの saveValues() メソッドを呼び出します。安全対策として、マップ・オブジェクトを Collections.unmodifiableMap() に渡し、その状態の変更が試行された場合には例外をスローするラッパー・マップを返すようにしています。変更不可能なマップは、ラッパー・オブジェクトの setDataMap() メソッドでリポジトリーに保管されます。
リスト 13. リポジトリーへの現行 JSF ビューのフォーム・データの保管
package autosave;
...
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
...
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class AutoSaveListener implements PhaseListener {
...
public void saveCurrentView() {
// Get the faces context of the current request.
FacesContext ctx = FacesContext.getCurrentInstance();
// Get the root component of the current view.
UIViewRoot root = ctx.getViewRoot();
// Create a new data map.
Map<String, Object> dataMap = new HashMap<String, Object>();
// Store the component values into the data map.
DataMapRepository.saveValues(root, dataMap);
// Make the data map unmodifiable.
dataMap = Collections.unmodifiableMap(dataMap);
// Get the managed bean instance wrapping the data repository.
RepositoryWrapper wrapper = RepositoryWrapper.getManagedBean(ctx);
// Store the data map into the repository.
wrapper.setDataMap(ctx, dataMap);
// Stop request processing.
ctx.responseComplete();
}
...
}
|
第 1 回で説明したように、フォームの自動保存がアプリケーション・ロジックに干渉してはいけません。自動保存の後にデータ・モデルを更新したり、アクション・メソッドを呼び出したりすることは禁物です。したがって、saveCurrentView() は入力コンポーネントの値を保存した後、リクエストの処理を停止し、自動保存されたデータ (実際はユーザーがフォームに部分的に入力したデータ) がアプリケーションのデータ・モデルに保管されないようにする必要があります。このような理由から、saveCurrentView() は faces コンテキストの responseComplete() メソッドを呼び出して、JSF リクエストの処理ライフ・サイクルに干渉しないように通知しています。
JSF フェーズ・イベントを処理する
第 1 回で説明したように、フォーム・データを保存するのは、JSF 検証フェーズで JSF フレームワークによって JSF コンポーネントのすべての値が変換され、検証されてからでなければなりません。afterPhase() メソッド (リスト 14 を参照) は PhaseEvent パラメーターのフェーズ ID を確認するだけでなく、Ajax-Request ヘッダーを取得して現行のリクエストがフォーム・データを自動保存するかどうかも調べます。このヘッダーが設定される場所は、第 1 回で紹介した AutoSaveScript.js ファイルの submitFormData() 関数です。ヘッダーの値が Auto-Save であれば、afterPhase() メソッドが saveCurrentView() を呼び出し、JSF コンポーネントの有効な値をデータ・リポジトリーに保管します。
Ajax リクエストを示すヘッダーがない場合は、ユーザーが Web フォームの Submit ボタンをクリックしたことを意味します。この場合、afterPhase() は faces コンテキストにメッセージが含まれているかどうかを確認します。メッセージがなければ、JSF 検証フェーズの後にafterPhase() が呼び出されたということなので、ユーザーが送信したデータは有効です。フォーム・データが有効な場合、現行のユーザーとビューの組み合わせに対して以前に保存されたデータが clearDataMap() によってリポジトリーからクリアされます。
リスト 14. JSF フェーズ・イベントのリッスン
package autosave;
...
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
...
public class AutoSaveListener implements PhaseListener {
...
public void afterPhase(PhaseEvent e) {
if (!e.getPhaseId().equals(PhaseId.PROCESS_VALIDATIONS))
return;
FacesContext ctx = e.getFacesContext();
Map headers = ctx.getExternalContext().getRequestHeaderMap();
if ("Auto-Save".equals(headers.get("Ajax-Request"))) {
// Auto-Save Request. Save data into the repository.
saveCurrentView();
} else {
// The user must have clicked the Submit button.
if (!ctx.getMessages().hasNext()) {
// There are no error messages.
// This means the final submitted data is valid and
// the temporary auto-saved data is no longer needed.
RepositoryWrapper wrapper
= RepositoryWrapper.getManagedBean(ctx);
wrapper.clearDataMap(ctx);
}
}
}
...
}
|
リポジトリーのパーシスタンス実装
このセクションではサンプル・アプリケーションを拡張して、サーバーがシャットダウンする前にリポジトリーを保存し、サーバーの再起動時にリポジトリーをリストアするようにします。リポジトリーのパーシスタンスを実装するには、オブジェクトをシリアライズするのが簡単な方法となります。DataMapRepository クラス、収容されたデータ・マップ、そしてデータ・マップの要素はいずれもシリアライズ可能だからです。データ・マップに含まれるのは、JSF 仕様に従ってシリアライズ可能な JSF コンポーネントの値であることを覚えておいてください。
オブジェクトのシリアライズには周知の欠点がありますが、このサンプル・アプリケーションの場合はリポジトリーに保持されるフォーム・データ・インスタンスの数が制限されているため、パーシスタンス・ソリューションとして妥当です。また、アプリケーションとは関係のない問題が原因でサーバーがクラッシュし、それによってリポジトリーの一時データが失われたとしても大事には至りません。リレーショナル・データベースまたはオブジェクト・データベースに基づく信頼性の高いソリューションには CPU リソースがさらに必要となりますが、リソース追加の理由が 10 秒ごとに更新される部分的なユーザー入力を保管するためとなると、その追加を正当化するのは無理な話です。それでもなお、リポジトリーの状態はサーバーやアプリケーションの再起動によって影響されないようにしなければなりません。
ServletContextListener を使用する
サンプル・アプリケーションの DataMapPersistence クラスは javax.servlet.ServletContextListener インターフェースを実装するため、アプリケーションの起動時および停止時には通知を受けられます。リスナー・メソッドは、getDataFile() メソッドによって返される File を使用してデータ・リポジトリーをシリアライズ、デシリアライズします (リスト 15 を参照)。データ・ファイルが保持されるのは、サンプル・アプリケーションの WEB-INF ディレクトリーです。
リスト 15. データ・リポジトリーのファイル取得
package autosave;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextListener;
...
import java.io.File;
public class DataMapPersistence implements ServletContextListener {
private File getDataFile(ServletContext sctx) {
String path = sctx.getRealPath("/WEB-INF/repository.ser");
if (path == null)
return null;
return new File(path);
}
...
}
|
データ・リポジトリーをロードする
サーブレット/JSP コンテナーは、アプリケーションの初期化中に contextInitialized() メソッドを呼び出します。リスト 16 に示すこのメソッドは、リポジトリー・オブジェクトをシリアライズし、loadedRepository という名前のコンテキスト属性を設定します。この属性には、後で JSP/JSF EL を使ってアクセスすることができます。サーブレット・コンテキストの属性は、application スコープに保持される JSP/JSF 変数に相当します。ロードされたリポジトリーはその後、RepositoryWrapper Bean の repository プロパティーを設定するために使用されます。
リスト 16. アプリケーション初期化中のデータ・リポジトリーのロード
package autosave;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
...
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class DataMapPersistence implements ServletContextListener {
...
public void contextInitialized(ServletContextEvent e) {
ServletContext sctx = e.getServletContext();
File dataFile = getDataFile(sctx);
if (dataFile == null || !dataFile.exists())
return;
try {
ObjectInputStream in = new ObjectInputStream(
new BufferedInputStream(
new FileInputStream(dataFile)));
try {
// Read the data repository from the file.
Object repository = in.readObject();
// Store the loaded repository into the application scope.
sctx.setAttribute("loadedRepository", repository);
} finally {
in.close();
}
} catch (Exception x) {
sctx.log("Loading Error", x);
}
}
...
}
|
contextInitialized() 呼び出しの前には、サーブレットや JSP ページは一切呼び出されません。これはつまり、JSFフレームワークがまだ初期化されていない可能性があるということです。したがって、contextInitialized() は JSF フレームワークが管理する RepositoryWrapper Bean の repository プロパティーを設定することができません。この設定を行うのは faces-config.xml ファイルです。このファイルでは、application スコープの loadedRepository 変数を安全に使用して管理 Bean の repository プロパティーを設定することができます (リスト 17 を参照)。
リスト 17. ロードしたデータのリポジトリー・ラッパーへの保管
<faces-config>
...
<managed-bean>
<managed-bean-name>repositoryWrapper</managed-bean-name>
<managed-bean-class>autosave.RepositoryWrapper</managed-bean-class>
<managed-bean-scope>application</managed-bean-scope>
<managed-property>
<property-name>repository</property-name>
<value>#{loadedRepository}</value>
</managed-property>
...
</managed-bean>
...
</faces-config>
|
データ・リポジトリーを保存する
アプリケーションのシャットダウン時には、サーブレット/JSP コンテナーが contextDestroyed() メソッドを呼び出します。contextDestroyed() メソッドは JSF フレームワークによって管理される RepositoryWrapper インスタンスを取得した後、ラッパー Bean からデータ・リポジトリーのコピーを取得し、そのコピーをデータ・ファイルにシリアライズします (リスト 18 を参照)。
リスト 18. リポジトリーのデータをファイルに保存した上でのアプリケーションのシャットダウン
package autosave;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
...
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class DataMapPersistence implements ServletContextListener {
...
public void contextDestroyed(ServletContextEvent e) {
ServletContext sctx = e.getServletContext();
File dataFile = getDataFile(sctx);
if (dataFile == null)
return;
try {
ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream(dataFile)));
try {
// Get a copy of the data repository from the wrapper bean.
RepositoryWrapper wrapper
= RepositoryWrapper.getManagedBean(sctx);
Object repository = wrapper.getRepository();
// Serialize the data repository into the file.
out.writeObject(repository);
} finally {
out.close();
}
} catch (Exception x) {
sctx.log("Saving Error", x);
}
}
}
|
リスト 19 に示すのは、web.xml ファイルにサーブレット・コンテキスト・リスナーとして DataMapPersistence クラスを構成する方法です。
リスト 19. サーブレット・コンテキスト・リスナーの構成
<web-app ...>
...
<listener>
<listener-class>autosave.DataMapPersistence</listener-class>
</listener>
...
</web-app>
|
まとめ
この連載第 2 回では、セッション間でユーザーを識別する方法、フォーム・データにスレッド・セーフなリポジトリーを実装する方法、JSF 入力コンポーネントの値を保管およびリストアする方法、そしてデータ・リポジトリーにパーシスタンスを実装する方法を説明しました。最終回となる第 3 回では、保存されたデータを JSF フォームに入力する方法とともに、JSF アプリケーションで他の JavaScript 手法を使用する方法について説明します。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Sample application | wa-aj-jsf2.zip | 19KB | HTTP |
|---|
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | |  | Andrei Cioroianu は、カスタム Java EE 開発および Ajax/JSF コンサルティング・サービスのプロバイダー、Devsphere のシニア Java 開発者兼コンサルタントです。彼への連絡には、www.devsphere.com に用意されたコンタクト・フォームを利用してください。 |
記事の評価
|