ふと 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 件のコメント:
コメントを投稿