2010年11月9日

Google API Java Client

ふと 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 を使う必要が... という話題であることは否めませんが、とりあえずやってみた、ということで。

コメントを投稿