Hatena::ブログ(Diary)

明日の鍵

2009-10-18

はてなブックマークAtomAPI

先日の予告どおり*1

はてなブックマークアプリにてAtomAPIの通信をしている部分のクラスを公開します。

必要なライブラリ

AndroidSDKだけでは足りないので、以下のライブラリをビルドパスにつっこみます。

バージョンは現時点で最新の1.4を使用します。

ソース

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.TimeZone;

import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpException;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.protocol.HTTP;

public class HatenaAPI {
  public static final String URI_HATENA_B_ROOT_ATOM_END_POINT = "http://b.hatena.ne.jp/atom";
  public static final String URI_HATENA_B_POST = "http://b.hatena.ne.jp/atom/post";
  public static final String URI_HATENA_B_FEED = "http://b.hatena.ne.jp/atom/feed";

  private static final String XML_HATENA_B_POST = "<entry xmlns=\"http://purl.org/atom/ns#\"><title>dummy</title><link rel=\"related\" type=\"text/html\" href=\"%s\" /><summary type=\"text/plain\">%s</summary></entry>\n";
  private static final String XML_HATENA_B_EDIT = "<entry xmlns=\"http://purl.org/atom/ns#\"><title>%s</title><summary type=\"text/plain\">%s</summary></entry>";

  private static final String CONTENT_TYPE_APPLICATION_ATOM = "application/x.atom+xml";
  private static final String USER_AGENT_JAVA = "Sun Java/5.0";

  private HttpClient mClient;
  public String mUser;
  public String mPassword;

  public HatenaAPI() {
    mClient = new DefaultHttpClient();
    mClient.getParams().setParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE, false);
  }

  public HatenaAPI(String user, String password) {
    this();
    mUser = user;
    mPassword = password;
  }

  public boolean isAvailableUser() throws NoSuchAlgorithmException, HttpException, IOException {
    HttpUriRequest request = new HttpGet(URI_HATENA_B_ROOT_ATOM_END_POINT);
    request.addHeader("X-WSSE", getWSSEHeaderValue(mUser, mPassword));
    return mClient.execute(request).getStatusLine().getStatusCode() == HttpStatus.SC_OK;
  }

  public int postBookmark(String url, String comment) throws NoSuchAlgorithmException, HttpException, IOException {
    HttpPost post = new HttpPost(URI_HATENA_B_POST);
    post.setHeader(HTTP.CONTENT_TYPE, CONTENT_TYPE_APPLICATION_ATOM);
    post.addHeader("X-WSSE", getWSSEHeaderValue(mUser, mPassword));
    post.addHeader(HTTP.USER_AGENT, USER_AGENT_JAVA);
    StringEntity entity = new StringEntity(String.format(XML_HATENA_B_POST, url, comment), HTTP.UTF_8);
    entity.setContentType(CONTENT_TYPE_APPLICATION_ATOM);
    post.setEntity(entity);
    return mClient.execute(post).getStatusLine().getStatusCode();
  }

  public int editBookmark(String editUrl, String title, String comment) throws NoSuchAlgorithmException, ClientProtocolException, IOException {
    HttpPut put = new HttpPut(editUrl);
    put.setHeader(HTTP.CONTENT_TYPE, CONTENT_TYPE_APPLICATION_ATOM);
    put.addHeader("X-WSSE", getWSSEHeaderValue(mUser, mPassword));
    put.addHeader(HTTP.USER_AGENT, USER_AGENT_JAVA);
    StringEntity entity = new StringEntity(String.format(XML_HATENA_B_EDIT, title, comment), HTTP.UTF_8);
    entity.setContentType(CONTENT_TYPE_APPLICATION_ATOM);
    entity.setChunked(false);
    put.setEntity(entity);
    return mClient.execute(put).getStatusLine().getStatusCode();
  }

  public int deleteBookmark(String editUrl) throws NoSuchAlgorithmException, ClientProtocolException, IOException {
    HttpDelete delete = new HttpDelete(editUrl);
    delete.setHeader(HTTP.CONTENT_TYPE, CONTENT_TYPE_APPLICATION_ATOM);
    delete.addHeader("X-WSSE", getWSSEHeaderValue(mUser, mPassword));
    return mClient.execute(delete).getStatusLine().getStatusCode();
  }

  public String getBookmarks(int of) throws NoSuchAlgorithmException, ClientProtocolException, IOException {
    HttpGet get = new HttpGet(URI_HATENA_B_FEED + "?of=" + of);
    get.setHeader(HTTP.CONTENT_TYPE, CONTENT_TYPE_APPLICATION_ATOM);
    get.addHeader("X-WSSE", getWSSEHeaderValue(mUser, mPassword));
    HttpResponse response = mClient.execute(get);

    if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
      return null;
    }

    BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()), 256 * 1024);
    String line = null;
    StringBuffer buffer = new StringBuffer();
    do {
      line = reader.readLine();
      if (line != null) {
        buffer.append(line).append("\n");
      }
    } while (line != null);

    return buffer.toString();
  }

  protected final String getWSSEHeaderValue(String user, String password) throws NoSuchAlgorithmException, UnsupportedEncodingException {
    byte[] nonceB = new byte[8];
    SecureRandom.getInstance("SHA1PRNG").nextBytes(nonceB);
    SimpleDateFormat zulu = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
    zulu.setTimeZone(TimeZone.getTimeZone("GMT"));
    Calendar now = Calendar.getInstance();
    now.setTimeInMillis(System.currentTimeMillis());
    String created = zulu.format(now.getTime());
    byte[] createdB = created.getBytes("utf-8");
    byte[] passwordB = password.getBytes("utf-8");

    byte[] v = new byte[nonceB.length + createdB.length + passwordB.length];
    System.arraycopy(nonceB, 0, v, 0, nonceB.length);
    System.arraycopy(createdB, 0, v, nonceB.length, createdB.length);
    System.arraycopy(passwordB, 0, v, nonceB.length + createdB.length, passwordB.length);

    MessageDigest md = MessageDigest.getInstance("SHA1");
    md.update(v);
    byte[] digest = md.digest();

    StringBuffer buf = new StringBuffer();
    buf.append("UsernameToken Username=\"");
    buf.append(user);
    buf.append("\", PasswordDigest=\"");
    buf.append(new String(Base64.encodeBase64(digest)));
    buf.append("\", Nonce=\"");
    buf.append(new String(Base64.encodeBase64(nonceB)));
    buf.append("\", Created=\"");
    buf.append(created);
    buf.append('"');
    return buf.toString();
  }
}

使い方

読んで、感じ取ってください。

質問あれば、コメントにて

参考にしたサイト

yohei-y:weblog: Java からはてなフォトライフAtomAPIを使う

JavaではてなブックマークAtomAPIでpostするサンプル - wildcatsの日記

2009-10-14

はてなブックマークのコメントの仕様

意外と

知らなかった。

最大のタグの個数 -> 10個
最大のコメントの長さ -> 200Byte
タグはコメントの長さに含まれない

POSTもしくはPUTする前にチェックしてあげると、親切

2009-10-13

はてなブックマークにPOSTできない

もう解決したけど

一応メモ

マルチバイトでPOSTしたければ、Entityを作るときに文字コードを設定してあげないと400(BadRequest)が返ってくる

あとで個別に設定するメソッドもあるけど、そっちじゃダメ

一緒に設定してあげることが大切

ソース

StringEntity entity = new StringEntity(String.format(XML_HATENA_B_POST, url, comment),HTTP.UTF_8);

なんだか

この手のエラー改修が早くなってきているようなw

2009-10-12

はてなブックマークにPUTできた

chunkedとは

HTTP/1.1から導入された転送符号化のこと

Keep-Aliveを実現するために内容の長さを知りたいのだけど、Content-Lengthが設定されていない場合*1、長さが分からないから、chunkedを使うみたいですね。

ここが分かりやすかったです。

なぜはてなブックマークAPIのPUTで必要だったのか

分かりません。

できたんだからいいやって感じです。

追記

間違えました。

Entity::setChunked(:boolean)にfalseを設定して成功したんだから、はてなのサーバがchunkedに対応していなかったってことですね。

*1CGIなど動的なコンテンツ

2009-10-11

はてなブックマークにPUTできた

さっきの今だけど*1PUTできた!

Entity::setChunked(:boolean)っていうメソッドがあって、これにfalseを設定したら見事ステータスコード200が!

でもchunkedってなんだー?

調べている時間ないからまた後日調べよう!

あぁ、それにしても嬉しい

追記

調べました。

http://d.hatena.ne.jp/tomorrowkey/20091012/1255331578

はてなブックマークにPUTできない

編集機能を持たせようとして、実装しているんだけど、なぜか日本語が入っているとPUTできない。

ステータスコード400(Bad Request)が返ってくる

apache-httpライブラリを使っているんだけど、Commons-httpライブラリならPUTできることから文字コードandroidの環境の類ではなさそう

POSTできなかったときもこういう状態だったので、また同じような設定があるんだろうなぁと想像中*2

順調だったのに、ここにきて足踏み

いまからSOUL REBELっていうレゲエイベント行ってきます