目次へ戻る

Javaウェブフレームワーク「Wicket」の使い方

RSS Readerを作る その1

矢野 勉

このエントリーを含むはてなブックマーク

目次

気持ちのいいウェブプログラミング

このページでは、ウェブ・アプリケーション・フレームワークWicketの使い方について紹介します。Wicketの特徴は「HTMLとJavaだけですべてが完了する」ということでしょう。

Struts以降、ほとんどのウェブ・アプリケーション・フレームワークはJSP + Java + XMLで構成されてきました。そしてXMLの使用頻度がどんどん上がってきています。さらにJSPにも、各ウェブ・アプリケーション・フレームワークごとに独自のタグがあったり、そもそもJSPでもない独自の言語だったりもします。

ウェブ・アプリケーションの開発者は、他にもCSSとかJavaScriptとかSQLとか、さまざまな言語を駆使しなければいけません。ただウェブページを作るだけでも大変なのに、開発を楽にするフレームワークがさらに山ほどのXMLを要求したのでは、アプリケーション開発が楽しくなくなっても仕方がありません。XMLを書いててもプログラムを書いてる気がしなくて、面白くないですよね。

Wicketなら、最初のセッティングを終わらせると、あとはほとんどJavaとHTMLのコードを書いていくだけ。Javaプログラマは(仕事で仕方がなくやってるという人以外は)楽しいからJavaをやっているわけですから、Javaで書いている時が一番気持ちがいい。自分の書いたJavaコードがそのまま画面の動作に直結している...そんな気持ちよさがWicketにはあります。

Wicketの使いどころ

Wicketは何でもかんでも解決してくれるフレームワークではありません。Ruby on Railsのようなフルスタックのフレームワークではありませんから、DBに関連することは何も行いません。最近はフルスタックが流行りですが、JavaにはJavaの文化があって、いろんなフレームワークを好きに組み合わせて使おう、というのはJavaらしい文化ではないでしょうか。

WicketはMVCでいうところのV(ビュー)とC(コントローラ)を担当します。Strutsの使いどころが、Wicketの使いどころになると思ってもらえば間違いないと思います。

Wicketの考え方

Wicketは、ウェブ・アプリケーションでは当たり前の「リクエスト・レスポンス」という考え方を捨てることを目指しています。「コンポーネントによるプログラミング」「ウェブ・アプリケーションにオブジェクト指向を」がWicketが目指すところ。もちろんHTTPベースのアプリケーションである以上、リクエストとレスポンスというものがある、と言うことは知っておかなければいけませんが、基本的な操作においてはリクエストとレスポンスを意識することはないでしょう。

代わりにWicketではウェブアプリケーションをGUIアプリケーションのように扱います。Wicketの製作者はSwingの開発をしていたということですので、そのプログラミング心地はSwingのコードを書いているのに近いものがあります。

つまり、Swingのようにコードで画面にコンポーネントを構成して表示する、という流れです。徹底したコンポーネント指向が貫かれています。リクエスト・レスポンス前提の考え方で行くとその「気持ちよさ」を体験できないので、頭を切り替えましょう。

Swingと同じように、コンポーネントの入力値検証もコンポーネントにValidatorを設定することで行います。Commons Validaterのように、別途XMLを用意して...なんてことはありません。

コンポーネントの入力値は、コンポーネント自体ではなく、自分の指定したJavaBeansに自由にバインドできるように作られています。コンポーネントの値=モデルのプロパティという状態にしようと思えば出来てしまいます(Ruby on Railsのように)。

Swingが得意な人には、「JGoodies Data Binding」と言えばわかるでしょうか。コンポーネントとドメインモデルがあって、それをWicketが相互にバインドしてくれます。コンポーネントと、ドメインモデルのプロパティとを、簡単にバインドできてしまいます。すべてJavaで制御するので簡単にできます。

画面制御も、表示の切り替えも、モデルを含めたコンポーネントの接続も全部Javaで書ける....書いてみると本当に気持ちが良いです。ウェブ・アプリケーションの作成ってなんだか「オブジェクト指向」っぽくないと思っていませんでしたか? 単純なデータのやり取りと、それを処理する手続きからなる「手続き型」になっていませんか? ウェブ・アプリケーションの開発を「もっとJavaっぽい、まともなオブジェクト指向にしようぜ」という考えの上に作られたWicketフレームワークは、継承やリスナを使ってとことん拡張可能になっています。

さらに「アンマネージド(unmanaged)」であることもWicketらしさの一つです。Wicketはコンテナではありません。多くのフレームワークが「コンテナ」化していって、オブジェクトは基本的にコンテナに作ってもらうもの、となってきているなか、Wicketは「オブジェクトを生成するときはプログラマが自由にnewして作ればいい」という姿勢をとっています。ですからWicketの各コンポーネントはプログラマが自分でnewして、自分でページにセットして、自分で値を取り出すのであって、コンテナが制御するものではないのです。

このアンマネージドな姿勢がWicketをプログラマ寄りの、プログラマが自分でプログラムを制御しているという感じを抱かせる原因の一つでしょう。

では早速、Wicketを使う準備をしましょう。

まずはダウンロード

ともかく、Wicketをダウンロードしましょう。

Wicketのホームページ

この記事はバージョン1.2.3リリース版をもとに書かれています。現在Wicket 2.0の開発が進められているようですが、2.0ではJ2SE 5.0完全対応をするようなので、また使い方はいろいろ変わるかもしれません。まあ2.0の開発には時間がけっこうかかると思うので、1.2でお楽しみください。

さらに「wicket-extensions」というものもダウンロードしておきましょう。これにはWicket本体には含まれていない、便利な拡張機能が入っています。タブを実現するクラスとかあって面白いですよ。

もしSpringとWicketとの連携に興味があれば、「wicket-spring」も面白いと思います。アノテーションを記述すると、そこにSpring BeanがバインドされるというWicket機能拡張版です。

「wicket-example」はソースの書き方の勉強になります。Wicketはソースの書き方の自由度が結構高くて、奨励されない書き方もあっさりできてしまうところがあります。しかもそのまま動いてしまったりもするので困ったものです。その辺もこの記事では書いておきたいと思ってますが、迷ったらexampleの書き方を調べてみてください。

インストール

Wicket自体はどこかに解凍するだけでかまいません。解凍すると、libフォルダの中に必要なjarファイルがまとまって入っています。下記の4つのjarをクラスパスに設定すれば、それでいつでもWicketを使い始められます。Antタスクに設定するもよし、IDEのライブラリとして登録するもよし、お好みの開発手法でどうぞ。

おなじみの設定 - Javaサーブレットとしての設定

WicketはJava Servlet APIの上に構築されたサーブレートです。なので、最低限のサーブレットの設定は必要です。これがWicketにおける唯一のXML設定でしょう。web.xmlの設定です。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <servlet>
        <servlet-name>RssReader</servlet-name>
        <servlet-class>wicket.protocol.http.WicketServlet</servlet-class>
        <init-param>
            <param-name>applicationClassName</param-name>
            <param-value>rssreader.Application</param-value>
        </init-param>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>RssReader</servlet-name>
        <url-pattern>/rss/*</url-pattern>
    </servlet-mapping>
             
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
	<welcome-file>
            index.jsp
        </welcome-file>
    </welcome-file-list>
</web-app>
        

今回はRssReaderという名前のサーブレットを定義しました。Wicketでのサーブレットクラスは「wicket.protocol.http.WicketServlet」になるのが普通です。これがリクエストの処理とイベントへの変換を行います。init-parameterで指定しているapplicationClassNameというパラメータが、実際に作成するアプリケーションのクラスになります。ここでは「rssreader.Application」としておきました。

Wicketにはデスクトップアプリケーションのように、ひとつのApplicationクラスがあります。そのクラスをここで指定することで、WicketServletがアプリケーションにイベントを渡してくれるようになります。

これを読んでいる方はサーブレットのxml設定なんてわかるでしょうから、さっそくアプリケーションの作成に入りますよ。

Wicketアプリケーションの作成

今回作ろうとしているアプリケーションの 完成イメージがありますので、一度触ってみてください。URLを入力するとRSSフィードを読み込んで表示します。この完成イメージのプログラムが こちらからダウンロードできますので、とりあえずプログラム全体をまとめて見たい方はどうぞ。私はIDEとしてNetbeansを使っているので、ダウンロード・ファイルもNetbeans用のプロジェクトファイルです。Netbeansユーザーなら普通に開けるはずです。NetbeansのビルドはAntを使っているので、Antが入っている人はantコマンドで自分でビルドすることもできます。Eclipseの人はご自分でソースを組み込んでもらう必要がありますが、ソース数は対したことありませんのでご安心を。

ただしこれはあくまで完成イメージであって、実際に作っていくプログラムでは私が記事を書いている最中に「この部分はもっと説明しておこう」とか「こういうことしたら面白いね」とか思えばどんどん追加していくので、もう少し規模が大きくなると思います。あくまで参考程度にご覧下さい。

さてweb.xmlにアプリケーション・クラスとして設定した「rssreader.Application」クラスを作りましょう。

import rssreader.pages.ListPage;
import wicket.protocol.http.WebApplication;

public class Application extends WebApplication {
    public Application() {
    }

    public Class getHomePage() {
        return ListPage.class;
    }
    
    @Override
    protected void init() {
        super.init();
        getRequestCycleSettings().setResponseRequestEncoding( "UTF-8");
        getMarkupSettings().setDefaultMarkupEncoding( "Shift_JIS");
    }    
}
        

まずはこのくらいです。WicketのアプリケーションクラスはWebApplicationクラスを継承します。WebApplicationクラスで唯一abstractメソッドとして宣言されているのが「getHomePage()」メソッドです。

getHomePage()メソッドは、アプリケーションにアクセスした場合に最初に表示されるPageクラスを返します。Pageクラスは、WicketにおいてWebページを表すクラスで、ページに表示されるすべての要素、値、サブミットの受け付け、ページ遷移などを司る、最も中心的なクラスです。詳細は後ほど説明するとして、ここはListPageという名前のクラスを返却することにします。アプリケーションにアクセスすると、まずはListPageというページが表示されるということですね。しかし、ListPageというクラスはまだ作っていませんので、この段階ではコンパイルエラーになります。

init()メソッドはスーパークラスのWebApplicationクラスにあるinit()をオーバーライドしています。このメソッドはアプリケーション起動後の初期化を行うためのメソッドです。初期化処理は必ずここに書き、コンストラクタには書きません。というのは、コンストラクタ実行中にはまだWicketは中途半端な状態にあるため、初期化を行うためにはタイミングが早すぎるのです。あくまで生成後に呼び出されるinit()を使うのが正しい作法です。

ここで行っているのは、日本語サイトを構築する際には意識せざるを得ない、文字エンコード設定です。

Wicketにはさまざまな「Setting」オブジェクトがあり、WebApplicationクラスのgetXXXSettings()というメソッドで取り出せます。ここでは「getRequestCycleSettings()」でリクエスト・レスポンスについての設定を行うためのオブジェクトを取り出し、リクエスト・レスポンス時の文字エンコード、つまりブラウザとサーバ間でやりとりされる文字エンコードを指定しています。

「getMarkupSettings()」ではWicketが使うHTMLテンプレート・ファイルの処理方法を設定するためのオブジェクトを取り出しています。setDefaultMarkupEncoding()メソッドは、Wicketが読み込むHTMLファイルのエンコードを指定しています。

今回は、敢えてテンプレート・ファイルのエンコードを「Shift_JIS」に、リクエスト・レスポンスのエンコードを「UTF-8」にしました。できればマークアップ・ファイルとリクエスト・レスポンスのエンコードは一致させたほうが良いと思いますが、実際の開発ではサーバー環境や開発環境の問題もあって、二つが異なってしまうこともあります。そういう場合でも、Wicketの設定値で変更できるという意味で設定してみました。

ちなみに、デフォルト値を一切設定しなかった場合は、両者とも「UTF-8」に設定されますし、マークアップ・エンコーディングについてはHTMLファイルがXHTML形式で書かれているなら、ファイル先頭に書くXML宣言「<?xml version='1.0' encoding='xxxxxx'?>」で指定したエンコードが優先的に使用されます。

WebApplicationクラスで最低限設定しないといけないことはこれだけです。ただこれでは何も表示できませんね。このアプリケーションは最初にListPageというページを表示しますが、まだそのページを作ってませんからね。

Pageクラスの作成

Wicketでは、各HTMLページをPageというインターフェース経由で操作します。このPageインターフェースをimplements(実装)するのが、WebPageクラスです。プログラマはこのWebPageクラスを継承して各ページに対応するクラスを作っていくことになります。

Wicketの特徴的な機能の一つは、シンプルなHTMLそのものをページ・テンプレートとして使用することです。Velocityのようにvmファイルを作ったり、JSPのようにカスタムタグだらけのJSPファイルを作ることはありません。Dreamweaverなどで作成した拡張子htmlのファイルをページ・テンプレートとして使用します。

さらにXMLファイルを使わないことが身上のWicketですから、WebPageクラスとHTMLテンプレートを結びつけるために設定ファイルを使いません。Wicketは非常にシンプルなルールで各Pageクラスが使うhtmlテンプレート・ファイルを探します。

「クラスパス上の、Pageクラスと同じパッケージ階層の同じ名前のhtmlファイルを使用する」という決まりに従って探すのです。

今回は前述のApplicationクラスで指定したように、初期ページとして「rssreader.pages.ListPage」というクラスを使います。Pageクラスには必ず対となるHTMLテンプレート・ファイルが必要で、Wicketは自動的にテンプレートを見つけ出します。上記決まりごとに従って、WicketはこのListPageクラスに対応するテンプレートファイルは「/rssreader/pages/ListPage.html」だと判断するのです。

原理がわかったところで、まずはテンプレート・ファイルを作りましょう。お好みのIDEで「rssreader.pages」というパッケージを作ってください。そしてその下にListPage.htmlを作成します。内容はまずは以下のようにしましょう。

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
  <head>
    <title>List Page</title>
    <style type="text/css" media="screen">
    <!--
        h1   { font-size: 16pt; background-color: orange; padding-top: 2px; padding-bottom: 2px }
    -->
    </style>
  </head>
  <body>
    <h1>エントリ一覧</h1>
    <div wicket:id="test">テスト</div>
  </body>
</html>
        

ただのXHTMLファイルです。ただ一ヶ所、<div>要素に「wicket:id」という属性が指定されています。WicketのPageクラスはこのwicket:idを使ってHTML上の要素を見つけて、変更するのです。

まずはdiv要素内の「テスト」を「Hello, world!」に置き換えましょう。Pageクラスを作るのです。

同じrssreader.pagesパッケージ内に、ListPage.javaというファイルを作ります。内容は次のようになります。

package rssreader.pages;

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

public class ListPage extends WebPage {
    public ListPage() {
        super();
        add( new Label("test", new Model("Hello, World!")));
    }
}
        

WebPageクラスを拡張したListPageクラスを定義しています。中身は単純にコンストラクタが一つあるだけです。今流行りのフレームワークではオブジェクト生成はフレームワークに任せるようになっているものが多いですが、Wicketのページクラスはあくまでもコンストラクタを使って生成されます。

Wicketは「アンマネージド」であることを身上としています。POJOであることよりもアンマネージドであることを選んでいるようにも見えます。ですからコンテナやファクトリからオブジェクトを取り出すのではなく、フレームワークが勝手に生成したオブジェクトを使うのでもなく、あくまでもnewでオブジェクトを生成するというJavaの普通のやり方にこだわります。フレームワークに管理された(マネージド)環境ではなく、プログラマが制御する(アンマネージド)環境を提供するのです。

この短いコードでポイントとなるのは、次の一行です。

        add( new Label("test", new Model("Hello, World!")));
        

この一行はWicketのもっとも基本的なコードであり、最も特徴的なポイントでもあります。 WebPageクラスは「コンポーネント」と呼ばれるオブジェクトの「コンテナ」です。コンテナにはさまざまなコンポーネントを「add」することができます。ページの上にボタンやテキストフィールドやセレクトリストが乗っているようなイメージです。ボタン、テキストフィールドなどがコンポーネントです。

もちろんWicketですからコンポーネントはnewで生成するだけでOKです。ここでは「Label」というコンポーネントを生成し、ページに貼り付けています。

Wicketのコンポーネントにはすべて「id」を振らなければなりません。ウェブ・コンポーネントのコンストラクタでは、常に第一引数がidを表しています。このidがHTMLテンプレートとコンポーネントを結びつけているのです。

上記の例では「new Label( "test"...)」の「test」がidです。コンポーネントのidはHTMLテンプレート上の「wicket:id」属性と結びついています。

少し前に作成したHTMLテンプレートを見てください。

<div wicket:id="test">テスト</div>
        

「wicket:id="test"」という部分に注目です。これはwicket:id属性を指定した<div>ブロック全体が、Wicketのコンポーネントとして扱われることを示しています。このdiv要素は、testというidを持つコンポーネント、つまりLabelとして扱われます。

もうひとつ、すべてのコンポーネントには「モデル」というオブジェクトが対になっています。これはコンポーネントのもつ「値」を示すオブジェクトです。Labelは文字を表示するためのコンポーネントですが、このLabelに結びついたモデルが、Labelの表示する文字を決定します。今回は「Hello, World!」という文字列で初期化したModelを設定しています。

このプログラムをTomcatやJettyのようなサーブレット・コンテナにディプロイすると、HTMLで「テスト」と書いていた場所に「Hello, World!」と表示されます。div要素がLabelコンポーネントに置き換えられ、Modelに設定した値が表示されたわけです。

このようにWicketのページクラスはページにウェブ・コンポーネントをaddしていくことで作成します。HTMLの基本的なタグにはすべてコンポーネント・クラスが用意されていて、それらとHTMLのタグをIDで結びつけることによってページを生成するわけです。

HTMLとJavaのクラスが1対1で対応していて、さらにページ内のタグとJavaのコンポーネントが1対1で対応している。ページ・オブジェクトは「アンマネージド」つまりコンテナによって生成されるのではない.....この単純さがWicketが軽量フレームワークと言われる理由なのでしょう。

Wicketのモデルとモデルオブジェクト

前節でLabelに対してモデルを設定しましたね。前節ではモデルのことを「コンポーネントのもつ『値』を示すオブジェクト」だと書きました。

「値を示す」と書いたのは、モデルはそれ自体が「値」を意味するものではないからです。ここは重要なところなので十分に注意してください。Wicketにおけるモデルとは値ではなく「実際の値を指し示すもの」です。値のLocatorだと思えばいいでしょう。Wicketではこのロケータを「モデル(Model)」と呼び、実際の値のことを「モデル・オブジェクト(Model Object)」と呼びます。

すべてのモデルはIModelインターフェースを実装します。IModelには「getObject()」というメソッドがあり、このgetObject()が実際の値を返します。コンポーネントと実際の値の間にモデルが挟まっているようなイメージです。

どうしてこんな構造にしているのでしょうか。コンポーネントに直接値を、LabelならString値を格納してしまえばいいのではないでしょうか? そこが重要なポイントです。

Labelは文字を表示する際に、自身のもつモデルのgetObject()を呼び出します。このときgetObject()が文字を返しさえすれば、Labelはその動作について一切気にしません。getObject()がただのフィールド値を返したのであろうと、プロパティファイルからロードしたのであろうと、データベースにクエリを投げて取得したのであろうと、一切気にしません。ただgetObject()が文字を返してくれさえすればいいのです。

ですからプログラマが独自のIModelの実装クラスを作り、そのgetObject()の中でWebサービスのリモートメソッドを呼び出してその結果を返すように定義した上で、それをLabelにセットしてもいいわけです。Labelは何事も無かったように動作することでしょう。

上記の例では、単純に「new Model("Hello, World!")」として文字を内包したモデルを生成して渡しています。このモデルのgetObject()はもちろん「Hello, World!」という文字列を返すので、Labelは「Hello, World」と表示します。

Labelコンポーネントは、コンポーネントとなったHTMLタグの内側の文字を自身のモデルの値で置き換えます。今回は、もともとHTMLテンプレートに書かれていた「テスト」が「Hello, World!」に置き換わるわけです。

このようなLabelの使い方は一般的ですので、Labelにはユーティリティ的なコンストラクタも用意されています。上記の例は「new Label( "test", "Hello, World!")」と書いても同じように動作します。つまりモデルではなく文字列をそのまま渡すのです。これはあくまで利便性を考えたコンストラクタであって、内部的にモデルが生成されてセットされます。

モデルにもいろいろな種類があることを示すために、ここで違うモデル・クラスを使うように変更してみましょう。頻繁に使うモデルの一つである「PropertyModel」クラスを使います。

その名の通り、PropertyModelは他のオブジェクトのプロパティ値を返すモデルです。以下のように生成します。

new PropertyModel( javaBeans, "propertyName");
        

上記のモデルのgetObject()は、javaBeans.getPropertyName()の結果を返します。

PropertyModelの「"propertyName"」部分は、プロパティの名前だけでなく、OGNL式を記述できます。例えば次のようなTelevision, Makerという単純なJavaクラスを考えてみてください。

package rssreader.examples;

public class Television {
    Maker maker;
    
    /** Creates a new instance of Television */
    public Television( Maker tvMaker) {
        this.maker = tvMaker;
    }
    
    public Maker getMaker() {
        return this.maker;
    }
}
        
package rssreader.examples;

public class Maker {
    String name;
    
    public Maker( String name) {
        this.name = name;
    }
    
    public String getName() {
        return this.name;
    }
}
        

TelevisionのgetMaker()はMakerオブジェクトを返します。あるテレビのメーカー名を取得するPropertyModelを作るには下記のようにOGNL式を記述します。

package rssreader.pages;

import rssreader.examples.Maker;
import rssreader.examples.Television;
import wicket.markup.html.WebPage;
import wicket.markup.html.basic.Label;
import wicket.model.PropertyModel;

public class ListPage extends WebPage {
    Television tv;
    
    public ListPage() {
        super();    
        this.tv = new Television( new Maker("パナソニック"));
        add( new Label("test", new PropertyModel( tv, "maker.name")));
    }
}
        

「new PropertyModel( tv, "maker.name")」の部分がPropertyModelを生成しているところです。このPropertyModelのgetObject()は、tvのmakerプロパティのさらにnameプロパティ、つまり「tv.getMaker().getName()」の結果を返すようになります。

このクラスを使って再度実行してみると、画面には「パナソニック」と表示されるはずです。 OGNL式は単純なプロパティアクセスだけでなく、リストやマップなど多彩なオブジェクト階層に対応していますので、調べてみればPropertyModelの強力さが分かるかと思います。JSPにおける式言語と同じように、どんなに深い階層のプロパティでも簡単に表示できます。

もっと遊んでみましょう。getModel()メソッドが何をやろうと、画面に表示できる文字列を返しさえすればLabelは気にしません。では、自分で独自のModelクラスを作って、getModel()を用意してやればいろいろできそうです。

ListPageクラスのコンストラクタを次のように書き換えてみましょう。

public ListPage() {
    super();
    add( new Label("test", new AbstractReadOnlyModel() {
        public Object getObject(Component component) {
            return new SimpleDateFormat("yyyy年MM月dd日").format( new Date());
        }
    }));
}
        

WicketのAbstractReadOnlyModelという抽象モデルクラスを継承した独自のモデルを作っています。Wicketではここで使っている「無名内部クラス」記法を非常に多用しますので慣れておきましょう。SwingでもActionListenerを生成する時によく使いますが、それと同じことです。

AbstractReadOnlyModelはIModelインターフェースが定義するほとんどのメソッドの実装を提供しますが、唯一getObject()メソッドだけをabstractとして未実装にしているクラスです。プログラマがgetObject()をオーバーライドすることで完成させるわけです。まさにここで使っているように無名内部クラスとして実装クラスを用意することを想定したクラスです(もちろん、普通にextendsしてもサブクラス化できますが)。

今回のgetObject()の実装は、実行時の日付をSimpleDateFormatを使って「2006年11月11日」という形式に整形したものを返します。LabelがモデルのgetObject()を呼び出すとその時刻が返ってくるわけです。

実行してみてください。今度は表示したその時の日付が表示されますね?

このように、Labelという単純に文字を表示するコンポーネント一つをとっても、モデルを介することで柔軟に表示内容を変更できるという利点があるわけです。

さてウェブ・コンポーネントとモデルの概要が分かったところで、今度はユーザーの入力値をどうやって受け取るか、に入りましょう。Wicketではここでもモデルを使います。

モデルへの値のセットと検証

Wicketのモデルについて、すべてのモデルはIModelインターフェースを実装していると書きました。IModelインターフェースが定義しているメソッドはgetObject()だけではなく、もう一つ「setObject()」というメソッドも定義しています。

入力系のウェブ・コンポーネントはHTMLフォームのサブミット時に入力値を自身のモデルにセットします。その際に呼ばれるのがIModelのsetObject()です。モデルを介してJavaのプログラムとHTMLフォームがやり取りしているわけです。Java側ではgetObject()を適切に実装してやることでHTMLの表示内容を制御できます。HTMLフォームはsetObject()を使って入力値をJava側に返却できるわけです。

setObject()で値がセットされるよ、だけなら説明も何もいらないのですが、ここでWicketの値検証機能が出てきます。

たとえばStrutsのアクション・フォームではユーザーの入力した値はとりあえずアクション・フォームにセットされます。Wicketでは、setObject()により値がセットされる前に、少なくとも二つの処理を経由します。一つが「入力値検証(Validation)」で、もう一つが「値変換(Conversion)」です。今回は入力値検証を説明します。

入力値の検証には大きく二種類あります。その入力値単体での検証と、他の同一フォームの入力値との関係の検証です。 Wicketではその双方をサポートしています。

まずは単体での検証を説明しましょう。Wicketのフォーム・コンポーネントにはValidatorというオブジェクトをセットすることが出来ます。Validatorには例えば「RequiredValidator(入力必須の検証)」や「NumberValidator(数値かどうか、さらにその範囲の検証)」といった基本的なものがあらかじめ用意されています。すべてのValidatorに共通しているのは、「IValidator」インターフェースを実装しているということだけです。

しかしほとんどの場合Validatorを自作する必要はないでしょう。他のValidatorでは適切でない場合でも、ほとんどの場合は「PatternValidator」という、「入力値が指定した正規表現にマッチするかどうか」を判定するValidatorで済ませられると思います。

それでもかなり特殊な検証を行いたい場合は、IValidatorを直接implementsするのではなく、StringValidatorという抽象クラスを継承するのが流儀です。実際、IValidatorの唯一のメソッドである「validate()」はdepricatedという指定がされているのです。

StringValidatorの「onValidate()」というメソッドにHTMLフォームの入力値がそのまま文字列として渡ってきますので、StringValidatorをサブクラス化してonValidate()をオーバーライドし、その文字列を検証するためのメソッドを書いてやればいいわけです。

今回はRSSリーダーのフィードURLを入力するフォームを作ります。前にサンプルとして作ったHTML「ListPage.html」を書き換えて、次のようなHTMLを作りました。

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
  <head>
    <title>List Page</title>
    <style type="text/css" media="screen">
    <!--
        h1   { font-size: 16pt; background-color: orange; padding-top: 2px; padding-bottom: 2px }
    -->
    </style>
  </head>
  <body>
    <div wicket:id="feedback">ここにエラー内容を表示</div>
    <h1>エントリ一覧</h1>
    <div wicket:id="test">テスト</div>
    
    <h1>フィードURL登録</h1>
    <form method="POST" wicket:id="urlForm">
      新規URL: <input type="text" name="urlTextField" value="" size="100" width="100" wicket:id="urlTextField" />
                    <input type="submit" value="登録" name="submit" wicket:id="submitBtn" />
    </form>    
  </body>
</html>
        

変わったところは「ここにエラー内容を表示」と書かれたdivブロックと、「フィードURL登録」と書かれたフォーム定義です。それぞれwicket:idが付けられていることが分かると思います。エラー内容のところには「feedback」と、フォームには「urlForm」、さらにフォーム内のインプット項目には「urlTextField」、サブミットボタンには「submitBtn」と付けました。新しく4つのWicketのコンポーネントが追加されたわけです。

この4つのコンポーネントをJava側で作り、ページにaddする必要があります。 Java側に戻りましょう。フォームを作る作業をコンストラクタに書いてしまうとプログラムの見通しが悪くなるので、ここはフォームを作るためのメソッドを用意しましょう。

    /**
     * create a form for input new RSS-feed url.
     */
    void createUrlForm() {   
    
    } 
        

まずはフォームを作ります。Wicketではフォームはそれ自体がページのようなコンテナで、フォームに対して「フォーム・コンポーネント」をadd出来るようになっています。ですから、インプット項目やサブミットボタンを作るにはまず始めにフォーム自体を作る必要があります。

フォームの生成は簡単です。ただnewしてaddするだけですから。

        final Form urlForm = new Form( "urlForm");
        add( urlForm);
        

Labelを貼り付けたのとなんら分かるところはありません。HTMLに書いたwicket:idと同じ「urlForm」をフォームのコンストラクタにIDとして指定しているだけです。上記フォームにはモデルを設定していませんが、これはフォームそれ自体には表示する内容がありませんし、値を入力することもできないものだからです。フォームにモデルを設定することもできることはできるのですが、これは「フォームに張り付いたコンポーネントに共通のモデル」という意味になって、Labelとは異なる意味になってきます。このあたりをここで扱うには複雑な話ですので割愛します(CompaundPropertyModelの話をするまでは必要ないでしょう)。

次にこのフォームにフォーム・コンポーネントを貼り付けます。最初に「urlTextField」を作りましょう。これも簡単です。WicketにはHTMLの「type="text"」のインプット項目に対応する「TextField」というコンポーネントがあるので、これをフォームに貼り付けるだけです。先ほどのプログラムに一行付け加えて、下記のようになります。

        final Form urlForm = new Form( "urlForm");
        add( urlForm);
        final TextField txtFld = new TextField( "urlTextField", new Model());
        urlForm.add( txtFld);
        

TextFieldコンポーネントに「urlTextField」というIDを付けてnewし、それをフォームにaddしています。ここではモデルとして単純にModelクラスをnewしただけのものを渡しています。今回はプログラム制御で何かを表示したいわけではなく、ただ単に入力値を受け取るための入れ物が欲しいだけなので、シンプルにModelを使えば十分なのです。

ここでこの入力コンポーネントに検証機能を追加しましょう。単体コンポーネントの検証機能です。 先ほど作ったTextFiledコンポーネントを入力必須にします。

入力必須にするにはRequiredValidatorというValidatorを使えばいいのですが、ある入力項目を必須項目にすることは非常に多く行うことなので、コンポーネント自体にその指定をするためのメソッドが用意されています。このメソッドが内部的にRequiredValidatorを自身にセットするため、プログラマがRequiredValidatorを使う必要はありません。

txtFld.setRequired(true);
        

これでTextFieldは入力必須項目になります。 次に入力値の検証を行うようにしましょう。今回入力させるのはURLですから、入力値が「http://」で始まっていなければエラーとするようにしましょう。ここでは前述した「PatternValidator」を使って正規表現を使った検証を行います。

txtFld.add( new PatternValidator( "^http://.*$"));
        

このように、Validatorをコンポーネントに設定するには、ページにコンポーネントを貼り付けるのと同じように「add」メソッドを使います。Validatorはいくつでも貼り付けられます。今回使ったPatternValidatorは、コンストラクタに指定した正規表現とマッチしない入力値を拒否するValidatorです。「^http://.*$」は「先頭がhttp://にマッチして、あとは文末まで任意の文字」という意味になります。

ところで、validatorで検証が失敗した場合、その値はどうなるのでしょうか。Wicketは検証を通らない限りモデルのsetObject()を決して呼び出しません。不完全な値はプログラム側に渡さないのです。入力検証に失敗した場合にはエラーメッセージを画面に表示します。

そのようなメッセージを「フィードバック」と言います。HTMLを書いた時に「feedback」というwicket:idを振ったdiv項目がありましたね。これがフィードバック表示を行う場所です。

Wicketにはフィードバック表示のためのコンポーネントがあります。そのコンポーネントをFeedbackPanelといいます。このFeedbackPanelをページのどこかに貼り付けておくと、検証エラーがそこに表示されるようになるのです。FeedbackPanelにはいろいろな機能がありますが、まずは「Validatorが検証失敗したら、エラーメッセージがFeedbackPanelに表示される」と覚えておきましょう。

FeedbackPanelもコンポーネントですから、ページにaddしなければいけません。

add( new FeedbackPanel("feedback"));
        

これだけで問題ありません。

ここまでで、createForm()メソッドは下記のようになりました。

    void createUrlForm() {   
        final Form urlForm = new Form( "urlForm");
        add( urlForm);       
        TextField txtFld = new TextField( "urlTextField", new Model());
        txtFld.setRequired(true);
        txtFld.add( new PatternValidator( "^http://.*$"));
        urlForm.add( txtFld);
        
        add( new FeedbackPanel("feedback"));
    }    
        

もうここまででも、いままでのフレームワークとは全く異なったコードを書いていることに気がつきましたか? これがWicketの面白さです。

さて、まだHTMLに書いたコンポーネントを作りきっていませんね。submitBtnが残っています。

Wicketはフォームのサブミットを受け取るために「イベント」と呼ばれる機構を使います。ブラウザでフォームのサブミット・ボタンが押されると、Wicket側では「サブミット・イベント」が発生し、特定のメソッド(イベントハンドラ)が呼ばれる仕組みです。

フォームについては、フォームの「onSubmit()」というメソッドがイベントハンドラとなっています。プログラマはonSubmit()をオーバーライドすることでサブミット後の処理を書けるわけです。例えば

final Form urlForm = new Form( "urlForm") {
    protected void onSubmit() {
        //ここにサブミット時の処理を書く。
    }
};
        

とFormの無名内部クラスを作成してonSubmit()をオーバーライドしてやれば、簡単にサブミット処理を書けます。

しかしフォームのonSubmit()には欠点が一つあります。フォーム上に複数のボタンがある時のハンドリングが複雑になるのです。フォームに複数のボタンが合った場合、どのボタンを押しても同じonSubmit()が呼ばれてしまうので、切り分けが大変なのです。

ですから、実はサブミット・ボタンに当たるコンポーネント自体にもonSubmit()があり、こちらを使うことでより細かい、ボタン毎のサブミット処理を書けます。今回はそちらを使います。個人的には、こちらのやり方の方がボタンが一つの場合でもプログラムの見通しがいいように思います。

urlForm.add( new Button("submitBtn") {
    @Override
    protected void onSubmit() {
        //ここにサブミット処理を書く。
    }
});
        

このコードはフォームに「submitBtn」というIDのButtonコンポーネントを貼り付けています。貼り付けるその時に無名クラス記法でもって「onSubmit」をオーバーライドしてます。Wicketはサブクラス化によって機能を拡張する方法を多用しますが、それはサブクラスのファイルをたくさん作るのではなく、無名クラスで簡単にサブクラス化を行えるJavaの便利さを利用することを意図したものです。APIリファレンスを見ると、さまざまなクラスのさまざまなメソッドがオーバーライドすることで簡単に機能を変更できるようにデザインされていることに驚くでしょう。無名クラスは非常に便利ですので、是非使ってください。

さて今回は検証結果を確かめる意味で、入力値を単純に画面に表示する機能をここに書きましょう。入力値はさきほど作ったフィードバック・パネルに表示します。

サブミットが発生するとモデルの値を取り出す必要がありますね。モデルの値(モデル・オブジェクト)はPropertyModelを使ってフィールドにマッピングしておけば簡単に使えるのですが、今回は敢えて使っていません。TextFieldコンポーネントを作った際に、そのモデルとしてModelクラスを単純にnewしたものを渡しましたね。ここではそのTextFieldのモデルとモデル・オブジェクトを明示的にとり出す方法を使っています。

まず、ページ上のコンポーネントはFormクラスの「get()」メソッドにコンポーネントIDを渡すことで簡単に取り出せます。ところがこのonSubmit()メソッドはFormクラスではなくButtonクラスのメソッドであることに注意してください。onSubmit内でgetを読んでも、Buttonのgetを呼ぼうとしますので、無意味な操作になってしまいます。ですからButtonの貼り付けられたFormを取り出さなければいけません。

そのためのメソッドが「getParent()」です。これは自身が貼り付けられている親コンポーネントを返します。getParent()とget()を組み合わせれば、簡単に親フォームの別コンポーネントを取り出せますね。

コンポーネントが取り出せたら、今度はコンポーネントのモデルとその値(モデル・オブジェクト)を取り出します。

コンポーネントのモデルは「getModel()」メソッドで取り出せます。つまりモデル・オブジェクトを取り出すには

IModel model = component.getModel();
String value = (String)model.getObject( this);
        

とすることで取り出せます(getObject()の引数はComponentで、値を取り出そうとしているコンポーネントを入れるのが普通です。実はほとんどの場合無視されているようです)。

しかしコンポーネントからモデルを取り出してその値を取り出すなんて操作は頻繁に行うものですから、これも1命令で行うためのメソッドがあります。上記の2行は次の1行にまとめられます。

String value = (String)component.getModelObject();
        

さらに、モデルというのはHTMLページの入力値の入れ物ですから、ほとんどの場合モデル・オブジェクトの型はStringです。ですからキャストもなくしてしまう便利メソッドがあります。上の一行はさらに次のように書き換えられます。

String value = component.getModelObjectAsString();
        

さらにこの値を画面に表示します。表示する場所は前述したようにフィードバック・パネルです。

フィードバック・パネルに情報を表示するのは簡単です。ページを含めたすべてのウェブ・コンポーネントにはフィードバック・パネルに情報を表示するためのメソッドがあるのです。どのコンポーネントにも下記の5つのメソッドがあります。

表示したい情報の重要度によって、いつでもいずれかのメソッドを呼べばメッセージが画面に表示されるのです。またこの重要度によって表示方法(色とか)を変更することも出来ます。

では今まで取り上げたやり方を使って、submitBtnを完成させましょう。

urlForm.add( new Button("submitBtn") {
    @Override
    protected void onSubmit() {
        Component urlField = this.getParent().get( "urlTextField");
        String newUrlStr = urlField.getModelObjectAsString();
        info( "入力値は: " + newUrlStr);
    }
});
        

これで完成です。ここまでで、HTMLに追加した4つのコンポーネントをすべて作りました。この「createUrlForm()」メソッドをページのコンストラクタから呼び出してやれば、実際に動くプログラムになります。

package rssreader.pages;

import java.text.SimpleDateFormat;
import java.util.Date;
import wicket.Component;
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.form.validation.PatternValidator;
import wicket.markup.html.panel.FeedbackPanel;
import wicket.model.AbstractReadOnlyModel;
import wicket.model.IModel;
import wicket.model.Model;

public class ListPage extends WebPage {
    public ListPage() {
        super();
        
        add( new Label("test", new AbstractReadOnlyModel() {
            public Object getObject(Component component) {
                return new SimpleDateFormat("yyyy年MM月dd日").format( new Date());
            }
        }));
        createUrlForm();
    }
    
    void createUrlForm() {   
        final Form urlForm = new Form( "urlForm");
        add( urlForm);       
        TextField txtFld = new TextField( "urlTextField", new Model());
        txtFld.setRequired(true);
        txtFld.add( new PatternValidator( "^http://.*$"));
        urlForm.add( txtFld);
        
        add( new FeedbackPanel("feedback"));
        
        urlForm.add( new Button("submitBtn") {
            @Override
            protected void onSubmit() {
                Component urlField = this.getParent().get( "urlTextField");
                String newUrlStr = urlField.getModelObjectAsString();
                info( "inputed value is: " + newUrlStr);
            }
        });
    }    
}
        

ListPage.javaの内容は最終的には上記のようになったはずです。ウェブ・アプリケーションのプログラムではなく、まるでデスクトップ・アプリケーションのコードのようですね。セッションもリクエストもどこにも出てきません。出てくるのは、コンポーネントだけです。

実行してみてください。入力フォームがでてきますね。そこに試しに何も入れずにボタンをおしてみましょう。エラーが表示されましたね?

エラー表示サンプル画像

「'urlTextField' 欄 は必須です。」というメッセージが表示されました。コンポーネントに設定した「setRequired( true)」がうまく働いています。また自動的に日本語が表示されました。Wicketは内部にデフォルトのエラーメッセージを定義したファイルを持っていますが、このファイルには各言語版があり、自動的にロケールを判断してメッセージを出すのです。

では今度は適当な文字を入れてみてください。試しに「sample」と入れてみると、今度は「'sample' はパターン '^http://.*$' にマッチしません。」と表示されます。これもPatternValidatorがちゃんと動いている証拠です。

このようにWicketではValidatorをコンポーネントに設定することで簡単に検証を行うことができるのです。でも一つまだ言及していませんね。この検証はコンポーネント単体での検証(単体検証)であって、フォーム上の複数のコンポーネント値の関係を検証できていません。今度はそれをやってみましょう。

フォーム全体の検証

まず、今の画面では入力項目が一つしかないので、そもそも関係性検証が行えません。ここはシンプルに、チェックボックスを二つ追加して、そのチェックボックスのいずれかがオンでなければエラーにしてみます。あまり意味がない機能ですが、サンプルということで勘弁してください。

HTMLは下記のようになります。

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
  <head>
    <title>List Page</title>
    <style type="text/css" media="screen">
    <!--
        h1   { font-size: 16pt; background-color: orange; padding-top: 2px; padding-bottom: 2px }
    -->
    </style>
  </head>
  <body>
    <div wicket:id="feedback">ここにエラー内容を表示</div>

    <h1>エントリ一覧</h1>
    <div wicket:id="test">テスト</div>
    
    <h1>フィードURL登録</h1>
    <form method="POST" wicket:id="urlForm">
      新規URL: <input type="text" name="urlTextField" value="" size="100" width="100" wicket:id="urlTextField" />
              <input type="submit" value="登録" name="submit" wicket:id="submitBtn" />
      <br />
      チェック:<input type="checkbox" wicket:id="check1"><input type="checkbox" wicket:id="check2">
  </form>    
  </body>
</html>
        

見ての通り、登録ボタンの下に「チェック:」とラベルのついた二つのinput項目が追加されただけです。

続いてJavaのソースを見てみましょう。ListPage.javaのcreateUrlForm()メソッド内に下記のソースを追加します。

final CheckBox check1 = new CheckBox( "check1", new Model( Boolean.TRUE));
final CheckBox check2 = new CheckBox( "check2", new Model( Boolean.TRUE));
urlForm.add( check1);
urlForm.add( check2);

urlForm.add( new AbstractFormValidator() {
    public FormComponent[] getDependentFormComponents() {
        return new FormComponent[] {check1, check2};
    }
    public void validate(Form form) {
        if( check1.getConvertedInput() == Boolean.FALSE && check2.getConvertedInput() == Boolean.FALSE) {
            error( "チェックボックスがチェックされていません。");
        }
    }
});
        

最初の4行はシンプルですので分かると思います。CheckBoxコンポーネントを作成し、そのモデルとしてModelクラスを渡しています。本来ならばこういう入力値はPropertyModelを使ってページのプロパティにマッピングするといいのですが、今回は入力検証のサンプルであって受け取った値を使うことがないので、単純にModelを使っています。ちなみに値としてBoolean.TRUEを渡していますが、これはCheckBoxのモデル・オブジェクトはBoolean型だという決まりがあるからです。モデルの値がBoolean.TRUEならチェックが入り、Boolean.FALSEならチェックがオフになります。Booleanであってbooleanプリミティブでないのは、プリミティブはモデルのsetObject()でセットできないからです。オブジェクトしかセットできませんからね。

後半のコードが検証部分です。フォーム上の複数の項目を使って検証を行うには、FormValidatorを使います。FormValidatorはIFormValidatorを実装したクラスであれば何でもいいですが、ここでもやはり標準的なベースクラスとしてAbstractFormValidatorが用意されています。普通はこのAbstractFormValidatorをサブクラス化すればいいでしょう。

サブクラスで実装しなければいけないメソッドは二つです。「getDependentFormComponent()」メソッドは、このフォーム検証の対象となるコンポーネントの配列を返さなければいけません。Wicketはここで返されたコンポーネントのすべてで単体検証が成功しないと、フォーム検証を行いません。単体での検証が成功してない以上検証はもはや失敗であり、その値も得体の知れないもののままです。そのままではフォーム検証が成功するはずがありません。

「validate()」が実際の検証を行うところで、このメソッドが呼び出された時には検証対象となるコンポーネントそれぞれについての検証が成功していることが保証されています。ここでは組み合わせの検証だけを行えばよいのです。

検証にはonSubmit()の時のようにgetModelObjectAsString()を使えばよいと思うかもしれませんが、違います。Wicketが検証をクリアしていない値をモデルにセットすることはありません。フォーム検証の段階では文字通り検証の途中のわけですから、まだモデルに値はセットされてないのです。モデルにセットされる前の生の入力値を検証する必要があります。

各フォーム・コンポーネントには「getInput()」という生の入力値を取り出すメソッドがあります。これでHTMLフォームが実際にリクエストとして投げた値そのものが取り出せます。今回のチェックボックスであれば、チェックボックスがオンであれば「on」という文字列が、オフであればnullがセットされます。

しかしもう一つ便利なメソッドがあります。チェックボックスのようにユーザが文字を入力できないコンポーネントの場合、実際に送られてくる文字列は特定できますね。チェックボックスならonしか来ないと考えることができるわけです。Wicketはチェックボックスのモデルに値をセットする時に、この文字列を自動的にBoolean.TRUEに変換するのです。そもそも変換できない値が来るかも知れないのであれば生の入力値を扱う必要がありますが、チェックボックスの場合は変換後の値を使ったほうが便利ですよね。

フォーム・コンポーネントの「getConvertedInput()」メソッドはそのためのメソッドで、モデル向けに変換された値を返します。 上記のソースではcheck1とcheck2の変換後の値を取り出し、いずれかが未チェック(Boolean.FALSE)の場合に、エラーとしてフィードバックパネルにメッセージを表示しているのです。

余談ですが、前述のソースの「getDependentFormComponents()」や「validate()」の中でcreateUrlFormメソッドのローカル変数「check1」と「check2」にアクセスしていることに気がついたでしょうか。これが内部クラスの便利なところで、(staticでない)内部クラスはそのクラスが宣言されたスコープでアクセス可能なすべての変数にアクセスできます。たとえそれがメソッド内のローカル変数であっても、内部クラスが宣言されたメソッド内の変数であればアクセスできます。ただし制限が一つだけあって、アクセスしようとしている変数が「final」と指定されてなければいけません。check1とcheck2がfinalと宣言されているのはそのためです。

Wicketでは内部クラスを(特に無名内部クラスを)非常に多用します。これはValidatorやonSubmit()で同じページ上の他のコンポーネントにアクセスすることがよくあるからです。特にonSubmit()では全入力値をまとめてデータベースに要求を投げたり、別のページへパラメータとして渡したりする必要があるので、ページに自由にアクセスできることが非常に重要になります。

ですからコンポーネントをnewする際は可能な限りfinalを付ける癖をつけたほうがよいでしょう。変数にfinal修飾子を付ける習慣は、変数の予期しない変更を避ける意味で良いプラクティスと言われている面もあります。

内部クラスは(外部変数にfinal修飾子が必要という面で)クロージャに至らない劣化クロージャとか言われますが、一方で上記のAbstractFormValidatorの無名サブクラス作成のように、その場で複数のメソッドをオーバーライドしてサブクラスを作りつつ、なおかつ外部の変数にアクセスできるという、クロージャにはないパワーもあります。無名内部クラスは指定したクラスの完全なサブクラスであって、実行されることだけが目的のクロージャと比べて、その点においてはより強力です。Wicketは内部クラスのパワーを利用したほうが楽にプログラムが組めるようになっていますので、是非finalをつけまくって、内部クラスのパワーを堪能してください。

以上までの説明で、Wicketのセッティング、ページとHTMLの関係、それにユーザー入力値の受け取り方と検証の仕方を説明しました。入出力はプログラム作成の基本ですから、いままでの話はきっとWicketを使っていく上での基礎になってくれると思います。検証などは、どんなValidatorがあるのか調べてみると面白いでしょうし、サブクラスの定義の仕方などを検討してみると理解も深まるでしょう。

最も重要なのはウェブ・コンポーネントにValidatorをaddするという部分です。ウェブ・コンポーネントにはValidator以外にもさまざまなものをaddして機能を拡張することが出来ます。そのいずれの場合でも役に立つのが、コンポーネントにオブジェクトをaddするときのWicketなやり方「内部クラス」です。このコードの書き方は是非覚えてください。

メッセージを変える

Validatorでエラーメッセージが表示されるようになりましたが、どうもこのメッセージは戴けませんね。さすがにユーザーに「'sample' はパターン '^http://.*$' にマッチしません。」なんてメッセージを表示するわけにはいかないでしょう。

Wicketではエラーメッセージも簡単に変更できます。 デフォルトではWicketがあらかじめ定義しているプロパティ値が使用されているのですが、これを規則に従ったプロパティファイルを作ることで独自のメッセージに上書きすることができます。

方法は簡単です。ページ毎にページ名と同じプロパティファイルを作ればそれが優先的に読み込まれるようになっているのです。今回はListPageというページ用のメッセージファイルを作りますので、ListPage.javaと同じところに「ListPage.properties」というファイルを作れば自動的に読み込まれます。

では変更内容を整理しておきましょう。 まずは「'urlTextField' 欄 は必須です。」というメッセージの「urlTextField」という部分が問題です。これはwicket:idがそのまま表示されています。画面上には「新規URL」と表示していますので、メッセージ上も「新規URL」と表示したいところです。

次にメッセージ自体の変更です。RequiredValidatorのメッセージ「'urlTextField' 欄 は必須です。」は「○○には必ず入力してください」という形にしましょう。PatternValidatorはもっと具体的な感じで「URLがおかしいです。http://ではじまるURLを入力してください。」と表示することにします。

やり方は簡単です。 まずurlTextFieldの名前ですが、Wicketはエラーメッセージ表示時に各コンポーネントの「ラベル」を用いてメッセージを表示します。このラベルはデフォルトではwicket:idが使われていますが、プロパティファイルで簡単に置き換えることができます。

「ListPage.properties」にラベルを変更したいコンポーネントのwicket:idをフォーム名からドット区切りで書けばいいだけです。

urlForm.urlTextField = 新規URL
        

これでurlFormというフォーム上のurlTextFieldのラベルが「新規URL」に変わります。この状態で再度アプリケーションを実行し、何も入力しないまま登録ボタンを押せば、メッセージは「'新規URL' 欄 は必須です。」に変わります。

メッセージ自体の変更も同じ感じです。Validatorのエラーメッセージを変更するには、プロパティのキーにValidatorのクラス名を指定します。下の実例を見ればわかるでしょう。

RequiredValidator = ${label}には必ず入力してください。
PatternValidator = URLがおかしいです。http://ではじまるURLを入力してください。
        

これでRequiredValidatorとPatternValidatorのエラーメッセージがListPageクラス内限定で変更されるわけです。

ところで上記のRequiredValidatorのメッセージには「${label}」という項目がありますね。これはメッセージ変数でこの場合にはコンポーネントのラベル名を表示します。つまりは「新規URL」と表示するわけです。

各Validator毎に使えるメッセージ変数は決まっています。PatternValidatorでは(今回は使っていませんが)${pattern}、${input}、${name}、${label}といったメッセージ変数が使えます。どのような変数が使えるかは各ValidatorクラスのAPIリファレンス(JavaDoc)に明記されていますので、使う時には目を通しておくといいでしょう。

さて、いままで3行分のプロパティ値を設定しました。これで実行すれば、メッセージが変わります。簡単ですね。

エラー表示サンプル画像2

エラー表示サンプル画像3

フォーム毎にメッセージファイルを持つ

さて前節でメッセージ変更が出来るようになったわけですが、もう一つ戴けないところがありますね。PatternValidatorです。PatternValidatorのメッセージを「URLがおかしいです。http://ではじまるURLを入力してください。」と変更しましたが、この変更の仕方ではListPageページ上のすべてのPatternValidatorがこのメッセージを表示してしまいます。あくまでこのメッセージはurlForm上でのPatternValidatorに限定したメッセージなので、できればurlForm用のプロパティを持ちたいところです。

これも簡単です。WicketはFormのクラス名と同じ名前のプロパティファイルがあれば、ページのプロパティファイルでメッセージを見つからない場合にフォームのプロパティファイルを検索するようになります(順番が「ページ」「フォーム」の順番であることに注意してください。ページのプロパティファイルに既に定義されていればそちらの値が使用されます)。

この機能を使ってフォーム独自のプロパティを持つには、一点だけプログラムを変更する必要があります。urlFormは今のプログラムではWicket標準の「Form」クラスを使っています。これを独自のクラスにしてしまいましょう。フォームに機能を追加するわけではないので、単純にコンストラクタだけをもつクラスをListPage.java内にstatic内部クラスとして宣言します。

public class ListPage extends WebPage {
    public static class URLForm extends Form {
        public URLForm(String id) {
            super(id);
        }
    }
(以下は変わらず)
        

そしてフォームを作成していた部分を上記のURLFormクラスをnewするように変更します。

    void createUrlForm() {   
        final Form urlForm = new URLForm( "urlForm");
(以下は変わらず)
        

これで完成です。URLFormという独自のクラスを使うようになりましたので、このフォーム用の独自のプロパティファイルを持つことができます。

名前はページのプロパティと同じように、「フォームクラス名.properties」になります。ここで注意が必要です。今回の場合「URLForm.properties」というプロパティファイルを作ればよさそうですが、違います。static内部クラスのクラス名はJavaのクラスファイル名規約に従って解釈されます。ですので「URLForm」ではなく「ListPage$URLForm」になるのです。

では、ListPage.propertiesに書いた3つのメッセージ定義を「ListPage$URLForm.properties」にコピーし、ListPage.propertiesファイルを削除してください。その上で再実行してみてください。メッセージは前と変わらず表示されるはずです。ページ用のプロパティファイルはもうありませんので、ちゃんとフォーム用のプロパティファイルが読まれていることが分かります。

まとめ

この回ではWicketの初期設定を行いました。Wicketでほぼ唯一のXMLファイル「web.xml」の設定と、すべてのWicketアプリケーションが備えるべき「Application」クラスの作成を行いました。その中で日本語でウェブサイトを構築する際には必ず考慮しなくてはいけない「文字コード」の設定についても説明しました。Wicketではユーザーに出力する際の文字コードとテンプレートの文字コードを別々に設定できます。さらにテンプレートについてはXML宣言での文字コード設定が最優先に見られます。

さらにWicketにおけるページ制御の基本となるWebPageクラスの作成、それにWicketの最も基本的なオブジェクトである「モデル」の使い方を説明しました。モデルの「getObject」「setObject」を介してユーザーページの入力情報の設定と取得が行われています。

入力値検証にはコンポーネント単体での検証とフォーム全体の検証があること、Wicketにおける検証は「Validator」オブジェクトをコンポーネントにaddすることによって行われること、さらにメッセージを表示する「Feedback Panel」の使い方を見ました。WicketではJavaの(無名)内部クラスが多用され、Validatorでもそのパワーが役立ちます。

最後にエラーメッセージの変更の仕方、フォーム毎にValidatorのメッセージを変更する方法を紹介しました。

次回は...

次の回では、RSSリーダーを作成する上で必要なフィードの処理を行う、 ROMEライブラリを使ったエントリの取得を説明します。ROME自体はWicketのプログラミングとは直接は関係ありませんが、エントリを取得してそれをウェブ・ブラウザ上に表示するために、ListViewコンポーネントを使った繰り返し処理を使うことになるでしょう。 今回作ったところまでのコードはここからダウンロードできます。

ご意見などは...

セキュリティを確保したコメントシステムまで作っている余裕がなかったので、コメントは 私のはてなダイアリーまでお願いします。

もしくは、この記事へトラックバックをどうぞ。

トラックバックURL:

http://www.javelindev.jp/wicket/Trackback?article=1
注意:トラックバック元にこのページのURLが含まれていない限り受け付けられません。

トラックバック一覧:

テストです。更新のテストです。
2006年12月13日
http://www.javelindev.jp/wicket/doc/tutorial01 そのうち使ってみようと思ってます。
2007年08月05日
インストール方法はGoogleで検索するとすぐにでてくるJavaウェブフレームワーク「Wicket」の使い方に書いてあったので、それを見ながら作業開始。ただ、インストールするバージョンを1.3.0-beta4にしたので若干作業が変わった。 まず、1.3.0-beta4にはjunitやlog4jが入って
2007年10月27日