Wicketで始めるオブジェクト指向ウェブ開発

第3回WicketでTwitterアプリケーションを作る(前編)

今回からは、Wicketを使って実際にアプリケーションを作成することで、Wicketによるアプリケーション作成方法を紹介していきます。

業務的なアプリケーションを作っても面白くもないでしょうし、今流行のTwitterのタイムラインを表示するアプリケーションを作ろうと思います。

twitter4jを使ったアプリケーションを作る

とはいえ、この連載で一から十まで解説していくと何回あっても足りません。作成済みのアプリケーションのソースコードを追うことで、Wicketの使い方を紹介しようと思います。

次のURLから、完成したアプリケーションのソースコードをダウンロードできます。

このアプリケーションは、いままでのサンプルと同じようにmvn jetty:runコマンドによって起動することができます。

アプリケーションを起動して「http://localhost:8080/wicket-sample/」にアクセスすると、ログイン画面が表示されます。あなたのログインIDとパスワードを入力して確定すると、あなたのTwitterタイムラインが表示されます[1]⁠。

図1 Twitterアプリケーションのイメージ
図1 Twitterアプリケーションのイメージ

このアプリケーションはWicketのサンプルとして用意したものであって、Twitter APIの解説用のものではありません。Twitterへの接続はすべて、twitter4jというライブラリを介して行っています。twitter4jを使うと、TwitterのAPIをほとんど意識することなく、Javaプログラム上からTwitterの情報を扱うことができます。

フォームによる入力の受け付け

今回取り上げるのはTwitterにログインする処理です。ログイン画面は次のような非常にシンプルなものです。

図2 ログイン画面イメージ
図2 ログイン画面イメージ

非常にシンプルな画面ではありますが、このプログラムにはHTMLフォームを使ってユーザ入力を受け付け、検証し、ログイン成功すればタイムラインページに遷移する、という一連の流れが組み込まれています。このプログラムを理解することで、Wicketがユーザ入力をどのように扱うか、が理解できます。

フォームの構築

ダウンロードしたプロジェクトフォルダ内の「src/main/java/jp/gihyo/wicket/page/Login.java」ファイルと「src/main/java/jp/gihyo/wicket/page/Login.html」ファイルが、ログインページを表示するプログラムとHTMLです。

Wicketでは、HTMLフォームはFormというクラスで表します。HTML要素<form>に対してwicket:idを付け、WicketのFormオブジェクトを適用することで、フォームをプログラムから制御できるようになります。

HTMLフォーム上にあるすべての要素は、Formオブジェクトの子要素になります。いままでのサンプルではLabelコンポーネントをPageに対してaddしてきましたが、HTML上で<form>の子要素となっている要素は、Wicketプログラム上でもFormの子コンポーネントとするため、PageではなくFormにaddします。Wicketでは、HTML要素の階層構造とオブジェクトの階層構造は一致するのです。

Login.javaの中に、次のような行があります。

リスト1 Formへの子コンポーネント適用
Form<Void> form = new Form<Void>("loginForm");
add(form);

TextField<String> loginNameField = new TextField<String>("loginName", new PropertyModel<String>(this, "userName"));
loginNameField.add(new PatternValidator("[a-zA-Z1-9_]*"));
loginNameField.setRequired(true);
form.add(loginNameField);

PasswordTextField passwordField = new PasswordTextField("password", new PropertyModel<String>(this, "password"));
passwordField.add(StringValidator.maximumLength(20));
passwordField.setRequired(true);
form.add(passwordField);

Loginクラスのコンストラクタ内での処理です。まず最初に、Formオブジェクトを生成しています。Login.htmlを見ると、<form>要素には「loginForm」というwicket:idがつけられています。そのため、Formの第1引数であるIDには、同じ「loginForm」を渡します。

そのあとにTextFieldというコンポーネントと、PasswordTextFieldというコンポーネントを生成しています。

loginNameFieldとpasswordFieldはLoginページにではなく、Formオブジェクトに対してaddされていることに注目してください。HTML要素の階層と合わせているのです。

もし階層を誤ると、Wicketはエラーメッセージを表示して停止します。Wicketのエラーメッセージは総じて非常に細かく的確に問題を表示しますので、英語だとおそれずにじっくり読んでみてください。

試しに、サンプルプログラムのTextFieldに渡す第1引数を「loginName」から「logimName」に変更すると、Wicketの表示する詳細なエラーメッセージを確認することができます。

フォームとモデル

Formは、Wicketがユーザの入力を受け付けるためのコンポーネントです。Formの上にはTextFieldやPasswordTextFieldといった入力コンポーネントが配置されます。

ユーザが入力値はどこに格納されるのでしょうか。Servletでは、HttpServletRequestオブジェクトから、キーを使って取得しました。Wicketではどうでしょうか。

Wicketのデータ受け付け方法は、データ表示の時と同じです。Labelの表示データはモデルから取得しました。入力データは、逆にモデルに渡すのです。そのためにIModelインタフェースが定義しているメソッドが「setObject()」です。

モデルに渡すと言っても、モデルは多くの場合、データを保持しません。Labelの例でも、モデルのgetObject()メソッドは、データを「どこかから」取ってくるのであって、モデル自身がデータを持っているわけではないのでした。実際、Labelのときには、実行時に現在時刻を生成して返す特殊なモデルを作成しました。

setObject()でも、モデルに渡されたデータは「どこかに」格納されます。setObject()メソッドを独自実装すれば、格納先がオブジェクトのフィールドであろうが、データベースであろうが、別アプリケーションへのデータ転送であろうが関係ありません。コンポーネントはだたsetObject()を呼びます。あとはモデルがそのデータをどこかに格納します。

Wicketはこの「モデル」の概念によって、コンポーネントを非常に柔軟なものにしています。コンポーネントは事実上、データ格納場所がどのような場所であろうが対応することができます。コンポーネントはデータをモデルに渡すだけだからです。

PropertyModelでプロパティにデータを格納する

では実際のプログラムを見てみましょう。今回のプログラムでは、入力項目が2つあります。loginNameとpasswordです。それぞれのコンポーネント生成コードをみると、第2引数としてPropertyModelオブジェクトを渡しています。loginNameコンポーネントでは、次のようなコードでした。

リスト2 PropertyModelの生成
new PropertyModel<String>(this, "userName")

TextFieldコンポーネントは、HTMLの<input type="text">要素に適用されるコンポーネントで、機能は単純です。ユーザ入力値をモデルに格納するだけです。

PropertyModelオブジェクトは、コンポーネントから渡されたデータを別オブジェクトのプロパティに格納するモデル実装です。今回のコードでは、コンポーネントからもらったデータを「this」オブジェクトの「userName」プロパティに格納します。thisはLoginページ自身を表しています。LoginオブジェクトのuserNameプロパティに値を格納するのです。

ここでPropertyModelの特殊な機能について触れておく必要があります。Javaにおいてオブジェクトの「プロパティ」とは、Java Beans仕様に基づき、オブジェクトのsetter/getterメソッドを意味することがほとんどです。WicketのPropertyModelでもそれは同じですが、より簡便にデータを扱えるように、オブジェクトのフィールドもプロパティとして扱います。優先順位はメソッドの方が高く、メソッドが見つからなければフィールドを探します。

Login.javaファイルを見ると、プロパティ名「userName」に対応するsetterメソッド「setUserName()」は見つかりません。しかし「userName」という名前の、String型フィールドは存在します。このPropertyModelは、ユーザ入力値をuserNameフィールドに格納するのです。

PropertyModelとプロパティ式

PropertyModelは、第1引数にプロパティを探すオブジェクト、第2引数にプロパティ式を指定します。⁠プロパティ式」とは、OGNL(Object Graph Navigation Language)という式言語を簡略化した、プロパティアクセス用式言語です。今回「userName」と単純に書いた部分は、たとえば「user.address.name」のように、プロパティ名をドットで区切ってプロパティに連鎖アクセスすることもできるのです。

プロパティ式を使うと、オブジェクトのプロパティ(あるいはフィールド)に加え、Mapや配列の要素にも非常に簡単にアクセスできます。プロパティ式の書き方については、WicketのJavadocにて「PropertyResolver」クラスを参照すると、おおよその機能がわかるでしょう。

モデルがデータとコンポーネントを結びつける

PropertyModelはモデルのパワーを示す一例です。ほかにもプロパティファイルから情報を引き出すResourceModelなど、モデル実装が用意されています。独自のデータ処理を伴う場合には、IModelインタフェースを実装した独自のモデルクラスを作成します。独自のIModel実装を作ることで、どのようなデータ格納場所であっても、コンポーネントと結びつけられます。

モデルは、実際のデータとコンポーネントとを結びつける役割をするのです。

入力規制を指定する

では、入力におけるコンポーネントの役割とはなんでしょうか。出力においては、アウトプットされるHTMLタグの制御という、重要な役割を担っています。入力においては、ただモデルにデータを渡すだけなのでしょうか。

コンポーネントは、入力値がモデルに渡しても大丈夫なのかを確認する役割を持っています。データのバリデーション(検証)処理です。

Wicketのコンポーネントに入力規制を行わせるには、コンポーネントに対してIValidatorオブジェクトをaddします。サンプルコードでも、loginNameFieldとpasswordFieldそれぞれに、バリデータの追加が行われています。

リスト3 コンポーネントへのバリデータ適用
loginNameField.add(new PatternValidator("[a-zA-Z1-9_]*"));
loginNameField.setRequired(true);
(中略)
passwordField.add(StringValidator.maximumLength(20));
passwordField.setRequired(true);

このコードでは、loginNameFieldに対してPatternValidatorを、passwordFieldに対してStringValidatorを適用しています。また、それぞれにsetRequired(true)を呼び出しています。PatternValidatorは、ユーザ入力値が正規表現とマッチするかどうかを検証します。StringValidatorは文字列の長さを規制するバリデータです。maximumLengh()メソッドにより、入力文字数が指定値までであることを検証します。setRequired(true)は、コンポーネントへの入力が必須であることを指定します(内部的にRequiredValidatorが適用されます⁠⁠。未入力のままにすると、エラーとして扱われます。

そのほかにも、さまざまなバリデータがあらかじめ用意されています。JavadocのIValidatorインタフェースを参照すると、IValidatorインタフェースを実装したクラス一覧をみることができます。

単体コンポーネントに適用するバリデータだけでなく、複数のコンポーネントに対する検証を行う「IFormValidator」も存在します。

バリデータとモデルの関係

Wicketのバリデータは、モデルに対して重要なことを保証しています。それは、コンポーネントに適用されたすべてのバリデータ、フォームバリデータの検証をパスしない限り、モデルには絶対にデータが渡されないということです。モデルに値が渡されたということは、検証は絶対にパスしているのです。

この保証はプログラムを分かりやすくします。モデルに渡されてプログラムからアクセス可能となったユーザ入力値が、データとして正しいものなのかどうか、気にする必要はありません。その検証を行うのはバリデータの仕事であり、独自の検証をしたいのならば、独自のバリデータを作成すればよいのです。

Wicketは定義ファイルを使わず、すべてをプログラムから行います。バリデータもコンポーネントやモデルのように自由に作成することができます。フォームバリデータを使えば複雑な検証も思うがままです。

逆に、この保証を活用するために、あなた自身も「検証はバリデータで行う」というルールを守るのが重要です。役割を綺麗に分けることで、プログラムは分かりやすくなります。また、バリデータというオブジェクトにまとめることで、検証機能を再利用することも可能となります。

検証失敗の通知

バリデータが入力データの検証をしてくれるといっても、検証が失敗したことが分からなければ、ユーザもプログラマも困ります。

バリデータの検証失敗内容を画面に表示するのは簡単です。Wicketには検証失敗はもちろん、あなたがユーザに通知したい情報を簡単に表示するためのコンポーネントとして、FeedbackPanelが用意されています。

次のようにFeedbackPanelをページ上に貼っておくだけで、バリデータによる検証が失敗したとき、FeedbackPanel内に情報が表示されます。

リスト4 FeedbackPanelの適用
add(new FeedbackPanel("feedback"));

FeedbackPanelは空の<div>要素に適用するのが一般的です。メッセージは全て<div>の内側に出力されます。

Wicketは、標準バリデータでの検証エラーについては、日本語を含めて多くの言語向けメッセージファイルを既に持っています。FeedbackPanelをページに適用しておくと、これらの標準メッセージが表示されます。たとえば、PatternValidatorに指定した正規表現にマッチしない入力を行うと、サンプルプログラムは次のようにエラーを表示します。

図3 PatternValidatorの検証エラー表示
図3 PatternValidatorの検証エラー表示

フォームのサブミットを受け付ける

Webアプリケーションでは、ユーザの入力はHTMLフォームがサブミットされてはじめて、Webアプリケーションに通知されます[2]⁠。Wicketでも例外ではなく、サブミットボタンがクリックされてはじめて、Wicketに通知がされます。

Wicketにおけるサブミット後処理の書き方はシンプルです。サブミットは<input type="submit">ボタンがクリックされることで行われます。ならば、<input type="submit">タグにコンポーネントを適用することで、タグを制御できるようにすればいいのです。

Wicketがサブミットボタンを制御するために用意しているコンポーネントが、Buttonクラスです。ButtonクラスにはonSubmit()というメソッドがあり、ユーザがブラウザ上のボタンをクリックすると、WicketはそれをonSubmit()メソッドの呼び出しへと変換します。プログラマはただ、ButtonオブジェクトのonSubmit()メソッドをオーバーライドし、ボタンがクリックされたときの動作を記述すればよいのです。

リスト5 サブミットボタンのクリック時に動作するプログラムを書く
Button submit = new Button("login") {
  @Override
  public void onSubmit() {
    //ここにボタンをクリックしたときのプログラムを書く
  }
};

Wicketでは、このコード例のように、匿名サブクラス記法を使って「その場で」サブクラスを生成してオーバーライドを行う、という手法を多く使用します。実際のところ、そのように書くことを前提にしてフレームワークが作られている面もあります。コンポーネントのふるまいを変更するときにいちいち別クラスとして作らなければいけないのでは面倒です。匿名サブクラス記法では、気軽にサブクラスを作ることができます。デスクトップ・アプリケーション作成で多用されるこの手法を、Wicketはうまく取り込んでいます。

前述したように、Wicketはすべてのバリデータをパスしない限り、モデルへデータを渡しません。ButtonのonSubmit()メソッドは、モデルへのデータ引き渡し完了後に呼び出されます。

onSubmit()が呼び出されたときには、すでに入力値は検証済みであることが保証されているということです。そのため、いきなり入力値を使い始めてもかまいません。

ユーザセッションを使って、ページをまたぐ情報を保存する

サブミットの受け付け方法が分かったので実際にonSubmit()を実装してTwitterにログインする処理を記述したいところですが、サンプルプログラムのコードを理解するには、Wicketにおけるユーザセッションの取り扱いについて解説しておく必要があります。

次回は、Wicketがユーザセッションをプログラム可能なオブジェクトとして扱うことを説明し、ログイン画面を完成させます。

おすすめ記事

記事・ニュース一覧