GAE/jからAPIを通してはてなブックマークにPOSTする方法(暫定)

概要

現在、Diigoというブックマークサービスを起点として、様々なサービスにクロスポストするプログラムをGAE上に構築中です。

今回はその第二回目として、GAEからはてなブックマークへのAPIを使った投稿を行います。

主な内容は以下の通りです。

  1. はてなブックマークAPIからの読み出し
  2. はてなブックマークAPIを使った投稿

はじめに

前回、多少のトラブルに遭いつつもDiigoからブックマークの読み出しに成功した私は、勢いもそのままに、はてなブックマーク(以下はてブ)へのPOSTに取り掛かりました。(前回について詳しくは以下を参照)

GAE/jからWeb APIを通してDiigoのブックマークをGETする - MshrKatoの日記

初めにはてブを選んだのは、国内で主に使われるブックマークサービスがはてブである事、またはてブtwitterと機能連携している為、これを押さえることで同時にtwitterにも投稿できる事が理由です。

そういう訳で実装を始めてみたものの、GAEは情報が少なく、ただPOSTするだけでも一苦労でした。特に、文字コード周りで躓きました。この記事では特に、その辺りをまとめていきます。

はてなブックマークAPI読み出し

はてなブックマークAPI仕様に関しては、以下のページにまとめられています。
はてなブックマークAtomAPI - Hatena Developer Center

まずはブックマークの読み出しを行いましょう。
これは以下のURLにGETメソッドでリクエストする事で実現できます。

http://b.hatena.ne.jp/atom/feed

現在は非推奨なようですが、はてなではWSSE認証を行う事ができる為、今回はこれを使ってfeedを取得します。

これを実装した例が、以下のコードです。但し、WSSE認証には以下のページで紹介されているXWsseクラスを利用させて頂きました。
杉浦とホームページ製作〜Blojsom Hack Guide「Blojsom の外部API」

//定数
String USER = "<user>";
String PASS = "<password>";
int SUCCESS_CODE = 200;

//認証情報生成
XWsse xWsse = new XWsse(PASS);
StringBuilder xHeader = new StringBuilder();
xHeader.append("UsernameToken, ");
xHeader.append("Username=\"").append(USER).append("\",");
xHeader.append("PasswordDigest=\"").append(xWsse.getDigest()).append("\", ");
xHeader.append("Nonce=\"").append(xWsse.getNonceEncoded()).append("\", ");
xHeader.append("Created=\"").append(xWsse.getDate()).append("\"");

// ブックマーク取得
URL connectUrl = new URL("http://b.hatena.ne.jp/atom/feed");
HttpURLConnection connection =  (HttpURLConnection)connectUrl.openConnection();
connection.setRequestProperty("X-WSSE", WSEEHeader());

//接続して応答を得る
if (connection.getResponseCode() == SUCCESS_CODE) {
    // OK
    InputStream input = connection.getInputStream();
    BufferedReader reader = new BufferedReader(new InputStreamReader(input,"UTF-8"));
    String line;
    StringBuilder response = new StringBuilder();
    while ((line = reader.readLine()) != null) {
        // ...
        response.append(line);
    }
    reader.close();

    //processing

} else {
    // Server returned error code.
}

あくまで例なので、例外処理などは省きました。また、はブックマークを取得したいユーザ名に、は適切なパスワードに置き換えて下さい。

こうすると、レスポンスとしてatomフィードが得られます。公式な仕様はこちら*1で確認できますが、これで完全に理解できたら凄いと思います。Javaオブジェクトへの変換にはライブラリを利用する方が良いでしょう。

ただ、これを実装した時の私はAtomという物が全く分かっていなかった為、純粋なXML文書のつもりでJDOMを使って処理してしまいました。(JDOMはJava向けに提供されているXML文書を扱う為のライブラリで、当ブログでは過去にYahoo形態要素解析APIからの返答を処理する時に利用しました*2。)

その為以下の処理は非推奨ですが、一応JDOMを使った切り出しの例を示しておきます。

//XMLを分解して読み出す
Document doc = new SAXBuilder().build(new StringReader(response.toString()));
Element root = doc.getRootElement();
Namespace xmls = Namespace.getNamespace("http://purl.org/atom/ns#");
Namespace xmlsTags = Namespace.getNamespace("dc", "http://purl.org/dc/elements/1.1/");

List<Element> entrys = root.getChildren("entry", xmls);
Pattern p = null;
Matcher m = null;
for(Element entry : entrys){
    //URL
    String url = entry.getChild("link", xmls).getAttributeValue("href");

    //title
    String title = entry.getChildText("title", xmls);

    //annotationとコメント
    String comment = entry.getChildText("summary", xmls);
    ArrayList<String> annotationsList = new ArrayList<String>();
    p = Pattern.compile("\"(.+?)\"");
    m = p.matcher(comment);
    while(m.find()){
        annotationsList.add(m.group(1));
        comment = comment.replace(m.group(), "");
    }

    //タグ
    List<Element> tagsElements = entry.getChildren("subject", xmlsTags);
    ArrayList<String> tagsList = new ArrayList<String>();
    for(Element tag : tagsElements){
        tagsList.add(tag.getText());
    }

    String[] annotations = (String[])annotationsList.toArray(new String[0]);
    String[] tags = (String[])tagsList.toArray(new String[0]);

    //url, title, comment, annotations, tagsを処理
}

はてなブックマークへのAPIからのPOST

次にはてなブックマークへの投稿を行いましょう。
これは以下のURLにPOSTメソッドでリクエストする事で実現できます。

http://b.hatena.ne.jp/atom/post

この時POSTリクエストのボディ部分には、以下のAtomライクなXMLを渡します。

<entry xmlns="http://purl.org/atom/ns#">
  <title>dummy</title>
  <link rel="related" type="text/html" href="http://www.example.com/" />
  <summary type="text/plain">サンプルコメントです</summary>
</entry>

さてここで、summary要素に日本語が出てくる可能性が有ります。これが、中々の曲者です。

Javaのローカルアプリでは文字コードとしてUTF-8が使われる為、何も考えずとも問題無かったのですが、どうやらGAEではShift-JISが使われている様で(デバッガで動作中の文字コードを覗いて確認しました)、文字コードの変換が必要となります。

この時普通は

String utfStr = new String("utf-8にしたい文字列".getBytes("UTF-8"));

とする訳ですが、こうして作ったString型の変数を公式ドキュメント*3のPOSTメソッドで使われているOutputStreamWriterに渡してもうまく行かない事があります。(はてなから400BadRequestを返されます)

"事がある"という事はつまり、日本語が入っていてもうまく行く場合もあるという事です。

今回確認した現象としては、カタカナの「テ」もしくは「ト」が入る文字列の場合、BadRequestとなってしまいました。文字コード表などを見ても、この原因についてはさっぱり分からない状態です。

諦めようかとも思ったのですが、試行錯誤した結果、OutputStreamWriterの変わりにOutputStreamを使い、UTF-8に変換したバイト配列を直接渡す事で投稿ができる事が分かりました。

これらを実装した例が、以下のコードです。

//定数
int POST_SUCCESS_CODE = 201;
String bookMarkURL = "http://www.example.com/";
String[] tags = {"tag1", "tag2"};
String comment = "サンプルコメントです";

//接続
URL url = new URL("http://b.hatena.ne.jp/atom/post");
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("X-WSSE", WSEEHeader());

//ボディ部分を作成
StringBuilder postText = new StringBuilder();
postText.append("<entry xmlns=\"http://purl.org/atom/ns#\">");
postText.append("<title>dummy</title>");
postText.append("<link rel=\"related\" type=\"text/html\" href=\"").append(bookMarkURL).append("\" />");
postText.append("<summary type=\"text/plain\">");
for(String tag : this.tags){
    postText.append("[").append(tag).append("]");
}
postText.append(comment);
postText.append("</summary>");
postText.append("</entry>");

//Send request
OutputStream  wr = connection.getOutputStream();
wr.write(postText.toString().getBytes("UTF-8"););
wr.close ();

if(connection.getResponseCode() == POST_SUCCESS_CODE){
    //OK
}

まとめ

今回、GAE/j上からはてなブックマークAPIを通して、ブックマークの読み出しとPOSTを行いました。

この時、文字コードの処理に問題があるらしく、UTF-8への明示的な変換と、OutputStreamの使用が必要となった事がポイントでした。

次回はtumblrへの投稿に挑戦していきますが、その前に、今回裏でチャレンジしていた、はてなAPIOAuth認証についてまとめたいと思います。