私がこの記事を執筆している間に、夏は終わりを告げようとしており、新しい学年が始まろうとしています (訳注: 著者が在住している米国では 9月に新しい学年が始まります)。そして Twitter のサーバーは、世界中の Twitter マニアの近況や Twitter マニアではない人達の近況を、休むことなく吐き出し続けています。北米に住む私達の大部分にとって、話題は海辺のパーティーからフットボールに、そして屋外で行っていた活動は室内でできるものに切り替わりつつあります。それに合わせて、Twitter にアクセスするための Scala クライアント・ライブラリー Scitter を再度取り上げる時期が来たようです。
Scitter の開発について説明した前回までの記事を読んでいる方は、Scitter ライブラリーを利用すると、さまざまな Twitter API を介して Twitter ユーザーの友達やフォロワー、タイムラインなどの情報を取得できることをご存知かと思います。ただしこのライブラリーには、近況のアップデートを POST する機能が欠けています。Scitter に関する記事の最終回である今回は、このライブラリーに欠かせないメソッドである update()、show()、destroy() と作成するのが楽しい機能 (end と rate) を追加して締めくくります。そのなかで、Twitter API の詳細と、Scala によって Twitter API をうまく利用する方法、そして Scala で Twitter API を利用する上で避けられないプログラミング上の問題を克服するためのヒントについても学びます。
この記事を皆さんが読む頃には、Scitter ライブラリーは公開ソース管理リポジトリーに置かれているはずです。もちろん、この記事にもソース・コードを含めてありますが、ソースのベースは変更される可能性があることに注意してください。つまり、プロジェクトのリポジトリーに置かれているコードは、この記事で紹介するコードと多少あるいは大幅に異なっている可能性があります。
これまでの Scitter の開発では、HTTP GET を使用した操作に焦点を絞ってきました。その大きな理由は、GET を使った呼び出しは非常に容易であり、また私は手軽に Twitter API を使いたかったからです。Scitter ライブラリーに POST 操作と DELETE 操作を追加することは、公開という観点でも大きな一歩を意味します。これまで皆さんは、何をしようとしているのかを他の誰にも知られずに、自分の Twitter アカウントに対してユニット・テストを実行することができました。しかし近況メッセージを送信し始めると、皆さんが Scitter のユニット・テストを実行していることを世界中の人達が知ることになります。
Scitter のテストを継続する場合には、独自の「test」アカウントを Twitter に作成する必要があります。(Twitter にはテストを行ったり、モックを生成したりするための機能がありませんが、これは Twitter API を利用するコードを作成する上で、おそらく最大の欠点です。)
Scitter ライブラリーの新しい UPDATE 機能を説明する前に、これまでに作成したものを説明しましょう。(この記事には完全なソース・リストを含めてありませんが、これは Scitter の完全なソース・リストを記事に含めるには不適切なほど長くなり始めているためです。この記事を読みながら、別のウィンドウでコードを見てください。)
大まかに言えば、Scitter ライブラリーは以下の 4 つの部分に分割されます。
- API の一部として送受信されるリクエスト型とレスポンス型 (
User、Statusなど)。これらは case クラスとしてモデリングされます。 - 場合によると同じく API の一部である
OptionalParam型。これらの型も基底OptionalParam型を継承する case クラスとしてモデリングされます。 - 通信の基本部分や Twitter への匿名 (非認証) アクセスに使われる
Scitterオブジェクト。 - 指定の Twitter アカウントに認証アクセスするためのユーザー名とパスワードを保持する
Scitterクラス。
ファイルを比較的扱いやすいサイズに保つために、今回の記事からはリクエスト型とレスポンス型をそれぞれ別のファイルに分割してあることに注意してください。
目標がはっきりしたので、「読み出し専用」の 2 つの Twitter API を実装することから始めます。この 2 つというのは、(ユーザーのセッションを閉じる) end_session API と、(特定の期間、あるユーザー・アカウントがあと何回 POST できるかを指定する) rate_limit_status API です。
end_session API は、同じくアカウントに関する API である verify_credentials API と同様に、非常に単純な API です。認証リクエストを使ってこの API を単純に呼び出すと、この API は開いているセッションを閉じます。この API は比較的容易に Scitter クラスに実装することができます (リスト 1)。
リスト 1. Scitter に end_session を実装する
package com.tedneward.scitter
{
import org.apache.commons.httpclient._, auth._, methods._, params._
import scala.xml._
// ...
class Scitter
{
/**
*
*/
def endSession : Boolean =
{
val (statusCode, statusBody) =
Scitter.execute("http://twitter.com/account/end_session.xml",
username, password)
statusCode == 200
}
}
}
|
いかがでしょう。それほど容易ではないかもしれませんね。
end_session では、これまで Twitter API の中で扱ってきた他の API とは異なり、受信されるメッセージを HTTP POST で送信する必要があります。現状では、Scitter.execute メソッドは GET を使ってすべてを実行します。これはつまり、Scitter.execute メソッドは、GET を使用する API と POST を使用する API とを何らかの方法で区別する必要があるということです。
この問題はしばらく脇に置いておくとして、もう 1 つ明らかに変更が必要な点があります。POST を使用する API 呼び出しでは、名前と値のペアも execute() メソッドに渡す必要があります。(GET を使用する他の API 呼び出しの場合には、すべてのパラメーターは URL の行にクエリー・パラメーターとして含まれること、また含めることができることを思い出してください。POST を使用する場合には、HTTP リクエスト本体の中にパラメーターが含まれます。) Scala の場合、名前と値のペアと言えば、Scala の Map 型が必ず頭に浮かびます。つまり、POST の一部として送信されるデータ要素のモデリング方法として考えられる最も簡単な方法は、それらのデータ要素を Map[String,String] の中に入れて渡す方法です。
例えば、新しい近況メッセージを Twitter に渡す場合には (この場合、140 文字以下のメッセージを名前と値のペア (status と呼ばれます) の中に入れる必要があります)、リスト 2 のようになります。
リスト 2. Map の基本的な構文
val map = Map("status" -> message)
|
この構文を使用すると、Map を引数に取るように Scitter.execute() メソッドを再構成することができます。Map が空の場合には、POST ではなく GET を使う必要がある、と見なすことができます (リスト 3)。
リスト 3. execute() をリファクタリングする
private[scitter] def execute(url : String) : (Int, String) =
execute(url, Map(), "", "")
private[scitter] def execute(url : String, username : String,
password : String) : (Int, String) =
execute(url, Map(), username, password)
private[scitter] def execute(url : String,
dataMap : Map[String,String]) : (Int, String) =
execute(url, dataMap, "", "")
private[scitter] def execute(url : String, dataMap : Map[String,String],
username : String, password : String) =
{
val client = new HttpClient()
val method =
if (dataMap.size == 0)
{
new GetMethod(url)
}
else
{
var m = new PostMethod(url)
val array = new Array[NameValuePair](dataMap.size)
var pos = 0
dataMap.elements.foreach { (pr) =>
pr match {
case (k, v) => array(pos) = new NameValuePair(k, v)
}
pos += 1
}
m.setRequestBody(array)
m
}
method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
new DefaultHttpMethodRetryHandler(3, false))
if ((username != "") && (password != ""))
{
client.getParams().setAuthenticationPreemptive(true)
client.getState().setCredentials(
new AuthScope("twitter.com", 80, AuthScope.ANY_REALM),
new UsernamePasswordCredentials(username, password))
}
client.executeMethod(method)
(method.getStatusLine().getStatusCode(), method.getResponseBodyAsString())
}
|
execute() メソッドに対する最大の変更は、Map[String,String] パラメーターが導入されたこと、そしてこのパラメーターのサイズに基づいて判定を行う if 文が導入されたことです。この if 文によって、GET リクエストを扱うのか POST リクエストを扱うのかを判断します。Apache Commons の HttpClient では POST リクエストの本体を NameValuePairs の中で行う必要があるため、foreach() 呼び出しを使ってマップの要素に繰り返し処理を行います。この処理では、2 つの部分から成る pr というタプルとしてマップのキーと値を渡し、ローカル変数 k と v の中にキーと値を抽出し、これらの値を NameValuePair コンストラクターに対するコンストラクター引数として使います。
PostMethod の setParameter(name, value) API を使用すると、こうしたことのすべてを、もっと容易に行うことができます。私がリスト 3 の方法を選択した理由は教育的な観点からです。つまり Scala の配列では配列参照が val として定義されていますが、Java の配列と同じように相変わらず可変であるという事実をリスト 3 で示したのです。ともかく、実際のコードでは各 (k、v) タプルに対して PostMethod の setParameter(name, value) メソッドを使った方がはるかに適切である、ということを頭に入れておいてください。
もう 1 つ注意する点として、Scala コンパイラーは型推論をする際に、if/else によって返される「method」オブジェクトの型を適切に判断します。Scala は if/else によって GetMethod オブジェクトが返されたのか PostMethod オブジェクトが返されたのかを判断できるため、「method」に対する戻り型として直接の基底型、HttpMethodBase を選択します。これは、HttpMethodBase で利用できないメソッドを execute() メソッドの他の部分で利用することはできない、という意味でもあります。幸いなことに、ここではそうしたメソッドは必要ないため、少なくとも今のところ、それが問題になることはないでしょう。
リスト 3 の実装に潜む最後の問題は、execute() メソッドが GET 操作を扱うのか POST 操作を扱うのかを、Map を使って判断しているという事実と関係しています。もし、他の HTTP 操作 (PUT や DELETE など) を使う必要がある場合には、もう一度 execute() をリファクタリングする必要があります。ここまでの時点ではこの問題を気にする必要はありませんでしたが、先に進む際にはこのことを頭に入れておく必要があります。
このリファクタリングを実装する前に、ant test と入力して実行し、GET ベースの元々のリクエスト API がすべて変わらず動作することを確認しましょう。テストしてみると、実際に問題なく動作します。(この結果から、本番の Twitter API の変更や、Twitter サーバーを利用する上での変更が何もないという前提で話を進めます。) すべては適切に動作するので (少なくとも私のマシンでは適切に動作したので)、新しい execute() メソッドの実装は非常に簡単です。
リスト 4. Scitter v0.3: endSession
def endSession : Boolean =
{
val (statusCode, statusBody) =
Scitter.execute("http://twitter.com/account/end_session.xml",
Map("" -> ""), username, password)
statusCode == 200
}
|
これ以上簡単にならないほど簡単です。
次に、rate_limit_status API を実装する必要があります。この API には認証版と非認証版の両方があります。ここではこのメソッドを rateLimitStatus として Scitter オブジェクトと Scitter クラスの両方に実装します (リスト 5)。
リスト 5. Scitter v0.3: rateLimitStatus
package com.tedneward.scitter
{
object Scitter
{
// ...
def rateLimitStatus : Option[RateLimits] =
{
val url = "http://twitter.com/account/rate_limit_status.xml"
val (statusCode, statusBody) =
Scitter.execute(url)
if (statusCode == 200)
{
Some(RateLimits.fromXml(XML.loadString(statusBody)))
}
else
{
None
}
}
}
class Scitter
{
// ...
def rateLimitStatus : Option[RateLimits] =
{
val url = "http://twitter.com/account/rate_limit_status.xml"
val (statusCode, statusBody) =
Scitter.execute(url, username, password)
if (statusCode == 200)
{
Some(RateLimits.fromXml(XML.loadString(statusBody)))
}
else
{
None
}
}
}
}
|
これも非常に単純だと思います。
POST を使用できる新しいバージョンの HTTP 通信レイヤーが用意できたので、Twitter API の中心とも言える、update 呼び出しに取り組むことにします。当然ですが、update を呼び出すためには、status という少なくとも 1 つの引数を取る POST が必要です。
この status パラメーターには、認証ユーザーの Twitter フィードに POST するための 140 文字以下のメッセージが含まれています。またオプションのパラメーターとして、in_reply_to_status_id も含まれています (in_reply_to_status_id は、POST されたアップデートが別のアップデートに対する返信である場合に、その返信の対象となるアップデートの ID を指定します)。
これで update 呼び出しに対する説明は終わりです (リスト 6)。
リスト 6. Scitter v0.3: アップデート
package com.tedneward.scitter
{
class Scitter
{
// ...
def update(message : String, options : OptionalParam*) : Option[Status] =
{
def optionsToMap(options : List[OptionalParam]) : Map[String, String]=
{
options match
{
case hd :: tl =>
hd match {
case InReplyToStatusId(id) =>
Map("in_reply_to_status_id" -> id.toString) ++ optionsToMap(tl)
case _ =>
optionsToMap(tl)
}
case List() => Map()
}
}
val paramsMap = Map("status" -> message) ++ optionsToMap(options.toList)
val (statusCode, body) =
Scitter.execute("http://twitter.com/statuses/update.xml",
paramsMap, username, password)
if (statusCode == 200)
{
Some(Status.fromXml(XML.loadString(body)))
}
else
{
None
}
}
}
}
|
このメソッドで、おそらく最も「変わっている」部分は、このメソッドの内部に定義されているネストされた関数でしょう。GET を使用する他の Twitter API 呼び出しの場合とは異なり、Twitter は POST される本体内部に POST のパラメーターが含まれているものと想定しています。つまり、POST のパラメーターを Map のエントリーに変えてから Scitter.execute() を呼び出す必要があります。しかしデフォルトの Map は、(scala.collections.immutable によって) 不変です。つまり Map を組み合わせることはできますが、既存の Map にエントリーを追加することはできません。(実際には追加できるのですが、追加しない方が適切です。これについては囲み記事「可変コレクション」を参照してください。
このちょっとした難問を解くための最も容易な方法は、渡される OptionalParam 要素のリスト (実際には Array[]) を再帰的に処理する方法です。ここでは各要素を分離して独自の Map エントリーにします。そして新しい Map (新しく作成された Map と再帰呼び出しによって返された Map で構成されます) を optionsToMap に返します。
こうしておけば、ネストされた optionsToMap 関数に OptionalParam の Array[] を渡すのは非常に簡単です。optionsToMap 関数では、返された Map と、status メッセージを含むように作成された Map とを連結します。最後に、この新しい Map をユーザー名とパスワードと共に Scitter.execute() メソッドに渡すと、Twitter のサーバーに送信されます。
ところで、このように延々と説明するよりも、実際にコードを見た方がわかりやすいはずです。このコードは非常にスマートなプログラミング方法を示しています。
update に渡されるオプション・パラメーターによって実行される内容は、理論的には GET ベースの他の API 呼び出しにオプション・パラメーターとして渡された場合に実行される内容と、ほとんど同じです。異なるのは、結果のフォーマットが URL 用の名前と値のペアではなく、POST に適した名前と値のペアになっていることのみです。
Twitter API で他の HTTP 操作をサポートする必要がある場合には (必要となる可能性が最も高いのは PUT と DELETE/ の 2 つです)、いつでも HTTP 操作を特定のパラメーター (例えば case クラスのセットなど) にし、execute() が 5 つの引数として、HTTP 操作、URL、名前と値のペアのマップ、そして (オプションとして) ユーザー名とパスワードを取るように設定することができます。そうすると、オプションのパラメーターを必要に応じて 1 つのストリングにしたり、または POST される一連のパラメーターにしたりすることができます。これを今後のために頭に入れておいてください。
取得対象の Twitter 近況メッセージの ID を指定して show を呼び出すと、Twitter 近況メッセージが 1 つ表示されます。update の場合と同様、show の呼び出しもほとんど自明です (リスト 7)。
リスト 7. Scitter v0.3: show
package com.tedneward.scitter
{
class Scitter
{
// ...
def show(id : Long) : Option[Status] =
{
val (statusCode, body) =
Scitter.execute("http://twitter.com/statuses/show/" + id + ".xml",
username, password)
if (statusCode == 200)
{
Some(Status.fromXml(XML.loadString(body)))
}
else
{
None
}
}
}
}
|
何か質問はあるでしょうか。
もう少しパターン・マッチングを試してみたいと思う読者のために、リスト 8 は show() メソッドを作成するための別の方法を示しています。
リスト 8. Scitter v0.3: 別の方法で作成する show
package com.tedneward.scitter
{
class Scitter
{
// ...
def show(id : Long) : Option[Status] =
{
Scitter.execute("http://twitter.com/statuses/show/" + id + ".xml",
username, password) match
{
case (200, body) =>
Some(Status.fromXml(XML.loadString(body)))
case (_, _) =>
None
}
}
}
}
|
この方法が if/else の方法よりも簡潔であるかどうかは美的感覚の問題以外の何物でもありませんが、こちらの方が簡潔だと言えることは確かでしょう。(おそらく、コードを見る人が Scala の「関数型」の部分に注目すればするほど、こちらの方が魅力的だと思うでしょう。)
パターン・マッチングによる方法には、if/else の方法に勝るメリットが 1 つあり、新しい条件 (今までにはないエラー条件や HTTP レスポンス・コードなど) が Twitter から返された場合、パターン・マッチングによる方法のほうが、それらの条件を明確に区別することができます。例えば、ある日 Twitter の仕様が変更され、本体そのものの中に 400 というレスポンス・コードと 1 つのエラー・メッセージを入れて返し、何らかのフォーマット・エラー (例えば Retweet を適切に行わなかったなど) の発生を示すようになったとしたら、パターン・マッチングによる方法のほうが if/else の方法よりも容易にレスポンス・コードと本体の内容の両方をテストすることができ、より明確な結果を得ることができます。
もう 1 つ注目すべき点として、リスト 8 を部分的に適用し、URL とパラメーターのみを必要とする関数を作成することも考えられます。しかし正直なところ、こうしたソリューションは問題を招くことになるので、私は手を出すつもりはありません。
実行されたアクションを Scitter のユーザーが取り消しできるようにする必要があるでしょう。そのためには、POST された Twitter 近況メッセージを削除する destroy 呼び出しが必要です (リスト 9)。
リスト 9. Scitter v0.3: destroy
package com.tedneward.scitter
{
class Scitter
{
// ...
def destroy(id : Long) : Option[Status] =
{
val paramsMap = Map("id" -> id.toString())
val (statusCode, body) =
Scitter.execute("http://twitter.com/statuses/destroy/" + id.toString() + ".xml",
paramsMap, username, password)
if (statusCode == 200)
{
Some(Status.fromXml(XML.loadString(body)))
}
else
{
None
}
}
def destroy(id : Id) : Option[Status] =
destroy(id.id.toLong)
}
}
|
ここまで完成すると Scitter クライアント・ライブラリーを「アルファ版」と見なすことができ、少なくとも単純な Scitter クライアントならば実装することができます。(私は、著者に特有の思い上がった習慣に従って、この Scitter クライアントの実装を「読者の演習」とすることにします。)
Scitter クライアント・ライブラリーの作成は興味深い演習でした。まだ Scitter は完全に本番で使える状態とは言えませんが、単純なテキスト・ベースの Twitter クライアントを実装するためには十分使用可能であり、公開できる状態にあります。このコードを使うと何ができるのか、また、より使いやすくするためにどんな機能が必要かを知るためには、コードを公開することが一番です。
私は Scitter に関する今回の記事と以前の記事のコードを、最初のリビジョンとして、Google Code でホストされる Scitter プロジェクトのホームページに公開しました。このライブラリーを自由にダウンロードして実際に試し、皆さんの意見を聞かせてください。またバグ・レポートやバグ修正、助言なども歓迎します。
皆さんは私のコードベースにこだわる必要はありません。Scitter の開発についての今回までの 3 回の記事を読むことで、Twitter API がどのように動作するかの感覚をつかめたはずです。また、Twitter API の使い方に関して別のアイデアを思いついたら、Scitter を捨て去り、皆さん自身の Scala クライアント・ライブラリーを作成してみてください。そうした、ちょっとした作業をしてみるのも楽しいものです。
では、これで Scitter に別れを告げ、Scala を使って問題を解決するための新しいプロジェクトを探すことにしましょう。それを楽しみながら、Scala での実際的なプログラミングを見つけた場合には、ぜひ私に知らせてください。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| Source code for this article | j-scala10209.zip | 812KB | HTTP |
学ぶために
- Scitter に関する前回までの 2 回の記事を含め、「多忙な Java 開発者のための Scala ガイド」シリーズ (Ted Neward 著、developerWorks) の他の記事も読んでください。
- 「The busy Java developer's guide to Scala: Don't get thrown for a loop!」(Ted Neward 著、developerWorks、2008年3月) は、私達が考えるほど頻繁には可変状態は必要ない、という、Scala の基礎となっている考え方を詳しく解説しています。
- 「多忙な Java 開発者のための Scala ガイド: コレクション型」(Ted Neward 著、developerWorks、2008年6月) は、Scala でのタプル、配列、リストについて解説しています。
- Twitter をさらに活用したい場合には、まず Twitter API のウィキから始めてください。
- 「Scala by Example」(Martin Odersky 著、2007年12月、PDF) は簡潔に、コードを中心に Scala を紹介しています。
- 『Programming in Scala』(Martin Odersky、Lex Spoon、Bill Venners の共著、2007年12月、Artima 刊) は 1 冊の本として Scala を紹介した最初の資料です。
- developerWorks の Java technology ゾーンには、Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。
製品や技術を入手するために
- Scitter ライブラリーをダウンロードしてください。このライブラリーは現在、Google Code に公開されています。
- Jakarta (Apache) Commons の HttpClient コンポーネントは、効率的で最新の豊富な機能を備えたパッケージであり、クライアント側での最新の HTTP 標準や推奨事項を実装しています。また Commons Logging コンポーネントと Commons Codec コンポーネントも必要です。
- Scala をダウンロードし、このシリーズと共に Scala を学んでください。
- SUnit は Scala の標準的なディストリビューションの一部であり、scala.testing パッケージの中にあります。
議論するために
- My developerWorks コミュニティーに加わってください。
