“appengine” カテゴリのアーカイブ


Directory APIで 2-legged OAuth を使って認証

Google Appsのユーザー情報を同期しているappengineのアプリケーションでProvisioning APIを使用していましたが、deprecatedになっていました。(2015年で終了。。)
今後は Admin SDK Directory API を使用しろとのことなので、2-legged OAuthを使った使用方法を調査していましたがなかなか情報が見つからずに苦戦。。
試行錯誤の末ようやく成功したのでまとめてみます。

Directory API は ユーザーやグループのUniqueIDも取得できるので、メールアドレスの変更などにも追随できて管理がより楽になります。

サンプルコードの実行条件

  • appengine/java で GoogleAppsと連携するアプリケーション
  • Google Apps Marketplace に登録してある
  • AppsにはMarketplaceからインストールしてある

認証にはMarketplaceのCONSUMER_KEYとCONSUMER_SECRETを使用します。
またAppsの管理者権限があるID(メールアドレス)が必要になります。

ApplicationManifestに追加するスコープ

Google Apps Marketplaceの ApplicationManifestに以下のスコープが必要になります。


    <Scope id="userProvisioningAPI">
        <Url>https://www.googleapis.com/auth/admin.directory.user.readonly</Url>
        <Reason>This app displays all members in domain.</Reason>
    </Scope>
    <Scope id="groupProvisioningAPI">
        <Url>https://www.googleapis.com/auth/admin.directory.group.readonly</Url>
        <Reason>This app displays all groups in domain.</Reason>
    </Scope>

サンプルコード

まずはOAuthパラメータをセットしてDirectoryAPIインスタンスをビルドします。
2-legged OAuth は1.0にしか対応していないようで、API_KEYも使います。



        NetHttpTransport TRANSPORT = new NetHttpTransport();
        JacksonFactory JSON_FACTORY = new JacksonFactory();

        // The 2-LO authorization section
        OAuthHmacSigner signer = new OAuthHmacSigner();
        signer.clientSharedSecret = CONSUMER_SECRET;
     
        final OAuthParameters oauthParameters = new OAuthParameters();
        oauthParameters.version = "1";
        oauthParameters.consumerKey = CONSUMER_KEY;
        oauthParameters.signer = signer;
        //oauthParameters.signRequestsUsingAuthorizationHeader(transport);
        
        // Directory API構築
        directory = new Directory.Builder(
            TRANSPORT, JSON_FACTORY, null)
        .setApplicationName(APP_NAME)
        .setDirectoryRequestInitializer(new DirectoryRequestInitializer(API_KEY))
        .setHttpRequestInitializer(oauthParameters)
        .build();
        

ユーザー一覧の取得例です。customKeysにadminアカウントをセットします。


        customKeys = new ArrayMap<String,Object>();
        customKeys.add("xoauth_requestor_id", ADMIN_ACCOUNT);

        Directory.Users.List list = directory.users().list();
        list.setUnknownKeys(customKeys);
        list.setCustomer("my_customer");
        list.setMaxResults(USER_PAGE_MAX);

        List<User> result = new ArrayList<User>();
        for(;;) {
            Users users = list.execute();
            for (com.google.api.services.admin.directory.model.User u: users.getUsers()) {
                result.add(u);
            }
            if (users.getNextPageToken() != null) {
                list.setPageToken(users.getNextPageToken());
                continue;
            }
            break;
        }

setCustomer(“my_customer”); とすると管理下の全てのユーザーが取得できる。
次のページが有るかどうかは、nextPageTokenで判定。

OAuth 2.0 には対応していないようなんですが、どうなんでしょうか。少なくとも今のところ2.0でのやり方を見つけられていません。

Comments Provisioning APIがdeprecatedになったのでDirectory APIに移行してみた はコメントを受け付けていません。



appengineのSearch Service(full-text search) はまだexperimentalですが非常に魅力的な機能です。
正式リリースされればデータの種類によってはDatastoreの代わりに使用することもできそう。最大の特徴は検索の柔軟性にあるでしょうか。
ただし使ってみていくつか不思議な仕様に気づいたので注意が必要。簡単にまとめてみます。

DATE型は時刻を持てない

DATE: a date with no time component とドキュメントにあるようにあくまで日付だけしか持てないようです。
timestampを管理したい場合などはちょっと工夫が必要ですね。
自分はNUMBER型を使用して、特定日時(例えば2010-01-01 00:00:00)からの秒数で保持するようにしました。
※NUMBER型の範囲は -2,147,483,647 ~ 2,147,483,647

DATE型はSDKにバグ?

eclipseの環境でtime部分をクリアしたDateをこのフィールドに渡しても、= のクエリーでヒットしません。
例えば今日が 2013-04-24 だとして、 new Date() をセットすると、date_field = 2013-04-24 でも date_field >= 2013-04-24 でもヒットしません。
production環境ではtime部分をクリアしてもしなくても = でヒットします。

TEXT型は同一フィールド名で複数の値を保持可能

addFieldを繰り返して同じフィールド名に異なる値を追加できるようです。


Document.newBuilder()
    .addField(Field.newBuilder().setName("text1").setText("hoge")
    .addField(Field.newBuilder().setName("text1").setText("fuga")
    .build();

HTML型も可能でしたがNUMBER型はExceptionが発生しました。

TEXT型に = でクエリーした場合、どういう条件でヒットする?

Numeric operators only match against numeric and date fields. と書いてありますので当然かもしれませんが、TEXT型に = を指定したクエリーを実行しても完全一致にはなりませんでした。
例えば、text_field = hoge というクエリーを実行した場合に

  • “hoge_2” はヒットしません。
  • “this is hoge” はヒットします。
  • “@hoge” はヒットします。

という結果でした。
さらにSDKでは全然結果が異なりました。。(これもバグ??)

完全一致が使えると(全文検索以外に)使い途が広がるのですが、文字種を限定するなど工夫が必要そうです。

とりあえず気づいたのはこんなところですがまだまだ落とし穴があるかもしれません。

Comments App Engine Search (Full text search) の不思議な仕様 はコメントを受け付けていません。



ソースコードは こちら に公開しています。

概要

データストアのRead/Writeの無料課金枠が結構シビアなのでもはやキャッシュなしではやっていけないと思ったのがこれを作るきっかけでした。(速度的にはデータストアの呼び出しがそんなに遅いとは感じないのであくまで課金対策が主眼です。)

既存のコードにできるだけ手を入れないで実現したかったのでApiProxyを使ってdatastoreのAPI呼び出しをhookし、protocol bufferのrequest,responseをそのままキャシュしてはどうかと思いつきました。

キャッシュ対象とするのはQuery(RunQueryメソッド)でGetはもともと安いので対象外としました。RunQueryメソッドをhookしてキャッシュにデータが存在すればdatastore APIは呼び出さずにキャッシュしたレスポンスを返す、キャッシュにデータが存在しなければそのままAPI呼び出しをしてresponseをキャッシュする、というしくみになっています。
同一カインドのデータがPutまたはDeleteされた場合はキャッシュを無効にするようにカインド毎のResetDateを持って管理しています。

WriteよりReadのほうが圧倒的に多いシステムではキャッシュヒット率が高くなるのでこのしくみは有効かと思います。
逆に同一カインドのエンティティが頻繁に更新されるようなシステムではあまり効果がないかもしれません。

必要なライブラリ

  • commons-logging-1.1
  • commons-lang-2.4
  • gdata-core-1.0

使い方

必要なjarファイルを追加して、web.xmlのfilterChain先頭に以下のフィルター設定を追加するだけです。


    <filter>
        <filter-name>CacheContextFilter</filter-name>
        <filter-class>jp.honestyworks.pbcache.ContextFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>CacheContextFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>

その他の特徴

  • App EngineのMemcache Quota Limit を超えるサイズのデータも問題なくキャッシュされます。(適切なサイズにchunkされて保存します。)
  • memcacheだけでなくThreadLocalなコンテキストにもキャッシュを保持します。(Request毎にクリアされます。)
  • 上記ローカルキャッシュは使わない設定にすることもできます。
  • Production環境でしか有効になりません。

制限

  • datastore APIのasyncCallはsyncCallにデグレードします。asyncCallの恩恵を受けているシステムでは使用しないほうが良いかと思います。

本題とは関係ないけど、ktrwjr が便利

テストをするにあたり、slim3に組み込まれているktrwjrを使ってみましたがすごく便利!これは素晴らしいです。

ダウンロード

オープンソースとしてありますので自己責任でご自由にお使いください。

フィードバック

使っていただけた方は些細な事でもフィードバックいただけると嬉しいです。protocol buffer はあまり詳しくないので探り探りの実装になってます。
フィードバックは私のtwitterまでお寄せください。 (@miztaka)

Comments App Engine Java用データストアの透過キャッシュを作りました はコメントを受け付けていません。


概要

appengineでJSON-RPCサーバーのようなものを作りたいときなど ServletRequest#getInputStream()で取得できるInputStreamを使いたい場合があります。ところがslim3のcontrollerでこれをやろうとするとInputStreamのIllegalStateExceptionが発生します。
Jettyや大概のサーブレットサーバーは getInputStreamとgetParameter(s)を同時には使えないようです。
JSON-RPCならslim3のcontrollerは使う必要ないじゃんといえばそれまでなのですができれば慣れているもので全てやってしまいたいというのも事実。そこで以下のようなworkaroundで回避します。

  1. StreamFilterを作って、そこでServletRequestをWrapperクラスに置き換える (一番最初にこのFilterが動くようにする)
  2. RequestWrapperでは getInputStreamをoverrideし、Streamを再利用可能にする

なお、このworkaroundは ServletInputStreamを2回使う – うさ☆うさ日記 にて紹介されているやり方とほぼ同じです。(ナイスポストありがとうございました!)

StreamFilter


public class StreamFilter implements Filter {

    public void destroy() {
        // TODO Auto-generated method stub

    }

    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest req = (HttpServletRequest)request;
        request = new BufferedServletRequestWrapper( req );
        chain.doFilter(request, response);
    }

    public void init(FilterConfig arg0) throws ServletException {
        // TODO Auto-generated method stub

    }

}

BufferedServletRequestWrapper


public class BufferedServletRequestWrapper extends HttpServletRequestWrapper {
    
    private byte[] buffer;

    public BufferedServletRequestWrapper(HttpServletRequest request) throws IOException {
        super( request );

        InputStream is = request.getInputStream();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte buff[] = new byte[ 1024 ];
        int read;
        while( ( read = is.read( buff ) ) > 0 ) {
            baos.write( buff, 0, read );
        }

        this.buffer = baos.toByteArray();
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new BufferedServletInputStream( this.buffer );
    }

}

BufferedServletInputStream


public class BufferedServletInputStream extends ServletInputStream {

    private ByteArrayInputStream inputStream;

    public BufferedServletInputStream(byte[] buffer) {
        this.inputStream = new ByteArrayInputStream( buffer );
    }

    @Override
    public int available() throws IOException {
        return inputStream.available();
    }

    @Override
    public int read() throws IOException {
        return inputStream.read();
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        return inputStream.read( b, off, len );
    }

}

参考

Comments slim3のcontrollerでServletInputStreamを使いたいとき はコメントを受け付けていません。