sessanの日記

主に開発技術関連でお勉強したことをまとめていくサイトです。

Jersey Client APIを使ってtwitter apiを呼び出し、JSON形式で受け取る

昨日のエントリーに続き、Jersey Client APIを使ってtwitter apiを呼び出すサンプルコードを説明する。
sessanの日記

昨日書いたサンプルコードでは、JSON形式で検索結果を受け取ってはいるが、受け取った全テキストデータをそのままString型の変数に入れていた。これでは複雑なアプリを作成するときにString型の変数をパースしたりして中身を抽出していく必要がある。
今日は、JerseyのJSONサポート機能(JAX-RSの規格外の独自機能だと思われる)を使って、受け取ったデータをJavaのオブジェクト型に格納する。Object-Relational-MappingならぬObject-JSON-Mappingか。

このJSONサポート機能は、内部的にJAXBのアノテーションとjettisonというJSONパーサ(Staxパーサらしい)を使っているそうだ。プログラマはJAXBのアノテーションを使って、JavaのオブジェクトにJSON中の属性名を指定するだけで、Jersey Client APIが受け取ったJSON形式のデータをJavaオブジェクトに変換してくれる。


この機能の実装するために、次のような手順で昨日のサンプルコードを変更する。

  1. NetBeansのライブラリでJAXBを追加
  2. JSONデータの受け皿となるJavaオブジェクトを定義
  3. 作ったJavaオブジェクトにJAXBのアノテーションを指定し、JSONの属性とマッピング
  4. Jerseyのクライアントが「Object-JSON」マッピングを実行するためのプロバイダーの設定
  5. Jerseyクライアントにプロバイダーと戻り値の型を指定して、twitter apiを呼び出す。

1.NetBeansのライブラリにJAXBを追加

これは、プロジェクトにJAXBのjarファイルたちを追加するだけ。NetBeansのプロジェクトで「ライブラリ」右クリックして追加するだけ。

2.JSONのデータの受け皿となるJavaオブジェクトを定義

3.作ったJavaオブジェクトにJAXBのアノテーションを指定し、JSONの属性とマッピング

2,3は同時に実装する。POJOを作って、JAXBのアノテーションを指定するだけ。
POJOはネストしているオブジェクトがあってもよい。

作ったPOJOはこちらの2つ。

package beans;

import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

/**
 *
 * @author masato1
 */
@XmlRootElement
public class SearchResultBean {

    @XmlElement(name = "max_id")
    public int maxId;
    @XmlElement(name = "since_id")
    public int sinceId;
    @XmlElement(name = "refresh_url")
    public String refreshUrl;
    @XmlElement(name = "total")
    public int total;
    @XmlElement(name = "results_per_page")
    public int resultsPerPage;
    @XmlElement(name = "page")
    public int page;
    @XmlElement(name = "completed_in")
    public String completedIn;
    @XmlElement(name = "query")
    public String query;
    @XmlElement(name = "results")
    public List<StatusBean> results;

    public SearchResultBean() {
    }
}

上のBeanから参照されているネストしたBean。
twitter api的には「ツイート」のことを「status」と呼んでいる模様。

package beans;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

/**
 *
 * @author masato1
 */
@XmlRootElement
public class StatusBean {

    @XmlElement(name = "profile_image_url")
    public String profileImageUrl;
    @XmlElement(name = "created_at")
    public String createdAt;
    @XmlElement(name = "from_user")
    public String fromUser;
    @XmlElement(name = "to_user_id")
    public int toUserId;
    @XmlElement(name = "text")
    public String text;
    @XmlElement(name = "id")
    public int id;
    @XmlElement(name = "from_user_id")
    public int fromUserId;
    @XmlElement(name = "to_user")
    public String toUser;
    @XmlElement(name = "geo")
    public String geo;
    @XmlElement(name = "iso_language_code")
    public String isoLanguageCode;
    @XmlElement(name = "source")
    public String source;
}

4.Jerseyのクライアントが「Object-JSON」マッピングを実行するためのプロバイダーの設定


上のサイトのExample4.4を参考に、というか、そのままプロバイダーを実装する。

作ったプロバイダーはこちら。

package config;

import beans.SearchResultBean;
import beans.StatusBean;
import com.sun.jersey.api.json.JSONConfiguration;
import com.sun.jersey.api.json.JSONJAXBContext;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.JAXBContext;

/**
 *
 * @author masato1
 */
@Provider
public class JAXBContextResolver implements ContextResolver<JAXBContext> {

    private JAXBContext context;
    private Class[] types = {SearchResultBean.class, StatusBean.class};

    public JAXBContextResolver() throws Exception {
        this.context = new JSONJAXBContext(JSONConfiguration.natural().build(), types);
    }

    @Override
    public JAXBContext getContext(Class<?> objectType) {
        return (types[0].equals(objectType)) ? context : null;
    }
}

5.Jerseyクライアントにプロバイダーと戻り値の型を指定して、twitter apiを呼び出す。

昨日のmainメソッドを変更する。

説明はソースコード中のコメント参照。

package client;

import beans.SearchResultBean;
import beans.StatusBean;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.GenericType;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.core.util.MultivaluedMapImpl;
import config.JAXBContextResolver;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;

/**
 *
 * @author masato1
 */
public class Main {

    private static final String TWITTER_SEARCH_API_BASE_URL = "http://search.twitter.com";
    private static final String TWITTER_SEARCH_API_JSON_PATH = "search.json";

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {

        //Jersey Client 設定
        ClientConfig config = new DefaultClientConfig();
        //作成したJAXBContextResoluverを設定。
        config.getClasses().add(JAXBContextResolver.class);

        //設定を元にして、Jersey Client 作成
        Client client = Client.create(config);

        //WebResourceの設定
        WebResource webResource = client.resource(TWITTER_SEARCH_API_BASE_URL);

        //クエリを追加
        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();
        queryParams.add("q", "@sessan60jp");

        //レスポンスを受け取る。
        SearchResultBean searchResult = webResource.path(TWITTER_SEARCH_API_JSON_PATH).queryParams(queryParams).accept(MediaType.APPLICATION_JSON_TYPE).get(new GenericType<SearchResultBean>() {
        });
        
        //デバッグ用に受け取ったレスポンスを表示
        System.out.println("max_id: " + searchResult.maxId);
        System.out.println("since_id: " + searchResult.sinceId);
        System.out.println("refresh_url: " + searchResult.refreshUrl);
        System.out.println("total: " + searchResult.total);
        System.out.println("results_per_page: " + searchResult.resultsPerPage);
        System.out.println("page: " + searchResult.page);
        System.out.println("completed_in: " + searchResult.completedIn);
        System.out.println("query: " + searchResult.query);

        for (StatusBean status : searchResult.results) {
            System.out.println("-------------------------------------------------------------------------------------------------------------------------");
            System.out.println("profile_image_url: " + status.profileImageUrl);
            System.out.println("created_at: " + status.createdAt);
            System.out.println("from_user: " + status.fromUser);
            System.out.println("to_user_id: " + status.toUserId);
            System.out.println("text: " + status.text);
            System.out.println("id: " + status.id);
            System.out.println("from_user_id: " + status.fromUserId);
            System.out.println("to_user: " + status.toUser);
            System.out.println("geo: " + status.geo);
            System.out.println("iso_language_code: " + status.isoLanguageCode);
            System.out.println("source: " + status.source);
        }

    }
}


実行結果はこんな感じ。
1点だけ重要な注意点があって、結果の「total」はtwitterのサービスから値が埋まってくるときと、なぜか、0で返ってくるときがある。totalに依存した分析アプリなどの実装はしないほうがよさげ。

max_id: 337682466
since_id: 0
refresh_url: ?since_id=21812518946&q=%40sessan60jp
total: 0
results_per_page: 15
page: 1
completed_in: 0.018797
query: %40sessan60jp

profile_image_url: http://a2.twimg.com/profile_images/609123382/messenger-picture2_normal.jpg
created_at: Sun, 22 Aug 2010 07:17:46 +0000
from_user: garmy
to_user_id: 0
text: ゲームは重要だよ! あーマリギャラ2、最後の1枚が撮れない RT: @sessan60jp: ゲームやればやるほど、人生が無駄になるなー、とわかりつつ、ハマるのはなぜだろう。
id: 337682466
from_user_id: 172962
to_user: null
geo: 
iso_language_code: ja
source: <a href="http://www.echofon.com/" rel="nofollow">Echofon</a>


profile_image_url: http://a0.twimg.com/profile_images/1007036000/miyakawa_taku_normal.jpg
created_at: Sun, 22 Aug 2010 05:27:47 +0000
from_user: miyakawa_taku
to_user_id: 101004484
text: @sessan60jp もう書店に並んでますよ。ついさっき買いました。
id: 332149609
from_user_id: 120677187
to_user: sessan60jp
geo: 
iso_language_code: ja
source: <a href="http://www.nibirutech.com" rel="nofollow">TwitBird</a>


profile_image_url: http://s.twimg.com/a/1281738360/images/default_profile_1_normal.png
created_at: Sun, 22 Aug 2010 05:09:44 +0000
from_user: ykondo813
to_user_id: 101004484
text: @sessan60jp 同意。今週はタクティクスオウガに消えてしまったよ。
id: 331122518
from_user_id: 110139039
to_user: sessan60jp
geo: 
iso_language_code: ja
source: <a href="http://twtr.jp" rel="nofollow">Keitai Web</a>


profile_image_url: http://a2.twimg.com/profile_images/609123382/messenger-picture2_normal.jpg
created_at: Fri, 20 Aug 2010 10:12:56 +0000
from_user: garmy
to_user_id: 101004484
text: @sessan60jp 何時くらい到着?
id: 178279471
from_user_id: 172962
to_user: sessan60jp
geo: 
iso_language_code: fi
source: <a href="http://www.echofon.com/" rel="nofollow">Echofon</a>


profile_image_url: http://a1.twimg.com/profile_images/577657037/c4_devart_fixed_glow_red___shadow_normal.PNG
created_at: Fri, 20 Aug 2010 07:26:51 +0000
from_user: Shishev
to_user_id: 0
text: RT @sessan60jp ipadをリモートワイプ、リモートロックするためのmobile meのアプリ。これがあれば空き巣に盗まれても多少はまし。 / アップル - MobileMe - なくしたiPhoneiPadを探せ... http://bit.ly/csFlzS
id: 170829401
from_user_id: 20991760
to_user: null
geo: 
iso_language_code: ja
source: <a href="http://twitterfeed.com" rel="nofollow">twitterfeed</a>

NetBeans6.9.1用のプロジェクトをzip圧縮したものは、こちら。全ソースコードがあります。
TwitterClient.zip 直