目次


Wicket: 動的 Web ページの作成およびテスト用の単純化されたフレームワーク

Comments

概要

Wicket は、Apache 傘下になって初のリリースが今年公開された Java Web 開発フレームワークです。このオープンソースの軽量なコンポーネント・ベースのフレームワークには、今までのさまざまな Web ベースのアプリケーション開発方法が結集されています。Wicket が目指しているのは、HTML ページの設計者と Java 開発者との明確な役割分担です。そのために、あらゆる WYSIWYG HTML 設計ツールを使って作成できる単純な HTML ベースのテンプレートをサポートしています。これらのテンプレートは、ほとんど変更を加えることなく動的なものにすることができます。

※訳注: 上記段落の最初の文は、原文どおりに訳すと、「Wicket は、最近発表された Java Web 開発フレームワークです。」のような感じになってしまい、Wicket 自体は 2005年ごろには既に存在していたのにあまりにも違和感があるため、「a recently launched Java Web development framework」のおおまかな意を汲んで訳してあります。

他のフレームワークと同様に、Wicket は Sun Microsystems のサーブレット API をベースに作成されています。ただし、モデル・ビュー・コントローラー (MVC) モデルをベースとしたフレームワーク (Struts など) とは異なり、Wicket では、サーブレットなどの技術では必要なリクエスト/レスポンス・オブジェクトの処理を開発者が作成する必要はありません。このタスクを排除することにより、Wicket は開発者がアプリケーションのビジネス・ロジックに専念できるようにしています。

リクエスト/レスポンス・オブジェクトを処理するコントローラーを作成してマルチスレッド化の問題を懸念する代わりに、Wicket 開発者が考えなければならないのは、再利用可能でステートフルなコンポーネントの作成についてです。そして、コントローラー・クラスやアクション・クラスを作成する代わりに、Wicket 開発者はページを作成し、そのページにコンポーネントを配置して、それぞれのコンポーネントがユーザー入力にどのような応答をするかを定義します。

HelloWorld サンプル・プログラム

Wicket を使用した Web ベースのアプリケーションの開発がいかに簡単かを納得してもらうため、これから単純なサンプル・プログラム「Hello World」を作成します。通常、Wicket での動的ページの開発には、以下の 2 つの成果物を作成する作業が伴います。

  • HTML テンプレート
  • Java ページ・クラス

: 実際の HTML ファイルとページ・クラスの名前が同じであること (例えば、HelloWorld.html と HelloWorld.java)、そして両方とも CLASSPATH 上に配置されていることを確認してください。ベスト・プラクティスとしては、この 2 つを同じディレクトリーに配置します。

HTML テンプレート (HelloWorld.html)

リスト 1 に、HelloWorld サンプルのテンプレート・ファイルを記載します。

リスト 1. HelloWorld.html
<html>
	<head><script type="text/javascript" ></script></head>
	<body bgcolor="#FFCC00">
		<H1 align="center">
			<span wicket:id="message">Hello World Using Wicket!</span>
		</H1>
	</body>
</html>

動的な Web ページにするには、ページのどのセクションが動的であるかを識別し、Wicket にコンポーネントを使ってこれらの動的セクションをレンダリングするように指示する必要があります。リスト 1 ではメッセージを動的に維持したいので、span 要素を使用してこのコンポーネントにマークを付け、wicket:id 属性を使用してコンポーネントを識別してあります。

Java ページ・クラス (HelloWorld.java)

HelloWorld.java サンプルのページ・クラスはリスト 2 のとおりです。

リスト 2. HelloWorld.java
package myPackage;

import wicket.markup.html.WebPage;
import wicket.markup.html.basic.Label;

public class HelloWorld extends WebPage
{
   	public HelloWorld()
	   {
		      add(new Label("message", "Hello World using Wicket!!"));
	   }
}

ページ・クラスのラベル・コンポーネント ("message") に指定した ID は、テンプレート・ファイルにある要素の Wicket ID (wicket:id="message") と一致していなければなりません。Wicket の Java ページ・クラスには、Web ページのすべての動的振る舞いが含まれます。HTML テンプレートとページ・クラスには、1 対 1 の関係があります。

最後に作成する必要があるのは、Application オブジェクトです。このオブジェクトは、アプリケーションを Web コンテナーにロードするときの開始点としての役割を持ちます。アプリケーションの初期設定と構成もこのオブジェクトで行います。例えば、アプリケーションのホーム・ページを定義するには、getHomePage() メソッドをオーバーライドし、アプリケーションのホーム・ページに対応するページ・クラスを返します (リスト 3 を参照)。

リスト 3. HelloWorldApplication.java
package myPackage;

import wicket.protocol.http.WebApplication;

public class HelloWorldApplication extends WebApplication {

	   protected void init() {
   	}

	   public Class getHomePage() {
		      return HelloWorld.class;
   	}
}

また、デフォルトのアプリケーション設定を変更したり、無効にしたりすることもできます。それには、init() メソッドを無効にした後、getXXXSettings() を呼び出して可変 Settings オブジェクトとのインターフェースを取得します。これらのインターフェースは、表 1 に記載するメソッドによって返され、アプリケーションに対応したフレームワーク設定を構成するために使用することができます。

表 1 に、アプリケーション全体に適用できる Application クラスの設定例を記載します。

表 1. アプリケーション全体の設定
メソッド使用目的r
getApplicationSettingsアプリケーション全体の設定
getDebugSettingsアプリケーションのデバッグ関連の設定
getExceptionSettingsアプリケーションの例外処理の設定
getMarkupSettingsアプリケーションのマークアップ関連の設定
getPageSettingsアプリケーションのページ関連の設定
getRequestCycleSettingsアプリケーションのリクエスト・サイクル関連の設定
getSecuritySettingsアプリケーションのセキュリティー関連の設定
getSessionSettingsアプリケーションのセッション関連の設定

表 2 に、Application クラスのアプリケーション全体の設定を適用する方法の例を記載します。

表 2. アプリケーション全体の設定例
実行内容
getApplicationSettings().setPageExpiredErrorPage(<Page class>)セッション・タイムアウトによってページの有効期限が切れた場合に表示する汎用ページを定義します。
getMarkupSettings().setDefaultMarkupEncoding("UTF-8")レンダリングに使用するマークアップの形式を設定します。
getSecuritySettings().setAuthorizationStrategy(<IAuthorizationStrategy Instance>)アプリケーションに使用する許可ストラテジーを設定します。

web.xml 構成ファイル

最後のステップでは、アプリケーションをロードして使用できるようにするために、Wicket サーブレット・クラスを定義し、web.xml 構成ファイルでこのクラスにアプリケーション・クラス名をパラメーターとして渡します (リスト 4 を参照)。

リスト 4. web.xml 構成ファイル
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
      PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
      "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
	<display-name>Wicket Hello World Example</display-name>
	<servlet>
		<servlet-name>HelloWorldApplication</servlet-name>
		<servlet-class>
			wicket.protocol.http.WicketServlet
		</servlet-class>
		<init-param>
			<param-name>applicationClassName</param-name>
			<param-value>myPackage.HelloWorldApplication</param-value>
		</init-param>
	</servlet>
	<servlet-mapping>
		<servlet-name>HelloWorldApplication</servlet-name>
		<url-pattern>/hello/*</url-pattern>
	</servlet-mapping>
</web-app>

これで、アプリケーションを War/Ear ファイルとしてパッケージ化して任意の Java EE (Java Platform, Enterprise Edition) ベースのサーブレット・コンテナー (Tomcat や WebSphere® など) にデプロイすると、URL http://<serverName:port>/warfileName/hello/ を指定することでアプリケーションを起動できるようになります (servername と warfilename は適切な値に置き換えてください)。この例では、http://localhost:8090/sample/hello を呼び出します (図 1 を参照)。

図 1. HelloWorld Wicket サンプル・アプリケーション

Wicket のライフ・サイクル

Wicket のライフ・サイクルについて十分な知識があると、Wicket をより効率的に使用できるようになります。ライフ・サイクルは、以下のステップからなります。

  • アプリケーションのロード
  • リクエストの処理
  • レンダリング

アプリケーションのロード

Wicket ベースのアプリケーションをロードするには、web.xml ファイルの中で Wicket サーブレットを定義します。この Wicket サーブレットは任意の Java EE ベースのアプリケーション・サーバーにロードすることができます (リスト 5 を参照)。

リスト 5. web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
      PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
      "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
	<display-name>Sample Wicket Application</display-name>
	<servlet>
		<servlet-name>SampleWicketApplication</servlet-name>
<servlet-class> wicket.protocol.http.WicketServlet </servlet-class>
		<init-param>
			<param-name>applicationClassName</param-name>
			<param-value>wicket.sample.SampleApplication</param-value>
		</init-param>
	</servlet>
	<servlet-mapping>
		<servlet-name>SampleApplication</servlet-name>
		<url-pattern>/sample/*</url-pattern>
	</servlet-mapping>
	<welcome-file-list>
		<welcome-file>index.html</welcome-file>
	</welcome-file-list>

</web-app>

指定するサーブレット・クラスは常に wicket.protocol.http.WicketServlet とし、applicationClassName パラメーターの値は WebApplication 型でなければなりません。この例では SampleApplicationWebApplication を継承しています。クライアントが URL /sample/* を使用してリスト 5 のアプリケーションを呼び出すたびに、サーバーは WicketServlet をロードし、それによってアプリケーション・クラスの単一のインスタンス、つまり SampleApplication が作成されます。

applicationClassName パラメーターには、WebApplication を継承するクラスの完全修飾名を指定する必要があります。アプリケーション・クラスが見つからない場合や、WebApplication を継承するアプリケーション・クラスではない場合、あるいはアプリケーション・クラスをインスタンス化できない場合には、WicketRuntimeException 型のランタイム例外がスローされます。

リクエストの処理

Wicket サーブレットがアプリケーション・クラスをロードすると、アプリケーション・クラスはセッションが存在しない場合には、そのセッション・ファクトリーを使用してサーブレット・リクエスト用のセッションを作成します。次に、アプリケーションは作成したセッション・オブジェクトを使用して RequestCycle オブジェクトを作成し、リクエストを処理するための呼び出しを RequestCycle に委任します。

続いてリクエスト・サイクルは、リクエストが処理される直前に onBeginRequest メソッドを呼び出し、サブクラスに前処理を行わせます。リクエスト・サイクルには複数の段階があり、現行の段階に応じて異なる命令をリクエスト・サイクル・プロセッサーに送信します。すべての命令を送信し終えると、リクエスト・サイクルは最終段階に到達します。図 2 に示すように、この最終段階がリクエスト処理の終了を示します。

図 2. Wicket によるリクエスト処理のシーケンス・フロー

リクエスト・サイクル・プロセッサーの役割は、リクエスト・サイクル中の命令を処理することです。リクエスト・サイクル・プロセッサーを使用する RequestCycle は、事前に定義された以下の順番でメソッドを呼び出します。

  • IRequestTarget resolve(RequestCycle, RequestParameters) を呼び出して、リクエストのターゲットを取得します。例えば、リクエストは bookmarkable ページを参照することも、あるいは前にレンダリングされたページに配置されたコンポーネントを参照することもあります。このターゲットを取得することが、リクエスト・サイクル・プロセッサーな主な役割の 1 つです。RequestParameters オブジェクトは、サーブレットのリクエスト・パラメーターから変換することが可能なすべてのオプション・パラメーターで構成され、これらのパラメーターに強い型付けをしたパラメーターとして機能します。
  • void processEvents(RequestCycle) は、コンポーネント (例えばonClick() および onSubmit() イベント・ハンドラー) での呼び出しなどのイベントを処理するように意図されています。
  • void respond(RequestCycle) を呼び出してレスポンスを作成します。つまり、Web ページを生成するか、リダイレクトを行うということです。
  • イベント処理中またはレスポンスの段階でキャッチされない例外が発生すると、毎回 void respond(RuntimeException , RequestCycle) を呼び出して、適切な例外レスポンスを生成できるようにします。

レンダリング

ページがページそのものをレンダリングするには、そのページに関連付けられたマークアップ (ページに並んで配置された HTML ファイル) をレンダリングします。MarkupContainer (ページのスーパークラス) は関連するマークアップのマークアップ・ストリームを繰り返し処理しながら、ID を基準にして、マークアップ内のタグに対応するコンポーネントを検索します。MarkupContainer (この例ではページ) は onBeginRequest() によってすでに作成されて初期化が済んでいるため、それぞれのタグの子はコンテナーで使用可能になっているはずです。コンポーネントが取得されると、そのコンポーネントの render() メソッドが呼び出されます。

Component.render() は、以下のステップに従ってコンポーネントをレンダリングします。

  1. コンポーネントの可視性を判断します。コンポーネントが可視でない場合、RequestCycle のレスポンスは NullResponse.getInstance() に変更されます。このレスポンスの実装は、単に出力を破棄するだけに過ぎません。
  2. Component.onRender() を呼び出し、コンポーネントのレンダリング実装が実際にコンポーネントをレンダリングできるようにします。
  3. クラスター全体で複製される場合に備え、すべてのコンポーネント・モデルを切り離してコンポーネント階層のサイズを削減します。

renderComponent メソッドは、コンポーネントのマークアップ・ストリームで次にあるタグの可変コピーを取得し、onComponentTag(Tag) を呼び出してサブクラスがそのタグを変更できるようにします。サブクラスがタグを変更すると、変更されたタグが renderComponentTag(Tag) によってレスポンスに書き出され、マークアップ・ストリームが次のタグに進みます。

次に、onComponentTagBody() が呼び出され、MarkupStream、そして開始タグとして書き出された ComponentTag が渡されます。これにより、コンポーネントはコンポーネント・タグの本体を作成するために必要なすべてのことを実行できるようになります。サブクラスが onComponentTagBody() で呼び出すことのできる操作の 1 つ、Component.replaceComponentTagBody() は、コンポーネントの本体のなかに含まれるマークアップを任意のストリングに置き換えます。そして最後に、フレームワークはコンポーネントの開始タグに対応する終了タグをすべて書き出します。

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

Wicket が提供する主要な機能の 1 つはカスタム・コンポーネントを簡単に開発できることで、Wicket に用意されている非常に柔軟なコンポーネント/プログラミング・モデルが、カスタム・コンポーネントの開発を至って簡単にします。カスタム・コンポーネントは HTML フィールドという形にすることも、ページ内で使用できるパネルという形にすることもできます。Web ベースのアプリケーションで再利用可能なコンポーネントの一般的な形態は、ヘッダー、フッター、ナビゲーション・バーなどです。

それではまず始めに、Wicket に固有のコンポーネント階層を見てください。この階層を使えば、ほとんど作業を行わずに新しいカスタム・コンポーネントを作成したり、コンポーネントを拡張して新しい機能を追加したりすることができます。

図 3. コンポーネント階層
Component hierarchy
Component hierarchy

Component

階層の頂点に位置する Component は、すべてのコンポーネントの抽象基底クラスとしての役割を持ち、以下をはじめとするさまざまな機能を提供します。

  • ID: コンテナー内で固有の非ヌル ID。getID() を使用して取得することができます。
  • モデル: HTML でレスポンスとしてレンダリングするデータを保持します。
  • 属性: コンポーネントが含まれるマークアップを操作するために、どのコンポーネントにも追加することができます。

WebComponent

WebComponent は、単純な HTML コンポーネント (Label、Image など) の基底クラスとして機能します。

MarkupContainer

MarkupContainer はすべての子コンポーネントを保持し、独自のマークアップを持ちません。子コンポーネントを追加、または置換するには、それぞれ add() メソッドと replace() メソッドを使います。子コンポーネントは OGNL 表記を使って取得することができます。例えば、get("a.b") を呼び出すと、それ自体の ID が「b」で、親コンポーネントの ID が「a」のコンポーネントが返されます。

WebMarkupContainer

HTML マークアップとコンポーネントのコンテナーとして機能します。マークアップ・タイプが HTML に定義される点を除けば、基底クラス MarkupContainer とほとんど同じです。

WebMarkupContainerWithAssociatedMarkup

WebMarkupContainer を継承し、ヘッダー・タグを処理するための追加機能を提供します。

Panel

Panel は再利用可能コンポーネントで、マークアップとその他のコンポーネントを保持します。このクラスを拡張することで、再利用可能なカスタム・コンポーネントを作成することができます。

Border

Border コンポーネントには独自に関連付けられたマークアップがあり、ボーダーをレンダリングする際に使用する関連マークアップ・ファイルの部分を定義するために使用することができます。

Page

すべてのページの抽象基底クラスとして機能する Page には、任意の Component ツリーを含めることができます。すべてのライフ・サイクル・イベント (onBeginRequestonEndRequest()onModelChanged() など) は、これらのイベントの操作、処理を対象とする Page のサブクラスによってオーバーライドすることができます。

Wicket 固有のコンポーネント階層を概説したところで、今度は Wicket の Panel コンポーネントを継承して独自のカスタム・コンポーネントを作成する方法を説明します。

図 4 を参照しながら、Wicket を使用してフッターなどのサンプル・カスタム・コンポーネントをコーディングする方法を見ていきましょう。

図 4. Footer コンポーネントのレイアウト
Footer コンポーネントのレイアウト

視覚的な表示部分を持つカスタム Wicket コンポーネントは通常、以下の成果物で構成されます。

  • HTML テンプレート (Footer.html、リスト 6 を参照)
  • オプションの JavaScript、スタイル・シート、または Image
  • 標準 Wicket ディストリビューションに付属のコンポーネント基底クラスのいずれかを継承する、HTML テンプレートに対応する Java Component クラス (Footer.java、リスト 7 を参照)
リスト 6. Footer.html
<wicket:panel>
	<hr>
	Copyright <span wicket:id="year">2008</span>. IBM Inc. All rights reserved.
</wicket:panel>
リスト 7. Footer.java
import java.util.GregorianCalendar;
import wicket.markup.html.basic.Label;
import wicket.markup.html.panel.Panel;

public final class Footer extends Panel {

  public Footer(String id) {
    super(id);
    add(new Label("year", "" + new GregorianCalendar().get(GregorianCalendar.YEAR)));
  }

}

注: リスト 6 とリスト 7 の成果物、HTML 成果物、そしてコンポーネント (Java クラス) のサーバー・サイドの表示部分は、例によってサンプル・パッケージに含めてあります。

コンポーネントの組み込み/使用

コンポーネントを (定義した後に) 使用するには、ただ単に、必要な Page クラス・ファイル (つまり、<Page>.java)、例えば Home.java (リスト 8 を参照) でコンポーネントをインスタンス化し、対応するテンプレート・ファイル (<Template>.html) ファイル (この場合はリスト 9 の Home.html) にカスタム・コンポーネントの ID を組み込んで呼び出すだけです。

リスト 8. Home.java
import wicket.markup.html.WebPage;
public class Home extends WebPage {

  public Home() {
    add(new Footer("footer"));
  }

}
リスト 9. Home.html
<html>
<head></head>
<body>
	Body of Home page.
	<span wicket:id="footer">Footer Info</span>
</body>
</html>

Wicket での検証

Wicket はクライアント・サイドとサーバー・サイド両方のフォーム検証をサポートします。検証は組み込みバリデーターという形でサポートされます。Wicket のバリデーターは、検証内容に応じて NumberValidatorStringValidatorPatternValidatorDateValidatorRequiredValidator などの数々のバリデーターの間で切り替わります。

また、Wicket に組み込まれていない検証を実行するカスタム・バリデーターを作成することも可能です。サーバー・サイドの検証の場合、フォームがサブミットされた後、Wicket はフォーム内に配置されたすべてのフォーム・コンポーネントを調べ、そこに配置されているすべてのバリデーターをコンポーネント入力に対して実行します。Wicket は、このプロセスでバリデーターがスローしたエラー・メッセージすべてを収集します。すると FeedbackPanel コンポーネントが、収集されたすべてのエラー・メッセージを表示します。

組み込みバリデーター

Wicket は、検証の操作方法を大幅に簡易化します。Wicket では、ページ・ファイルにフィールド・コンポーネントを作成するときに、そのフィールド・コンポーネントに任意のバリデーターを設定することができます。例えば、ユーザーが何らかの情報を入力しなければならない必須フィールドをページ内に作成するとします (リスト 10 を参照)。

リスト 10. TextField を必須入力にする方法
TextField firstNameComp = new TextField("firstName");
	firstNameComp.setRequired(true);

Wicket では FeedbackPanel コンポーネントがページ内のあらゆるフォーム関連のエラーをレンダリングします。実際には、FeedbackPanelPage 内に含まれるコンポーネントに加えられたすべてのタイプのフィードバック・メッセージを表示しますが、表示する必要があるメッセージ・タイプ (情報、エラー、デバッグ、警告など) にフィルタリングすることもできます。このコンポーネントをページに追加する方法は、リスト 11 のとおりです。

リスト 11. ページ・クラスに FeedbackPanel を追加する方法
add(new FeedbackPanel("feedBack"));

存在するすべての検証エラーを表示するには、"feedback" コンポーネントへの参照を HTML マークアップに追加します (リスト 12 を参照)。

リスト 12. テンプレート・ファイルに FeedbackPanel を組み込む方法
<span wicket:id="feedback"></span>

カスタム・バリデーター

組み込みバリデーターでフォーム・レベルの検証を処理できない場合には、カスタム・バリデーターを作成することができます。その方法は、AbstractFormValidator クラスをサブクラス化し、検証対象のフォーム・コンポーネントをパラメーターとして取るコンストラクターを使用するというものです。カスタム・バリデーターは、フォームをインスタンス化する時点でフォームに追加する必要があります。

一例として、フォーム内の特定の 2 つのフィールドのいずれかが空ではないことを確実にしなければならないとします。このような検証は組み込みバリデーターでは対応できないため、リスト 13 のようなカスタム・バリデーターを作成する必要があります。

ご覧のように、カスタム・バリデーターは、AbstractFormValidator クラスを継承して作成することができます。

リスト 13. EitherInputValidator.java
import java.util.Collections;
import wicket.markup.html.form.Form;
import wicket.markup.html.form.FormComponent;
import wicket.markup.html.form.validation.AbstractFormValidator;
import wicket.util.lang.Objects;

public class EitherInputValidator extends AbstractFormValidator {

   	/** form components to be validated. */
   	private final FormComponent[] components;

   	public EitherInputValidator(FormComponent f1, FormComponent f2) {
      		if (f1 == null) {
         			throw new IllegalArgumentException(
         				"FormComponent1 cannot be null");
      		}
      		if (f2 == null) {
         			throw new IllegalArgumentException(
         				"FormComponent2 cannot be null");
      		}
      		components = new FormComponent[] { f1, f2 };
   	}

   	public FormComponent[] getDependentFormComponents() {
      		return components;
   	}

   	public void validate(Form form) {
   		// we have a choice to validate the type converted values or the raw
   		// input values, we validate the raw input
   		final FormComponent f1 = components[0];
   		final FormComponent f2 = components[1];
   		String f1Value = Objects.stringValue(f1.getInput(), true);
   		String f2Value = Objects.stringValue(f2.getInput(), true);
   		if ("".equals(f1Value) || "".equals(f2Value)) {
         			final String key = resourceKey(components);
         			f2.error(Collections.singletonList(key), messageModel());
      		}
   	}
}

: 関連付けられたモデルを取得するのではなく、検証対象の値を取得するには、FormComponent.getInput を呼び出す必要があります。その理由は、検証が完了するまでモデルは更新されないためです。

カスタム・バリデーターを使用するには、これをフォームに追加し、検証を適用しなければならないフィールド要素を渡します (リスト 14 を参照)。

リスト 14. ページ・クラスでカスタム・バリデーターを使用する方法
	Form myForm = new Form("form");
	FormComponent f1 = new TextField("firstName");
	myForm.add(f1);
	FormComponent f2 = new TextField("lastName");
	myForm.add(f2);
	myForm.add(new EitherInputValidator (f1, f2));

そして最後に、検証が失敗した場合に表示するメッセージをプロパティー・ファイルに追加します (リスト 15 を参照)。

リスト 15. プロパティー・ファイル内の検証メッセージ
EitherInputValidator = Please enter data for either '${input0}' from ${label0}
					   or '${input1}' from ${label1}.

Wicket での Ajax の使用

Ajax (Asynchronous Javascript And XML) ではリッチ UI だけでなく、ハイパフォーマンス・アプリケーションを作成することもできます。Ajax対応のアプリケーションは、ページ全体をリフレッシュするのではなく、ページの必要な部分だけを更新するからです。こうすることによって、アプリケーションはデスクトップ・ベースのアプリケーションのように反応するようになります。このデスクトップ・アプリケーション風の反応を可能にするのはブラウザーの XMLHttpRequest の実装です。この実装では、クライアントがサーバーと非同期に通信し、HTML DOM API を使用してサーバーから受信したレスポンスに応じてページのコンテンツを動的に操作することができます。

Wicket は、ユーザーがサーバーとブラウザーとの間でデータを送信し、処理しなければならないという問題に対処します。データをクライアントに送信する代わりに、Wicket はサーバー・サイドでコンポーネントをレンダリングし、このレンダリングしたマークアップを送信します。この動作は Ajax ほど効率的ではないかもしれませんが、Ajax の振る舞いを開発するよりは遙かに簡単で時間もかかりません。例えば、Ajax を使用するクライアントで、ユーザー情報を表示するパネルを更新しなければならない場合に必要となる作業は、Wicket にパネルを更新するように指示することだけです。Wicket がクライアントに返したレスポンスを JavaScript で構文解析する必要も、クライアントで DOM を操作する必要もありません。

Wicket では、Ajax ベースのリクエストは振る舞いとしてモデル化されます。Wicket に備わっている IBehaviour インターフェースの複数の実装 (AbstractDefaultAjaxBehaviourAjaxFormComponentUpdatingBehaviour など) は、内部処理を行って respond() メソッドを呼び出し、AjaxRequestTarget に渡します。このターゲット・オブジェクトが、Ajax リクエストに応えてブラウザーに送信する必要がある実際のコンテンツを取得します。AjaxRequestTarget はこのターゲット自体に追加されたコンポーネントのみをレンダリングし、組み込み JavaScript 成果物が HTML の outerHTML プロパティーを初期化して、追加されたコンポーネントを再レンダリングします。

Ajax AutoCompleteName を例に挙げると、このアプリケーションはユーザーの入力に基づいて候補のリストを予測し、このリストからユーザーが値を選択できるようにします。

アプリケーションを構成するのは、テキスト・フィールド要素を持つページ (リスト 16 を参照)、メモリー内にある事前定義された名前 (同じく動的にできます) のリスト、そしてユーザーがテキストの入力を開始したときに候補の値リストを表示する Ajax の振る舞いです。

リスト 16. HTML ページ・テンプレート (AutoCompleteName.html)
<html>
  <head>
	<wicket:head>
		<style>
		  div.wicket-aa {
			font-family: Verdana,"Lucida Grande","Lucida Sans Unicode",Tahoma;
			font-size: 12px;
			background-color: white;
			border-width: 1px;
			border-color: #cccccc;
			border-style: solid;
			padding: 2px;
			margin: 1px 0 0 0;
			text-align:left;
		  }
		  div.wicket-aa ul {
		    list-style:none; padding: 2px; margin:0;
		  }
		  div.wicket-aa ul li.selected {
		    background-color: #FFFF00; padding: 2px; margin:0;
		  }
		</style>
	</wicket:head>
  </head>

  <body bgcolor="#FFCC00">
	<br>
	<br>
	<form wicket:id="form">
		<b>Name :</b> <input type="text" wicket:id="name" size="60" />
	</form>
  </body>
</html>
図 5. Wicket Ajax の例 (ロード時)
Wicket Ajax の例 (ロード時)
Wicket Ajax の例 (ロード時)

Wicketでは、この自動補完の振る舞いが AutoCompleteBehaviour クラスにモデル化されています。入力に応じて候補となるユーザーのリストを返すには、このクラスを継承し、Iterator getChoices(String input); メソッドの実装を提供する必要があります (リスト 17 を参照)。

リスト 17. ページ・クラス (AutoCompleteName.java)
package myPackage;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteTextField;
import wicket.markup.html.WebPage;
import wicket.markup.html.form.Form;
import wicket.model.Model;

public class AjaxWorld extends WebPage {
   	private List names = Arrays.asList(new String[] { "Kumarsun", "Ramkishore",
   	  "Kenneth", "Kingston", "Raju", "Rakesh", "Vijay", "Venkat", "Sachin" });

   	public AjaxWorld() {

      Form form = new Form("form");
      AutoCompleteTextField txtName = new AutoCompleteTextField("name", new Model()){

         protected Iterator getChoices(String input) {
            			List probables = new ArrayList();
            			Iterator iter = names.iterator();
            			while (iter.hasNext()) {
               				String name = (String) iter.next();
               				if (name.startsWith(input)) {
                  					probables.add(name);
               				}
            			}
            			return probables.iterator();
         		}
      	};
   	form.add(txtName);
   	add(form);
   }
}
図 6. Wicket Ajax の例 (Ajax レスポンス)
Wicket Ajax の例 (Ajax レスポンス)
Wicket Ajax の例 (Ajax レスポンス)

I18N サポート

Wicket で I18N (国際化) に対応するための手段は、ロケール固有のプロパティー・ファイルからメッセージを読み取ることです。そのためには、ページ・クラスで StringResourceModel クラスを使用するか、または HTML マークアップ・ファイルで <wicket:message> タグを使用します。また、StringResourceModel を使用して、表示対象のローカライズされたメッセージのフォーマットを設定することもできます。

<wicket:message> タグによる I18N

ページに表示する必要があるラベルやその他の情報は、ラベルをハード・コーディングしなくても、<wicket:message> タグを使ってローカライズすることができます (リスト 18 を参照)。

リスト 18. テンプレート・ファイル (MyPage.html)
<html>
<head>
<title></title>
</head>
<body>
	<form wicket:id="myForm">
		<wicket:message key="first-name">First Name</wicket:message>
	</form>
</body>
</html>

Wicket は <wicket:message> タグを検出するたびに、HTTP リクエスト・オブジェクトに設定されたロケールに基づき、そのロケール固有のプロパティーからキーの値を読み取ります。

StringResourceModel クラスによる I18N

テンプレート・ページで <wicket:message> タグを使う代わりに、ページで直接 StringResourceModel を使用して、入力キーに基づいてローカライズされたメッセージを取得することもできます (リスト 19 を参照)。StringResourceModel クラスの利点は、メッセージをレンダリングする前に、柔軟にフォーマット設定できることです。

リスト 19. ページ・クラス (MyPage.java)
public class MyPage extends WebPage {
   	public MyPage() {
      		Form form = new MyForm("myForm");
      		String firstNameLabel = new StringResourceModel("first-name").getString();
      		form.add(new Label("firstName", new Model(firstNameLabel)));
      		add(form);
   	}
}

リソース・バンドルの検索順

Java i18n システムでは通常、メッセージはロケールごとに検索されます。ロケールは HTTP リクエスト・ヘッダーの Accept-Language フィールドにあるロケールから自動的に抽出されて Wicket WebRequest オブジェクトに設定されます。その一方、getSession().setLocale(Locale.US) を使用して明示的にロケールを設定することもできます。

特定コンポーネントのロケールを上書きしなければならない場合には、そのコンポーネントで getLocale() を無効にすることで、そのコンポーネントと子コンポーネントが指定のロケールを使用するようになります。クライアントが優先ロケールを指定しなければ、Java コードのデフォルト Locale が使用されます。

Wicket は、コンポーネント階層に含まれるコンポーネントと同じ名前を持つすべてのリソース・ファイルを検索し、最後に該当するアプリケーションを検索します。ここで忘れてならないのは、メッセージが検出されると、Wicket が検索プロセスを停止することです。例えば、Wicket が MyPanel で使用されるメッセージを検索するとします。この場合、MyPanelMyPage 配下の MyForm 内に含まれていて、アプリケーションの名前が MyApplication だとすると、Wicket の検索順は以下のようになります。

  • MyPanel_locale.properties、…、それから MyPanel.properties
  • MyForm_locale.properties、…、それから MyForm.properties
  • MyPage_locale.properties、…、それから MyPage.properties
  • MyApplication_locale.properties、…、それから MyApplication.properties (..)

実際にはさらに 2 段階踏み込んで、Wicket は MyPanelMyFormMyPage、そして MyApplication の基底クラスのプロパティー・ファイルも調べます。MyPanelPanel からの直接の継承、MyFormForm からの直接の継承、MyPagePage からの直接の継承、そして MyApplicationApplication からの直接の継承である場合には、Wicket の検索順は以下のようになります。

  • MyPanel_locale.properties、…、それから MyPanel.properties
  • Panel_locale.properties、…、それから Panel.properties
  • MyForm_locale.properties、…、それから MyForm.properties
  • Form_locale.properties、…、それから Form.properties
  • MyPage_locale.properties、…、それから MyPage.properties
  • Page_locale.properties、…、それから Page.properties
  • MyApplication_locale.properties、…、それから MyApplication.properties (..)
  • Application_locale.properties、…、それから Application.properties (..)

上記のステップがすべて失敗すると、Wicket はデフォルトで labelIdLabel として使用します。

: MyFormMyPage の内部クラスとしてモデル化されている場合には、Wicket は MyPage$MyForm.properties という名前のリソース・ファイルを検索します。したがって、ベスト・プラクティスとしては、MyApplication.properties をサイト全体のメッセージとして使用し、その他すべてのプロパティー・ファイルの内容を無効にするという方法を採ることができます。

図 7. リソース・バンドルの検索順
リソース・バンドルの検索順
リソース・バンドルの検索順

Wicket を利用して作成したページのユニット・テスト

Wicket はコンテナー外でのユニット・テストをサポートするために、組み込みモック・オブジェクト・フレームワークを使用します。このフレームワークは、アプリケーションが相互作用する周辺環境のオブジェクトと、フレームワークとが Java EE サーブレット・コンテナー外部で実行されているとしても、構成されたとおりに確実に振る舞うようにします。その結果としてもたらされるのは、生産性の向上です。なぜなら、コンテナーを再起動しなくても、対象とするコンポーネントのユニット・テストに専念することができるからです。

モック・オブジェクトは、コード・ロジックの一部を、コードの他の部分とは切り分けてテストするために使用します。モック・オブジェクトには、実際のクラスのモックとして作成したクラスのビジネス・メソッドすべての振る舞いをテストで制御できるようにするためのメソッドが用意されています。

Wicket でのユニット・テストのサポートは、JUnit フレームワークの継承がベースとなっています。Wicket の wicket.util.tester.WicketTester クラスには、さまざまなユーザー・アクション (リンクのクリック、フォームのサブミットなど) や振る舞い (ページのレンダリング、エラー・メッセージの存在のアサートなど) を模倣する際に役立つ多数のヘルパー・メソッドが用意されています。

リスト 20 は本番用のページ (MyPage.java) の一例で、このページにはいくつかのコンポーネントと、フォームのサブミット、リンクのクリックなどのユーザー・アクションがあります。

リスト 20. ページ・クラス (MyPage.java)
import wicket.markup.html.WebPage;
import wicket.markup.html.basic.Label;
import wicket.markup.html.form.Button;
import wicket.markup.html.form.Form;
import wicket.markup.html.form.TextField;
import wicket.markup.html.link.Link;

public class MyPage extends WebPage {
   	public MyPage() {
      		MyForm form = new Form("myForm");
      		form.add(new Label("firstNameLabel", "First Name"));
      		form.add(new Label("lastNameLabel", "Last Name"));

      		form.add(new TextField("firstName"));
      		form.add(new TextField("lastName"));

      		form.add(new Button("Submit"));
      		form.add(new Link("nextPage") {
         			public void onClick() {
            				setResponsePage(new NextPage("Hello!"));
         			}
      		});
   	}
}

ページのレンダリングをテストするためのテスト・ケース

最低限行わなければならないテストの 1 つは、すべてのページがそれぞれに正しくレンダリングされることを確かめるためのテストです。このテスト (リスト 21 を参照) が正常に完了すれば、テンプレートとページ階層は確実に一致していることになります。

リスト 21. ページのレンダリングのテスト (MyPageRenderTest.java)
import wicket.util.tester.WicketTester;
import junit.framework.TestCase;

public class MyPageRenderTest extends TestCase {

   	private WicketTester tester;

   	public void setUp() {
      		tester = new WicketTester();
   	}

   	public void testMyPageBasicRender() {
      		WicketTester tester = new WicketTester();
      		tester.startPage(MyPage.class);
      		tester.assertRenderedPage(MyPage.class);
   	}
}

ページ・コンポーネントをテストするためのテスト・ケース

WicketTester クラスには、指定したページに、必要なすべてのコンポーネントがあることを検証するための組み込みメソッドがあります。検証では、このクラスの assertComponent() メソッドにコンポーネント・パスと、指定したそのパスにあることが期待されるコンポーネント・タイプを渡します (リスト 22 を参照)。指定するパスは、コンポーネントが組み込まれているページに対する相対パスでなければなりません。

リスト 22. ページ・コンポーネントのテスト (MyPageComponentsTest.java)
import junit.framework.TestCase;
import wicket.markup.html.form.TextField;
import wicket.util.tester.WicketTester;

public class MyPageComponentsTest extends TestCase {

   	private WicketTester tester;

   	public void setUp() {
      		tester = new WicketTester();
   	}

   	public void testMyPageComponents() {
      		WicketTester tester = new WicketTester();
      		tester.startPage(MyPage.class);

      		// assert rendered field components
      		tester.assertComponent("myForm:firstName", TextField.class);
      		tester.assertComponent("myForm:lastName", TextField.class);

      		// assert rendered label components
      		tester.assertLabel("myForm:firstNameLabel", "First Name");
      		tester.assertLabel("myForm:lastNameLabel", "Last Name");

   	}
}

OnClick ユーザー・アクションをテストするためのテスト・ケース

リンクのクリックといったユーザー・アクションをテストするには、WicketTester の clickLink() メソッドを使用して、リンク・コンポーネント ID のパスを渡し、その結果レンダリングされたページを検証します (リスト 23 を参照)。

リスト 23. ページでの OnClick アクションのテスト
public void testOnClickAction() {
		tester.startPage(MyPage.class);

		// click link and render
		tester.clickLink("nextPage");

		tester.assertRenderedPage(NextPage.class);
		tester.assertLabel("nextPageMessage", "Hello!");
	}

public void testNextPageRender() {
		// provide page instance source for WicketTester
		tester.startPage(new TestPageSource() {
			public Page getTestPage() {
				return new NextPage("Hello!");
			}
		});

		tester.assertRenderedPage(YourPage.class);
		tester.assertLabel("nextPageMessage", "Hello!");

	}

フォームをサブミットするユーザー・アクションをテストするためのテスト・ケース

Wicket でのフォームのサブミットをテストするには、Wicket の wicket.util.tester.FormTester クラスを使用します。このクラスには、フォーム内に配置されたフィールド・コンポーネントの入力値を設定し、続いてフォームをサブミットするための API があります。フォームをサブミットすると表示されるページが、メッセージ「Welcome to Wicket」が含まれる Welcome.java だとすると、フォームのサブミットをテストするコードはリスト 24 のようになります。

リスト 24. ページでの OnSubmit アクションのテスト
public void testFormSubmit ()
   {
      		// Create the FormTester object
      		FormTester ft = tester.newFormTester("myForm");

      		// Set the input values on the field elements
      		ft.setValue("firstName", "Kumar");
      		ft.setValue("lastName", "Nadar");

      		// Submit the form once the form is completed
      		ft.submit("Submit");

      		// Check the rendered page on form submission
      		tester.asserRenderedPage(Welcome.class);
      		// verify the message on the rendered page
      		tester.assertInfoMessage(new String[]{"Welcome to Wicket"});

	}

まとめ

Wicket のように POJO (Plain Old Java Object) を主体としたフレームワークを使用すれば、Web ベースのアプリケーションを煩わしさのない単純な方法で素早く作成することができます。HTML やその他のマークアップにプログラミング・コードが入り混じることはないため、UI 設計者は容易にフレームワークによるタグ付けを認識し、手を付けないようにすることができます。

Wicket を使えば、Java-Swing のような方法でページを作成することができ、XML 構成ファイルを使い過ぎないようにするための手段となるだけでなく、開発したページのユニット・テストを行う包括的な方法も提供されるのです。


ダウンロード可能なリソース


関連トピック

  • Apache Wicket 資料の「Control where HTML files are loaded from」を読んでください。
  • Wicket ホーム・ページにアクセスしてください。ここには Wicket がどのように機能するかをより深く理解するためのチュートリアルも用意されています。
  • Wicket サンプルにアクセスして、コア関数を実証する選り抜きのサンプルを調べてください。
  • Apache Wicket を入手して自分で試してみてください。
  • Wicket フレームワークのクイック・スタート・アプリケーション、Qwicket をダウンロードしてください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=355274
ArticleTitle=Wicket: 動的 Web ページの作成およびテスト用の単純化されたフレームワーク
publish-date=11042008