Hibernate と同様に、Objectify-Appengine は POJO を Google App Engine データストア (Bigtable としても知られています) に容易にマッピングして利用できるようにしています。この連載の前半では Objectify と、その主要な機能として JPA アノテーション (サブセットのみを使用)、そして GAE データストアに統合するカスタム・アノテーションについて紹介しました。また、Objectify での関係の扱い方を説明し、GAE のフィルタリングとソートの概念をサポートするクエリー・インターフェースの例を記載しました。
第 2 回となる今回の記事では、焦点をドメイン・モデリングから、Web アプリケーションを完成させて GAE にデプロイできるようにする作業へと移します。ただし本題に移る前に、Objectify の索引付け機能とキャッシング機能を紹介するために、もう少しドメイン・モデルについての説明を続けたいと思います。大量のデータを GAE に移そうと計画している場合には尚更のこと、アプリケーションを GAE にデプロイする際には索引付けとキャッシングが非常に重要な機能となります。
Objectify-Appengine での索引付けとキャッシング
デフォルトでは、ドメイン・オブジェクト内で定義されたすべてのプロパティーに索引が付けられます (したがって、Google の下位レベルのEntity
API に定義されたプロパティーも同様です)。リレーショナルの世界でそうであるように、索引を付けることでデータストアの検索が容易になるため、エンド・ユーザーにとって検索時間が短縮されることになります。その一方、索引付けにはコストが伴います。新しいオブジェクトが作成されたときには (リレーショナルにおける「行」や、Bigtable における「Entity
」 など)、その新しいデータを組み込むために索引を更新しなければなりません。
GAE を利用し始め、実際のデータを GAE に保管したり、GAE からデータを取得したりするとなると、索引付けは (実際に費用が発生する) 現実の問題となります。GAE アカウントにサインアップした時点で、自動的に 200 の索引が無料で提供されますが、索引の数がそれ以上になると料金がかかってきます。同様に、CPU の使用時間が 1 日あたり無料で使える 6.50 時間を超えると、Google への借金が増えていくことになります。したがって、選択的に索引を付けるのが当然の道理です。ある特定のプロパティーに索引を付ける必要がなければ (つまり、そのプロパティーでドメイン・オブジェクトを検索する予定がなければ)、そのプロパティーの索引を無効にするのが妥当でしょう。リスト 1 に、Objectify の @Unindexed
アノテーションを使って特定の索引を無効にする方法を説明します。
第 1 回で作成した Retweet
オブジェクトには、索引付け機能を追加して拡張してあります。どのように変更されているかは、サンプル・コードの内容を調べるとわかります。リスト 1 では、@Unindexed
を使用して、Retweet
のユーザーの実名プロパティーの索引を無効にしています。実名のプロパティーには、screenName
の対語として userName
という名前を付けました。ユーザーの写真の URL を示すプロパティーである userPicture についても索引を無効にしてあります。このプロパティーは、見た目に優れた UI レポートを作成するための新しいフィーチャーとして追加したものです。
この 2 つのプロパティーでRetweet
を検索しようとは思っていないので、これらの索引を無効にするのはもっともなことです。何か変更があった場合には、もちろん @Unindexed
アノテーションを取り除くことができます。リスト 1 に、改良後の Retweet
オブジェクトのプロパティーを記載します。
リスト 1. 改良後の Retweet
public class Retweet @Id private String id; private String screenName; private Date date; private String tweet; private Long influence; private Key<User> owner; private Long tweetId; @Unindexed private String userName; @Unindexed private String userPicture; //... } |
リストの終わりに太字で示されている 2 つのプロパティー、userName
と userPicture
は新しいプロパティーで、どちらも索引が無効になっています。元の userName
は、Twitter と統合しやすいように screenName
という名前に変更しました (Twitter では、ユーザーの名前 (私の場合は Andrew Glover) がユーザー名となり、スクリーン・ネームがハンドル (aglover) となります)。
索引付けと同じく、キャッシングによってもエンド・ユーザーのエクスペリエンスが改善されます。キャッシングによってデータストアとの往復が省かれるため、読み取りの時間が短縮されるからです。Google App Engine ではデータのキャッシングに memcache を使用します。Objectify が memcache に接続するために使用するのは @Cached
アノテーションで、@Cached
をドメイン・オブジェクトに追加するだけで、データが memcache に保管され (対応する Java™ オブジェクトが保管されるのではありません)、アプリケーションの読み取り操作でメモリーからそのデータを取得できるようになります。
@Cached
アノテーションは、User
と Retweet
という 2 つのドメイン・オブジェクトに追加しました。リスト 2 に、更新後の User
オブジェクトと、このオブジェクトにさらに追加した @Unindexed
プロパティーを示します。
リスト 2. キャッシングするのは簡単です!
@Cached public class User { @Id private String name; @Unindexed private String token; @Unindexed private String tokenSecret; //... } |
Objectify の query
呼び出しは相変わらずデータストアに対して行われることに注意してください。memcache を使用するのは、データストアと対話するための、その他のすべての呼び出し (get
など) です。例えばリスト 2 の User
オブジェクトでは、対応するドメイン・オブジェクトをロードするために、findByName
メソッドが Objectify の get
呼び出しを使用します (名前がキーであることを思い出してください)。User
がすでに作成されていれば、そのデータがキャッシュされるため、以降の findByName
の呼び出しではデータストアではなくキャッシュからデータが取得されます。memcache 呼び出しでの GAE のクォータはデータストア呼び出しでのクォータよりも大きいため、可能な場合には常に memcache を使用することが理にかなっています。
少なくとも Objectify を使ってできることに関して言えば、ドメイン・オブジェクトの更新は完了しました。次は、アプリケーションを GAE にデプロイします。先月の記事で説明したように、私が目標としているのは、ユーザーが所有しているリツイートをマイニングして、どのリツイートが最も影響力のあるフォロワーによるリツイートであるかを調べることのできる Web アプリケーションを構築することです。
全体的に見ると、User
と Retweet
で構成されたこの Web アプリケーションには、いくつもの実行内容があります。まず、ユーザーは OAuth によって Twitter にユーザー本人であることを証明しなければなりません。続いて、アプリケーションはユーザーのリツイート・データを取得するためにバックグラウンド・プロセスを開始します。それと同時にダッシュボードを表示し、Ajax 呼び出しを行ってリツイート・データを取得します。このデータは、GAE データストア (またの名を Bigtable) に永続化されていなければなりません。
アプリケーションのドメインの側面については先月すでに作成してあるので、後はこのドメインを Twitter の認可システムに統合し、対応するデータを取得して表示すればよいだけです。
第 1 回の記事を読んでいればご存知のとおり、Twitter はユーザーによる認可 (権限の付与) に OAuth を使用します。必要な OAuth の形式は、アプリケーションをデスクトップ環境、モバイル環境、Web 環境のどれにデプロイするかによって決まります。ユーザーによる OAuth を使った認可について、その他に知っておかなければならないこととしては、以下の 2 つがあります。
- アプリケーションがログインおよびパスワードを保管する必要はありません。
- 認可は、Twitter などの信頼できるプロバイダーに委譲されます。
認可は、各種のトークンを交換することによって行われます。Retweet アプリケーションを Web 環境にデプロイしている場合、最初のステップとなるのはアプリケーションを Twitter に登録することです。登録する際に提供するいくつかの情報には、キーとして使用されるコールバック URL も含まれますが、この URL は実行時に変更することができます。アプリケーションを登録すると、コンシューマー・キーとコンシューマー・シークレットが提供されます。コンシューマー・シークレットについてはもちろん、秘密にしておく必要があります。
次に、Twitter にサインインして特定のユーザーのリツイートを取得する許可をそのユーザーから得なければなりません。ユーザーは、特定の情報を Twitter に送信することによってアクセス権限を付与します。すると、Twitter に登録しておいたコールバック URL が呼び出されます。Twitter はコールバック URL を呼び出す際に、いくつかのトークンを一緒に渡します (URL パラメーターとして渡されるこれらのトークンは、プログラムによって取得することができます)。
認可のワークフローを処理するために、2 つのサーブレットを使用します。最初のサーブレットでは Session
オブジェクトを作成し、Twitter に対してコールバック URL を登録し、認証のためにユーザーを Twitter にリダイレクトします。この認証プロセスでは、この後すぐにわかるように、サンプル・アプリケーションにユーザーの代理を務める権限も与えます。2 番目のサーブレットでは、コールバック時の処理を行います。つまり Twitter からのレスポンスを処理し、対応する User
オブジェクトを作成します。
GAE Web アプリケーションを素早く構築するために、ここでは Groovy フレームワークである Gaelyk の簡潔な構文とコーディング・ショートカットを利用します。Gaelyk でまず始めに必要な作業は、アプリケーションのカスタム URL を定義することです。それには、URL routes ファイルを使用することができます。Gaelyk の URL routes ファイルは基本的に DSL で構成されているため、(1) 読んで理解できる URL を作成すること、(2) 基礎となる技術を隠すこと、ができます。リスト 3 に、このマッピングの最初の部分を記載します。
リスト 3. routes ファイルの先頭部分
get "/login", forward: "/login.groovy" get "/tcallback", forward: "/tcallback.groovy" |
リスト 3 には、2 つのカスタム URL を定義しました。/login
で .groovy の側面を隠すことによって、この URL が遥かに読みやすくなることに注目してください。この DSL が記述している内容は、HTTP GET
が your_domain/login
などの URL に適用されている場合は、そのリクエストを /login.groovy
アセットに転送するという単純なシーケンスです。Gaelyk の場合、この転送先はサーブレット (実際には groovlet) となります。
リスト 4 を見るとわかるように、私のログイン groovlet も至って単純です。
リスト 4. ログイン groovlet
import twitter4j.TwitterFactory def twitter = new TwitterFactory().getOAuthAuthorizedInstance("...", "...") def requestToken = twitter.getOAuthRequestToken("http://b-50-1.appspot.com/tcallback") if(!session){ session = request.getSession(true) } session.setAttribute("requestToken_token", requestToken.token) session.setAttribute("requestToken_secret", requestToken.tokenSecret) redirect(requestToken.getAuthorizationURL()) |
リスト 4 ではまず、私自身のコンシューマー・キーとコンシューマー・シークレットを使って TwitterFactory
インスタンスを作成します。TwitterFactory は Twitter4J ライブラリーに含まれています。続いて、RequestToken
インスタンスを取得します。このプロセスで渡しているのは、私独自のコールバック URL (リスト 3 で定義した http://b-50-1.appspot.com/tcallback) です。ユーザーがアクセス権を付与した後は、このコールバック URL が Twitter によって呼び出されることになります。リストの最後の部分では、データが Web アプリケーションに返されるときに必要になる 2 つの情報が、HttpSession
オブジェクトに組み込まれています。そして、ブラウザーが Twitter の Web サイトにある認可用の URL にリダイレクトされます。
リスト 5 に定義したコールバック・ハンドラーは、単に必要な情報を Twitter から取得して、そのデータを HttpSession
オブジェクト内に含まれている情報にリンクします。その後、新しい User
を作成しますが、データストアに既存の User
オブジェクトがある場合には、groovlet が単純に、そのオブジェクトを新しいクレデンシャル・データで更新します。
リスト 5. コールバック・ハンドラー
import java.util.Date import twitter4j.TwitterFactory import com.b50.gretweet.User def twitter = new TwitterFactory().getOAuthAuthorizedInstance("...", "...") def accTok = twitter.getOAuthAccessToken( session.getAttribute("requestToken_token"), session.getAttribute("requestToken_secret"), request.getParameter("oauth_verifier")) def scrname = twitter.getScreenName() session.setAttribute("screenname", scrname) new User(scrname, accTok.getToken(), accTok.getTokenSecret()).save() defaultQueue << [ countdownMillis: 1, url: "/retweetpull", taskName: "twitter-pull-${new Date().time}", method: 'POST', params: [id: scrname] ] redirect "/dashboard/${scrname}" |
groovlet は呼び出された時点で、Twitter から oauth_verifier
という重要なパラメーターを取得します。このパラメーターはキーとシークレットと組み合わせられることで、その後ユーザーの代理として機能するための権限の役割を果たします。groovlet はまた、session
オブジェクトもユーザーの Twitter ハンドルで更新します。その後、User
オブジェクトが新しく作成されるか、更新されます。
defaultQueue
を扱うコードは、リクエストを GAE キューに入れるための Gaelyk のマジックです (これについては、次のセクションで詳しく説明します)。リスト 5 のアクションの最後の部分では、ユーザーのブラウザーが新しい URL にリダイレクトされます。このリダイレクトを示すコードは、get "/dashboard/@name", forward: /dashboard.gtpl?tname=@name
にマッピングされます。つまり、この URL を使って接続される Web ページは tname
というパラメーターを受け取ります。このパラメーターは、ユーザーの Twitter スクリーン・ネームです。
GAE でアプリケーションを構築しようと思ったら、従わなければならないルールがいくつかあります。Google では使用できるライブラリーと使用できないライブラリーを定義しているだけでなく、サーブレットのレスポンスに時間制限も設けています。サーブレットに許可されている着信リクエストの最大処理時間は、約 30 秒です。サーブレットがレスポンスを送信するまでにそれ以上の時間がかかると、GAE は HTTP 500 というステータス・コードの形でエラーを生成します。
Google には、時間制限を設ける正当な理由があります。Google は、ユーザーのアプリケーションを必要に応じて、しかもユーザーの支援なしでスケーリングすることができなければならないからです。したがって、作成したコードを処理するのにあまりにも時間がかかることが判明した場合には (つまり、自分で制御することができない他のシステムとインターフェースをとるコードや、単にお粗末にコーディングされたコード)、そのアプリケーションをもう一度検討する必要があります。
GAE 開発者は、自分の作成したアプリケーションが実行されているマシンにアクセスすることができません。実際、そのマシンの構成を知ることも、メモリーをどれだけ使用できるのかを知ることさえもできません。しかし、パフォーマンスの問題が表面化してくる場所は他にあります。通常、パフォーマンスの問題の原因となるのは I/O であり、GAE では I/O の問題が浮上してくるのは、大抵は (Twitter などの) 他のシステムとやりとりする場合です。
Twitter に対する API 呼び出しは信頼性に欠けることがあります。それは、Twitter ユーザーにはご存知のとおり、Twitter のサービスはたまに停止することがあるからです。応答時間もまた、容量過負荷によって遅くなる可能性があります。そのため、Twitter のデータを利用するアプリケーションを構築する場合には、多少の計画が必要になってきます。GAE の場合には、さらにキューイングも必要です。
Google App Engine では、タスクをバックグラウンドで非同期処理することを可能にするキューイング・メカニズムを提供しています。面白いことに、タスク自体はサーブレットであり、キューに入れるのは URL とオプション・パラメーターです。この仕組みはリスト 5 を見るとわかります。GAE キューを使用すれば、長期間実行される可能性のあるプロセスを簡潔なタスク (サーブレット) に分割してキューに入れることにより、これらのタスクをバックグラウンドで実行させることができます。もちろん、タスクが何か興味深い処理を行うのであれば、そのデータをデータストアに保存しておき、後で取得できるようにすることも可能です。これがまさに、私がユーザーのリツイートに対して行おうとしていることです。
ユーザーが Twitter で認証されたら、Twitter の API を利用してリツイートを取得する別のサーブレットへの URL を用意し、その URL をキューに入れます。ここでは GAE のデフォルト・キュー (「default」という名前のキュー) を使用しますが、必要な場合には固有のキューを作成することもできます。リスト 6 は、リクエストを GAE のデフォルト・キュー (リスト 5 にも記載されているキュー) に入れるためのコードです。
リスト 6. リクエストをキューに入れるためのコード
defaultQueue << [ countdownMillis: 1, url: "/retweetpull", taskName: "twitter-pull-${new Date().time}", method: 'POST', params: [id: scrname] ] |
リスト 6 に記載されているのは、/retweetpull
URL に対するリクエストです。この URL は適切に post "/retweetpull", forward: "/retweetpull.groovy"
というコードにマッピングされます。これは、HTTP POST
リクエストです。このリクエストでは、ユーザーのスクリーン・ネームとして id
というパラメーターも渡しています。キューイング・タスクの名前は一意の名前でなければならないことに注意してください。それには、日時スタンプを追加することで一意性をもたらすという方法が役立つと思いますが、リスト 6 ではもう 1 つの方法として、タスクが実行されるまでのカウントダウン時間を定義しています。
GAE キュー内にあるタスクは、単なるサーブレットです。したがって、URL はキュー「リーダー」によって呼び出されます。リスト 7 に、私が実行しようとしている具体的なサーブレットを記載します。
リスト 7. 非同期で実行されるサーブレット
import twitter4j.TwitterFactory import twitter4j.http.AccessToken import com.b50.gretweet.User import com.b50.gretweet.Retweet def user = User.findByName(params['id']) def twitter = getTwitter(user) def retweets = [] def statuses = twitter.getRetweetsOfMe() statuses.each { status -> def tid = status.getId() def dt = status.getCreatedAt() def txt = status.getText() users = twitter.getRetweetedBy(status.getId()) users.each { usr -> retweets << new Retweet(usr.getScreenName(), tid, dt, txt, usr.getFollowersCount(), usr.getProfileImageURL().toString(), usr.getName()) } } user.addRetweets(retweets) def getTwitter(user){ return new TwitterFactory().getOAuthAuthorizedInstance("...", "...", new AccessToken(user.token, user.tokenSecret)) } |
リスト 7 のサーブレットが最初に行っているのは、User
インスタンスの検索です。このインスタンスは、実際には memcache から取得されます。続いて、許可されている処理として、サーブレットはユーザーの代わりに Twitter セッションを確立します。Twitter セッションが確立したら、Twitter4J の API を使用してリツイート (例えば twitter.getRetweetsOfMe()
など) の一覧を取得します。そして各リツイートを処理して Retweet
オブジェクトに変換し、最終的にすべての Retweet
オブジェクトを List
の中に配置します (そのために使用する Groovy の便利な <<
演算子は、実際には単なる add
メソッドです)。一連のリツイートは (addRetweets
メソッドによって) User
インスタンスに追加された時点で保存されます。
ここまでの作業で、この Web アプリケーションに不可欠な部分のコーディングが完了しました。次のステップは、どのように通信するかを決めることです。すなわち、アプリケーション・データを取得するためのリクエストに使用するフォーマットを決めなければなりません。
JSON は、XML に代わるインターネット・データ交換の新しい共通語であるとも言われています。JSON は XML と比べると軽量であるため、読みやすく、構文解析するのも簡単です。次に記載する 2 つのリストを見比べてみると、その違いがわかることでしょう。最初に、XML で表現したリツイート・データを記載します。
<retweet> <screenname>BarackObama</screenname> <influence>5901913</influence> <date>2010-11-11</date> <tweet>"I want our veterans to know: We remember..."</tweet> <tweetid>2758011831459840</tweetid> <picture>http://a3.twimg.com/profile_images/784...</picture> <username>"Barack Obama"</username> </retweet> |
次に、上記と同じデータを JSON を使って表現します。
{ "screenname":"BarackObama", "influence":5901913, "date":"2010-11-11" "tweet":"I want our veterans to know: We remember...", "tweetid":2758011831459840, "picture":"http://a3.twimg.com/profile_images/784...", "username":"Barack Obama" } |
JSON 文書はマップのように見えることに注目してください。この文書には名前があり、その名前に値が対応付けられています。この方式では、例えばドメイン・オブジェクトのようなものを極めて単純に表現できますが、JSON の単純さを見くびることは禁物です。JSON ではリストを表現することも可能で、さらにリストを値にすることもできます。JSON のマークアップは XML のマークアップよりも簡潔なので、対応するデータを見た目で容易に理解することができます。さらに、JSON の簡潔さは、ブラウザーの処理とネットワーク帯域幅に負担をかけがちな Ajax システムでも強みとなります。
以上の理由から、私はブラウザーとサーバーとの間のやりとりでは XML ではなく、JSON を使用することを選びます。特に、このアプリケーションのように Ajax をフロントエンドで使用するアプリケーションには、率先して JSON を使用するようにしています。java.lang.Object
の toString
メソッドがオブジェクトを体系化する上で便利なメカニズムとなるように、toJSON
メソッドによって、Web アプリケーションのドメイン・オブジェクトを JSON 対応にすると役立つと思います。そこで、最初のステップとして、JSONable というインターフェースを定義します (リスト 8 を参照)。
リスト 8. toJSON メソッドが定義されたインターフェース
public interface JSONable { String toJSON(); } |
次に、ドメイン・オブジェクトに上記のインターフェースを実装させます。この振る舞いを提供するために使っているのは、JSON-lib
として知られる重宝なライブラリーです。このライブラリーには JSON 構造を作成するための単純な下位レベルの API があります。例えば Retweet
を表現する場合、toJSON
メソッドはリスト 9 に記載するような内容になります。
リスト 9. Retweet の toJSON
public String toJSON() { JSONObject json = new JSONObject(); json.put("screenname", this.screenName); json.put("influence", this.influence); json.put("date", this.getFormattedDate()); json.put("tweet", this.tweet); json.put("tweetid", this.tweetId); json.put("picture", this.userPicture); json.put("username", this.userName); return json.toString(); } |
この JSON 文書のフォーマットは明示的に制御することにしました。私はキャメル・ケースには興味ありませんが、Java コードの規約により、キャメル・コードに従って各単語の先頭を大文字にして連結しています。したがって Retweet
では userName
がプロパティーとなっていますが、JSON 文書では username
のままにしてあります。リスト 9 では表現したい各プロパティーを定義するために、JSON-lib
の JSONObject
とその put
呼び出しを使用しました。また、owner
プロパティーは除外してあります。
個々のオブジェクトを JSON として表現させるというパターンは、オブジェクトが集約された JSON 文書を作成するときに重宝します。リツイートのコレクション (例えば、影響力順にランクを付けた上位 3 つのリツイートのコレクション) を表現するにしても、このパターンを使うことで対処することができます。3 つの Retweet
オブジェクトのコレクションがあるとして、この 3 つのオブジェクトをひと回り大きな JSON 文書に含めるには、そのすべての処理を行うサーブレットをコーディングするだけでよいのです (リスト 10 を参照)。
リスト 10. 分析サーブレット
import com.b50.gretweet.User import com.b50.gretweet.Retweet import net.sf.json.JSONObject response.contentType = "application/json" def user = User.findByName(params['name']) def tweets = user.listAllRetweetsByInfluence() def topthree = tweets.unique(Retweet.getScreenNameComparator()) def justthree = ((topthree.size() >=3) ? topthree[0..2] : topthree[0..topthree.size()]) def jsonr = new JSONObject() jsonr.put("influencers", justthree*.toJSON()) println jsonr.toString() |
リスト 10 では、レスポンス・タイプは application/json
に設定されています。この設定が、ブラウザーに対して JSON コンテンツが送られてくることを事前に通知します。次に行っているのは、必要な User
インタンスの検索です。そのために、このサーブレットにはユーザーのスクリーン名がパラメーターとして渡されます。検索に続いて、関連するすべての Retweet
オブジェクトが取得されます。
Comparator
を使用しているのは、それぞれのユーザーからの Retweet
は 1 つだけしか返されないようにするためです。これで、かなり影響力のあるユーザーが特定の 1 人のユーザーに対してさまざまなツイートをリツイートしたとしても、1 つのリツイートしかリストアップされなくなります。リスト 11 に、この Comparator
を記載します。
リスト 11. リツイートの Comparator
public static Comparator<Retweet> getScreenNameComparator(){ return new Comparator<Retweet>() { @Override public int compare(Retweet arg0, Retweet arg1) { if(arg0.screenName.equals(arg1.screenName)){ return 0; }else{ return (arg0.influence > arg1.influence) ? 1 : -1; } } }; } |
リスト 10 の justthree
変数は、3 つの Retweet
オブジェクトが含まれる List
です。Groovy の便利で素晴らしいショートカットでは、*.
の呼び出しによって、コレクションに含まれるすべてのオブジェクトの toJSON
メソッドを呼び出すことができます。
最終的なコードは、リスト 12 のような JSON 文書になります (読みやすいように、スペースと改行を追加しました)。
リスト 12. JSON でのリツイートの一覧
{ "influencers": [ { "screenname":"stuarthalloway", "influence":2310, "date":"2010-08-17", "tweet":"Podcast w/@stuarthalloway about Clojure http://bit.ly/bFBRND...", "tweetid":21451426508, "picture":"http://a0.twimg.com/profile_images/51921564/stu-small_norm...", "username":"stuarthalloway" }, { "screenname":"aalmiray", "influence":1023, "date":"2010-08-10", "tweet":"New weekly podcast series airs today @ devWorks! @matthewmcc...", "tweetid":20812977124, "picture":"http://a3.twimg.com/profile_images/584851991/twitterProfil...", "username":"Andres Almiray" }, { "screenname":"wakaleo", "influence":1020, "date":"2010-09-28", "tweet":"new article published: \"MongoDB: A NoSQL datastore with (all...", "tweetid":25796403122, "picture":"http://a2.twimg.com/profile_images/1164962298/jfsmart_norma...", "username":"John Ferguson Smart" } ] } |
これで、サーブレットが Retweet
の一覧を JSON フォーマットで出力するようになったので、このデータをダッシュボード Web ページに接続する作業に取り掛かれます。
いくつか前のセクションでセットアップしたバックグラウンド処理は、アプリケーションの観点から見ると、ほとんどユーザーが介入することなく自動的に行われる処理になっています。ユーザーが Twitter とのセッションを認証すると、ジョブがキューに入れられ、すべてのリツイートの一覧を取得する処理が実行されます。私の次のタスクは、アプリケーションの対話をユーザー・エクスペリエンスに調和させることです。これも同じく、ユーザーが介入しない自動的な処理にしなければなりません。そこで、ユーザーに別のリンクをクリックさせたり、追加のページをロードさせたりする代わりに、Ajax を少し使って円滑に処理が実行されるようにしようと思います。
ユーザーがこの Web アプリケーションに対して、Twitter でそのユーザーの代理として振る舞う権限を与えると、そのユーザーは一種のダッシュボードにリダイレクトされます。最終的に、このダッシュボードに表示されるレポートが影響順にリツイートを一覧表示することになります。ユーザーがクリックしてこのページにジャンプした時点では、もちろんレポートはまだ作成されていません。したがって、そのページを非同期でロードする必要があります。
JSON を返す非同期マジックを扱うために作成したサーブレットは、リスト 10 に記載したとおりです (このサーブレットには、tanalysis.groovy という名前を付けました)。このサーブレットを Ajax によって Web ページから呼び出すのは、それほど難しい作業ではありません。実のところ、jQuery などのライブラリーを使えば作業を単純化することができます。ダッシュボードに表示されるリツイート分析レポートを非同期で更新するという目標を達成するために、これからいくつかの作業を行います。まず、ページのロードが完了した時点で、jQuery の getJSON
メソッドによって Ajax 呼び出しを実行します。このメソッドが呼び出すのはリスト 10 の分析サーブレットです。サーブレットの呼び出しでは、パラメーターとしてカレント・ユーザーも渡します。ただしサーブレットにアクセスする直前に、別の JavaScript 関数がお馴染みのスピナー・アイコンを作成します。このアイコンが、進行中のアクティビティーを示します。
サーブレットが JSON 文書で応答すると、getJSON
メソッドが HTML DOM のセクションを 3 つのリツイートからなる一覧で更新します。そして最後に別の JavaScript イベントが起動され、スピナーをオフにします。
リスト 13 は、dashboard.gptl ファイルに含まれている Ajax コードです。
リスト 13. dashboard.gptl
<script type="text/javascript"> $(document).ready(function() { $.getJSON('../tanalysis', {name: "${params['tname']}" }, function(data) { $.each(data.influencers, function(){ $('.statuses').append('<li class="hentry status"> <span class="thumb" > <img width="48" height="48" src="' + this.picture + '" class="photo fn" alt="Chris Burns"></span><span class="status-body" > <span class="status-content">' + '<strong><a class="tweet-url screen-name" href="https://twitter.com/' + this.screenname + '">' + this.username + '</a></strong>' + ' Followers: ' + this.influence + '<br/><span class="entry-content">Tweet ' + '<a target="_blank" rel="nofollow" href="https://twitter.com/' + this.screenname + '/status/' + this.tweetid + '">' + this.tweetid + '</a> <img width="13" height="11" src="../images/new_window_icon.gif"/>' + ': ' + this.tweet + '</span>'+ '</span></span></li>') }); }); }); $('.log').ajaxStart(function() { $(this).text('Loading data...'); $(".statuses").text(''); $(".spinnericon").css('display', 'block'); }); $('.log').ajaxStop(function() { $(this).text(''); $(".spinnericon").css('display', 'none'); }); </script> |
jQuery と JavaScript によって、いかに簡単に JSON レスポンスを構文解析できるかに注目してください。getJSON
メソッドは基本的に、JSON のクロージャーを呼び出します (これには data
という名前を付けました)。JSON 文書内の要素にアクセスするのは至って簡単で、実のところ、名前によってアクセスしているに過ぎません。この例では、data
が最初に指しているのはdata.influencers
というコレクションです。このコレクションに含まれる項目に対して each
メソッドを呼び出すことで、それぞれの値を取得します (例えば it.tweet
など)。
リスト 13 で、getJSON
メソッドの後に定義している ajaxStart
とajaxStop
というイベントに注目してください。これらのメソッドは、HTMLdiv
要素内のスピナー・アイコンをそれぞれ配置、除去します。
基本的にはこれで、ユーザーを Twitter の OAuth で認証し、そのユーザーのデータを非同期で取得してから、Web ページを非同期で更新する実用的な Web アプリケーションが出来上がりました。
残る問題は、セキュリティーだけです。
鋭い読者の方は、リスト 13 のコードには若干問題があることにお気付きでしょう。それは、リツイート・レポートはある 1 人のユーザーを対象に生成されますが、そのユーザーの名前は Twitter のユーザー (あるいは、この Web アプリケーションにデータがあるユーザー) であればどのユーザーの名前でも構わないという問題です。この場合、悪質な誰かが無作為にユーザー名を渡すことは可能でしょうか。答えは、可能です。けれども、その張本人が Twitter にログインしていない限り、レポートは表示されません。水面下で、私はちょっとしたセキュリティーの罠を仕掛けておいたからです。その罠とは、指定されたパラメーター名がカレント・ユーザーのセッションで現在使用されている名前と一致するかどうかを調べる ServletFilter
です。一致する名前がなければ、リクエストはログイン画面に戻されます。
リスト 14. ServletFilter によるユーザー認証
package com.b50.gretweet; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class UserFilter implements Filter { private ServletContext context; @Override public void destroy() {} @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { String uri = ((HttpServletRequest)req).getRequestURI(); HttpSession sess = ((HttpServletRequest)req).getSession(); String username = uri.replace("/dashboard/", ""); if(sess != null){ String sessname = (String) sess.getAttribute("screenname"); if(!sessname.equals(username)){ ((HttpServletResponse)res).sendRedirect("/"); } } chain.doFilter(req, res); } @Override public void init(FilterConfig arg0) throws ServletException { this.context = arg0.getServletContext(); } } |
このセキュリティー・メカニズムは簡潔なものとは言えませんが、このアプリケーションと記事には役立ちます。
この 2 回の連載では最初に、Objectify-Appengine を紹介しました。これは、リレーショナル・データベースでの Hibernate とよく似た、GAE のデータストアに対応したマッピング層です。第 1 回では Objectify を使用して素早くアプリケーションをモデル化する方法を具体的に説明し、第 2 回でこのアプリケーションを Twitter の認証システムである OAuth に関連付けました。I/O や GAE キューなどのアプリケーションのバックエンド機能を築き上げるために使用したのは Gaelyk です。そして XML ではなく、JSON を使うことによって、ブラウザーとサーバーとの間のやりとりを容易にしました。最後に、フロントエンドで Ajax を少し使用することで、エンド・ユーザーのエクスペリエンスがより円滑になるようにしました。これ以外に必要となった作業は、ほとんどありません。データベースの管理やキャッシングなどは、GAE が行ってくれるためです。
GAE には 30 秒以内でリクエストとレスポンスを完了しなければならないといった慣れないルールもありますが、例えば非同期キューのように、そのようなルールに対処するためのメカニズムも用意されています。多くの点で、Java 開発 2.0 を完璧に具現化している GAE は、安上がりで迅速な開発を促進すると同時に、ユーザーが期待するようになったスケーラビリティーと信頼性をも提供します。
0 件のコメント:
コメントを投稿