2010年11月3日水曜日

GoogleAppEngineのヘッダサイズ

appengineでmixiのsimplepostのリダイレクトを作成しようとすると、

「java.lang.ArrayIndexOutOfBoundsException: 4096」

が発生してしまいました。
リダイレクト時のヘッダーが長すぎるようです。

mixiのsimplepostの日記は本文が全角1万文字まで
リダイレクトが可能なのですが、
どうもjettyの制限(現在は設定可能)やバグの部分が関係しているようです。

http://jira.codehaus.org/browse/JETTY-1093
http://jira.codehaus.org/browse/JETTY-1112

http://groups.google.co.jp/group/google-appengine-java/browse_thread/thread/488d57c223b8173c?pli=1

http://code.google.com/p/googleappengine/issues/detail?id=2413


設定を追加する可能性は低いとみて、
直接リダイレクトせずにクライアントからJavaScriptでリダイレクトをかけようと思います。

2010年10月3日日曜日

mixi Graph APIの認証

http://developer.mixi.co.jp/connect/mixi_graph_api/api_auth

にある認可方法のJava実装の説明です。


まず、使用するには開発者登録を行い
https://sap.mixi.jp/
で新規サービス追加を行っておく必要があります。

まずURLを作って以下にリダイレクトします。

String endPoint = "https://mixi.jp/connect_authorize.pl";

String url = endPoint + "?response_type=code";

String key = "キー";
String scope = "r_profile";
String display = "pc";

url += "&client_id=" + key;
url += "&scope=" + scope;
url += "&display=" + display;

return redirect(url);

※redirect関数はslim3の関数です。
 このURLにリダイレクトすればOKです。

するとmixiの認証画面に飛びます。
許可がおりるとサービスを作成した際のURLに遷移します。
キーにはサービス作成後に生成される「Consumer Key」を指定しておきます。
r_profileの指定は使用するサービスによって変更します。

認可手順のところに記述してありますね。




//コードを取得
String token = requestScope("code");
String endPoint = "https://secure.mixi-platform.com/2/token";
String args = "";

String key = "サービスのKEYを指定";
String scret = "サービスのSECRETを指定";
String uri = "http://mixi.latest.secondarykey.appspot.com/callback";

args += "grant_type=authorization_code";
args += "&client_id=" + key;
args += "&client_secret=" + scret;
args += "&code=" + token;
args += "&redirect_uri=" + uri;

String json = post(endPoint,args);
Map jsonMap = createJsonMap(json);

String email = "メールアドレス";
String url = "http://api.mixi-platform.com/2/search/people?q=" + email;
//String url = "http://api.mixi-platform.com/2/people/@me/@self";

get(url,jsonMap.get("access_token"));

return forward("oauth.jsp");


飛んできたURLの引数で「code」がきますので
それを元にURLを作成します。
「redirect_uri」は、、、、何設定するのかな?多分合ってない気がする。。。
それをPOSTで投げます。
post()はオリジナルでHttpURLConnectionを利用して投げています。

その戻り値はjsonで戻ってきます。

{"refresh_token":"a4a634845360522a85cc030e043cb869d2536aa",
"expires_in":900,
"access_token":"8b17d199e43c879c1a015019cf55d9ecd0e2503",
"scope":"r_profile"}

こういう感じのjsonです。

この「access_token」を利用して、利用したいサービスにサクセスします。
ここではPeople lookup APIにアクセスして、
マイミクの情報を取得しています。

get()ではHttpURLConnectionに対して

connection.setRequestProperty("Authorization", "OAuth " + oauth);


と認証情報にアクセストークンを指定する必要があります。

このブログを記述している時点では
メールアドレスによる検索は認可されたユーザの
マイミクでないと検索できないようで、404を返してきていました。
テスト時に自分のメールアドレスを検索してて
「なんで404なんじゃー!」って思っていましたが、仕様のようです。

2010年4月24日土曜日

ExCellaで帳票

最近ExCellaを触ってます。

実際はGoogleAppEngineでサービスを目標にしていて、
一部ExCella自体をいじりながらの開発です。
一応Excelが出力できたのでブログを起こしておきましょう。

やっぱりGAEでネックになるのは
ファイルシステムとの問題です。

まず、core側にあるExporterのTextExporterとWorkbookExporterを
削除しました。
単純にいうとまずはスモールスタートの為に
ExcelOutputStreamExporterのみをサポートする事にしました。

本来はXLS周りを使用してみたかったのですが
POI自体(ExCella内部で使っている)が、テンプレートの読み込み途中で
xmlファイルを保存している感じがあって、落ちてしまいます。

なので一旦はExcelに集中。


ExCella自体がファイルへの出力重視的な実装になっていて
親クラスで「出力ファイル名」等を持つことが多いみたいなので
ひとまずそのあたりを削除。
エラーになるところを潰していくような作業になりました。


・・・実際は根本の部分でOutputStreamの設定する仕組みに変えるべきだと
。。。ブログ書いてる途中に思いまして、ちょっと実装しなおそうかな?
って思っています。

現状だと


ByteArrayInputStream inStream = new ByteArrayInputStream(template.getBytes());

ReportBook outputBook = new ReportBook(inStream,ExcelOutputStreamExporter.FORMAT_TYPE);

ReportSheet outputSheet = new ReportSheet("テンプレート","請求書");

outputBook.addReportSheet(outputSheet);

ByteArrayOutputStream stream = new ByteArrayOutputStream();
ReportProcessor reportProcessor = new ReportProcessor();
stream = reportProcessor.process(outputBook);

response.setHeader("Content-Disposition","attachment; filename=excel.xls");
response.setContentType("application/msexcel");
response.setContentLength(stream.toByteArray().length);

OutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(stream.toByteArray());
out.close();


こんな感じで動作します。

GAEでの動作なのでinputStreamは
テンプレート用のファイルをDataStore上に溜め込んでいます。

コードネームはdon-gabachoです。
あー。。。もういっかい書き直して
全部書き留めないとなぁ。。。

※2010/4/25 でリファクタリング完成
http://code.google.com/p/don-gabacho/wiki/ExCella?ts=1272171109&updated=ExCella
streamをBookから引き渡すように変更しています。

2010年3月26日金曜日

カーソルの変更

[Embed(source="Assets.swf", symbol="mx.skins.cursor.hogehoge")]

private var _hogehogeClass:Class;

こういう感じみたいですけど、
Assets.swfにあるイメージの一覧が欲しい。

2010年3月14日日曜日

FlashLiteで引数を埋め込む(Java)

先日友人がFlashLiteでの引数埋込に苦戦してた。
携帯で個体識別番号などの引数を元にFlashを書き換えたいらしい。

ここに方式はあるんだけど、、、って事でJava化してみた。




public static String inCode = "utf-8";
public static String outCode = "shift-jis";
/**
* FlashLite への引数の埋込
* @param oldFile
* @param argMap
* @return
*/
public static byte[] createArgEmbedSwf(byte[] oldFile, Map<String, String> argMap) {

ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
//rectbitの3ビット右シフト
byte rectbit = (byte)((int)oldFile[8] / 8);

//$headlen = ceil(((( 8 - (($rb*4+5)&7) )&7)+ $rb*4 + 5 )/8) + 12 + 5;
double dblHead = ((( 8 - ((rectbit*4+5)&7) )&7)+ rectbit*4 + 5 ) / 8;
//ヘッダ長を取得
int headLen = (int)Math.ceil(dblHead) + 12 + 5;

//引数部分の作成
byte[] doActionTag = createArg(argMap);

//$newsize = $oldsize+strlen($doactiontag);
int oldSize = oldFile.length;
//新しいサイズを取得
int newSize = oldSize + doActionTag.length;

//新しいヘッダを作成
byte[] newHeader = createHeader(oldFile,headLen,newSize);
//ヘッダ後のデータを取得
byte[] tail = createTail(oldFile,headLen);
try {
//タグ部分の書き込み
byteStream.write(newHeader);
byteStream.write(doActionTag);
byteStream.write(tail);
} catch (IOException e) {
e.printStackTrace();
}
return byteStream.toByteArray();
}
/**
* 新しいヘッダーの作成
* @param buf
* @param headLen
* @param newSize
* @return
*/
private static byte[] createHeader(byte[] buf, int headLen, int newSize) {

//ヘッダの位置を取得
//$head = $headtmp.fread($fr,$headlen-9);
//$newhead = substr($head,0,4).h32($newsize).substr($head,8);

byte[] newHeader = new byte[headLen];
//4バイトでint値をバイトに変換
byte[] newHeadSize = changeBytes(newSize,4);
//長さを変更
for ( int cnt = 0; cnt < headLen; ++cnt ) {
if ( cnt >= 4 && cnt < 8 ) {
newHeader[cnt] = newHeadSize[cnt-4];
} else {
newHeader[cnt] = buf[cnt];
}
}
return newHeader;
}
//3f 03 18 00 00 00 96 08
//00 00 6d 79 6e 61 6d 65
//00 96 08 00 00 74 61 77
//74 61 77 00 1d 00

//3f 03 -- タグはじまり, Type=12(DoActionタグ)
//18 00 00 00 -- タグ長さ 0x18 = 24 byte(ここからタグ終わりまでの長さ)

//96 -- ActionPush
//08 00 -- Pushするものの長さ, 8 byte
//00 -- Pushするものは文字列
//6d 79 6e 61 6d 65 00 -- "myname" (ord("m")=109=0x6d など。00は文字列終端を意味)

//96 -- ActionPush
//08 00 -- Pushするものの長さ
//00  -- Pushするものは文字列
//74 61 77 74 61 77 00 -- "tawtaw"

//1d -- ActionSetVariable
//00 -- タグ終わり
/**
* タグ用のバイトを作成
* @param argMap
* @return
*/
private static byte[] createArg(Map<String, String> argMap) {

List<Byte> byteList = new ArrayList<Byte>();

//タグの始まりを設定
//$tag = "\x3f\x03";
byteList.add((byte)0x3f);
byteList.add((byte)0x03);

//タグの長さを取得
//$taglen = calctaglen($dataarray);
int tagLen = calcArgLen(argMap);

//$tag .= h32($taglen);
add(changeBytes(tagLen,4),byteList);

//foreach($dataarray as $key => $value)
Iterator<Entry<String,String>> itr = argMap.entrySet().iterator();
//キー数回繰り返す
while ( itr.hasNext() ) {
Entry<String,String> entry = itr.next();

//$tag .= "\x96".h16(strlen($key)+2)."\x00".$key."\x00";
addString(entry.getKey(),byteList);
//$tag .= "\x96".h16(strlen($value)+2)."\x00".$value."\x00";
addString(entry.getValue(),byteList);

//$tag .= "\x1d";
byteList.add((byte)0x1d);
}
//終端のバイトを設定
//$tag .= "\x00";
byteList.add((byte)0x00);
return change(byteList);
}

/**
* 引数の長さを取得
* @param argMap 引数用のマップ
* @return
*/
private static int calcArgLen(Map<String, String> argMap) {
int rtn = 0;
Iterator<Entry<String,String>> itr = argMap.entrySet().iterator();
//キー数回繰り返す
while ( itr.hasNext() ) {
Entry<String,String> entry = itr.next();
String key;
String value;
try {
key = new String(entry.getKey().getBytes(inCode),outCode);
value = new String(entry.getValue().getBytes(inCode),outCode);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("文字列コード変換失敗",e);
}
//それぞれのバイト数とタグ用の11バイトを追加
try {
rtn += (key.getBytes(outCode).length + value.getBytes(outCode).length + 11);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("文字列コード変換失敗",e);
}
}
//終端分を追加
return rtn + 1;
}

/**
* ヘッダの後ろを取得
* @param buf
* @param oldSize
* @param headLen
* @return
*/
private static byte[] createTail(byte[] buf,int headLen) {

int oldSize = buf.length - headLen;
byte[] tail = new byte[oldSize];
int idx = 0;
//$tail = fread($fr, $oldsize-$headlen);
for ( int cnt = headLen; cnt < buf.length; ++cnt ) {
tail[idx] = buf[cnt];
++idx;
}
return tail;
}


/**
* リストから配列に変更
* @param byteList
* @return
*/
private static byte[] change(List<Byte> byteList) {
byte[] rtnByte = new byte[byteList.size()];
int idx = 0;
for ( Byte b : byteList ) {
rtnByte[idx] = b;
++idx;
}
return rtnByte;
}

/**
* バイト配列への変更
* @param i
* @param leng
* @return
*/
private static byte[] changeBytes( int i ,int leng){
byte[] b = new byte[leng] ;
if ( leng > 4) {
b[3] = (byte)((i >>> 24 ) & 0xFF);
}
if ( leng > 3) {
b[2] = (byte)((i >>> 16 ) & 0xFF);
}
if ( leng > 2) {
b[1] = (byte)((i >>> 8 ) & 0xFF);
}
if ( leng > 1) {
b[0] = (byte)((i >>> 0 ) & 0xFF);
}
return b;
}

/**
* リストへの追加
*/
private static void add(byte[] byteArray,List<Byte> byteList) {
for ( byte b : byteArray ) {
byteList.add(b);
}
}

/**
* 文字列の追加
* @param key
* @param byteList
*/
private static void addString(String key, List<Byte> byteList) {

String tmp = null;
try {
tmp = new String(key.getBytes(inCode),outCode);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("文字列変換失敗",e);
}

//$tag .= "\x96".h16(strlen($key)+2)."\x00".$key."\x00";
byteList.add((byte)0x96);
//文字列数を設定
byte[] lengByte;
try {
lengByte = changeBytes(tmp.getBytes(outCode).length + 2,2);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("文字列変換失敗",e);
}
add(lengByte,byteList);

byteList.add((byte)0x00);
//文字列を設定
try {
add(tmp.getBytes(outCode),byteList);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("文字列変換失敗",e);
}
byteList.add((byte)0x00);
}


byteのつなげ方とかが独自で、少ない時間での実装なので何か良い方法があるかも。
byte[]での変換に終わってるのは、GAE上での動作確認をしたかったからです。

blob型で保存して動的に変更させる事に成功しました。






byte[] newFile = FlashUtil.createArgEmbedSwf(targetFile.getBytes() , argMap);

try {
response.setContentType("application/x-shockwave-flash");
OutputStream out =
new BufferedOutputStream(response.getOutputStream());
try {
out.write(newFile);
} finally {
out.flush();
out.close();
}
} catch (IOException e) {
ThrowableUtil.wrapAndThrow(e);
}



こんな感じかな?

日本語文字化けしちゃうけど、、、、まぁ方式がわかれば解決するでしょう。

2010年3月2日火曜日

Slim3でクラウド開発を始める。その4


Slim3の開発はこんなブログより正式ドキュメントが有効です。
Slim3サイト
非公式と言われていますが充実した日本語サイトもあります。
Slim3日本語サイト




第二弾をやってから第一弾をやらないと、
アノテーションのところがプロジェクト名に依存しているので
大変な事になりそう(>_<)すみません。


今回はGoogleAppEngineへのデプロイです。

GooglePluginを入れると左上に以下のアイコンが出ます。
それをポチッとな。


するとデプロイの画面がでますので、
プロジェクトを選択して、管理者のメールアドレスとパスワードを入力します。




画像では切れていますが、SDKの設定があるのでそこをクリック!



出てきた画面にGoogleAppEngineにアプリケーションを登録した際の
IDを入力します。

これで設定は完了です。

デプロイしたらURLにアクセスしてください。
おそらくアプリケーションが表示されている。。。。はずです。

2010年2月27日土曜日

Slim3でクラウド開発を始める。その3


Slim3の開発はこんなブログより正式ドキュメントが有効です。
Slim3サイト
非公式と言われていますが充実した日本語サイトもあります。
Slim3日本語サイト




第一弾第二弾

そういえば少し前にGAEがDataNucleusを使ってるので
それを使って、HSQLDBにつなげた事もありました(>_<)


で本題です。
Slim3は同時にテストも出力してくれます。
なのでその設定をします。

まずプロジェクトを選択して、設定を行います。
その後、Javaのコードスタイル、インポートの編成を選択して
プロジェクト特有の設定を行います。



(java.lang.Math.*)に必要なインポート数を「1」に設定します。



終わったら画面を閉じて「ウィンドウ」から「設定」を選択します。


Javaのエディタにコンテンツ・アシストがあります。
そこにお気に入りがあるので選択して


以下を追加します。

org.hamcrest.CoreMatchers
org.junit.Assert
org.junit.matchers.JUnitMatchers


で「一般」を選択すると「ワークスペース」があります。


そこに「自動的にリフレッシュ」がありますんで


選択してください。

2010年2月22日月曜日

Slim3でクラウド開発を始める。その2


Slim3の開発はこんなブログより正式ドキュメントが有効です。
Slim3サイト
非公式と言われていますが充実した日本語サイトもあります。
Slim3日本語サイト





第一弾

さぁ第二弾です。
いよいよSlim3 RC1が近づいて参りました。
今回はsllim3-blankの名称を変更したい時の話をします。


まずプロジェクトで右クリックします。
その後リファクタを選んで、名前変更を選択します。
もしくはプロジェクトを選択して、F2でもOK。

出てきたボックスに対して、
自分の好きな名前(任意)を書きます。

するとプロジェクト名が変わります。
その後、build.xmlを選択して、
nameを変更します。

こんな感じですかね?

第三弾に続く。

FlexでGoogleAnalytics

Flexでサービスを作成していましたが、
HTMLの呼び出ししかアクセス解析ができてなくて
ViewStack化しているような位置のアクセス解析ができてなかったので
アクセスログを取りたいなぁ。。。って思ってたら
「gaforflash」たるものを見つけました。


ダウンロードしたファイルのなかに[lib/analytics.swc]がありますので
ライブラリに追加してあげます。

Flashで使う場合はもう一個の「lib/analytics_flash.swc」を
使うみたいですね。



private static var tracker:AnalyticsTracker = null;
public static function initTracker(object:DisplayObject):void {
if (tracker == null) {
tracker = new GATracker(object, "UA-?????-??", "AS3");
}
}
public static function viewPage(pageUrl:String):void {
if ( tracker == null ) return;
tracker.trackPageview(pageUrl);
}



巷に落ちてるサンプルだと、常にnewしているようでしたが、
なんか勿体なかったので、Singletonパターンで作成しておいて
viewPage()でURLを設定する形を行いました。

コンストラクタの第四引数に「true」を設定してあげたら
デバッグモードで動作して、アクセスを確認できるみたいです。

2010年2月21日日曜日

Slim3でクラウド開発を始める


Slim3の開発はこんなブログより正式ドキュメントが有効です。
Slim3サイト
非公式と言われていますが充実した日本語サイトもあります。
Slim3日本語サイト



Slim3に飛びついたのは、GoogleAppEngineで
ファイルのアップロードができるし、Strutsだったから
飛びついたのを覚えています。
現在はStrutsには対応していませんが
MVCが整った軽量なフレームワークです。

使用するにはeclipseとGoogle Plugin for eclipseが必要です。

「http://slim3.googlecode.com/svn」
[slim3-blank]をチェックアウトします。
※Downloadからblankをダウンロードしてインポートしても
  開発は開始できます。
現在SDKは1.3.1のようですね。

作成されたプロジェクトで右クリックを押して、
コンパイラのアノテーションのファクトリーパスを選択します。
右の画像の画面を出してjarを選択します。


まだアーリーアクセス(EA)なんですね(>_<)
完成度結構高いんですけどね。


んでプロジェクトで「war/WEB-INF/web.xml」を
開きます。
右の部分が自分の名前空間になりますから
何か適当な名前をつけましょう!

その後はプロジェクト直下にある
build.xmlを選択して右クリックで
実行を押して、下のantを選択します。





すると右の画面が出ます。

gen-controllerはコントロールを自動作成
gen-modelはモデルを自動作成してくれます。

gen-controllerで出てくるボックスに
URLを指定してあげるとその指定したURLを
操作するControllerとJSP、プラステストコードを出力してくれます。
modelはそのモデルクラスとテストコードを出力してくれます。


2010年2月13日土曜日

mixiにOAuthなRESTfulアクセス(Java)

最近サービスづいてきています。OAuthばっかりです。
今度はJavaでmixiにアクセスです!

まず、、、RESTful APIでアクセスするには
まずmixiアプリの登録が必要です。

http://developer.mixi.co.jp/appli/pc/pc_prepare/add_app_flow

アプリケーションを登録できたら、
アプリケーションに対してConsumer KeyとConsumer Secretが発行されます。

これを元にOAuthアクセスを行います。

まずHMAC-SHA1と呼ばれる暗号化を行う為のインスタンスを生成します。


byte[] secretyKeyBytes = secret.getBytes("UTF-8");
secretKeySpec = new SecretKeySpec(secretyKeyBytes,hmac);
mac = Mac.getInstance(hmac);
mac.init(secretKeySpec);


ここでのsecretはConsumer Secretに「&」を付与したものになります。
secretKeySpecはjavax.crypto.spec.SecretKeySpecになります。
macはjavax.crypto.Macですね。

URLに設定する引数を作成します。


params.put("oauth_consumer_key", KEY);
params.put("oauth_signature_method", "HMAC-SHA1");
params.put("oauth_timestamp", String.valueOf(cal.getTimeInMillis()).substring(0,10));
params.put("oauth_version", "1.0");
params.put("oauth_nonce", nonce);
params.put("xoauth_requestor_id", viewerId);
params.put("format", "atom");



このparamsはURLを作成する為のオリジナルクラスのマップです。

oauth_consumer_keyに設定しているKEYはConsumer Keyですね。
oauth_signature_methodは暗号化を示す"HMAC-SHA1"を固定で指定します。
oauth_timestampは時刻を設定します。
oauth_versionはmixiに指定された文字列
oauth_nonceはリクエスト毎に違うランダムな文字列です。UUID.randomUUID().toString()などで生成します。
xoauth_requestor_idは対象のユーザIDです。
formatはatomを設定していますが、最終的に取得するデータの形式でjsonでも取得できます。


次にシグネチャを取得する為の文字列を取得します。


String method = "GET";
String http ="http://" + endpoint + requestURI ;
String args = canonicalQS;
String toSign;
try {
toSign = URLEncoder.encode(method,"UTF-8")+"&"+
URLEncoder.encode(http,"UTF-8")+"&"+
URLEncoder.encode(args,"UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}


endpointはmixiのエンドポイント、、、って公開して良いのかな?
requestURIは/people/{id}/@allです。{id}は取得したい人のIDを設定します。
canonicalQSは前に設定したマップから取得したURLの引数です。

これらの文字列をそれぞれURLエンコードして”&”で連結します。

その文字列を最初に作ったmacを元にしてシグネチャを作成します。


String signature = null;
byte[] data;
byte[] rawHmac;
try {
data = stringToSign.getBytes(CHARSET);
rawHmac = mac.doFinal(data);
Base64 encoder = new Base64();
signature = new String(encoder.encode(rawHmac));
signature = signature.substring(0, hmac.length()-2);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(CHARSET + " is unsupported!", e);
}

try {
signature = URLEncoder.encode(signature, CHARSET).replace("+", "%20")
.replace("*", "%2A").replace("%7E", "~");
} catch (UnsupportedEncodingException e) {
}


この文字列には改行が入ってますので2文字削除しています。
そのあと、文字列の変換をかけてます。

で、、、作成されたシグネチャを元に、アクセスするURLを作成します。


String url = "http://" + endpoint + requestURI + "?" + canonicalQS + "&oauth_signature=" + sig;


でここにアクセスするとそのユーザの情報がXML化されて取得できるってわけです。
但し、ここで問題が発生します。
アクセスすると401エラーが返ってきます。

だので、、、mixiアプリの事を調べていくと。。。


対象ユーザが対象のmixiアプリを起動していない、もしくは起動から一定時間が経過した後にRESTful APIにアクセスを行った際には、
HTTPレスポンスコードとして「401 Unauthorized」が返却されます。


ををを、、、そういう事なのね。
ようはしばらくmixiアプリを起動した人の情報を
xoauth_requestor_idに設定してあげないと
アクセスできないってわけです。

APIを使うにはいろいろ画面遷移を考えないときついっぽいですね。
しかしこれでアクセスできました。
さぁ問題は何を作るかですね。。。。。



OAuthの仕様は
http://oauth.googlecode.com/svn/spec/ext/consumer_request/1.0/drafts/1/spec.html
にあります。

シグネチャがうまく取れるまで結構かかりました。
なのでここにある仕様(文字列)を元にテストを書きました。

実際のコードはその他サービスなどにアクセスしている為
複雑なクラスになっているのを平たく記述しているので
ペローンって感じのコードになっていますが、テストは通ります。



package jp.co.ziro.surpre.helper;

import static org.junit.Assert.*;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.junit.Test;


public class OAuthServiceHelperTest {

private static final String KEY = "dpf43f3p2l4k3l03";
private static final String SECRET = "kd94hf93k423kf44";
private static final String endpoint = "provider.example.net";
private static final String HMAC = "HmacSHA1";
private static final String REQUEST_URI = "/profile";
private static final String NONCE = "kllo9940pd9333jh";
private static final String REQUEST_METHOD = "GET";

@Test
public void testOAuthService() {

Map<String, String> paramMap = new TreeMap<String, String>();

//基本的な引数をすべて設定
paramMap.put("oauth_consumer_key", KEY);
paramMap.put("oauth_signature_method", "HMAC-SHA1");
paramMap.put("oauth_timestamp", "1191242096");
paramMap.put("oauth_version", "1.0");
paramMap.put("oauth_nonce", NONCE);

String canonicalQS = canonicalize(paramMap);

String method = REQUEST_METHOD;
String http ="http://" + endpoint + REQUEST_URI ;
String args = canonicalQS;

String toSign;
try {
toSign = URLEncoder.encode(method,"UTF-8")+"&"+
URLEncoder.encode(http,"UTF-8")+"&"+
URLEncoder.encode(args,"UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}

String hmac = hmac(toSign);
hmac = hmac.substring(0, hmac.length()-2);

assertEquals(hmac,"SGtGiOrgTGF5Dd4RUMguopweOSU=");
}

private String hmac(String stringToSign) {
String signature = null;
byte[] data;
byte[] rawHmac;
try {
String secret = SECRET + "&";
byte[] secretyKeyBytes = secret.getBytes("UTF-8");
SecretKeySpec secretKeySpec = new SecretKeySpec(secretyKeyBytes,HMAC);
Mac mac = Mac.getInstance(HMAC);
mac.init(secretKeySpec);
data = stringToSign.getBytes("UTF-8");
rawHmac = mac.doFinal(data);
Base64 encoder = new Base64();
signature = new String(encoder.encode(rawHmac));
} catch (Exception e) {
throw new RuntimeException(e);
}
return signature;
}


private String percentEncodeRfc3986(String s) {
String out;
try {
out = URLEncoder.encode(s, "UTF-8").replace("+", "%20")
.replace("*", "%2A").replace("%7E", "~");
} catch (UnsupportedEncodingException e) {
out = s;
}
return out;
}

private String canonicalize(Map<String, String> sortedParamMap) {
if (sortedParamMap.isEmpty()) {
return "";
}
StringBuffer buffer = new StringBuffer();
Iterator<Map.Entry<String, String>> iter = sortedParamMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, String> kvpair = iter.next();
buffer.append(percentEncodeRfc3986(kvpair.getKey()));
buffer.append("=");
buffer.append(percentEncodeRfc3986(kvpair.getValue()));
if (iter.hasNext()) {
buffer.append("&");
}
}
String cannoical = buffer.toString();
return cannoical;
}


}


元のコードは
http://bit.ly/bVSEgt
なども参考にしています。

2010年2月10日水曜日

URL短縮サービスbit.ly

Twitterでつぶやけるようにしたものの、
URLを貼り付けたら文章が長くなってしまうので
URL短縮サービスとつなげることにしました。

まずbit.lyにユーザ登録します。
するとアカウント情報のところにAPIKeyが発行されます。



String endPoint = "http://api.bit.ly/shorten";
paramMap.put("apiKey", APIKEY);
paramMap.put("login", LOGIN);
paramMap.put("version", VARSION);
paramMap.put("format", FORMAT);
paramMap.put("longUrl", longUrl);

※paramMapはURLの引数です。
 ちょっとクラス化しててうまく説明できないです。

URLを指定して、その他引数を渡します。
LOGINにはログイン用に登録したID、
versionには現在"2.0.1"、formatには"xml"を指定します。
※jsonでの取得もあるようです。

longUrlには変換対象のURLを指定します。


それでURLアクセスして取得してきたRESTに
"sortUrl"タグがありますので
それを利用すればOKです。


いやぁしかし、URL短縮ツールってURL短くするサービスだと思ってましたが
そのURLをアクセス解析なんかに使えるんですね。

メーリングリストとかでリンク送るときに使えば
アクセス数が簡易的にわかってよいかもですね。

あっちをたてればこっちがたたず

新しいサービスをつくろうとして悩んでます。
OAuthでつなげようと思いながら、それはあれで、あれはそれで。

結論はY化しようと思います。

2010年2月9日火曜日

OAuthとTwitter4J(Slim3とFlexも)

先日Flex(AS3)でOAuth認証を行いました。
しかし、セキュリティサンドボックスの問題により、棚上げしました。
セキュリティサンドボックスも何かまとめないとですね。

って事でWeb側でTwitterにアクセスしてサービスを実現する事にしました。
結局、プロキシになっちゃいました。
まぁ挙動さえ分かってしまえばこっちのもんです。

ちなみにOAuth部分はTwitter4Jに任せてますので
その挙動を見たければsourceを添付してあげて調べてください。

※ソースは一部関数化してるものを平らにしているので
変数名とかはおかしいかも。。。


Twitter twitter = new TwitterFactory().getInstance();
twitter.setOAuthConsumer(KEY, KEY_SECRET);
try {
RequestToken requestToken = twitter.getOAuthRequestToken();
String url = requestToken.getAuthorizationURL();
requestScope("url",url);
sessionScope("requestToken",requestToken);
} catch (TwitterException e) {
new RuntimeException(e);
}


まず、Twitterオブジェクトを取得してアプリケーションのキー値を設定します。
そこからURLを取得してきます。それを返します。

その後、Pinを取得して処理する場合に新たにRequestTokenを作成すると
おかしくなるのでセッションに設定しました。
※requestScope(),sessionScope()はSlim3のメソッドです。
リクエストやセッションに溜め込んでください。

そのリクエストを元にURLにアクセスします。
私の場合はFlexなので


//リンクを飛ばす
navigateToURL(new URLRequest(oauthUrl));


って感じになりました。

ここにアクセスすると横の画像のWebが別ブラウザで表示されます。
Twitterアカウントを入力すると認証され、



Pinが表示されます。
。。。。画像で表示されます。選択してコピーして
そのPinを自分のWebページなどで入力します。

。。。ここでコールバックなどをうまく使えば
多分何手かUIを改善できそうな気がするんですけどね。



私の場合はそれを元にPinを入力して再度サーバサイドで処理を行いました。


Twitter twitter = new TwitterFactory().getInstance();
twitter.setOAuthConsumer(KEY, KEY_SECRET);
String pin = requestScope("pin");
RequestToken requestToken = sessionScope("requestToken");

AccessToken accessToken = null;
try {
accessToken = twitter.getOAuthAccessToken(requestToken, pin);
} catch (TwitterException e) {
throw new RuntimeException(e);
}
String token = accessToken.getToken();
String tokenSecret = accessToken.getTokenSecret();


先ほど保存しておいたRequestTokenと取得したPinをリクエストから受け取り、
AccessTokenを取得します。
それによりトークンを取得する事が必要です。

私はここでSlim3 Datastoreを使用して、トークンを永続化しています。
ちなみにですが、私が作ってるのは認証アプリなので
認証をしたUserオブジェクト(GAE)も保存しています。


その後はログインしたユーザを元に
このトークンを取得してきて処理をします。


Twitter twitter = new TwitterFactory().getInstance();
twitter.setOAuthConsumer(KEY, KEY_SECRET);

AccessToken accessToken = new AccessToken(data.getToken(),data.getTokenSecret());
twitter.setOAuthAccessToken(accessToken);
twitter.updateStatus(tweet);


dataは永続化したオブジェクトです。
updateStatus()はtwitter4jのつぶやく機能です。

こんな感じでサービスを作成しました。
ユーザ情報を保存したくなくて、OAuthに対応しましたが、
色々とツールがあって簡単にできるもんですね。

2010年2月7日日曜日

TwitterアプリでOAuth認証

とあるサービスを作る過程でTwitterクライアントを
作ろうと思い、作り始めました。
始めはユーザIDとパスワードを入力してもらおうと考えていましたが
やはり自分のアプリにそれを保存しておくの億劫になり、
OAuth認証で作る事にしました。

まずここに行って
アプリケーションを登録する必要があります。

登録を終えるとアプリケーションのページができて、
キーが2つ発行されます。
「https://twitter.com/apps」で確認ができます。
ちなみに認証して使用しているアプリケーションを確認するには
「https://twitter.com/account/connections」
に行くと確認できます。

さてここで開発開始ですが。。。。
Javaの部分でサービスのアクセスを作成していたのですが、
認証部分はFlexで行った方が画面遷移とか簡単そうだったので
Flex側で書くことにしました。

Twitterさんのサイトに各言語におけるサンプルサイトを紹介しています。
https://twitterapi.pbworks.com/OAuth-Examples

そこでFlexのサイトがあったので
http://www.coderanger.com/blog/?p=59
これを元に作成しました。

なのでここのソースを元に説明していきます。

private var _twitauth:OAuthManager = new OAuthManager();
_twitauth.addEventListener( OAuthEvent.ON_REQUEST_TOKEN_RECEIVED, onRequestTokenReceived );
_twitauth.addEventListener( OAuthEvent.ON_REQUEST_TOKEN_FAILED, onRequestTokenFailed );
_twitauth.addEventListener( OAuthEvent.ON_ACCESS_TOKEN_RECEIVED, onAccessTokenReceived );
_twitauth.addEventListener( OAuthEvent.ON_ACCESS_TOKEN_FAILED, onAccessTokenFailed );
_twitauth.usePinWorkflow = true;
_twitauth.consumerKey = key.text;
_twitauth.consumerSecret = secret.text;
_twitauth.oauthDomain = "twitter.com";
_twitauth.requestToken();


この箇所はアクセス後の呼び出される関数を設定して
アプリケーションで取得したキーの設定を行っています。
サンプルではコントロールから取得している感じになっていますが
ここに設定してあげます。

_twitauth.requestToken();

で最初のリクエストを行っています。
この関数と同時にアプリケーション認証のサイトを別のブラウザで表示してくれます。

ここでユーザが許可し認証を行うと
アプリケーションが認証され、PINコードが表示されます。

そのPINをユーザにテキストボックスなどに入力してもらって


private function onSendPin():void
{
_twitauth.requestAccessToken( Number( pin.text ) );
}


この関数で送ってあげます。
このリクエストで成功した場合「onAccessTokenReceived()」が発生するので
そこでユーザの認証情報を取得できます。

あとはアプリケーションでこの情報を永続化するなどをして
アクセスを行います。
下の方にあるコードはその情報を使ってつぶやいているコードです。


認証を行うとこのようにアプリケーションが追加されます。



おそらくアプリケーションの設定でCallbackURLなどを使って
一部自動化できるような気もしていますが、一応これでつぶやく事ができました。
ただし、このブログ公開時点ではサーバサイドにあげた場合に
アクセスできないという不具合があるので何か間違っているのかもしれません。


このコードを参考にしてオープンソース化されている方もいらっしゃるので
http://code.google.com/p/q-oauth/
ぜひお使いください。

2010年1月30日土曜日

Efflex

Efflexを使用していますが、
どうも最新のエフェクトが使いたくて、
ソースをダウンロードしてきました。

だけど、、、コンパイルエラー。
Papervisionが入ってないからだと、swcを落としてきましたが
ダメでした。調べてみると結構古いバージョン(GreatWhite Alpha)を
使用しているらしく、ダメでした。

だけど冷静に考えるとswc自身が持っている可能性があると思い
libsに入れてみる。通った~!!!

って思いましたが単独でエラーになってます。
何点か問題になった点を修正して使えるようになりました。
結構かかりました。