ふと Java の Atom パーサーが必要になりましたので google-api-java-client を使うことにしました。 このライブラリが便利なのは、Atom と JSON を切り替えて処理できることです。 もともとは gdata-java-client として開発されていたライブラリなので、ある程度は枯れているような気もします。 現在の最新バージョンは 1.2.0 のアルファなので本当に安定しているかは定かでありませんが、とりあえずこれを使うことにします。
この記事では、ライブラリを取り込んで、入力として Atom を受け取ってみることにします。
プロジェクトへの取り込み
プロジェクトのページから zip ファイルをダウンロードして展開すると、いくつかの .jar ファイルを含め、次のファイルおよびディレクトリがあります。
- LICENSE ... Apache License Version 2.0 のライセンス表記です。
- dependencies/ ... 依存するライブラリ (.jar ファイル) があります。
- google-api-client-1.2.0-alpha-javadoc.jar ... ライブラリのドキュメントです。
- google-api-client-1.2.0-alpha-sources.jar ... ライブラリのソースファイルです。
- google-api-client-1.2.0-alpha.jar ... ライブラリ本体です。
- packages/ ... 個別のパッケージです。
- readme.html ... 上記の説明が書かれています。
readme.html を読んでみると "dependent library jars" のリンクがありますので、これをさらに読みます。 Android 以外の環境の場合は jackson-core-asl と kxml2 が必要になります。 HTTP 通信に Apache HTTP ライブラリを使う場合には httpclient、httpcore、commons-logging も必要になります。 なお、AppEngine で利用する場合には httpclient は使えません。独自にソケットを開こうとするためです。 (該当の HTML ファイルにも Warning で書かれています。)
今回は AppEngine で使いたかったので、結局のところ、次の3つのライブラリをクラスパスに追加しました。
- google-api-client-1.2.0-alpha.jar
- jackson-core-asl-1.5.3.jar
- kxml2-2.3.0.jar
サンプルを眺める
YouTube のデータを取得するサンプル に目を通します。 重要そうなクラスは次の3つです。 サンプルには HttpResponse は現れていませんが、メソッドチェーンによって省略されているだけです。
- HttpTransport ... ターゲットとするデータソースに対する一般的な設定を受け持ちます。
- HttpRequest ... 具体的なリクエストです。
- HttpResponse ... 通信の結果オブジェクトです。
次に com.google.api.client.googleapis.xml.atom の package-summary を読みます。 なぜかここに詳しい記述があります。 まず、トランスポートを生成し、コンテントタイプに合わせてパーサを設定します。
private static GoogleTransport setUpGoogleTransport() throws IOException {
GoogleTransport transport = new GoogleTransport();
transport.applicationName = "google-picasaatomsample-1.0";
transport.setVersionHeader(PicasaWebAlbums.VERSION);
AtomParser parser = new AtomParser();
parser.namespaceDictionary = PicasaWebAlbumsAtom.NAMESPACE_DICTIONARY;
transport.addParser(parser);
// insert authentication code...
return transport;
}
次は、トランスポートからリクエストを生成して、パーサで処理します。
public static AlbumFeed executeGet(GoogleTransport transport, PicasaUrl url) throws IOException {
url.fields = GData.getFieldsFor(AlbumFeed.class);
url.kinds = "photo";
url.maxResults = 5;
HttpRequest request = transport.buildGetRequest();
request.url = url;
return request.execute().parseAs(AlbumFeed.class);
}
これで AlbumFeed 型のオブジェクトを得られます。 エラー処理には try-catch を利用します。
try {
...
} catch (HttpResponseException e) {
if (e.response.getParser() != null) {
Error error = e.response.parseAs(Error.class);
// process error response
} else {
String errorContentString = e.response.parseAsString();
// process error response as string
}
throw e;
}
AlbumFeed クラスは事前に定義しておくもので、XML の構造とマップさせます。 詳しくは上記の package-summary に書かれています。 マップできるデータ型は com.google.api.client.util.FieldInfo#parsePrimitiveValue に記述されています。 String や Long はプリミティブとして分かりやすいと思いますが、日付の扱いは java.util.Date ではなく、 com.google.api.client.util.DateTime を使って RFC3339 形式に統一します。
なお、FieldInfo のプリミティブとして定義されていない型は newInstance で生成することになります。 java.util.Date の場合は現在時となります。
実際に使ってみる
Atom の feed と entry を定義して、入力データを処理してみます。 次の4つのクラスを定義します。 簡単のためにデータのプロパティはかなり省略しています。
- Feed ... Atom の feed を表現します。
- Entry ... Atom の entry を表現します。
- Parser ... パーサーを定義 / 設定します。
- Driver ... 処理を実行します。
まずは名前空間とルート要素を定義します。Map を使っていることからも分かるように、複数の名前空間を利用できます。 別名で指定したものは @Key アノテーションの引数で区別できます。 たとえば、OpenSearch で件数を管理する場合には、名前空間のマップに openSearch を追加して @Key("openSearch:totalResults") のようにします。
Feed.java
public class Feed {
public static final XmlNamespaceDictionary NAMESPACE_DICTIONARY = new XmlNamespaceDictionary();
{
Map<String, String> map = NAMESPACE_DICTIONARY.namespaceAliasToUriMap;
map.put("", "http://www.w3.org/2005/Atom");
}
@Key
public DateTime updated;
@Key("entry")
public List<Entry> entry;
}
feed は entry のコレクションなので Feed クラスで Entry クラスのリストを保持します。 その中身は次のようになります。色々な例を見るために、<summary> 要素を content プロパティ名に別名で対応付けています。
Entry.java
public class Entry {
@Key
public String id;
@Key
public String title;
@Key("summary")
public String content;
@Key
public DateTime updated;
}
これらを処理するパーサーを次のように定義します。 パーサーの定義は利用シーンに合わせて自由に実装して構わないと思いますが、パーサーの設定と実際の処理を分離しておくことで、データ形式を柔軟に変更できるようになります。 たとえば、Atom ではなく JSON を使いたい場合には JsonHttpParser を追加し、JSON-C を使いたい場合には JsonCParser を追加します。 これらは parseAs メソッドで呼ばれますが、内部的にはコンテントタイプに応じて呼び出されます。 application/atom+xml の場合には Atom のパーサー、 application/json の場合には JSON のパーサーか JSON-C のパーサーになります。 JsonCParser は JsonHttpParser を継承しています。 詳細はそれぞれのソースコードで確認できます。
Parser.java
public class Parser {
public void setUpTransport(HttpTransport transport) {
AtomParser parser = new AtomParser();
parser.namespaceDictionary = Feed.NAMESPACE_DICTIONARY;
transport.addParser(parser);
}
public void executeGet(HttpTransport transport, String targetUrl)
throws HttpResponseException, IOException {
HttpRequest req = transport.buildGetRequest();
req.setUrl(targetUrl);
// Debug use.
// System.out.println(req.execute().parseAsString());
Feed feed = req.execute().parseAs(Feed.class);
if (feed.entry == null) {
// no entry for now.
return Collections.emptyList();
}
System.out.println("Got feed at " + feed.updated);
for (NotifyEntry e : feed.entry) {
System.out.println("Entry: " + e.id + " at " + e.updated);
System.out.println("Title: " + e.title);
System.out.println(e.content);
}
}
}
パーサーを切り替えても処理の部分は変わらないのは嬉しいと思います。 もちろん、サーバ側がそのようなデータ構造を保持していることが前提となります。 Atom は RFC4287 で定義されていますのでブレが少ないと思いますが、JSON はオレオレフォーマットで定義してしまうこともありますので、データを設計する場合には注意が必要です。
ここまで準備ができると、後は実行するだけになります。 トランスポートを設定してパーサーに処理させます。 targetUrl は適当に設定してください。 おそらく、エントリーのタイトルと中身が出力されるのではないでしょうか。 コメントの "set default headers." の部分で "Authorization" ヘッダーに適切な値を割り当てると、OAuth の認証越しにデータを取得できるはずです。
Driver.java
public class Driver {
public void execute(String targetUrl) {
HttpTransport transport = new HttpTransport();
// set default headers.
Parser parser = new Parser();
parser.setUpTransport(transport);
parser.executeGet(transport, targetUrl);
}
}
まとめ
google-api-java-client を使って Atom を処理しました。 YouTube や Calendar あるいは Docs などを使うときには専用のクラスが既に定義されていますが、 Google 以外のサイトからの Atom っぽいフィードを簡単かつ統一的に扱えるのは便利そうです。 また、Atom を XML として扱うのではなく、JSON-C のように軽量で同じデータ構造を持つ形式で扱うように変更することもできます。
そもそも Java を使う必要が... という話題であることは否めませんが、とりあえずやってみた、ということで。
0 件のコメント:
コメントを投稿