2010/07/08

Twitter4J 2.1.2を改造してGoogle AppEngineからTwitPicにOAuthEchoで画像を投稿する。

Twitter4J2.1.2では(執筆時はまだ開発中)、画像の投稿機能が提供されます。現在実装されているのは、YFrogへの投稿機能(OAuthEcho, Basic認証)とTwitPicへの投稿機能(Basic認証のみ)です。
pos2witではTwitPicへのOAuthEchoによる投稿を行いたかったので、できるように改造してみました。

まず、Twitter4Jのサイトから、バージョン2.1.2をダウンロードして展開して下さい。この中に含まれる、twitter-core内のソースを改造しますので、このソースが改造できる環境を整えて下さい(僕はEclipse上にプロジェクトを作成し、ソースをコピーしました。)。環境を整えてコンパイルすると、twitter4j.internal.logging内のいくつかのソースでコンパイルエラーが起こります。参照しているクラスがないためで、ここは何も考えずにCommonsLogging, Log4J, SLF4J関連のソースを削除します(それぞれLoggerとLoggerFactoryがあるのでそれらを削除)。

次にgithubのtwitter4j.utilから、ImageUpload.javaをダウンロードし、twitter4j.util内にコピーします。このファイル内で、画像投稿機能が実装されています。
コピーするとコンパイルエラーが出るので、まずはこれを潰します。

twitter4j.http.OAuthAuthorizationに、次のメソッドを追加します(これはgit内のソースには実装されています)。

public List<HttpParameter> generateOAuthSignatureHttpParams (String method, String url) {
long timestamp = System.currentTimeMillis() / 1000;
long nonce = timestamp + RAND.nextInt();

List<HttpParameter> oauthHeaderParams = new ArrayList<HttpParameter>(5);
oauthHeaderParams.add(new HttpParameter("oauth_consumer_key", consumerKey));
oauthHeaderParams.add(OAUTH_SIGNATURE_METHOD);
oauthHeaderParams.add(new HttpParameter("oauth_timestamp", timestamp));
oauthHeaderParams.add(new HttpParameter("oauth_nonce", nonce));
oauthHeaderParams.add(new HttpParameter("oauth_version", "1.0"));
if (null != oauthToken) {
oauthHeaderParams.add(new HttpParameter("oauth_token", oauthToken.getToken()));
}

List<HttpParameter> signatureBaseParams = new ArrayList<HttpParameter> (oauthHeaderParams.size());
signatureBaseParams.addAll(oauthHeaderParams);
parseGetParameters (url, signatureBaseParams);

StringBuffer base = new StringBuffer (method).append("&")
.append(encode(constructRequestURL(url))).append("&");
base.append(encode (normalizeRequestParameters(signatureBaseParams)));

String oauthBaseString = base.toString();
String signature = generateSignature (oauthBaseString, oauthToken);

oauthHeaderParams.add (new HttpParameter("oauth_signature", signature));

return oauthHeaderParams;
}


次に、ImageUpload.java内にTwitPic用のOAuth Echo対応ImageUploadを実装します。
同ファイル内に、下記のソースをコピーします。下記ソースは、同ファイル内のYFrogOAuthUploaderをコピーしてTwitPic用に修正したものです。


// import部分
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import twitter4j.conf.Configuration;
import twitter4j.conf.ConfigurationContext;
import twitter4j.internal.org.json.JSONException;
import twitter4j.internal.org.json.JSONObject;

// TwitPicOAuthUploader
public static class TwitPicOAuthUploader extends ImageUpload {
private String apiKey;
private OAuthAuthorization auth;

// uses the secure upload URL, not the one specified in the YFrog FAQ
private static final String TWITPIC_UPLOAD_URL = "http://api.twitpic.com/2/upload.json";
private static final String TWITTER_VERIFY_CREDENTIALS = "https://api.twitter.com/1/account/verify_credentials.json";

public TwitPicOAuthUploader(String apiKey, OAuthAuthorization auth) {
this.apiKey = apiKey;
this.auth = auth;
}

public String upload(File image) throws TwitterException{
throw new UnsupportedOperationException(); // AppEngineではFileInputStreamが使えない。
}

public String upload(String message, String fileName, InputStream fileBody) throws TwitterException {
// step 1 - generate verification URL
// String signedVerifyCredentialsURL = generateSignedVerifyCredentialsURL();

// step 2 - generate HTTP parameters
HttpParameter[] params = {
new HttpParameter("key", apiKey)
, new HttpParameter("message", message, true)
, new HttpParameter("media", fileName, fileBody)
};

// step 3 - upload the file
Configuration config = ConfigurationContext.getInstance();
config.getRequestHeaders().put("X-Auth-Service-Provider", TWITTER_VERIFY_CREDENTIALS);
StringBuilder b = new StringBuilder();
b.append("OAuth realm=\"http://api.twitter.com/\"");
for(HttpParameter p : auth.generateOAuthSignatureHttpParams("GET", TWITTER_VERIFY_CREDENTIALS)){
if(p.getName().equals("oauth_signature")){
try {
b.append(", ").append(p.getName()).append("=\"").append(
URLEncoder.encode(p.getValue(), "ISO8859_1")
).append("\"");
} catch (UnsupportedEncodingException e) {
throw new TwitterException(e);
}
} else{
b.append(", ").append(p.getName()).append("=\"").append(p.getValue()).append("\"");
}
}
config.getRequestHeaders().put("X-Verify-Credentials-Authorization", b.toString());
System.out.println("X-Auth-Service-Provider: " + TWITTER_VERIFY_CREDENTIALS);
System.out.println("X-Verify-Credentials-Authorization: " + b.toString());
HttpClientWrapper client = new HttpClientWrapper(config);
HttpResponse httpResponse = client.post(TWITPIC_UPLOAD_URL, params);

// step 4 - check the response
int statusCode = httpResponse.getStatusCode();
if (statusCode != 200) {
throw new TwitterException("Twitpic image upload returned invalid status code", httpResponse);
}

try{
String response = httpResponse.asString();
return new JSONObject(response).getString("url");
} catch(JSONException e){
throw new TwitterException("Unknown Twitpic response", httpResponse);
}
}
}


上記のソースをペーストすると、twitter4j.internal.http.HttpParameterに必要なメソッドが無いためにコンパイルエラーが発生します。下記のフィールド及びメソッドを、HttpParameterクラスに追加して下さい。


// フィールドを追加
private boolean noUrlEncode = false;
private InputStream fileBody = null;
...

// コンストラクタを追加
public HttpParameter(String name, String value, boolean noUrlEncode) {
this.name = name;
this.value = value;
this.noUrlEncode = noUrlEncode;
}

public HttpParameter(String name, String fileName, InputStream fileBody) {
this.name = name;
this.file = new File(fileName);
this.fileBody = fileBody;
}
...

// メソッドを追加
public boolean isUrlEncodeUnnecessary(){
return this.urlEncodeUnnecessary;
}

public boolean hasFileBody(){
return null != fileBody;
}

public InputStream getFileBody(){
return fileBody;
}


これでコンパイルエラー自体は消えますが、追加されたパラメータに対応してリクエスト作成機能を修正する必要があります。リクエスト作成機能はtwitter4j.internal.http.HttpClientImpl.java内にあるので、下記のように改造します(赤字が改造部分)。

                                // 241行目あたり
if(param.isFile()){
write(out, boundary + "\r\n");
write(out, "Content-Disposition: form-data; name=\"" + param.getName() + "\"; filename=\"" + param.getFile().getName() + "\"\r\n");
write(out, "Content-Type: " + param.getContentType() + "\r\n\r\n");
BufferedInputStream in = new BufferedInputStream(
param.hasFileBody() ? param.getFileBody() : new FileInputStream(param.getFile())
);
int buff = 0;
...

// 258行目あたり
logger.debug(param.getValue());
if(param.isUrlEncodeUnnecessary()){
out.write(param.getValue().getBytes("UTF-8"));
} else{
out.write(encode(param.getValue()).getBytes("UTF-8"));
}

write(out, "\r\n");
...


この修正が必要なのは、
  • オリジナルのコードではFileInputStreamを使っているが、これはAppEngineでは使えない。
  • オリジナルのコードでは常にパラメータをURLEncodeしているが、メッセージをURLEncodeするとTwitPicで復元されない(URLEncodeされた状態で記録される)

という問題に対処するためです。

これでTwitPicに画像を投稿できるようになります。サンプルコードはこんな感じ。投稿にはTwitPicのAPIKEYが必要なので、ここからアプリケーションを登録し、APIKEYを入手して下さい。
 public void test() throws Exception{
String apiKey = TwitPicのAPI KEY;
Twitter t = new TwitterFactory().getOAuthAuthorizedInstance(new AccessToken(
Twitterユーザのaccess_token
, Twitterユーザのaccess_token_secret
));
OAuthAuthorization auth = (OAuthAuthorization)t.getAuthorization();
String message = "投稿するメッセージ。";
String url = new ImageUpload.TwitpicOAuthUploader(apiKey, auth).upload(
message, "duke.jpg", getClass().getResourceAsStream("duke-on-gae.jpg")
);
// twitterには、別途投稿する必要がある(uploadでは写真がTwitPicに投稿されるのみ)。
t.updateStatus(message + " " + url);
}


だいぶ汚い改造になってしまいましたが、とりあえず投稿することはできるようになりました。もう少し整理して、Twitter4Jにうまく取り込んでもらえるよう働きかける予定です。

AJDT2.1.0 Released

AJDT2.1.0がリリースされました。リリースノートに書かれている変更点をざくっと紹介します。主にITD(Inter Type Definition)関連の機能が強化されました。

  • ITD-Aware Search
    アスペクトによる定義の追加を考慮したJava Searchが正しく行われるようになりました(例: ITD定義、String Person.getName()に対して参照の検索を行うと、ITDされたメソッドを使用しているコードがヒットする)。

  • ITD rename refactoring
    名前変更のリファクタリング機能がITDに対応しました。アスペクトのITDでの名称変更(例: String Person.getName()をString Person.getFirstName()に)を行うと、影響を受ける定義(例: Personの派生クラスでのgetNameメソッド(オーバーライド))も名称変更されます。

  • ITD-aware renaming of getters and setters
    フィールドの名称変更を行う際に、ITDによって実装されていたそのフィールドのgetter及びsetterの名称変更も行えるようになりました。nameをfirstNameに変更した場合に、ITDで実装されたgetNameがgetFirstNameに、setNameがsetFirstNameに変更されませす。(但し、Personの派生クラスでgetNameをオーバーライドしていた場合、そのメソッドは変更されません(あくまでgetter/setterのみ)。個人的にはこれも変更されて欲しいところ。常に@Override付けといた方が良さそうですね。)

  • Pull-out refactoring
    フィールドやメソッドを、ITDとして特定のアスペクトに移動するリファクタリング。パッケージエクスプローラでフィールドやメソッドを選んで右クリック -> Refactory -> Pull Out ITD... と選択すると、既存のアスペクトに切り出せます。

  • Aspect-aware type renaming
    クラス名の変更を行う際に、それを参照しているアスペクト内での定義(ITD)も適切に変更されるようになりました。

  • @Test and Intertype declarations
    ITDによって@Testが付加されたメソッドを追加した場合、そのクラスをテストすると、@Testが付加されたメソッドもテストの対象になります。

  • AspectJ-aware PDE Build (Helios only)
    AspectJをPDEに統合したビルドが提供されるようになったそうです。AspectJに依存したプラグインを作る際に、Javaプラグインと同じウィザード等を使えるようになりました。詳しくはこちら

  • Modernize the build server
    ビルドサーバーをモダンなものに変えたそうです。詳しくはこちら

  • AspectJ 1.6.9
    最新のAspectJ 1.6.9がバンドルされています。readmeはこちら

  • Bug Fixes
    その他、いくつかのバグが修正されています。


最初ITDがサポートされるようになった頃は、IDE上はコンパイルエラーになっていましたが、バージョンを重ねるにつれて洗練され、遂にリファクタリングまで正常に動くようになりました。今回の機能強化で、ITDされたメソッド・フィールドも元からクラスに定義されていたものとほぼ同じように扱われるようになり、心おきなく使えるようになったのではないでしょうか。