ラベル Java の投稿を表示しています。 すべての投稿を表示
ラベル Java の投稿を表示しています。 すべての投稿を表示

2012年3月17日土曜日

DropboxにRESTfulアクセス

少しDropboxにデータを送り込みたいとなって
エンジンをGoogleAppEngineで送り込む事になったので少し調べました。

SDKはiOS,Android(Java),Ruby,Pythonがあります。
今回はJavaを選択したので見ましたがHttp「s」URLConnectionを使用しており、
これがAppEngineでサポートしてないので実行環境でエラーが起こります。

ってことでRESTfulアクセスで行なってみました。

準備
まずここからアプリを登録してクライアントキーを作成します。
作成するとこんな感じ。
AccessTypeは後から変えられません。 「App folder」は専用のディレクトリのみアクセス可能なもの。 「Full Dropbox」はすべてのファイルアクセスです。 専用のディレクトリのみでOKだったんですけど、Fullにしました。
リクエストトークンの取得
https://api.dropbox.com/1/oauth/request_tokenにアクセスを行います。 で戻り値(line)は「oauth_token_secret=biv7oeyp3nvkb1g&oauth_token=ql0l5qpk8h10rjz」みたいな感じです。 buildOAuthHeader()はSDKのコードにもありますが、
    private static String buildOAuthHeader(boolean accessToken) {
        Map<String,String> map = new HashMap<String,String>();
        map.put("oauth_version","1.0");
        map.put("oauth_signature_method","PLAINTEXT");
        map.put("oauth_consumer_key",APP_KEY);

        String sig = null;
        if ( accessToken ) {
            map.put("oauth_token",TOKEN_KEY);
            sig = encode(APP_SECRET) + "&" + encode(TOKEN_SECRET);
        } else {
            sig = encode(APP_SECRET) + "&";
        }
        map.put("oauth_signature",sig);

        StringBuilder buf = new StringBuilder();
        buf.append("OAuth ");

        Iterator<Entry<String, String>> itr = map.entrySet().iterator();
        while ( itr.hasNext() ) {
            Entry<String, String> entry = itr.next();
            buf.append(entry.getKey() + "=" + entry.getValue() + ",");
        }
        String data = buf.toString();
        return data.substring(0,data.length()-1);
    }
といった感じでヘッダに設定してあげてます。
認可用のURL作成
lineにある「oauth_token」の値を使います。
        System.out.println("https://www.dropbox.com/1/oauth/authorize?oauth_token=" + argsMap.get("oauth_token"));
        System.in.read();
read()はブラウザからの入力待ちです。 ブラウザを立ち上げるとDropboxに認証してその後許可のアクションがあります。
このアクセスの際に「oauth_callback」を指定しておくとコールバックしてくれます。 操作後の挙動としては ・許可(あり):コールバックURLに戻る ・許可(なし):許可画面に遷移 ・拒否:Dropboxのhomeに遷移 って感じです。 ここで良くOAuthでの認可コードを取るようなコードを書いてましたが、 コールバックを指定したとしてもそれはDropboxにはありません。 ※ただしコールバックにはuidとoauth_tokenを付けてくれるのでユーザ特定は可能です。 ようは「認可」を行った時点でアクセストークンは取れる状態になるってわけです。
アクセストークンの取得
認可されていれば以下ができます。
        TOKEN_KEY    = argsMap.get("oauth_token");
        TOKEN_SECRET = argsMap.get("oauth_token_secret");
        url = new URL("https://api.dropbox.com/1/oauth/access_token");
        connection = (HttpURLConnection)url.openConnection();
        connection.setDoInput(true);
        connection.addRequestProperty("Authorization", buildOAuthHeader(true));
        connection.setRequestMethod("GET");

        inStream = connection.getInputStream();
        input = new BufferedReader(new InputStreamReader(inStream));

        line = input.readLine();
リクエストトークンで取ってきていた値を設定してbuildOAuthHeader()の値をtrueの方で動かします。 アクセストークンもリクエスト時と同じように 「oauth_token_secret=biv7oeyp3nvkb1g&oauth_token=ql0l5qpk8h10rjz」 という値で入ってきます。 これでアクセス可能になります。
ファイルのアップロード
使用するのはファイルのアップロードだけだったので PUTでbodyに設定してあげてアップします。
    public static void put(String name,byte[] data) throws Exception {

        URL url = new URL("https://api-content.dropbox.com/1/files_put/dropbox/" + name);
        HttpURLConnection connection = (HttpURLConnection)url.openConnection();
        connection.setDoInput(true);
        connection.setDoOutput(true);
        connection.addRequestProperty("Authorization", buildOAuthHeader(true));
        connection.setRequestMethod("PUT");

        OutputStream outputStream = connection.getOutputStream();
        outputStream.write(data);
        outputStream.close();

        InputStream inStream = connection.getInputStream();
        BufferedReader input = new BufferedReader(new InputStreamReader(inStream));
        String line = "";
        while ((line = input.readLine()) != null) {
            System.out.println(line);
        }
    }
ここでURLにある「dropbox」です。 「Full Dropbox」を選択した場合はこれでOKなのですが、 「App folder」の場合は「sandbox」を指定してあげます。 前者はフルなのでユーザのディレクトリ通りに設定していったりするのですが、 sandboxの場合、「apps/[アプリID]」が使用されて、そこのアクセスのみが可能です。 フルアクセスのViewerアプリを作らない限り、後者を選択した方が良いでしょうね。 ※ブログ書き始めてアプリを変更しました。
感想
DropboxのAPIって使うとは思ってなかったのでアクセスするとは思いませんでした。 まぁ本家が作っているクライアントが同期とかもかなり優秀なので あまりアクセスする人はいないのかもしれませんね。 ただファイルアクセスはかなり簡単にできたので、 クラウドから一時的に持ってくるツールは作りやすいかな?と感じました。 何点か気になるのは ・POSTアクセスのAPIがGETで取得できる ・PLAINTEXTを使用している ・一度認可されていると許可とかが出ない って感じですかね? ちなみに自分以外に公開するには 「My Apps」のところで「Additional users」をONにする必要があります。 製品バージョンと特に制限はないみたいに見えます。 ※開発中は5人だけっぽい感じには書いてありますけど。

2012年3月10日土曜日

Xtionで画像を作ってみる

XtionでDepthGeneratorで深度を取得できますが、
カメラ的に使うにはImageGeneratorを使用します。

今回はJavaで取得してみます。


設定値
実はここではまりました。。。
<Node type="Image" stopOnError="false" >
 <Configuration>
  <MapOutputMode xRes="640" yRes="480" FPS="30"/> 
 </Configuration>
</Node>
とConfigに記述しておきます。 画面を指定してない場合、320x240で取得できます。 なんでハマったかというと 描画する予定の画面(640x480)に対して、 320x240のデータを埋め込んでしまった為、うまく描画できないという 現象に引っかかったからです。 なので作成する画像が320x240の場合は指定しなくてもOKです。
画像データの作成
imageGen = ImageGenerator.create(context);
でインスタンスを取得。 描画処理等の永久ループの箇所に
  ImageMetaData imageMD = imageGen.getMetaData();
  ImageMap imageMap = imageMD.getData();
  ByteBuffer image = imageMap.createByteBuffer();
  while ( image.remaining() > 0 ) {
   int pos = image.position();
   byte pixel = image.get();
      imgbytes[pos] = pixel;
  }
という風にbyte[]を作成します。 imageMapにある値はwidth*height*3になります。 *3の値はRGBを表しています。 表示する際には深度の時と同じように
        DataBufferByte dataBuffer = new DataBufferByte(imgbytes, width*height*3);
        WritableRaster raster = Raster.createInterleavedRaster(dataBuffer, width, height, width * 3, 3, new int[]{0, 1, 2}, null); 
        
        ColorModel colorModel = new ComponentColorModel(
          ColorSpace.getInstance(ColorSpace.CS_sRGB), 
          new int[]{8, 8, 8}, 
          false, 
          false, 
          ComponentColorModel.OPAQUE, 
          DataBuffer.TYPE_BYTE);

        bimg = new BufferedImage(colorModel, raster, false, null);
        g.drawImage(bimg, 0, 0, null);
となります。 (gは画面上にあるpaint用のGraphicsオブジェクトです) これでカメラのように画像を出力する事が可能です。
( ー`дー´)キリッ。。。もちろん動きますよ。
例えば
私のアプリはWebSocketと連携させているので この画像データをそのままWebSocketで送ってみようと思っています。 しかしそれはXtionとはあまり関係ないので 例えばですが1枚無人の背景データを用意してみて、 人間として認識された部分を背景データと重ねて塗りつぶす。 というプレデターごっこ(おそらくある程度マージンがあるので微妙に人の形が浮かび上がる)等が できるかなぁ。。。と思っています。 Depthとかの話とか書いてないから何か微妙な説明になるかなぁ、、、 もう少し、Generator周りとかがかっこよくなったら、記述したいと思います。

2012年2月23日木曜日

GoogleAnalytics(v3)でアクセスする

先日ブログにも書きましたがV2でのアクセスであり、ClientLoginだったので
今度はv3でのアクセスをやってみます。今回もJavaです。
Googleの日本語の資料はいつも通り古くほぼv2を使っているので
しばらくは英語の文献の方が良いと思います。


準備
まずここからクライアントライブラリをダウンロードしてきます。 で ・google-api-client-1.6.0-beta.jar ・google-http-client-1.6.0-beta.jar ・google-oauth-client-1.6.0-beta.jar ・dependencies/gson-1.7.1.jar ・dependencies/guava-r09.jar ・dependencies/jackson-core-asl-1.9.1.jar を読み込みます。 Analytics用のライブラリはここにあります。
OAuthでのアクセスする準備
Google+のアクセス時に説明しましたが、API使用したり、クライアントID取得の為に GoogleAPIConsoleでAnalyticsをONしておく必要があります。 「Services」をクリックしてAnalyticsをON
「API Access」をクリックして、クライアントIDを発行します。 私自身はWebアプリで使いたいですけど、一旦アクセスする為に「installed applications」を選びました。 違いはWebアプリケーションの場合と違い、コールバックURLが固定値になり、 手作業で認可コードを設定するっていう手順が増えるだけです。
認可を行う
さて準備ができたので、アクセスしてみます。
  String authorizationUrl = new GoogleAuthorizationRequestUrl(CLIENT_ID, REDIRECT_URL, "https://www.googleapis.com/auth/analytics.readonly").build();
  System.out.println(authorizationUrl);
  BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
  String authorizationCode = null;
  try {
    authorizationCode = in.readLine();
  } catch (IOException ioe) {
   throw new RuntimeException("Error",ioe);
  }
GoogleAuthorizationRequestUrlに取得したクライアントIDとリダイレクトURLを設定します。 三番目の引数はスコープ(何にアクセスできるか)です。 認証用のURLを作成してコンソールに出力しreadLine()で認可コードを待ち受けます。 ブラウザを立ち上げて、コンソールに出力されたURLにアクセスすると Googleの認証画面(マルチアカウントの場合、ユーザ選択画面等もあります)が表示され 認可画面が現れて認可を行うと、認可コードを出力してくれるのでそれをコンソールに入力します。
アクセストークンの取得
コンソールに入力された認可コードを元にアクセストークンをとりにいきます。 APIコンソールで取得したIDとSecret、それと認可コードでAccessTokenResponseを生成
  NetHttpTransport httpTransport = new NetHttpTransport();
  JacksonFactory jacksonFactory = new JacksonFactory();
  AccessTokenResponse response = null;
  try {
    response = new GoogleAccessTokenRequest.GoogleAuthorizationCodeGrant(
        httpTransport, jacksonFactory, CLIENT_ID, CLIENT_SECRET, authorizationCode,
            REDIRECT_URL).execute();
  } catch (IOException ioe) {
   throw new RuntimeException("Error",ioe);
  }

認可コードが間違ってなかったらこれでOKのはず。 responseにアクセストークン、リフレッシュトークン等が取得されます。
Analyticsのオブジェクトを生成
取得したAccessTokenResponseを元に
GoogleAccessProtectedResource googleAccessProtectedResource = 
  new GoogleAccessProtectedResource(response.accessToken, httpTransport, jacksonFactory,CLIENT_ID, CLIENT_SECRET, response.refreshToken);
Analytics analytics = Analytics.builder(httpTransport, jacksonFactory)
         .setHttpRequestInitializer(googleAccessProtectedResource)
         .setApplicationName("test")
         .build();
というような感じでAnalyticsオブジェクトを生成します。
データを取得してみる
さてやっとアクセスですね。
  Get apiQuery;
  try {
   apiQuery = analytics
     .data()
     .ga()
     .get("ga:" + "13012476", "2011-09-01", "2011-09-30",
       "ga:visits,ga:pageviews");
  } catch (IOException e) {
   throw new RuntimeException("Error", e);
  }

  apiQuery.setSort("-ga:visits");
  apiQuery.setMaxResults(50);

  try {
   GaData data = apiQuery.execute();
   Map totalsMap = data.getTotalsForAllResults();
   for (Map.Entry entry : totalsMap.entrySet()) {
    System.out.println(entry.getKey() + " : " + entry.getValue());
   }
  } catch (IOException e) {
   throw new RuntimeException("Error", e);
  }
取得してきたanalyticsオブジェクトのdata().ga().get()をおこなってGetオブジェクトを生成します。 第1引数はProfileIdになります。v2で言っていたTableIdですね。id自体は数値などで入っている為、 「ga:」をつけてやる必要があります。 これをexecute()するとデータが取れてきます。 この取得方法だと出力は「ga:visits,ga:pageview」が取得できます。 取得できるDimensionやMetricsなどはここにあります。
ProfileのIdについて
前述した「ga:13012476」についてですが v2の時はEntries()などで比較的容易に取得できましたけど。。。
   Management management = analytics.management();

   Accounts accounts = management.accounts().list().execute();
   List<Account> accountList = accounts.getItems();
   Account account = accountList.get(0);
   String accountId = account.getId();

   Webproperties prop = management.webproperties().list(accountId)
     .execute();
   Webproperty webproperty = prop.getItems().get(0);
   String webId = webproperty.getId();

   Profiles profiles = management.profiles().list(accountId, webId)
     .execute();
   Profile profile = profiles.getItems().get(0);
   String profileId = profile.getId();

   System.out.println("accountId:" + accountId);
   System.out.println("webId:" + webId);
   System.out.println("profileId:" + profileId);
わかりやすくすべてのデータについて、itemsの1件目を取得してます。 Accountはマルチアカウント用なのかな、、、私の環境では1件でした。 WebPropertyは解析しているサイト数が出てきました。 その中からプロファイルを取得する感じですね。 少しAnalytics自体のデータ構造がわかってないのでこれで正しいのか不明です。 execute()時にAPIを発行しているはずなので、、、うーんって感じ。
感想
なんかライブラリが多すぎて少し引きました><。 実際実装する際にはライブラリではなく、RESTfulアクセスすると思います。 なぜかというと、個人(セッション)に対して何を残せば良いかがわかりにくく アクセスなどが隠蔽されているので何か使いにくいというか、、、って感じです。 ※いやこのコード位の事やるにはいいんですけどね。 まぁJSONからデータのオブジェクト作ったりするのが大変だったりもするんですけど。

GoogleAnalyticsにアクセスしてみる


そのまま勢いでv3のOAuthを作りました。

Google AnalyticsはWeb解析ソリューションです。
何も考えずにサイトにJavaSriptコードを埋め込む事でWebの訪問者などを調べる事をできます。
素晴らしいサービスであれこれ機能がありますが、
今回はAPIにJavaでアクセスしてみたいと思いました。

準備
・gdata-core-1.0.jar ・gdata-analytics-2.1.jar ・google-collections-1.0.jar を読み込みます。 見つけて行く過程で「google-api-service-analytics-v3-1.3.3-beta.jar」を見つけました。 これでアクセスするのが良かったかもなのですが、 ちょっと使い方がわからなかったので後日試します。 サンプルを元におこなってみます。
まずは認証
gdataに関してOAuthやAuthSubなどの認証方式がありますが、ClientLoginで試しました。
AnalyticsService service = new AnalyticsService("applicationName");
service.setUserCredentials(username, password);
ClientLoginはOAuthなどを触った事がある人ならわかると思いますが あまり好ましいと思える認証方式ではありません。 実際文献にも「てめーで使うようなアプリにしか使うなよ」って書いてあります。 OAuthなどを行いたかったのですが、今回はアクセスしてみる事を目標にしてますので 一旦この方式でアクセスしてみます。
サイトの一覧を取得
AnalytisServiceからアカウントの情報を取得してきます。
URL feedUrl = new URL(""https://www.google.com/analytics/feeds/accounts/default""); 
AccountFeed service.getFeed(feedUrl, AccountFeed.class);
for (AccountEntry entry : accountFeed.getEntries()) {
}
AccountFeedのgetEntries()によってそのアカウントが所有しているデータを取得する事が可能です。
サイトの訪問者を見てみる
AccountEntryには「TableId」というものがあって、それによりどのサイトかを識別できるようになってます。 そのテーブルIDを元にDataQueryを作成します。
    DataQuery query = new DataQuery(new URL(DATA_URL));
    query.setIds(tableId);
    query.setStartDate("2012-01-01");
    query.setEndDate("2012-01-31");
    query.setDimensions("ga:browser");
    query.setMetrics("ga:visits,ga:bounces");
setDimensions()は何を取ってくるか? setMetrics()何を取ってくるか?です。 まぁ日付はそのままですね。 この場合、ブラウザについての訪問者を調べています。 このデータクエリに対して
query.setSort("-ga:visits");
query.setFilters("ga:browser!@Explorer");
DataFeed dataFeed = service.getFeed(query, DataFeed.class);
というようにソートやフィルタをつけてあります。 この場合訪問者数の降順、「Explorer」とついたブラウザの排他ですね。 ・・・Googleのサンプルに悪意を見た。 これで取得したDataFeedに情報が入ってますので
    for (DataEntry entry : dataFeed.getEntries()) {
      System.out.println("\tBrowser: " + entry.stringValueOf("ga:browser"));
      System.out.println("\t\tVisits: " + entry.stringValueOf("ga:visits"));
      System.out.println("\t\tBounces: " + entry.stringValueOf("ga:bounces"));
      System.out.println("\t\tBounce rate: "
          + entry.longValueOf("ga:bounces") / (double) entry.longValueOf("ga:visits"));
    }
みたいな感じで情報にアクセスできます。
感想
まぁサービスに埋め込むにはまだ色々と考えないとなぁと思いました。 ※まぁとにかく認証ですよね。 案外調べていてPHPで文献(ブログ)を出している人が多そうでした。 まぁ現状では埋め込むサイトはPHPの方が多そうですね。 Javaでアクセス解析必要なサービスって少し前なら想像できませんが 今はGoogleAppEngineもあるので、Javaでこういったものが必要なサービスも増えてくると思います。 1.6辺りからGoogleAppEngine対応されているみたいで

2012年2月2日木曜日

EPUBファイルの作り方

EPUB作りたいって人がいたので少しJavaで実装してみた。

EPUBとは
EPUBとは電子書籍の形式で最近3.0が仕様確定したのは 知ってたんだけど、内容自体はZIP形式で少し特徴があり 単純にZIPしてはいけないので注意が必要。
EPUBの構成
・mimetype (非圧縮ファイル) ・META-INF/container.xml (構成ファイルを指定) ・EPUB(任意)/****.opf (構成ファイル) という構成。 opfファイルの中身やその後の構成についてはEPUBの仕様説明になるので 一応ここから割愛。
mimetypeについて
ファイルの内容自体は固定で 「application/epub+zip」を書き込むファイルを作ってやる。 特徴なのは「非圧縮で先頭」である必要があるので ZipOutputStreamを開始して最初のエントリーを以下のように非圧縮にしてやれば良い。
        String mime = "application/epub+zip";
        ZipEntry entry = new ZipEntry("mimetype");
        byte[] mimeData;
  try {
   mimeData = mime.getBytes("UTF-8");
  } catch (UnsupportedEncodingException e1) {
   throw new RuntimeException(e1);
  }
        entry.setSize(mimeData.length);
        entry.setCompressedSize(mimeData.length);
        entry.setCrc(0x2CAB616F);
        entry.setMethod(ZipEntry.STORED);

        // 出力先 Entry を設定する。
        try {
   zos.putNextEntry(entry);
         zos.write(mimeData);
         zos.closeEntry();
  } catch (IOException e) {
   e.printStackTrace();
  }

META-INF/container.xmlについて
これは単純に構成ファイルの位置指定なので
    private String OPF_PATH = "EPUB";
    private String OPF_FILE = "book.opf";

 private void createContainerXml() {
     String xml = "<?xml version=\"1.0\"?>";
     xml += "<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">";
     xml += "<rootfiles>";

     String fileName = OPF_PATH + "/" + OPF_FILE;
     xml += "<rootfile full-path=\"" + fileName + "\" media-type=\"application/oebps-package+xml\"/>";
     xml += "</rootfiles>";
     xml += "</container>";

     try {
      ZipEntry entry = new ZipEntry("META-INF/container.xml");
   put(entry,xml.getBytes("UTF-8"));
  } catch (UnsupportedEncodingException e) {
   throw new RuntimeException(e);
  }
 }

みたいな感じで固定の値を設定してあげる。 put()関数は単純にZipOutputStreamにエントリーしている関数です。
opfファイルについて
META-INF/container.xmlファイルに指定したopfファイルに EPUBが構成するようなファイルを指定していくらしい。 EPUBのサンプルとしてGoogle Codeにおいてあったのでこちらで実験した。
ZIPする時の注意点
実際の時とは違うんだけど、Windowsのファイルシステムでやっていると 「¥」でパスを設定してしまい、AdobeのViewerはそれを認識できない(EPUBの仕様?)ので それを変換する必要がある。
     entryName = entryName.replaceAll("\\\\", "/");
おなじみのですね><v それと将来的にはメモリ上でやるのでこの失敗はないと思うんだけど、 ファイルシステムを検索してEntryを作っていると 「/META-INF/container.xml」 とEntryする事もあり、ZIP自体はこれでもOKなんだけど、 「META-INF/container.xml」 としてやらないと読み込んでくれなかった。 ※これもViewerの仕様がEPUBの仕様かは不明。 一応これでサンプルのファイルを利用してZIPで固めたらOKだった。
サービス化を考えている友人へ
固めるところは技術的に見れば簡単なんだけど、 実際は「opfファイル」をサービスとして提供しなければ行けないところで 本を作っている人がこれを書きこむってのはサービス上はありえないわけで、 まぁこんな圧縮程度で止まっている場合ではないって事です。

2011年6月7日火曜日

Evernoteにアクセスする その7 ユーザ情報編


さてNotebookの内容に入って行こうと思ってたんですけど、
冷静に考えるとユーザ名がない事に気づいた。
ログアウトとかに表示したいですよね。

なのでユーザの情報を取得しようと思います。

THttpClient userStoreTrans;
try {
userStoreTrans = new THttpClient(USER_URL);
} catch (TTransportException e) {
throw new RuntimeException("クライアントアクセス時の例外",e);
} catch (NullPointerException e) {
throw new RuntimeException("クライアントアクセス時の例外",e);
}

TBinaryProtocol userStoreProt = new TBinaryProtocol(userStoreTrans);
UserStore.Client userStore = new UserStore.Client(userStoreProt, userStoreProt);

User user = userStore.getUser(getAccessToken());


こんな感じです。UserStoreを取得するための
USER_URLには「https://sandbox.evernote.com/edam/user」ですね。
ノートの場合とアクセスするUrlが違います。

getUser()に対してアクセストークンを渡してあげます。
これで返って来るUserに情報が入っています。

getEmail()とかあるので「やべー」と思いましたがどうやら値は入ってないようですね。
Userを見る限り、UploadのLimit等も入っているのでその辺りを考慮してアップロード等を
行う必要があるようですが、一応アップ系の処理は行わないので使わないかな?

さてまた寄り道になりましたがひとまずそれを利用してログアウトのリンクを作成。
ノートブックの一覧等を利用して、ひとまずノートを選択するような仕組みにしてみました。

ここからアクセスできますけど、
sandboxのユーザがないと無理ですので、持っている方しかアクセスできません。

2011年6月6日月曜日

Evernoteにアクセスする その6 ノート一覧取得編


さて認証部分も終わったので、そろそろAPIでアクセスしないと
何やってるかわかんなくなってくるのでひとまずアクセスしてみよう!

実際にそこまでEvernote使いこなしているのか?という事も疑問でしたが
「メモ(ノート)を取る」という意味ではすごく良いアプリなんだな。。。と感じてました。

で、メモを活用するというアプリを考えた時に
「んじゃEvernote使えば?」というのが、このAPIにアクセスするきっかけになったわけです。

で、、、当初メモを取るアプリを考えていたんですけど、逆に考える事にしました。
メモはいくらでも、どんな媒体でも取るんだから、メモに情報を付加したり、
メモを取った後に色々眺める機能を作ってみようって。
まぁAPIアクセスを考えれるように、少しは追加とかもやるかもですけど。


で、evernoteにAPIでアクセスするまで、「ノートブック」と「ノート」の概念をあまり考えていませんでした。
なのでまずは一覧化して表示してみましょう。


NoteStore.Client noteStore = getClient();
List notebooks = noteStore.listNotebooks(getAccessToken());

for (Notebook notebook : notebooks) {
String guid = notebook.getGuid();
logger.info(notebook.getName() + ":" + guid);
NoteFilter filter = new NoteFilter();
filter.setNotebookGuid(guid);
NoteList noteList = noteStore.findNotes(getAccessToken(),filter, 0, 100);
for ( Note note: noteList.getNotes() ) {
logger.info(note.getTitle());
}
}


getClient()は前述の通り、SharedIdを元に取得してきます。
それとgetAccessToken()も認証の情報を元に取得してきます。

まずlistNotebooks()によりノートブックの一覧が取得できます。
Notebookには「guid」が存在し、それが一意の識別子になります。

それを元にNoteFilterを準備して、そこにGuidを設定してfindNotes()を行います。
第3、4引数は、オフセットとMaxの件数です。Maxの件数は、、、すべてってのは出来なさそう、、、

ひとまずこんなところですかね。
一応アプリではNotebookの一覧を表示して、クリックしたら、Noteの一覧が出るようなものを考えています。

次はNotebookについて記述していきましょう。

2011年6月4日土曜日

Evernoteにアクセスする その5 OAuth理解編


OAuthサンプルによって解説しようと思っていましたが、
結局暗号化を行って、テスト書いて、色々分かりやすくコードをいじってってこの状態になりました。
以下のコードでアクセスできます。


package jp.co.ziro.evernote.controller;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import java.util.logging.Logger;

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

import org.apache.commons.codec.binary.Base64;
import org.slim3.util.ApplicationMessage;

/**
*

* EvernoteへのOAuthアクセスクラス
* - 認証URL作成 setCallbackUrl()
* - アクセストークン取得時はsendRequest(tokenSecret)
* で処理を行います。
*
* 認証Urlの作成
* 1.コンストラクタにより、固定値を設定する
* 2.使用側は setCallbackUrl() にコールバックするUrlを設定する
* 3.createAuthorization() の戻り値が認証Urlを取得
* 4.getTokenSecret(),secretを取得(保存)
*
* 認証Urlにアクセスする
* 認可画面が表示され、そこでの結果がコールバックに渡される
*
* コールバックUrlで行う事
* 1.コンストラクタにより固定値を設定する
* 2.oauth_verifier,oauth_tokenがコールバックに呼ばれるのでそれを
* setToken() setVerifier() で設定
* verifierがnullだったら、認証が行われていない。
* 3.sendRequest()を行う。引数は認証Url時に作成したsecret
* 4.oauth_token にリクエストトークン、edam_shard にAPIアクセス用のIdが渡されます。
*
* これらでthriftのAPIを利用して、Evernoteにアクセスできます。
*
*

* @author secondarykey
*/
public class EvernoteRequest {

@SuppressWarnings("unused")
private static Logger logger = Logger.getLogger(EvernoteRequest.class.getName());

/**
* EverNoteのAPI ConsumerKey
*/
private static final String CONSUMERKEY = ApplicationMessage
.get("evernote.api.key");
/**
* EvernoteのAPI ConsumerSecret
*/
private static final String CONSUMERSECRET = ApplicationMessage
.get("evernote.api.secret");

/**
* アクセスするUrl
*/
private static final String BASEURL = ApplicationMessage
.get("evernote.api.baseUrl");

/**
* リクエストToken取得用のUrl
*/
private static final String requestUrl = BASEURL
+ ApplicationMessage.get("evernote.api.requestTokenUrl");

/**
* 認証のUrl
*/
private static final String authorizationUrlBase = BASEURL + ApplicationMessage.get("evernote.api.oauthUrl");

/**
* 文字コード
*/
private static final String CHARSET = "UTF-8";
/**
* キー作成用のGETメソッド名
*/
private static final String REQUEST_METHOD = "GET";
/**
* OAuth nonces用のランダム値生成
*/
private static final Random random = new Random();


/**
* ベースのURL
* @return リクエストするBaseUrl
*/
public static String getBaseUrl() {
return BASEURL;
}

/**
* リクエストするパラメーター
*/
private Map parameters = new TreeMap();

/**
* TokenSecret
* getTokenSecret()時に空の場合、Exceptionを発生
*/
private String tokenSecret = null;
/**
*

* コンストラクタ
* パラメータ固定値を設定する
*

*/
public EvernoteRequest() {
setParameter("oauth_consumer_key", CONSUMERKEY);
setParameter("oauth_timestamp", getTimestamp());
setParameter("oauth_nonce", Long.toHexString(random.nextLong()));
setParameter("oauth_version", "1.0");
//暗号化を行う
setParameter("oauth_signature_method", "HMAC-SHA1");
}
/**
* タイムスタンプを作成
* @return 現在時刻の秒をString化した値
*/
private String getTimestamp() {
return Long.toString(System.currentTimeMillis() / 1000);
}

/**
* コールバックURLを設定する
* @param callbackUrl コールバックUrl
*/
public void setCallbackUrl(String callbackUrl) {
setParameter("oauth_callback", callbackUrl);
}

/**
* リクエストトークンを設定
* @param requestToken コールバックされたリクエストトークン
*/
public void setToken(String requestToken) {
//それぞれをリクエスターに設定
setParameter( "oauth_token", requestToken);
}

/**
* Verifierの設定
* @param verifier コールバックされたverifier値
*/
public void setVerifier(String verifier) {
setParameter( "oauth_verifier", verifier);
}
/**
* リクエスト時の引数設定
* @param name パラメータ名称
* @param value パラメータの値
*/
private void setParameter(String name, String value) {
parameters.put(name, value);
}

/**
* Encodes this request as a single URL that can be opened.
*/
private String getUrl() {
return requestUrl + "?" + join();
}

/**
* HTTP引数の連結
* @return Url引数にして返す
*/
private String join() {
StringBuilder sb = new StringBuilder();
//開始フラグ
boolean firstParam = true;

//引数数繰り返す
for (Map.Entry parameter : parameters.entrySet()) {
//最初の処理
if (firstParam) {
firstParam = false;
} else {
sb.append('&');
}

//キー値を取得
sb.append(parameter.getKey());
//連結子を設定
sb.append('=');
try {
//値をエンコードして設定
sb.append(URLEncoder.encode(parameter.getValue(), CHARSET));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("エンコード時の例外", e);
}
}
return sb.toString();
}

/**
* リクエストを行う
* @param
*
* @throws IOException
* if a problem occurs making the request or getting the reply.
*/
public Map sendRequest(String tokenSecret) {
//Signatureを設定
setParameter("oauth_signature", createSignature(tokenSecret));
//アクセスする
HttpURLConnection connection;
try {
connection = (HttpURLConnection) (new URL(getUrl())).openConnection();
} catch (MalformedURLException e) {
throw new RuntimeException("アクセスに対する例外",e);
} catch (IOException e) {
throw new RuntimeException("アクセスに対する例外",e);
}

int responseCode;
String responseMessage;
try {
responseCode = connection.getResponseCode();
responseMessage = connection.getResponseMessage();
} catch (IOException e) {
throw new RuntimeException("結果取得を行う",e);
}
// リクエストが正常じゃない場合
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new RuntimeException("Server returned error code: " + responseCode + " " + responseMessage);
}

return createResponseMap(connection);
}

/**
*

* 暗号化を行う
* 1. メソッド名&アクセスUrl&HTTPの引数の文字列を作成
* 2. secretをTokenと連結してsecretをバイト化
* 3. Secret値を元にSignatureStringを暗号化を行う
* 4. その値をBase64で16進数かする
*

* @param tokenSecret リクエストToken取得時に発行されたSecret
* @return 暗号化したSecret値
*/
private String createSignature(String tokenSecret) {

String encodeUrl = encode(requestUrl);
String encodeJoin = encode(join());
String signatureBaseString = REQUEST_METHOD + "&" + encodeUrl + "&" + encodeJoin;

// 署名対象のテキストを作成
String secret = CONSUMERSECRET + "&";
//TokenSecretが存在する場合
if (tokenSecret != null) {
secret += tokenSecret;
}
byte[] secretyKeyBytes;
try {
secretyKeyBytes = secret.getBytes(CHARSET);
} catch (UnsupportedEncodingException e2) {
throw new RuntimeException("エンコード時の失敗",e2);
}

String hmac = "HmacSHA1";
SecretKeySpec secretKeySpec = new SecretKeySpec(secretyKeyBytes,hmac);
Mac mac;
try {
mac = Mac.getInstance(hmac);
mac.init(secretKeySpec);
} catch (NoSuchAlgorithmException e1) {
throw new RuntimeException("暗号化インスタンス取得失敗",e1);
} catch (InvalidKeyException e1) {
throw new RuntimeException("暗号化インスタンス取得失敗",e1);
}

byte[] data;
try {
data = signatureBaseString.getBytes(CHARSET);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(CHARSET + " is unsupported!", e);
}
byte[] rawHmac = mac.doFinal(data);
Base64 encoder = new Base64();

return new String(encoder.encode(rawHmac));
}

/**
* 値のUrlエンコード
* @param value エンコードする値
* @return エンコードした値
*/
private String encode(String value) {
String rtnData;
try {
rtnData = URLEncoder.encode(value,CHARSET);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("エンコード時の例外",e);
}
return rtnData;
}
/**
* レスポンスの値を取得
*
* @param connection
* @return
*/
private Map createResponseMap(HttpURLConnection connection) {

BufferedReader bufferedReader;
String result;
try {
bufferedReader = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
result = bufferedReader.readLine();
} catch (IOException e) {
throw new RuntimeException("読み込み時の例外", e);
}

Map responseParameters = new HashMap();
for (String param : result.split("&")) {
int equalsAt = param.indexOf('=');
if (equalsAt > 0) {
String name = param.substring(0, equalsAt);
String value;
try {
value = URLDecoder.decode(param.substring(equalsAt + 1),
CHARSET);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("エンコード時の例外", e);
}
responseParameters.put(name, value);
}
}
return responseParameters;
}

/**
* 認証Urlの作成
* @return
*/
public String createAuthorizarionUrl() {
//リクエスト
Map reply = sendRequest(null);
String requestToken = reply.get("oauth_token");
//認証Urlの作成
String authorizationUrl = authorizationUrlBase + "?oauth_token=" + requestToken;
//TokenSecretを設定する
tokenSecret = reply.get("oauth_token_secret");
return authorizationUrl;
}

/**
* TokenSecretの取得
* @return
*/
public String getTokenSecret() {
if ( tokenSecret == null ) {
throw new NullPointerException("TokenSecretが設定されていない");
}
return tokenSecret;
}


}



まずコンストラクタで各種固定値の設定を行います。


  • oauth_consumer_key:KEY値

  • oauth_signature_method:「PLANTEXT」「HMAC-SHA1」のどちらか

  • oauth_timestamp:現在の秒

  • oauth_nonce:ランダムの値(リクエスト毎に一意)

  • oauth_version:「1.0」固定



Evernoteのサンプルではoauth_signatureにSECRET値を指定していますが、
HMAC-SHA1の際は暗号化を行うので、まだ指定していません。
oauth_signature_methodはPLANTEXT、HMAC-SHA1が存在し、サンプルはPLANTEXTが指定されています。
PLANTEXTを推奨(つうか使って)しているのも珍しい気もします。

HTTPにアクセスする時に利用するMapをTreeMapを利用していますが
暗号化時に引数をソートする必要があるのでTreeMapでKey値のソートを行っているわけです。

コンストラクタで設定した後はコールバックUrl(oauth_callback)を指定します。
Evernoteの認可画面により処理が行われるとそのUrlに戻ってきます。

認可画面に行くためにリクエストトークンを取得しに行きます。
createAuthorizarionUrl()ですね。

ここでsendRequest()でEvernoteにアクセスしています。
oauth_signatureをcreateSignatuer()を行ってHMAC-SHA1の暗号化を行って指定しています。

返ってきたResponseから値を取得します。
レスポンスにリクエストトークン(oauth_token)とSecret(oauth_token_secret)が指定されています。
リクエストトークンはhttps://sandbox.evernote.com/?oauth_token=xxxxとして利用すると認可画面にアクセスする事ができます。
Secret値は、アクセストークン時にシグネチャ値のキー値とする時にConsumerSecretと合わせて使用します。

ここまでが認可画面のリンクを作成する流れです。

コールバックURLを指定していますが、認可画面にアクセスし、操作を行うとこのUrlに戻ってきます。
そちらの処理が以下になります。

認証に成功するとコールバックURLにoauth_token、oauth_verifierが付与されて来ます。
oauth_verifierが存在しない場合は認証で拒否された等ですので、エラー画面等を出力しましょう。

設定されていたら、アクセストークンを取得する為、
このクラスをnewしてインスタンス化します。※設定する値は一緒。

oauth_tokenをsetToken() ,oauth_verifierをsetVerifier()を利用して、設定します。
設定したらsendRequest()によりアクセストークンを取得するのですが、
この時に最初のアクセスで取得したtokenSecretを渡します。

取得に成功するとoauth_tokenにアクセストークン、edam_shardにEvernoteにアクセスする時に必要な値が入ってきます。
このアクセストークンさえあれば何でもできるわけです。


その値を利用してAPIのアクセスです。
thriftのライブラリを使っています。


String noteStoreUrl = noteStoreUrlBase + shardId;
THttpClient noteStoreTrans = new THttpClient(noteStoreUrl);
TBinaryProtocol noteStoreProt = new TBinaryProtocol(noteStoreTrans);

NoteStore.Client noteStore =
new NoteStore.Client(noteStoreProt, noteStoreProt);
List notebooks = noteStore.listNotebooks(accessToken);
for (Notebook notebook : notebooks) {
logger.info("Notebook: " + notebook.getName());
}


サンドボックス+"/edam/note/"+edam_shardの値を作成して
それでTHttpClientを生成、それを利用してTBinaryProtocolを生成しています。

それを利用して生成しているNoteStore.ClientはEvernoteのライブラリですね。
生成したオブジェクトでAPI呼び出しを行っています。
ここではlistNotebooks()です。そこにアクセストークンを渡してアクセスしています。


・・・さて、再度理解を深める為にOAuthをしっかり説明してみました。
説明がヘタすぎますけど、ご了承ください。

まぁとにかく次回はAPIで色々やってみます。

2011年6月3日金曜日

Evernoteにアクセスする その4 HMAC-SHA1編


ここまでサンプルを元にEvernoteにアクセスしてきましたが、
SimpleOAuthRequestの中身を見てOAuthアクセスへの理解を
深めておきましょう。。。。って思ったんですけど、PLANTEXTでアクセスしているので
HMAC-SHA1でアクセスしてみましょう。

以下が元ソースです。
見られる場合は展開してください。


/**
* Copyright 2008 by EverNote Corporation. All rights reserved.
*/

package com.evernote.oauth.consumer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
* This is a very simple implementation of an OAuth consumer request which can
* be used to ask an OAuth service provider for either a Request Token or
* an Access Token. It only handles PLAINTEXT authentication, and it only goes
* over a GET transport. As a result, it should only be used over SSL.
*
* @author Dave Engberg
*/
public class SimpleOAuthRequest {

/**
* Random number generator for creating OAuth nonces
*/
private static final Random random = new Random();

/**
* The URL of the OAuth service Provider that we should hit to request a
* token.
*/
private String providerUrl;

/**
* A mapping containing all of the OAuth parameters that will be passed in
* the reply.
*/
private Map parameters = new HashMap();

/**
* Constructs a request object that can be used to make token requests from
* an OAuth provider.
*
* @param providerUrl the base URL to request a Request or Access token
* @param consumerKey the OAuth consumer key, given by the Service Provider
* @param consumerSecret the OAuth consumer secret, given by the Provider
* @param tokenSecret if non-null, this is the previous oauth_token_secret
* that should be used in signing this request. If null, this will assume
* that this message does not include a token secret in its signature
*/
public SimpleOAuthRequest(String providerUrl, String consumerKey,
String consumerSecret, String tokenSecret) {
this.providerUrl = providerUrl;
setParameter("oauth_consumer_key", consumerKey);
String signature = consumerSecret + "&";
if (tokenSecret != null) {
signature += tokenSecret;
}
setParameter("oauth_signature", signature);
setParameter("oauth_signature_method", "PLAINTEXT");
setParameter("oauth_timestamp",
Long.toString(System.currentTimeMillis() / 1000));
setParameter("oauth_nonce",
Long.toHexString(random.nextLong()));
setParameter("oauth_version", "1.0");
}

/**
* Sets one of the query string parameters for the request that will be
* made to the OAuth provider. The value will be URL encoded before adding
* to the URL.
*
* @param name the name of the parameter to be set
* @param value the string value, unencoded
*/
public void setParameter(String name, String value) {
parameters.put(name, value);
}

/**
* Encodes this request as a single URL that can be opened.
*/
public String encode() throws IOException {
StringBuilder sb = new StringBuilder();
sb.append(providerUrl);
boolean firstParam = providerUrl.indexOf('?') < 0;
for (Map.Entry parameter : parameters.entrySet()) {
if (firstParam) {
sb.append('?');
firstParam = false;
} else {
sb.append('&');
}
sb.append(parameter.getKey());
sb.append('=');
sb.append(URLEncoder.encode(parameter.getValue(), "UTF-8"));
}
return sb.toString();
}

/**
* Sends the request to the OAuth Provider, and returns the set of reply
* parameters, mapped from name to decoded value.
*
* @throws IOException if a problem occurs making the request or getting the
* reply.
*/
public Map sendRequest() throws IOException {

HttpURLConnection connection =
(HttpURLConnection)(new URL(encode())).openConnection();
int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new IOException("Server returned error code: " + responseCode +
" " + connection.getResponseMessage());
}
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(connection.getInputStream()));
String result = bufferedReader.readLine();
Map responseParameters = new HashMap();
for (String param : result.split("&")) {
int equalsAt = param.indexOf('=');
if (equalsAt > 0) {
String name = param.substring(0, equalsAt);
String value =
URLDecoder.decode(param.substring(equalsAt + 1), "UTF-8");
responseParameters.put(name, value);
}
}
return responseParameters;
}

}



これをHMAC-SHA1で処理を行う場合は、oauth_signatureの指定に
・HTTPメソッド
・リクエストURL
・リクエストパタメータ
これらをURLエンコードして、&で結合してHMAC-SHA1で16進のダイジェスト値を作って
Base64エンコード後、URLエンコードした値をoauth_signatureに指定します。

オリジナルでEvernoteRequestクラスを作成してアクセスしてみましょう。
※動作確認用なので汚いのは許して><

以下がHMAC-SHA1でアクセスしたソース


/**
* Copyright 2008 by EverNote Corporation. All rights reserved.
*/

package jp.co.ziro.evernote.controller;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import java.util.logging.Logger;

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

import org.apache.commons.codec.binary.Base64;
import org.slim3.util.ApplicationMessage;

/**
* This is a very simple implementation of an OAuth consumer request which can
* be used to ask an OAuth service provider for either a Request Token or an
* Access Token. It only handles PLAINTEXT authentication, and it only goes over
* a GET transport. As a result, it should only be used over SSL.
*
* @author Dave Engberg
*/
public class EvernoteRequest {

@SuppressWarnings("unused")
private static Logger logger = Logger.getLogger(EvernoteRequest.class.getName());
private static final String consumerKey = ApplicationMessage
.get("evernote.api.key");
private static final String consumerSecret = ApplicationMessage
.get("evernote.api.secret");
private static final String urlBase = ApplicationMessage
.get("evernote.api.baseUrl");
private static final String requestUrl = urlBase
+ ApplicationMessage.get("evernote.api.requestTokenUrl");
/**
* ベースのURL
* @return
*/
public static String getBaseUrl() {
return urlBase;
}

/**
* Random number generator for creating OAuth nonces
*/
private static final Random random = new Random();

/**
* A mapping containing all of the OAuth parameters that will be passed in
* the reply.
*/
private Map parameters = new TreeMap();

/**
* Constructs a request object that can be used to make token requests from
* an OAuth provider.
*/
public EvernoteRequest() {
setParameter("oauth_consumer_key", consumerKey);
//暗号化を行う
setParameter("oauth_signature_method", "HMAC-SHA1");
setParameter("oauth_timestamp", getTimestamp());
setParameter("oauth_nonce", Long.toHexString(random.nextLong()));
setParameter("oauth_version", "1.0");
}
/**
* タイムスタンプを作成
* @return
*/
private String getTimestamp() {
return Long.toString(System.currentTimeMillis() / 1000);
}

/**
* Sets one of the query string parameters for the request that will be made
* to the OAuth provider. The value will be URL encoded before adding to the
* URL.
*
* @param name
* the name of the parameter to be set
* @param value
* the string value, unencoded
*/
public void setParameter(String name, String value) {
parameters.put(name, value);
}

/**
* Encodes this request as a single URL that can be opened.
*/
private String encode() {
return requestUrl + "?" + join();
}

/**
* HTTP引数の連結
* @return
*/
private String join() {
StringBuilder sb = new StringBuilder();
boolean firstParam = true;
for (Map.Entry parameter : parameters.entrySet()) {
if (firstParam) {
firstParam = false;
} else {
sb.append('&');
}
sb.append(parameter.getKey());
sb.append('=');
try {
sb.append(URLEncoder.encode(parameter.getValue(), CHARSET));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("エンコード時の例外", e);
}
}
return sb.toString();
}

/**
* 文字コード
*/
private static final String CHARSET = "UTF-8";
/**
* キー作成用のGETメソッド名
*/
private static final String REQUEST_METHOD = "GET";

/**
* Sends the request to the OAuth Provider, and returns the set of reply
* parameters, mapped from name to decoded value.
*
* @throws IOException
* if a problem occurs making the request or getting the reply.
*/
public Map sendRequest(String tokenSecret) throws IOException {
// 署名対象のテキストを作成
String secret = consumerSecret + "&";
if (tokenSecret != null) {
secret += tokenSecret;
}

String hmac = "HmacSHA1";

String encodeUrl = URLEncoder.encode(requestUrl,CHARSET);
String encodeJoin = URLEncoder.encode(join(),CHARSET);

String signatureBaseString = REQUEST_METHOD + "&" + encodeUrl + "&" + encodeJoin;
logger.info(signatureBaseString);

byte[] secretyKeyBytes = secret.getBytes(CHARSET);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretyKeyBytes,hmac);
Mac mac;
try {
mac = Mac.getInstance(hmac);
mac.init(secretKeySpec);
} catch (NoSuchAlgorithmException e1) {
throw new RuntimeException("暗号化インスタンス取得失敗",e1);
} catch (InvalidKeyException e1) {
throw new RuntimeException("暗号化インスタンス取得失敗",e1);
}

String signature = null;
byte[] data;
byte[] rawHmac;
try {
data = signatureBaseString.getBytes(CHARSET);
rawHmac = mac.doFinal(data);
Base64 encoder = new Base64();
signature = new String(encoder.encode(rawHmac));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(CHARSET + " is unsupported!", e);
}
setParameter("oauth_signature", signature);

HttpURLConnection connection = (HttpURLConnection) (new URL(encode())).openConnection();
int responseCode = connection.getResponseCode();
// リクエストが正常じゃない場合
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new IOException("Server returned error code: " + responseCode
+ " " + connection.getResponseMessage());
}
return createResponseMap(connection);
}

/**
* レスポンスの値を取得
*
* @param connection
* @return
*/
private Map createResponseMap(HttpURLConnection connection) {

BufferedReader bufferedReader;
String result;
try {
bufferedReader = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
result = bufferedReader.readLine();
} catch (IOException e) {
throw new RuntimeException("読み込み時の例外", e);
}

Map responseParameters = new HashMap();
for (String param : result.split("&")) {
int equalsAt = param.indexOf('=');
if (equalsAt > 0) {
String name = param.substring(0, equalsAt);
String value;
try {
value = URLDecoder.decode(param.substring(equalsAt + 1),
CHARSET);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("エンコード時の例外", e);
}
responseParameters.put(name, value);
}
}
return responseParameters;
}
}



・・・本当に汚い、、、
で、これを利用してindex時に


EvernoteRequest oauthRequestor = new EvernoteRequest();
String thisUrl = request.getRequestURL().toString();
String cbUrl = thisUrl.substring(0, thisUrl.lastIndexOf('/') + 1);
cbUrl = cbUrl + "callback";

oauthRequestor.setParameter("oauth_callback", cbUrl);
Map reply = oauthRequestor.sendRequest(null);

String requestToken = reply.get("oauth_token");
String tokenSecret = reply.get("oauth_token_secret");
String authorizationUrl = authorizationUrlBase + "?oauth_token=" + requestToken;

sessionScope("tokenSecret",tokenSecret);


という風にします。
すべての引数を指定した後にoauth_signatureを生成するので
sendRequest()にtokenSecretを持ってきています。(コールバック時に利用)

リクエストトークンとともにtokenSecretは取得できます。
アクセストークンを取得する際に使用するtokenSecretをセッションに残しています。
で以下がコールバックの処理。


EvernoteRequest oauthRequestor = new EvernoteRequest();

String requestToken = requestScope("oauth_token");
String verifier = requestScope("oauth_verifier");
String tokenSecret = sessionScope("tokenSecret");

oauthRequestor.setParameter( "oauth_token", requestToken);
oauthRequestor.setParameter( "oauth_verifier", verifier);

//取得する
Map reply = oauthRequestor.sendRequest(tokenSecret);


こちらはセッションからtokenSecretを取得して指定しているだけですね。

んーAuthrorization Headerでアクセスしたいですよねー。
アクセスもPOSTでもないですし。。。本物はそうしてみようっ。と。

次回、OAuthを読んでいくか、APIアクセスをもう少し行ってみるか悩んでます。
それを記述するのはOAuthの仕様なだけなのでそっち読めば良いかなぁ、、、と。

Evernoteにアクセスする その3 サンプルをシンプル編


サンプルはJSPでOAuthを分かりやすく記述していますが
手数が多く記述してあるので、実装としてはわかりずらいので
一旦整理してみましょう。

Google for Eclipse Plugin[更新サイト:http://dl.google.com/eclipse/plugin/3.6] と
Slim3 Plugin[更新サイト:http://slim3.googlecode.com/svn/updates/]をインストールします。

Slim3はGoogleAppEngineに最適化されたMVCフレームワークです。
非常に簡単なので説明は省きます。

indexを作成して


SimpleOAuthRequest oauthRequestor =
new SimpleOAuthRequest(requestTokenUrl,consumerKey,consumerSecret,null);

//現在のURLからコールバックURLを作成
String thisUrl = request.getRequestURL().toString();
String cbUrl = thisUrl.substring(0, thisUrl.lastIndexOf('/') + 1);
cbUrl = cbUrl + "callback";

oauthRequestor.setParameter("oauth_callback", cbUrl);
Map reply = oauthRequestor.sendRequest();

String requestToken = reply.get("oauth_token");
String authorizationUrl = authorizationUrlBase + "?oauth_token=" + requestToken;

//認証にリダイレクトを行う
return redirect(authorizationUrl);


設定値などはコールバックURL以外はそのままです。
SimpleOAuthRequestを利用して認証用のURLを作成してそのままリダイレクトをかけてます。
※もちろんログインボタンとか作りたいなら別ですけど。

これにより/indexにアクセスするとEvernoteの認可画面が表示されるはずです。
それではコールバックURLを指定してみましょう。
callbackを作成して



SimpleOAuthRequest oauthRequestor = new SimpleOAuthRequest( requestTokenUrl, consumerKey, consumerSecret, null);

String requestToken = requestScope("oauth_token");
String verifier = requestScope("oauth_verifier");
oauthRequestor.setParameter( "oauth_token", requestToken);
oauthRequestor.setParameter( "oauth_verifier", verifier);

Map reply = oauthRequestor.sendRequest();

String accessToken = reply.get("oauth_token");
String shardId = reply.get("edam_shard");
String noteStoreUrl = noteStoreUrlBase + shardId;

THttpClient noteStoreTrans = new THttpClient(noteStoreUrl);
TBinaryProtocol noteStoreProt = new TBinaryProtocol(noteStoreTrans);
NoteStore.Client noteStore =
new NoteStore.Client(noteStoreProt, noteStoreProt);

List notebooks = noteStore.listNotebooks(accessToken);
for (Notebook notebook : notebooks) {
logger.info("Notebook: " + notebook.getName());
}


これもサンプルと同じ実装ですね。
もちろん認証してなかったら~とかの処理は必要ですが、
何やっているかを分かりやすくするため、2つのURLだけで実装してみました。

ListのところはNotebookを返してたので?じゃなくしました。
THttpClient はライブラリに存在するthriftのAPIですね。
それらを利用してEvernoteのAPIを利用するイメージです。

SimpleOAuthRequestの実装を説明しないと何やってるかわかりずらいですね。
次回はSimpleOAuthRequestについて記述してみましょう。

2011年6月2日木曜日

Evernoteにアクセスする その2 OAuthアクセス編


前回サンプルを動作させてみました。
サンプルはOAuthの動作を見せる感じに作ってあります。
JSPに書かれていて処理がわかりずらいのでコードを解析しながら読みやすくしていきましょう。

最初の方は定数等を定義していますね。

private enum supportedRedirSchemas {FULL, EMBED};


は、iframeを利用する場合の処理の分岐ですね。
少し分かりづらいですが、基本的にindex.jspに対して、
「action」という引数を渡して処理を行ってます。


  1. getRequestTokenの処理

  2. Evernoteにアクセス(引数なし)

  3. 認証後getAccessTokenの処理

  4. AccessTokenによりlistNotebooks



という順序で処理を行っています。
これはRequestTokenを準備してEvernoteにアクセスして
認証結果をAccessTokenで取得。
最後にAPIにアクセスしている感じです。

まぁご存知「OAuth1.0」アクセスです。
まずはgetRequestTokenによるrequestTokenの取得です。

サンプルのはじめのリンクをクリックした時の処理


SimpleOAuthRequest oauthRequestor =
new SimpleOAuthRequest(requestTokenUrl, consumerKey, consumerSecret, null);

// Set the callback URL
String thisUrl = request.getRequestURL().toString();
 String cbUrl = thisUrl.substring(0, thisUrl.lastIndexOf('/') + 1);
 if (redirSchema != null && redirSchema.equals(supportedRedirSchemas.EMBED.toString())) {
  cbUrl = cbUrl + callbackEmbedUrl;
 } else {
  cbUrl = cbUrl + callbackUrl;
 }
 oauthRequestor.setParameter("oauth_callback", cbUrl);

out.println("Request: " + oauthRequestor.encode());
Map reply = oauthRequestor.sendRequest();
out.println("Reply: " + reply);
requestToken = reply.get("oauth_token");
session.setAttribute("requestToken", requestToken);


ここで大事なのは
 com.evernote.oauth.consumer.SimpleOAuthRequest
クラスです。

コンストラクタではリクエストする為の引数を作っています。
その後、iframe埋め込みかを判定して、戻ってくるURLを変更しています。
サンプルを動作させるとわかりますがEMBEDを選んだ場合はiframeが出現して、
callback.jspが表示され、そのiframe上で認証をかけます。
なのでまぁその辺はあまり関係ないです。(OAuth後に呼び出すURLを変更するだけ)

sendRequest()によりrequestTokenUrlにリクエストして、RequestTokenを取得しにいっています。
戻り値のマップはResponseの値です。


その下で取得したRequestTokenでURLを作成しています。


String authorizationUrl = authorizationUrlBase + "?oauth_token=" + requestToken;
if (redirSchema != null && redirSchema.equals(supportedRedirSchemas.EMBED.toString())) {
authorizationUrl = authorizationUrl + "&format=microclip";
}


埋め込みの場合は少しいじってますね。
このURL(Evernote)にアクセスする事で認可できるわけです。
このURLが2番目のURL。

JSPでまどろっこしいのですが、
基本的にはURL生成からリダイレクトすればいきなり認証も可能ですね。

そのURLにアクセスすれば



という風にEvernoteへの認可画面(認証済なのでこういう画面ですが認証してないとログイン画面)が出ます。

これに認可されると指定してあるCallbackUrlに処理がきます。

コールバックで戻ってくる最初の処理がこちら。


requestToken = request.getParameter("oauth_token");
verifier = request.getParameter("oauth_verifier");
session.setAttribute("verifier", verifier);


ちなみに「oauth_verifier」がnullの時は認可がされなかった時になります。
とにかくセッションに貯めてますね。。。このサンプル。
実際使う場合はコールバックURLでRequestTokenもverifierも取得して判定すれば良いでしょう。

で3番目のリンクをクリックすると


// Send an OAuth message to the Provider asking to exchange the
// existing Request Token for an Access Token
SimpleOAuthRequest oauthRequestor =
new SimpleOAuthRequest(requestTokenUrl, consumerKey, consumerSecret, null);
oauthRequestor.setParameter("oauth_token",
(String)session.getAttribute("requestToken"));
oauthRequestor.setParameter("oauth_verifier",
(String)session.getAttribute("verifier"));
out.println("Request: " + oauthRequestor.encode());
Map reply = oauthRequestor.sendRequest();
out.println("Reply: " + reply);
accessToken = reply.get("oauth_token");
String shardId = reply.get("edam_shard");
session.setAttribute("accessToken", accessToken);
session.setAttribute("shardId", shardId);


この処理を行っています。
再度SimpleOAuthRequestを利用してアクセストークンを取得しています。
・・・まぁセッションにまた貯めてますねぇ。。。

で最後にAPIにアクセスしています。


String noteStoreUrl = noteStoreUrlBase +
session.getAttribute("shardId");

out.println("Listing notebooks from: " + noteStoreUrl);
THttpClient noteStoreTrans = new THttpClient(noteStoreUrl);
TBinaryProtocol noteStoreProt = new TBinaryProtocol(noteStoreTrans);
NoteStore.Client noteStore =
new NoteStore.Client(noteStoreProt, noteStoreProt);
List notebooks = noteStore.listNotebooks(accessToken);

for (Object notebook : notebooks) {
out.println("Notebook: " + ((Notebook)notebook).getName());
}


これによりノートブック一覧が取得できます。
ひとまずOAuthアクセスはこんな感じでできます。

SimpleOAuthRequestを読んでもらえればわかりますが、
パラメータを設定してアクセスしてあげてくれるだけです。

なのでOAuthの仕組みさえわかっていれば簡単です。
※サンプルは流れさえ押さえれば、わかるのですが
 JSPだけでアクセスしてるのでかなり複雑な書き方になっています。

OAuth1.0の解説で一番わかりやすいのはゆろよろ氏が記述している
OAuthプロトコルの中身をざっくり解説してみるよだと思います。
ご覧になってください。


さて今回はサンプルを元にやりましたので
次回Slim3を使ってアクセスしてみましょう!

2011年6月1日水曜日

Evernoteにアクセスする その1 サンプル動作編


少し前にメモアプリを考えていて、
appengineに保存しようと思っていたんですけど、
なんとなくevernoteに残そうと思い立ったのでアクセスしてみた。

まず「http://www.evernote.com/」にアクセスすると
サイト下部に「開発者の方へ」とあるのでそこをクリック。



「WebサービスAPI」の説明があるのでそこを読んでクリック



そこで入力フォームがあるので、各種入力してSUBMITを行います。




そこから3日程待ちます。。。(´・ω・`)
※サンプル動かしてみてわかったのですが、
Webとして申込むとクライアントアプリのアクセスはできません。
おそらくクライアントアプリでWebのAPIアクセスは無理です。


すると英文でKeyとSecretが送られてきます。
一応これはsandboxなので終わったらできあがったよーってメール送ると
本番環境でも使えるようになるみたいです。(それは後日書くかな。。。)


さて開発に入ります。
・・・その前にユーザをsandnox側で登録しておく必要があります。


まずは
http://www.evernote.com/about/developer/api/
からサンプル等の入ったZIPをダウンロードしてきます。




まずダウンロードしたファイルを解凍します。
\evernote-api-1.19\sample\java\oauth\src
がソースになります。(warもあるのでそれでもOK)

java側にはOAuth用(com.evernote.oauth.consumer.SimpleOAuthRequest)のソースが存在します。
webappにはWebアプリ用のソースが存在します。

ライブラリには

log4j-1.2.14.jar
libthrift.jar
evernote-api-1.19.jar

を使ってますね。


展開したinde.jspの20行目位に

static final String consumerKey = "xxxxxxx";
static final String consumerSecret = "xxxxxxxx";

というコードがあるのでそこをメールできたKEYとSECRETに変更する。
これでAPIにアクセス可能になります。

これで動作できるようになります。




。。。サンプルが汚かったのでちょっとまだ読んでないで動作確認のみです。
ソース読みながらOAuthのアクセス(他と一緒の感じですけど)と
API等を見ながらアクセスしてみようと思います。

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年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
なども参考にしています。

2009年5月16日土曜日

Javaでセッション管理

JavaのWebアプリでオブジェクトをセッションに
登録、削除した時にイベントを発生する場合
HttpSessionBindingListenerをimplementするとイベントを発生することができます。


public class Account implements HttpSessionBindingListener {

@Override
public void valueBound(HttpSessionBindingEvent arg0) {
}

@Override
public void valueUnbound(HttpSessionBindingEvent arg0) {
}
}


って感じです。

2009年4月23日木曜日

GAEでStruts

さて我が心のフレームワークStruts1.3を
GAEで展開したいと思います。

えーと。。。ひとまずblankアプリからですかね?
まずStrutsのappをおとしてきて、blankのwarを展開します。

で、、、展開したディレクトリから
jspはプロジェクトのwar下に置きます。

index.jspとpages/Welcome.jsp

WEB-INF下のlibとxmlファイル3つ。。。
web.xmlは上書きするかはお任せします。

これらを展開します。

でsrc下にある「MessageResources.properties」をsrcにコピーします。

でappengine-web.xmlに

<sessions-enabled>true</sessions-enabled>

を追記します。


でデプロイ。これで動作します。


。。。素直にGWTにするべきか悩ましい。。。

2009年4月20日月曜日

GAEでGroovy!

はてさて知人がGroovyを使いたいということだったので
何となくですがやってしまいました。

まずweb.xmlに

<servlet>
<servlet-name>GroovyServlet</servlet-name>
<servlet-class>groovy.servlet.GroovyServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>GroovyServlet</servlet-name>
<url-pattern>*.groovy</url-pattern>
</servlet-mapping>



を追加します。
って独自クラスを作ろうとしてたら
GroovyにServletてびっくりしました。

servletとservlet-mappingは
まとめて記述する必要があるので
独自のサーブレットがある場合は気をつけて編集しましょう。

でwar/WEB-INF/groovyというディレクトリを作成して
test.groovyファイルを作成します。


html.html {
head {
title "Hello"
}
body {
p "Hello Groovy World!"
}
}




これでデプロイして「ドメイン/test.groovy」でアクセスすれば
Groovy動作が完了しますね。簡単です。


このソースの前にScriptEngineManagerを使って
文字列実行できるかテストしたんですけど。。。。
ScriptEngineManagerってJava6標準なんですね。驚きました。




・・・あー忘れてました。Groovyのjarが必要ですね。
Groovyの本家からバイナリでもダウンロードして
jarをクラスライブラリに追加しないと動作しません。

2009年4月19日日曜日

GAEで認証アプリケーション

GoogleAppEngineで認証できるアプリケーションを作成してみましょう!

サンプルにもある通り、

UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();


と行うとユーザがログインしているかどうかがわかります。
userがnullの場合はログインしてない状況です。


userService.createLogoutURL(request.getRequestURI()
userService.createLoginURL(request.getRequestURI())


と行うとログイン、ログアウトのパスを取得できるので
それに対してAタグを書いてあげます。

引数の「request.getRequestURI()」は
ログイン後に戻るページですので変更すれば画面の遷移を変えることができます。


ここで彷徨ってしまったのは
独自ドメイン時のログイン画面です。
私の作ったアプリケーションの認証を作成しようとしたら
GoogleAppsの認証画面が出てきました。

「えー独自ドメインは自分のドメインの認証なのっ!」


・・・と勘違いしちゃいましたが違いました。
どうやらアプリケーションを作成した時に


でドメイン認証をチェックしたみたいです。
・・・1時間位さまよいました。。。
逆に考えれば独自認証のアプリも作れるってことですね!




はてさて情報の取得やログインへの遷移は可能になりましたが
大事なのはアプリケーションのセキュリティです。

・・・ってJAASを使用した方なら簡単ですね。
web.xmlを設定してあげればOKです。


ほぼ
ここと同じだと思いますが
server.xmlはいらないし「manage」っていうロールはないですね。
adminはアプリの管理者がそういう扱いになりそうです。

GoogleAppEngine for Java

先日一部公開されましたGoogleAppEngineのJava版を使ってみましょう。

まずEclipseにプラグインをインストールします。
3.4の場合は以下のサイトでOKです。
http://dl.google.com/eclipse/plugin/3.4

GAEのサイトに行って登録を行います。
※最新情報だとJavaは2万5千人先着?
 まぁどんどん増やしそうですけど。

GAEのサイトで「CreateProject」を行います。
ここで登録するIDはアプリケーションIDとして唯一ですが
後で独自ドメインでの運用も可能になりますので
唯一なら何でもよいでしょう。


その後eclipseでGoogleのアイコンの

「Web Application Project」を作成します。


その次の画面でプロジェクト名はeclipse上での名称、
Packageは作成を行うパッケージを指定します。

GWTを使う時と使わない時で下のチェックボックスをはずして
作成を行います。GWTかGAEのどちらかは必須のようです。
GAEを使わないと。。。多分ただのWebアプリケーションになります。


その後「Deploy App Engine Project」で
公開するわけですが、プロジェクトの設定で
アプリケーションIDを指定して開始する必要があります。
「アプリケーションID」はGAEのサイトで発行したIDです。
認証にはプロジェクトを作成した時のGoogleのIDを指定します。

これだけで完了です。

StrutsのBlankApplicationは動作しましたけど、
Tilesを組み入れたアプリはいまのところ動作を確認していません。
load-on-startupが効かないみたいだからその辺なのかな?

log4jなのも少しダサい気もしますけど。。。
JDOも触ってみましょう!



月間500万PVまで大丈夫。。。って
どれだけのものを作らせようとしているのか。。。
まぁ今のところ使い道はないですけど
何か作りたいですね。

2008年12月6日土曜日

Javaのサーバモード

起動方式を変更します。

以前仕事で(その時はWindowsサーバ)で
デフォルトで結合試験を通り、いざシステムテストの負荷テストを
行ったら、でかいクエリを10発連続で飛ばしたら止まりました。。。

はてさてなんでだろう。。。とやっていて、
コネクション数、セッション継続時間、ソケット有効とかも変更しましたが
一応以下の文面も追加しました。

メモリを変更しないと動かない時が多かったので
これがわかりやすい問題の1つでした。
調べると難しい話しも多いのですが、
システム搭載の物理メモリの半分ぐらい使って良いのではないでしょうか?

アウトオブメモリに陥ったらどうぞお試しあれ。
※実装を直す暇があれば、適切かどうかを試すのが賢明かも。

環境変数に


CATALINA_OPTS=-Xmx256M -Xms256M -Xss256K -server


を追加します。
そうすると起動時に環境変数として盛り込まれます。

但し、Windowsのサービス起動(Tomcat.exe)だと、
この変数の影響がでない現象がありました。
何やらサービスの起動はレジストリからVMの値を
決定しているらしい。。。という結論に至りました。