2013年3月10日日曜日

Google+ Sign-in / Moment API

Google+ Platformの大きなアップデート、Google+ Sign-inが発表されました!詳細はニュース記事などに譲るとして、今回はとりあえずサインインしてAPIを呼び出すところまでやってみたのでまとめます。
こちらを参考に、ちょっと分からなかったところを補いつつ、Moment APIでApp Activityを送信するところまでやってみます。環境は、前回の記事で構築したものとなります。

準備その1:Client情報の登録

まずはGoogle+のClient IDとClient Srcretを取得します。公式ドキュメントのStep 1に従えばいいので省略します。

準備その2:必要なjarを配置

今回はMaven2で依存するjarを管理しているので、pom.xmlに設定を追加しておきます。WEB-INF/lib/にコピーも忘れずにしておきます。

  <repositories>
    <repository>
      <id>google-api-services</id>
      <url>http://google-api-client-libraries.appspot.com/mavenrepo</url>
    </repository>
  </repositories>

 ...

    <dependency>
      <groupid>com.google.api-client</groupid>
      <artifactid>google-api-client</artifactid>
      <version>1.13.2-beta</version>
    </dependency>
    <dependency>
      <groupid>com.google.api-client</groupid>
      <artifactid>google-api-client-servlet</artifactid>
      <version>1.13.0-beta</version>
    </dependency>
    <dependency>
      <groupid>com.google.api-client</groupid>
      <artifactid>google-api-client-appengine</artifactid>
      <version>1.13.2-beta</version>
    </dependency>
    <dependency>
      <groupid>com.google.apis</groupid>
      <artifactid>google-api-services-plus</artifactid>
      <version>v1-rev57-1.13.2-beta</version>
    </dependency>
    <dependency>
      <groupid>com.google.apis</groupid>
      <artifactid>google-api-services-oauth2</artifactid>
      <version>v2-rev9-1.7.2-beta</version>
    </dependency>

実装その1:CSRF対策

サインインボタンを表示するControllerを作成します。
この後説明するAccess Tokenの保存処理が不正に呼び出されないように、ランダムなパラメータを生成してセッションに保存しておきます。
同時に、Access Tokenの保存処理を呼び出すJavascriptにパラメータを渡せるように、リクエストにセットしておきます。

    @Override
    public Navigation run() throws Exception {
        String token = new BigInteger(130, new SecureRandom()).toString(32);
        sessionScope("validateToken", token);
        requestScope("validateToken", token);
        return forward("test.vm");
    }

実装その2:Javascriptライブラリの読み込み

Oauth2のAuthorization Code取得まではJavascriptでやるようなので、test.vmでGoogle+ライブラリを読み込んでおきます。
公式ドキュメントのStep 3のコピペでOKです。

実装その3:サインインボタンの設置

test.vmにサインインボタンを設置します。ここがポイントなのですが、
Moment APIを使う場合、data-requestvisibleactionsが必須になります。おそらくAPIの認可が必要ということなのでしょう。見事にハマったorz
他に指定できるパラメータはこちらに載っているのでご参照ください。

<div id="signinButton">
    <span class="g-signin"
        data-scope="https://www.googleapis.com/auth/plus.login"
        data-clientid="あなたのClient ID"
        data-redirecturi="postmessage"
        data-accesstype="offline"
        data-cookiepolicy="single_host_origin"
        data-callback="signInCallback"
        data-requestvisibleactions="http://schemas.google.com/AddActivity">
    </span>
</div>

実装その4:callback関数の実装

上記サインインボタンに設定したcallback関数を実装します。この中でAuthorization Codeを元にAccess Tokenを取得するリクエストを送信します。パラメータには、CSRF対策用のパラメータも設定します。

function signInCallback(authResult) {
    if (authResult['code']) {

        // Hide the sign-in button now that the user is authorized, for example:
        $('#signinButton').attr('style', 'display: none');

        // Send the code to the server
        jQuery.ajax({
            type: 'POST',
            url: '/api/storetoken',
            success: function(result) {
                
            },
            data: {
                code: authResult['code'],
                validateToken: '$validateToken',
            }
        });
    } else if (authResult['error']) {
        alert('Google+ Authorication Error');
    }
}

実装その5:Access Tokenの保存

JavascriptのPOSTを受けるControllerを実装します。まずはCSRF対策でパラメータチェックします。
その後、Authorization CodeとClient ID、Client Secretを使ってAccess Tokenを取得し、セッションに保存します。

    @Override
    public Navigation run() throws Exception {
        if (!validateToken()) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            Map<string string=""> data = new HashMap<string string="">();
            data.put("state", "403");
            return json(data);
        }

        String clientId = "あなたのClient ID";
        String clientSecret = "あなたのClient Secret";
        String code = asString("code");

        HttpTransport transport = new NetHttpTransport();
        JsonFactory jsonFactory = new JacksonFactory();
        GoogleTokenResponse tokenResponse = new GoogleAuthorizationCodeTokenRequest(transport, jsonFactory,
            clientId, clientSecret, code, "postmessage"
        ).execute();
        GoogleCredential credential = new GoogleCredential.Builder()
            .setJsonFactory(jsonFactory)
            .setTransport(transport)
            .setClientSecrets(clientId, clientSecret).build()
            .setFromTokenResponse(tokenResponse);
        Oauth2 oauth2 = new Oauth2.Builder(transport, jsonFactory, credential).build();
        Tokeninfo tokenInfo = oauth2.tokeninfo().setAccessToken(credential.getAccessToken()).execute();
        if (tokenInfo.containsKey("error")) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            Map<string string=""> data = new HashMap<string string="">();
            data.put("state", "403");
            return json(data);
        }
        if (!tokenInfo.getIssuedTo().equals(clientId)) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            Map<string string=""> data = new HashMap<string string="">();
            data.put("state", "403");
            return json(data);
        }

        sessionScope("gPlusId", tokenInfo.getUserId());
        sessionScope("gAccessToken", credential.getAccessToken());

        Map<string string=""> data = new HashMap<string string="">();
        data.put("state", "200");
        return json(data);
    }

    protected boolean validateToken() {
        String specified = requestScope("validateToken");
        String token = sessionScope("validateToken");
        if (token.equals(specified)) {
            return true;
        }
        return false;
    }

    protected Navigation json(Object data) {
        try {
            JsonFactory jsonFactory = new JacksonFactory();
            JsonGenerator generator = jsonFactory.createJsonGenerator(response.getWriter());
            generator.serialize(data);
            generator.flush();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return null;
    }

実装その6:Moment API 呼び出し

最後に、取得したAccess Tokenを使ってMoment APIを呼び出すControllerを実装します。

    @Override
    public Navigation run() throws Exception {
        String gPlusId = sessionScope("gPlusId");
        String gAccessToken = sessionScope("gAccessToken");

        String clientId = "あなたのClient ID";
        String clientSecret = "あなたのClient Secret";
        String applicationName = "あなたのアプリ名";

        HttpTransport transport = new NetHttpTransport();
        JsonFactory jsonFactory = new JacksonFactory();
        GoogleCredential credential = new GoogleCredential.Builder()
            .setJsonFactory(jsonFactory)
            .setTransport(transport)
            .setClientSecrets(clientId, clientSecret).build()
            .setAccessToken(gAccessToken);

        Plus plus = new Plus.Builder(transport, jsonFactory, credential).setApplicationName(applicationName).build();

        Moment moment = new Moment();
        moment.setType("http://schemas.google.com/AddActivity");
        ItemScope itemScope = new ItemScope();
        itemScope.setId("target-id-1"); 
        itemScope.setType("http://schemas.google.com/AddActivity");
        itemScope.setName("The Google+ Platform");
        itemScope.setDescription("hogehoge!");
        moment.setTarget(itemScope);
        Moment momentResult = plus.moments().insert("me", "vault", moment).execute();

        MomentsFeed moments = plus.moments().list("me", "vault").execute();

        return redirect("/");
    }

自分の英語力不足なのか、公式ドキュメントからは分からなくてハマったところも多かったですが(Oauth2クラスとかTokenInfoクラスを使うのにどのjarが必要か、とか)、なんとか出来ました。
Client IDなどを設定ファイルにしたりなどリファクタリングもまだまだ必要ですが。
道具は揃ったので、これをどう使っていくか考えてみようと思ってます!

2013年3月1日金曜日

Slim3 + Velocity

Google App Engine for Javaを使いたくなり、評判の良さそうなフレームワークとしてSlim3を導入し、独断と偏見でJSPをやめてVelocityをテンプレートエンジンとして利用することにしました。
今回の内容は、その際の開発環境セットアップの備忘録となります。

開発環境

それぞれ現時点での最新版を使います。
  • Eclipse Classic 4.2.1 Mac OS X 64bit
  • Slim3 1.0.16
  • Velocity 1.6.2 + Velocity Tools 2.0

Slim3インストール

基本的にここを参考にEclipseにGoogle PluginとSlim3 Pluginをインストールします。ダウンロード先のURLがEclipseのバージョンに一致しているか確認します。今回は以下を使います。

https://dl.google.com/eclipse/plugin/4.2

http://slim3.googlecode.com/svn/updates/

Pluginのインストールが終わったら、Blankプロジェクトを作成します。Explorerで右クリック -> New -> Others -> Slim3と進めばOKです。上記Wikiに書いてあることのうち、workspaceの設定以外はPluginが勝手にやってくれます。

Velocityインストール

こちらを参考にさせて頂きました。が、いくつか補足。

まずは、http://velocity.apache.org/download.cgiからVelocity Toolsをダウンロード、解凍します。lib内にあるvelocity-1.6.2.jarとvelocity-tools-2.0.jar、それから依存関係のあるcommonsライブラリをコピーして、
先ほど作成したBlankプロジェクトのWEB-INF/lib/にペーストし、ビルドパスに追加します。

次に、src以下にVelocityViewServletを拡張したクラスを作成します。ひとまず以下のように実装しておけばOKです。

package test.hoge;

import java.util.Enumeration;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.velocity.Template;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.context.Context;
import org.apache.velocity.tools.view.VelocityViewServlet;

public class VelocityServlet extends VelocityViewServlet {

    private static final long serialVersionUID = 1L;

    @Override
    protected Template handleRequest(HttpServletRequest request,
            HttpServletResponse response, Context ctx) {
        String uri = request.getRequestURI();
        String templatePath = getTemplatePath(uri);

        Enumeration attrNames = request.getAttributeNames();
        while(attrNames.hasMoreElements()) {
            String key = (String) attrNames.nextElement();
            ctx.put(key, request.getAttribute(key));
        }

        Template template;
        try {
            template = Velocity.getTemplate(templatePath, "UTF-8");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return template;
    }

    protected String getTemplatePath(String uri) {
        return "/WEB-INF/template" + uri;
    }
}

作成したら、このサーブレットを使用するようにweb.xmlに設定を追加します。

    <servlet>
        <servlet-name>VelocityServlet</servlet-name>
        <servlet-class>test.hoge.VelocityServlet</servlet-class>
       <init-param>
            <param-name>org.apache.velocity.toolbox</param-name>
            <param-value>/WEB-INF/velocity-tools.xml</param-value>
        </init-param>
        <init-param>
            <param-name>org.apache.velocity.properties</param-name>
            <param-value>/WEB-INF/velocity.properties</param-value>
        </init-param>
        <load-on-startup>10</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>VelocityServlet</servlet-name>
        <url-pattern>*.vm</url-pattern>
    </servlet-mapping>

これで、拡張子が.vmのものはVelocityServletを使用するようになります。velocity-tools.xmlとvelocity.propertiesはとりあえず空でいいので作成しておきます。

次に、appengine-web.xmlに以下を追加します。ついでにセッションは有効にしておきましょう。

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

    <static-files>
        <include path="**.js" />
        <include path="**.css" />
        <include path="**.png" />
        <include path="**.jpg" />
        <include path="**.jpeg" />
        <include path="**.swf" />
        <include path="**.gif" />
        <include path="**.ico" />
        <include path="**.html" />
    </static-files>
    <resource-files>
        <include path="**.jsp" />
        <include path="**.vm" />
    </resource-files>

Controller + .vmファイル作成

ここまで来たら、Antのgen-controller-without-viewでSlim3のControllerを作成します。JSPファイルの代わりに手動で.vmファイルをWEB-INF/template/に作成しましょう。.vmファイルの置き場所は拡張VelocityServletに書いてあるので、WEB-INF/の好きな場所に変えてしまってOKです。
Controllerと.vmの準備が出来たら、ローカル環境でサーバを起動してブラウザでアクセスできるか確認します。

ちなみに、Slim3 + Velocityの処理の流れは大まかに以下のようになっています。
  1. Controller#run()が呼ばれる
  2. **.vmにforward
  3. forwardで指定された拡張子が.vmなのでVelocityServletが起動
  4. pathで指定された.vmファイルがレンダリングされる
Velocityを入れるとJSPに比べればパフォーマンス劣るというのをちらっと見かけたのですが、とりあえず気にせずこの環境で色々作ってみようと思います。