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

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

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

そこで今回は、このユーザセッションの取り扱いについて解説します。

ユーザセッションにアクセスする

サーブレットでプログラムを作るとき、ユーザごとに用意されるデータ格納領域としてHttpSessionオブジェクトを使用することができました。Wicketでも、同様の仕組みがあります。

とはいえ、Wicketではページ自身がフィールドを持つことができ、Wicketがページの状態を保存してくれます。あるページオブジェクトはユーザ固有のもので、同じページクラスから作られたページでも、ユーザが異なれば、別のオブジェクトが割り当てられます。このようなページの割り当てと保存は、Wicketが裏側で行います。

この仕組みがあるために、サーブレットに比べて、ユーザセッションの用途は限られています(必要な情報はページ自身に持てばよいのですから⁠⁠。

しかしながら、アプリケーションにはページ内で完結する情報と、複数のページにまたがって、ユーザがログインしている間はずっと使いたい情報とがあります。後者の情報を扱うには、HttpSessionのような機能は便利です。

Wicketのユーザセッションは、Sessionクラスのサブクラスとして作成します。このサブクラスには、プログラマが自由にメソッドやフィールドを追加できます。サーブレットのHttpSessionはただ使うことができるだけで、情報の保存・取得はsetAttribute()、getAttribute()メソッドで行いました。どんな情報であれ、これらのメソッドで保存・取得していたため、アプリケーション全体でHttpSessionにどのような情報を格納しているのかを調べることは大変でした。

Wicketでは独自のSessionサブクラスに、自由にメソッドを追加できます。自分で作ったサブクラスを一瞥すれば、アプリケーション全体でSessionにどのような情報が保存されているのか、一目瞭然です。また、メソッドを定義することで、データの出し入れがJavaらしい型安全性を持つことになります。

独自のセッションクラスを設定する

サンプルプログラムでは「jp.gihyo.wicket.AppSession」という、Sessionクラスのサブクラスを用意しています。この独自のサブクラスをWicketに使わせる設定を行う必要があります。

Wicketでは、この設定もプログラムで行います。jp.gihyo.wicket.WicketApplicationクラス内に、次のメソッドを記述しています。

リスト1 newSessionメソッドのオーバーライド
@Override
public Session newSession(Request request, Response response) {
  return new AppSession(request);
}

WicketApplicationクラスは、今回のサンプルアプリケーションにおけるWebApplicationサブクラスです[1]⁠。WebApplicationクラスについては、第1回でもご紹介しました。

Wicketはユーザセッションをはじめて使用する時に、WebApplicationクラスのnewSession()メソッドを呼び出すことで、セッションオブジェクトを作成します。newSession()メソッドをオーバーライドすることで、独自のSessionサブクラスを生成することができます。

ログイン状態を保存する

ユーザがTwitterにログインできた場合、その状態はページをまたがって維持されるでしょう。そのため、Twitterへのログイン処理は、AppSessionクラスにプログラムします。HttpSessionと違って、ユーザセッションにメソッドを定義できる利点を生かします。

ユーザがログイン成功すると、twitter4jのTwitterオブジェクトをユーザセッション内に保存し、いつでもAppSessionから取り出せるようにします。次のコードがログイン処理です。

リスト2 Twitterオブジェクトの作成とログインチェック
private Twitter twitterSession;
private User twitterUser;
private String lastUnauthorizedMessage;

(中略)

public boolean login(String userName, String password) {
  Twitter client = new TwitterClient(userName, password);
  try {
    User user = client.verifyCredentials();
    if(user != null) {
      twitterUser = user;
      twitterSession = client;
      return true;
    }
  } catch (TwitterException ex) {
    if(ex.getStatusCode() == HttpServletResponse.SC_UNAUTHORIZED) {
      lastUnauthorizedMessage = ex.getMessage();
      return false;
    }
  }
  return false;
}

Twitterクラスは、今回Twitterにアクセスするために使うtwitter4jが用意しているクラスです。このオブジェクトを介して、Twitterのステータス情報を取得できます。

ここでは、Twitterクラスの代わりにTwitterクラスを独自にサブクラス化したTwitterClientクラス(完全修飾クラス名はjp.gihyo.wicket.TwitterClient)を生成しています。本記事の執筆段階ではtwitter4jのTwitterクラスにはシリアライズに関するバグがあり、セッションがシリアライズされた場合に正しく復元されません。TwitterClientクラスはそのバグを回避するために今回用意したサブクラスです。正しくシリアライズから復元できるようにしたこと以外には、もとのTwitterクラスと機能上の違いはありません。

Twitterへのログインチェックは、TwitterオブジェクトのverifyCredentials()メソッドで行います。ユーザ名とパスワードが正しければ、Userオブジェクトが返されます。逆にログインに失敗すれば、TwitterExceptionがスローされます。

login()メソッドはUserオブジェクトを取得できればログイン成功と見なしてtrueを返します。逆に、TwitterExceptionがスローされればログイン失敗と見なしてfalseを返します。

ポイントとして、ログイン成功時にも失敗時にも、生成された情報をAppSessionのフィールドに保存していることです。成功すればUserオブジェクトを保存しますし、失敗すれば失敗理由を保存します。

AppSessionには、保存された情報にアクセスするためのメソッドも用意されています。

お仕着せのHttpSessionをただ使うだけでなく、独自のオブジェクトとして作ることができることで、ユーザセッションのイメージがサーブレットから大きく変わっていることが分かるでしょうか。Wicketでは、ユーザセッションはHttpSessionのような、ただの入れ物ではありません。ユーザセッションはオブジェクトです。それはプログラム可能であり、メソッドを持つことができ、メソッド呼び出しによって状態が変化するのです。

ログイン処理を実装する

AppSessionにlogin()メソッドを定義しましたので、ユーザがログインボタンを押した時の処理は、AppSessionのlogin()メソッドを呼び出すだけになりました。ここまでできれば簡単です。

リスト3 Twitterにログインする
Button submit = new Button("login") {
  private static final long serialVersionUID = 1L;
  @Override
  public void onSubmit() {
    AppSession session = AppSession.get();
    if(session.login(userName, password)) {
      setResponsePage(MyTimeline.class);
    } else {
      error(getString("unauthorized") + ": " + session.getLastUnauthorizedMessage());
    }
  }
};

先ほど登場した、<input type="submit">タグに適用するButtonコンポーネントの実装です。このButtonコンポーネントでは、onSubmit()メソッドをオーバーライドし、ボタンがクリックされるとログインを行います。

セッションオブジェクトの取得

ログインするにはAppSessionを取得しなければいけません。Wicketでは、Session.get()を呼び出すことでいつでもSessionオブジェクトを取り出すことができます。Session.get()を呼び出した時にセッションがまだ未作成であれば、先ほど定義した、WebApplicationのnewSession()メソッドが呼び出されます。つまり、Session.get()の戻り値はnewSession()メソッドの戻り値と同じです。通常であれば、次のように、AppSessionにキャストして使用します。

リスト4 Sessionオブジェクトの取得
AppSession session = (AppSession)Session.get();

しかし、使うたびにキャストを行うのも面倒です。今回のサンプルプログラムでは、AppSessionにget()というstaticメソッドを定義し、キャストした結果のオブジェクトを返すようにしました。これでいつでも「AppSession.get()」を呼ぶことで、AppSessionオブジェクトにアクセスすることができます。

ログイン処理を行う

セッションオブジェクトを準備できれば、あとはlogin()メソッドを呼ぶだけです。login()メソッドの引数はユーザ名とパスワードですが、それらはPropertyModelを介してページのフィールド「userName」「password」に格納されています。ユーザ名とパスワードの入力を制御するコンポーネントにはsetRequired(true)とセットされていましたので、もし未入力であれば入力検証段階ではじかれ、モデルに入力値は渡りません。onSubmit()が呼び出されたということは、ユーザ名とパスワードは必ず入力されていて、フィールドにその値が入っていることが保証されています。そのため、プログラムでは、いきなりフィールドを使用しているのです。

login()メソッドはログイン成功すればtrueを返しますし、失敗すればfalseを返します。よって、単純なif文で成功時、失敗時の処理を分岐できます。

成功したときには、Twitterタイムライン表示ページへと遷移します(そのページはまだ作っていませんが、MyTimelineというクラスです⁠⁠。

Wicketでのページ遷移は、これもまたプログラムから行います。プログラム中でsetResponsePage()メソッドを呼び出すことで、次にブラウザへ表示すべきページを設定できます。Wicketではページはクラスですから、setReponsePage()メソッドにクラスを渡せば、そのページが表示されます。ページ遷移のための定義ファイルなどはありません。

また、setResponsePage()には、次のようにクラスだけではなくオブジェクトを渡すこともできます。

リスト5 自分で生成したページに遷移する
setResponsePage(new MyPage(arg1, arg2));

MyPageはあなたが用意したページクラスだと考えてください。あなたが用意したのですから、コンストラクタも自由に作成できます。コンストラクタ引数を介して、自由にデータを受け取ることができます。Wicketでは、必要な情報はリクエストパラメータではなく、コンストラクタ引数を介して渡すこともできるのです。

今回は、次のページへ渡す情報が一切ありません。Twitterから情報を取得するためのTwitterオブジェクトは、login()メソッドが成功すればAppSessionオブジェクト内に保存されるため、次ページでも自由にアクセス可能だからです。

エラーメッセージを表示する

ログインが成功すればsetReponsePage()で次のページに遷移すればよいですが、失敗した場合には、遷移せずに今のページに失敗したことを表示したいところです。

Wicketでは、バリデータによるエラーメッセージは自動的にFeedbackPanelに表示されます。バリデータと同じように、独自のメッセージをFeedbackPanelに表示できれば便利です。

そのためのメソッドが、サンプルプログラムで使っているerror()です。ページを含むすべてのComponentクラスには、debug(), info(), warn(), error(), fatal()というメッセージ出力用メソッドが用意されています。これらのメソッドに指定したメッセージは、FeedbackPanelに表示されます。

プロパティファイルから文字列を取り出す

サンプルプログラムでは、AppSessionに保存されている、最後にログイン失敗した時のエラーメッセージをerror()メソッドに渡しています。しかしその頭に、getString()メソッドの結果値を連結しています。getString()とは何でしょうか。

Wicketは、アプリケーションの国際化を簡単に行えるように、プロパティファイルからの文字列取得を簡単に行えるように作られています。getString()は、引数をキーとしてプロパティファイルを検索して、見つかった文字列を返してくれます。サンプルプログラムでは「unauthorized」というキーでプロパティファイルに登録されているメッセージを取得しています。

では、メッセージはどのプロパティファイルに定義すればいいのでしょうか。これも簡単です。HTMLファイルと同じなのです。Wicketは、ページと同名の.propertiesファイルを用意すれば、それを「ページ用のプロパティファイル」として扱います。getString()メソッドは、ページ用のプロパティファイルからメッセージを探すのです[2]⁠。さらに、プロパティファイル検索時には、ユーザのロケール(国/言語情報)も参考にします。デフォルトでは、Wicketはブラウザが送ってくるロケール情報をもとに自動的にユーザのロケールを決めます。日本のユーザであれば、おそらく「ja_JP」というロケールになります。Wicketは「ページ名_ja_JP.properties」⁠ページ名_ja.properties」⁠ページ名.properties」の順にプロパティファイルを探し、最初に見つかったものを使用するのです。

Loginクラスのプロパティファイルは「Login.properties」となります。サンプルプログラムでは、Login.propertiesファイルに次のようにメッセージを設定しました。

リスト6 プロパティファイルのメッセージ定義
unauthorized = ログインに失敗しました。

サンプルプログラムでは、ログインに失敗した理由情報(Twitter4jから取り出したもの)の頭に「ログインに失敗しました。」という文字列を付与して、FeedbackPanelに表示するのです。

この例のように、画面に日本語を表示する際には、プロパティファイルに文字列を定義するのが良いプラクティスです。そうしておくことで、いざというときに国際化するのが簡単です。WicketではgetString()によって簡単にプロパティファイルから文字列を取得できます。

Wicketがプログラマを支える

ここまででログインを行うLoginページの実装は完了です。

Loginクラスは40行程度の短いプログラムですが、この中にWicketが提供する機能がつまっています。Formとコンポーネントで入力を受け付ける方法、コンポーネントに設定されたPropertyModelを介して、入力値がページのフィールドに結びつけられるところ、コンポーネントに適用するバリデータによって、モデル更新前に必ず検証が行われるところ、FeedbackPanelへのエラーメッセージ自動表示、Session.get()による独自セッションオブジェクトの取得、setResponsePage()メソッドによるページ遷移、getString()によるプロパティファイル自動検索、error()メソッドによるFeedbackPanelへのメッセージ表示など、いろいろな機能を使用しました。Wicketが提供する機能を使うことで、プログラムはシンプルに、本当に行いたいことだけを記述しています。

Wicketは豊富な機能を単純なメソッド呼び出しで使えるよう、デザインされています。Wicketが土台となり、プログラムのための環境を作り上げているのです。

次回は、Twitterのタイムライン(発言の一覧リスト)を表示することで、Wicketがどうやってリスト状のHTMLを生成するのか、を紹介したいと思います。

おすすめ記事

記事・ニュース一覧