前回までで、Twitterにログインしタイムライン表示用ページに遷移するところまでは実現できました。今回は、いよいよTwitterタイムラインを表示します。
Twitterタイムラインは、Twitterユーザの発言をリスト上に表示します。HTML上で見ると、同じ構造のタグが発言数分、ずらりと並んだ形をしています。JSPでそのような構造を作るときには、Javaのfor文もしくは同じような動作をするカスタムタグを使って、タグを繰り返し表示することで実現しました。
Wicketでは、このような繰り返しリスト全体を「繰り返し項目コンポーネント」として扱います。それがListViewクラスです。
ListViewによるコンポーネントの繰り返し
ListViewはHTML要素を繰り返すときに使うコンポーネントです。ListViewに対応するwicket:idは、「繰り返したいHTML要素」につけます。たとえば、次のようにすれば、repeatViewというwicket:idのついた<tr>タグ(内側の<td>タグも含めた全体)を繰り返し生成することができます。
この<tr>要素を繰り返すためのプログラムが、次のプログラムです。
ListViewがデータを表示する際には、自身のモデルのgetObject()を呼び出します。モデルはgetObject()呼び出しの結果としてListを返します。ListViewはListの要素ひとつひとつを使ってpopulateItem()メソッドを呼び出します。populateItem()メソッドの引数は常にListItemで、このListItemが表の1行を表します。上記例でいえば、ListItemは<tr>タグを表しています。まさに、wicket:idをつけたタグです。wicket:idをつけたタグが、ListItemとして繰り返しpopulateItem()メソッドに渡されるのです。
ListViewに渡しているモデル「TwitterStatusListModel」はList<Status>型オブジェクトを返します。ListViewはこのListからStatusオブジェクトを1つずつ、populateItem()メソッドに渡します。Statusは行を表すコンポーネントであるListItemのモデルから取り出すことができます。item.getModelObject()が、ListItemからStatusを取り出しています。
あとはStatusオブジェクトを使って1行分のコンポーネントを組み立てるだけです。ListItemにコンポーネントを追加することで、<tr>タグの子要素を組み立てることができます。
この例では、wicket:id="userName"に対応するLabelコンポーネントを適用しています。LabelコンポーネントはStatusオブジェクトから取得できるスクリーンネームを表示します。
LoadableDetachableModelでリクエスト中のキャッシュを行う
ListViewの作り方がおおよそ分かりました。しかし、TwitterStatusListModelのような、Statusのリストを返すモデルがなければこのプログラムは動きません。
サンプルコード内のjp.gihyo.wicket.page.simple.Timelineクラスでは、TwitterStatusListModelの代わりに次のようなプログラムでTwitterのステータスを取得しています。
Statusクラスは、twitter4jが提供するTwitterのステータスを表現するクラスです。TwitterタイムラインはListView<Status>によって表示しますので、必要なモデル型はIModel<List<Status>>です。ListViewは常に自分が使うオブジェクトのListをモデルに期待します。IModel<List<Status>>に適合するモデルであれば何でもかまいません。
ここでは、見慣れないクラスLoadableDetachableModelを使いました。
LoadbaleDetachableModelクラスは、リクエスト毎の初回アクセス時にデータをロードし、キャッシュするモデルです。一度キャッシュされたデータは、レスポンスが返るまでは使い回されます。Twitterのステータス取得はTwitterサイトへのリモートAPI呼び出しを伴いますので、非常に重い処理です。リクエスト中に何度も呼ばれるとアプリケーション全体が重くなります。LoadableDetachableModelはこのようなケースに便利なモデル実装です。
Statusの取得
LoadbaleDetachableModelのload()メソッドをオーバーライドすることで、データのロード処理を記述します。
サンプルでは、前回までに作ったAppSessionクラスのgetTwitterSession()メソッドを使って、ログイン時に生成したTwitterオブジェクトを取得し、getFriendsTimeline()メソッドでタイムラインを取得します。結果値はList<Status>ですので、モデルの型と一致します。
getFriendsTimeline()は失敗する可能性がある操作です。失敗した場合にはTwitterExceptionがスローされますので、キャッチしてエラーメッセージを表示します。ここでは、FeedbackPanelに情報を表示するerror()メソッドを呼び出しています[1]。
また、エラーの時でもモデルは何らかの値を返す必要がありますので、空のリストを返却しています。
このモデルをListViewに渡すことで、タイムライン表示リストを作ることができます。
タイムラインの表示
モデルができてしまえば、タイムラインはListViewのpopulateItem()メソッドを実装することで簡単に作成できます。
サンプルコードでは、次のようにListItemに次々とコンポーネントを追加することで、1行分のコンポーネントを構築しています。
見慣れないクラスはあるでしょうが、プログラム例「ListViewの使い方」で見たのと同じように、ListItemコンポーネントにLabelなどのコンポーネントを追加していっているだけなことがわかるでしょう。
ここではじめて出てきたコンポーネントが2つあります。1つはExternalLinkクラスで、その名の通りアプリケーション外部へのリンクを動的に生成するためのコンポーネントです。最も単純な使い方は、この例のように、第2引数としてリンク先URL文字列を渡すことです。第3引数はページ上に表示するリンク文字列です。
もう1つはImageコンポーネントです。画像を動的に生成して表示するコンポーネントです。HTMLの<img>タグに適用します。
Imageコンポーネントの第2引数は、WebResourceです。これは、Wicketがページ以外のリソースを扱うために扱うためのクラスです。
WebResourceとResourceStream
WebResourceは画像や文字列、ファイルといった情報を扱うためのクラスです。Wicketではそれらを「Resource」というクラスで扱います。WebResourceクラスは、Wicketが標準で提供している、Resourceの唯一のサブクラスです。
Resourceの一番大きな役割は、ResourceStreamを作り出すことです。ResourceStreamは、サーバ側にあるファイルなどのリソースとブラウザの間にある「データの流れ(ストリーム)」を表してます。ブラウザはストリームを読み込むことで、サーバからリソースを取得するのです。
サンプルでは、WebResourceのgetResourceStream()メソッドをオーバーライドすることでResourceStreamを作り出しています。ResourceStreamにはFileResourceStream, ZipResourceStreamなど、標準で多くのクラスが提供されていますが[2]、ここでは特定URLからデータを取得してストリーム化するためのUrlResourceStreamを使用しています。
今回のサンプルにおけるImageコンポーネントの役割は、ユーザのアイコン画像を表示することです。画像URLはstatus.getUser().getProfileImageURL()で取り出すことができますので、このURLをUrlResourceStreamに渡すだけで、簡単にResourceStreamを生成できます。
Imageコンポーネントを<img>タグに適用すると、WebResourceへのURLを動的に生成し、<img>タグのsrc属性に挿入します。ブラウザはsrc属性を見てWicketにアクセスしてきますので、WicketはWebResourceからResourceStreamを作りだし、ブラウザにデータを返却するのです。
WebResourceを使えば、画像はもちろんのこと、ファイルやバイナリデータなど、多彩なデータをWicketで扱うことができます。ResourceStreamにも多彩なクラスが用意されており、クラスを差し替えるだけで多くのことを実現できます。Wicketは、ストリームもオブジェクトとして扱い、再利用可能としているのです[3]。
次回:リンク
ウェブページにおいて必ず使うものといえば、やはりリンクでしょう。リンクがなければウェブは成り立ちません。
WicketではリンクはLinkというコンポーネントによって扱うことができます。Linkコンポーネントを使うことで、ページ上のあらゆるタグをクリック可能にできます。
次回は、Linkコンポーネントを使って各行にリンクを生成します。