SiNBLOG

140文字に入らないことを、極稀に書くBlog

Closure Templatesを使ってみた!

Google Closure Toolsの1つClosure Templatesを試してみました。
Closure Templates  |  Google Developers

Closure Templates単体でも使えますが、私はClosure Libraryと共に使っています。
Java版とJavaScript版がありますが、私が使ったのはJavaScript版です。

Closure TemplatesはTemplatesのためのファイルを作成し、それをコンパイルすることでjsができあがります。
まずは、Templateのためのファイルです。
book.soy

{namespace templates.book}

/**
 * Book.
 * @param isbn The isbn of the book.
 * @param name The name of the book.
 * @param link The link of the book.
 */
{template .book}
  <div class="book">
    <div class="{css amazon}"><a href="{$link}">amazon</a></div>
    <div class="{css isbn}">{$isbn}</div>
    <div class="{css name}">{$name}</div>
  </div>
{/template}

/**
 * Book List.
 * @param bookList
 */
{template .bookList}
  {foreach $book in $bookList}
    {call .book}
      {param isbn: $book.isbn /}
      {param name: $book.name /}
      {param link: $book.link /}
    {/call}
  {ifempty}
    No book list.
  {/foreach}
{/template}

templateのためのファイルは、soyという拡張子で作成します。
$xxxがtemplate変数となり、値を受け取る事ができるようになっています。
また、上記の例だと2つのtemplateを定義しています。
1つ目が{template .book}です。
中身は殆ど無く、divで囲って値を表示しているだけです。
2つ目は{template .bookList}です。
こっちは、foreachでループするようになっています。
また、中で{template .book}をcallしています。
このように、複数のtemplateを組み合わせることも可能です。


soyファイルが出来たら、SoyToJsSrcCompiler.jarで、Jsへとコンパイルします。
コマンドから実行可能ですが、Eclipseを使っているので、Antを利用してみました。
build.xml

<?xml version="1.0"?>
<project basedir="." default="soy_to_javascript">
	<target name="book">
		<property name="target" value="../index.html" />
		<antcall target="_create_soy" />
	</target>

	<target name="soy_to_javascript">
		<java jar="../../../../lib/SoyToJsSrcCompiler.jar" fork="true">
			<arg value="--outputPathFormat" />
			<arg value="../scripts/templates.js" />
			<arg value="--shouldProvideRequireSoyNamespaces" />
                        <arg value="book.soy" />
		</java>
	</target>
</project>

Closure TemplateがJavaScriptのテンプレートエンジンでいい気がしてきた - Lush Life
こちらを参考に作ったのですが、何故か自動では動いてくれず・・・。
自動で動かせるようになったら、また追記したいと思います。


因みにbook.soyをJsにコンパイルすると、以下のようになります。
templates.js

// This file was automatically generated from book.soy.
// Please don't edit this file by hand.

goog.provide('templates.book');

goog.require('soy');
goog.require('soy.StringBuilder');


templates.book.book = function(opt_data, opt_sb) {
  var output = opt_sb || new soy.StringBuilder();
  output.append('<div class="book"><div class="amazon"><a href="', soy.$$escapeHtml(opt_data.link), '">amazon</a></div><div class="isbn">', soy.$$escapeHtml(opt_data.isbn), '</div><div class="name">', soy.$$escapeHtml(opt_data.name), '</div></div>');
  return opt_sb ? '' : output.toString();
};


templates.book.bookList = function(opt_data, opt_sb) {
  var output = opt_sb || new soy.StringBuilder();
  var bookList26 = opt_data.bookList;
  var bookListLen26 = bookList26.length;
  if (bookListLen26 > 0) {
    for (var bookIndex26 = 0; bookIndex26 < bookListLen26; bookIndex26++) {
      var bookData26 = bookList26[bookIndex26];
      templates.book.book({isbn: bookData26.isbn, name: bookData26.name, link: bookData26.link}, output);
    }
  } else {
    output.append('No book list.');
  }
  return opt_sb ? '' : output.toString();
};

ただのJsなので、デバッグ時にはこの中にconsole.log()とかを埋め込めば、普通に実行されます。


次にtemplateを呼び出している箇所です。
app.js

goog.provide('xhrmanagerSample.App');
goog.require('goog.soy');
goog.require('templates.book');

goog.scope(function(){
    var gdom = goog.dom;
    var gsoy = goog.soy;

    /**
     * ajax get book list compete.
     * @param {Object} e
     */
    xhrmanagerSample.App.prototype.onGetBookListComplete = function(e){
        var bookList = gdom.getElement('book-list');
        gdom.removeChildren(bookList); //domの中身を吹き飛ばしている
        
        var data = {
            bookList: []
        };
        for (var i = 0, len = e['bookList'].length; i < len; i++) {
            data.bookList[i] = {
                isbn: e['bookList'][i]['isbn'],
                name: e['bookList'][i]['name'],
                link: e['bookList'][i]['link']
            };
        }
        gdom.append(bookList, gsoy.renderAsElement(templates.book.bookList, data));
    };
});

猛烈に省略しているのですが、呼び出しているの上記の部分です。
ajaxで取ってきたjsonの値を渡しているのですが、ここで少し困ったことがあります。
Closure Libraryを使っているため、最終的にClosure Compilerでコンパイルを行います。
その時に、templateの中の変数名なども短縮されます。
しかし、ajaxで取ってきたjsonの中の名前は短縮されませんので、一致しません。
そのため、上記のようにJsでリマッピングしてやります。
この辺りは、どうすれば良いのかさっぱり分からなかったので、伊藤殿に聞いてしまいました・・・。
https://plus.google.com/u/0/114061805681880927213/posts/TbB36g3PdNY
伊藤殿、いつもありがとうございますorz


因みにClosure Compilerをかけた後だと、{template .bookList}は以下のようになっていました。

function Ic(a, b) {
    var c = b || new V, d = a.Hb, f = d.length;
Uncaught TypeError: Cannot read property 'length' of undefined
    if (0 < f)
        for (var e = 0; e < f; e++) {
            var h = d[e], l = h.ic, p = h.name, h = h.link;
            (c || new V).append('<div class="book"><div class="', "amazon", '"><a href="', Dc(h), '">amazon</a></div><div class="', "isbn", '">', Dc(l), '</div><div class="', "name", '">', Dc(p), "</div></div>")
        }
    else
        c.append("No book.");
    return b ? "" : c.toString()
};

opt_data.bookListだったのものa.Hbに。
bookList26.lengthだったものが、d.lengthになっています。
ajaxで取得したjsonをそのまま渡していた時は、a.Hbでundefinedになっていまいました。
そのため、上記のようなリマッピングをしてやったのですね。


また、このエントリーでは色々と省略している部分があるので、伊藤殿のBlogも合わせて見ることをお勧めします。
Closure Stylesheets で CSS を最適化する (2) - WebOS Goodies


他にもこんなBlogを書いている方もいらっしゃいました。
Closure Templatesのオートエスケープが最強すぎる件 - teppeis blog

私はClosure Libraryと組み合わせて使っていますが、単体で使っても悪くないかもしれませんね。
jQuery.template()だとjspでは$がかち合って使えませんが、Closure TemplateならばJsにコンパイルされた後は、独自の文字はありませんので。


最後に、今回のサンプルの全ソースです。
book.soy
Google Code Archive - Long-term storage for Google Code Project Hosting.
build.xml
Google Code Archive - Long-term storage for Google Code Project Hosting.
app.js
Google Code Archive - Long-term storage for Google Code Project Hosting.


一応、動くものはこちらです。
ViewBookListボタンを押すと、本の一覧が出てくるだけですが・・・。
ログイン - Google アカウント

Slim3でClosure LibraryのUnitTestを使ってみた・・・が。

UnitTestが無いコードは、レガシーコード!と言われるぐらい昨今UnitTestは重要なものとなっています。
僕もフレームワークに求めるのはテストのしやすさなのかも!と思ってます。

ClosureLibraryにもUnitTestをするために機能があります。
JsUnitを使ったテストをサポートしており、ブラウザ上で実行できるようになっています。

こちらのBlog記事に詳しく書いて下さってます。
Closure Library 超入門 〜テスト編〜 - present


さて、それをSlim3で使う場合、どうするかですが、以下のようにしてみました。


war
| all_test.html //全テスト実行用のhtml

+--closurelibrary
|
+--ajaxsample
+--files
| scripts.js
| stylesheet.css
+--gss
| stylesheet.gss
+--scripts
| app.js
| deps.js
| ajaxsample_test.html //これがUnitTestのhtml
| index.html
| depswriter.bat


ただ、Production環境にはUnitTest用のhtmlをUploadしたくないので、
appengine-web.xmlのstatic-filesとresource-filesに
を設定します。


















これで、Production環境ではUnitTest用のhtmlが、404になるようになりました。


それでは、UnitTest用のhtmlの中身です。
all_test.html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>AllTest</title>
        <link rel="stylesheet" href="/closure-library/closure/goog/css/multitestrunner.css">
    </head>
    <body>
        <!--ここにテストランナーを表示-->
        <div id="runner">
        </div>
        <script type="text/javascript" src="/closure-library/closure/goog/base.js">
        </script>
        <script type="text/javascript">
            goog.require("goog.testing.MultiTestRunner");
            goog.require("goog.dom");
        </script>
        <script type="text/javascript">
            var testRunner = new goog.testing.MultiTestRunner();

            var testPaths = ["/closurelibrary/ajaxsample/ajaxsample_test.html"];
            testRunner.addTests(testPaths);

            testRunner.render(goog.dom.getElement("runner"));
        </script>
    </body>
</html>

サンプルから持ってきたもの、そのままです。
一番、上に張ったBlog記事から持ってきたものなので、そちらをご覧ください。

動くものが見たい方もいるかと思うので、Production環境でも動くようにしてみました。
中身は薄いですが・・・。
ログイン - Google アカウント

ajaxsample.html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Ajax Sample Test</title>
        <script src="/closure-library/closure/goog/base.js">
        </script>
        <script>
            goog.require('goog.testing.AsyncTestCase');
            goog.require('goog.testing.jsunit');
        </script>
        <script src="deps.js">
        </script>
    </head>
    <body>
        <div id="xhr-indicator">
            アクセス中
        </div>
        <script>
            goog.require('ajaxsample.App');
        </script>
        <script>
            var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();

            function testXhrIo(){
                ajaxsample.App.getInstance().sendHoge();
            }
        </script>
    </body>
</html>

そして、こちらが問題でして・・・。
タイトルの"・・・が"の部分です。
どう書けば、自分がやりたいUnitTestができるのか分からなかったのですね・・・。
とりあえず、ajaxsample.Appのメソッドを呼ぶことはできました。
しかし、今回テストしようとしたのは、Ajax部分です。
できれば、Serverとのやり取りの部分をMock化して、やりたかったのですが、
どうすれば良いのか分かりませんでした。

Google API Expertが解説する Closure Libraryプログラミングガイド

Google API Expertが解説する Closure Libraryプログラミングガイド

ClosureLibrary本にもテストフレームワークのことは触れられているのですが、
僕の応用力が足りないため、なかなか思いつかず・・・。

テストしようとしているAjaxのメソッドはtinywordと同じくXhrManagerを利用しています。
本にはXhrManagerのsend()メソッドをMockにするという手もあると書いてあるのですが、
どうやって?というレベルで止まってます・・・。


また、何故か置いてある以下の部分。

<div id="xhr-indicator">
    アクセス中
</div>

Ajax通信時にインジケーターを表示しているのですが、中でこのdivを取得しています。
そのため、これが無いとErrorになるので、置いているのですが、これもどうすれば良いのか・・・。
まだまだ、ClosureLibraryのUnitTestを扱うためには修行が必要そうです。


こちらもProduction環境で動くようにしてみました。
ログイン - Google アカウント
上にアクセス中とか出ているのが、インジケーターです。
妙にそれっぽく出ていますが、出そうとして出しているわけではなく、上記の理由により出ています。


以下がテストしようとしているソースです。
index.html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Ajax Sample</title>
        <link rel="stylesheet" href="/tinyword/files/stylesheet.css">
        <script src="/closure-library/closure/goog/base.js">
        </script>
        <script src="deps.js">
        </script>
        <!--
        <script src="files/scripts.js">
        </script>
        -->
    </head>
    <body>
        <h1 id="title">AjaxSample</h1>
        <div id="xhr-indicator">
            アクセス中
        </div>
        <!-- このdiv内にアプリケーションUIを表示する -->
        <div id="main">
        </div>
        <script>
        goog.require('ajaxsample.App');
        </script>
    </body>
</html>


app.js

goog.provide('ajaxsample.App');
goog.require('goog.events.EventHandler');
goog.require('goog.ds.DataManager');
goog.require('goog.ds.JsDataSource');
goog.require('goog.net.XhrIo');
goog.require('goog.net.XhrManager');
goog.require('goog.object');
goog.require('goog.fx.dom');
goog.require('goog.History');
goog.require('goog.Uri');

goog.scope(function(){
    var dom = goog.dom;
    var events = goog.events;
    var EventType = goog.events.EventType;
    var DataManager = goog.ds.DataManager;
    var JsDataSource = goog.ds.JsDataSource;

    /** @constructor */
    ajaxsample.App = function(){
        this.initialize_();
    };
    goog.addSingletonGetter(ajaxsample.App);

    // アプリケーションを初期化
    ajaxsample.App.prototype.initialize_ = function(){
        // イベントハンドラを管理するためのEventHandlerを生成
        this.eventHandler_ = new events.EventHandler(this);

        this.xhrManager_ = new goog.net.XhrManager();
        this.nextXhrId_ = 1;

        //インジケータ設定
        var indicator = dom.getElement('xhr-indicator');
        this.fadeInIndicator_ = new goog.fx.dom.FadeInAndShow(indicator, 10);
        this.fadeOutIndicator_ = new goog.fx.dom.FadeOutAndHide(indicator, 1000);
        this.fadeOutIndicator_.play(true);

        this.eventHandler_.listen(this.xhrManager_, goog.net.EventType.READY, this.onXhrReady_);
        this.eventHandler_.listen(this.xhrManager_, goog.net.EventType.COMPLETE, this.onXhrComplete_);
    };

    ajaxsample.App.prototype.sendHoge = function() {
       this.sendRequest('get', {
                'text': 'hoge'
            }, goog.bind(this.hogeComplete, this), 'PUT', {
                '#text': name
            });
    };

    ajaxsample.App.prototype.hogeComplete = function() {
        console.log('hogeComplete!!!');
    };

    ajaxsample.App.prototype.sendRequest = function(path, query, callback, method, opt_content){
        var url = goog.Uri.parse('/closurelibrary/ajaxsample/' + path);
        goog.object.forEach(query || {}, function(value, key){
            url.setParameterValue(key, value);
        }, this);
        var headers = {}, body = null;
        if (opt_content) {
            body = goog.json.serialize(opt_content);
            headers['Content-Type'] = 'application/json';
        }
        return this.xhrManager_.send(this.nextXhrId_++, url, method, body, headers, 0, goog.bind(this.processRequest_, this, callback));
    };

    ajaxsample.App.prototype.processRequest_ = function(callback, e){
        var xhr = e.target;
        if (xhr.isSuccess()) {
            callback && callback(xhr.getResponseJson('while(1);'));
        } else {
            alert(xhr.getResponseText());
        }
    };

    ajaxsample.App.prototype.onXhrReady_ = function(e){
        if (this.xhrManager_.getOutstandingCount() == 1) {
            this.fadeOutIndicator_.stop(false);
            this.fadeInIndicator_.play(true);
        }
    };

    ajaxsample.App.prototype.onXhrComplete_ = function(e){
        if (this.xhrManager_.getOutstandingCount() == 1) {
            this.fadeInIndicator_.stop(false);
            this.fadeOutIndicator_.play(true);
        }
    };

    ajaxsample.App.getInstance();
});

最後にGoogleCodeのリポジトリのリンクを貼っておきます。
Google Code Archive - Long-term storage for Google Code Project Hosting.

上記以外の物も色々と置いてますが、参考程度に・・・。

Slim3でClosure Libraryを使ってみた!

ClosureLibraryはGoogleが作成しているJavaScriptのLibraryです。
Gmail,Google CalendarなどGoogleのサービスで利用されています。

Closure Library  |  Google Developers

なかなか面白いもので、以下のような特徴があります。


豊富なUIコンポーネント
jQuery UIのような豊富なUIコンポーネントがあります。
GoogleCalendarで予定を入力する時に出てくるjQuery UI DatepickerのようなUIみたいなやつです。


豊富なツールのサポート
ClosureLibraryはいくつかのツールと共に利用します。

depwriter
jsの依存関係を解決するツールです。
ClosureLibraryでは、Javaのimportのような形で、依存しているライブラリを記述します。
それらを読み取って解決するのがdepwriterです。


closurebuilder
ClosureLibraryは、最後にコンパイルを行います。
とは言えDartのようなものではありません。
コンパイル前も、ブラウザ上で動作し、デバッグすることも可能です。
ただし、非常に多くのjsファイルとcssの読み込みが必要です。
そのため、本番環境に上げる前に、依存関係を解決し、1つのjsファイルに統合します。
その時に、ミニファイも行います。


closure-stylesheets
CSSの統合するツールです。
UIコンポーネントCSSや自分で作ったCSSを統合して、1つのcssファイルにします。


他にも色々ありますが、僕もまだかじった程度なので、詳しくはGoogle API Expartの伊藤 千光殿のブログと見ると良いでしょう。
Closure Library を使うべき 10 の理由 - WebOS Goodies

また、伊藤殿の書籍もあります。
今回作ったサンプルも、この書籍のサンプルを写経しました。

Google API Expertが解説する Closure Libraryプログラミングガイド

Google API Expertが解説する Closure Libraryプログラミングガイド

とりあえず、ClosureLibraryの初期設定は、やはり伊藤殿のブログを見ていただくとして・・・w
Closure Libraryによるアプリ開発のはじめ方 - WebOS Goodies


実際にSlim3で使う時に、やりたくなるのは以下の点。

  1. Local環境では、デバッグしたいので、コンパイル前の状態で動かしたい
  2. Production環境では、高速に動作させたいので、コンパイル後の状態で動かしたい

とりあえず、1つ目の条件を満たすためにwar直下にどーんとClosureLibraryを配置。
問題は、そうするとそのままProduction環境にどーんと上がってしまう点。
ClosureLibraryのファイル数はかなり多いため、GAEの3000ファイル制限を圧迫してしまう。
また、Production環境ではコンパイル後の統合されたjsを利用するため、必要ない。

それを解決してやるために、以下の設定をしてやる。
appengine-web.xml






たぶん、きっと、これで出来ているはず・・・。
GoogleGroupで教えて下さった方々ありがとうございました。
Google グループ

.batもはじいているのは、ClosureLibraryのツールを実行するのを.batで書いているため。
例えば、depwriterを実行するためのbatはこんな感じ。


@echo off

encode is shift-jis
commnet

echo if you run the depwriter please press the y key
set /p CONFIRM=

run depwriter

if "y" == "%CONFIRM%" (
echo depwriter
python ../closure-library\closure\bin\build\depswriter.py --root_with_prefix="scripts ../../../tinyword/scripts" --output_file=deps.js
pause
)

あんまり、batは詳しくないので、模倣する価値があるかは分かりませんが、参考程度に。


作成したもののファイル構成は以下の通り。
基本的に書籍のサンプルのままで、batファイルをそのまま突っ込んでる感じw


tinyword
| closure-stylesheets.bat
| closurebuilder.bat
| deps.js
| depswriter.bat
| index.html

+--files
| scripts.js //Production用
| stylesheet.css //Production用

+--gss
| stylesheet.gss

+--scripts
| app.js
| editor.js
| leftpane.js
| mapsplugin.js
| renaming_map.js
| rightpane.js
| savedialog.js
| savedialogplugin.js
| treecontrol.js
| treenode.js
//Production用と書いてあるファイルだけが、本番環境で実際に読み込むファイルです。
scripts配下のファイルを、コンパイルした結果が、files下のscripts.jsです。
gssはSassやLESSのようなCSS拡張機能を持ったものです。
また、ClosureLibraryと組み合わせることにより、CSSクラス名を短縮することもできます。
これを統合した結果が、files下のstylesheet.cssです。
今回はgss配下には1ファイルしかありませんが、複数ファイルを統合することも可能です。


実際に利用しているhtmlファイルが以下です。
コメントアウトしているところが、Local環境で使っていた部分です。
index.html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Tiny Word</title>
        <link rel="stylesheet" href="/tinyword/files/stylesheet.css">
        <!--
        <script src="/closure-library/closure/goog/base.js">
        </script>
        <script src="deps.js">
        </script>
        -->
        <script src="files/scripts.js">
        </script>
    </head>
    <body>
        <h1 id="title">Tiny Word</h1>
        <div id="xhr-indicator">
            アクセス中
        </div>
        <!-- このdiv内にアプリケーションUIを表示する -->
        <div id="main">
        </div>
        <!--
        <script>
        goog.require('tinyword.App');
        </script>
        -->
    </body>
</html>

この辺りも自動で変えれれば最高ですが、そこまではまだ思いつかず・・・。

最後に、動いているものを。
書籍のサンプルを写経したのですが、書籍はpythonなので、slim3に作り変えています。
が、途中で、力尽きたので、新規フォルダ作成と名前変更しか動きませんorz
ログイン - Google アカウント


それぞれの機能の細かい話も書きたいですが、とりあえずこんなところで!


因みに上記の書籍を含む、GoogleAPIExpertが書いた書籍6点が電子書籍化されています。
2012/5/31まで、6点まとめ買いで、割引が!
http://tatsu-zine.com/api-expert

GAE/JでGson利用時にjava.lang.VerifyErrorが発生する

Jsonを利用するライブラリとして、Gsonを利用しているのですが、最近Errorが出る。
しかも、それがProduction環境のみという、なかなか辛い状況。
更に原因がよく分からず、デプロイする度に変わるという辛い状況。

Errorが発生しているのは、Gsonのコンストラタ実行時。


Gson gson = new Gson();

普通にコンストクタを実行しているだけのつもり・・・。

環境はSDK 1.6.0, Slim3 1.0.15, gson2.1

Gson gson = new Gson();するだけのプロジェクトを作成して、デプロイしてみたところ
正常に動いているので、必ずしもgsonを使う部分に問題があるとも言い難い感じ・・・。

stackoverflowは以下の通り。


Error for /urlfetch/
java.lang.VerifyError: Cannot inherit from final class
at com.google.appengine.runtime.Request.process-74f4d481c055ff4e(Request.java)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:634)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:277)
at sun.reflect.GeneratedMethodAccessor5.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:616)
at java.lang.ClassLoader.loadClass(ClassLoader.java:266)
at org.sinsandbox.controller.urlfetch.IndexController.buildeTweetList(IndexController.java:66)
at org.sinsandbox.controller.urlfetch.IndexController.run(IndexController.java:36)
at org.slim3.controller.Controller.runBare(Controller.java:111)
at org.slim3.controller.FrontController.processController(FrontController.java:491)
at org.slim3.controller.FrontController.doFilter(FrontController.java:277)
at org.slim3.controller.FrontController.doFilter(FrontController.java:237)
at org.slim3.controller.FrontController.doFilter(FrontController.java:199)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
at org.slim3.datastore.DatastoreFilter.doFilter(DatastoreFilter.java:55)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
at org.slim3.controller.HotReloadingFilter.doFilter(HotReloadingFilter.java:192)
at org.slim3.controller.HotReloadingFilter.doFilter(HotReloadingFilter.java:157)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:388)
at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:182)
at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:765)
at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:418)
at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
at org.mortbay.jetty.Server.handle(Server.java:326)
at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542)
at org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.java:923)
at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404)
at com.google.tracing.TraceContext$TraceContextRunnable.runInContext(TraceContext.java:449)
at com.google.tracing.TraceContext$TraceContextRunnable$1.run(TraceContext.java:455)
at com.google.tracing.TraceContext.runInContext(TraceContext.java:695)
at com.google.tracing.TraceContext$AbstractTraceContextCallback.runInInheritedContextNoUnref(TraceContext.java:333)
at com.google.tracing.TraceContext$AbstractTraceContextCallback.runInInheritedContext(TraceContext.java:325)
at com.google.tracing.TraceContext$TraceContextRunnable.run(TraceContext.java:453)
at java.lang.Thread.run(Thread.java:679)

IndexController.buildeTweetList(IndexController.java:66)
上の行が、実際にErrorが発生している行で、Gson gson = new Gson();です。

よく分からないので、いじくり回していたのだけど、appengine-web.xmlを変えたら、何故か直った。
しかし、デプロイし直したから、直ったような気もするし、よく分からない。
また、別のAppでも同じErrorが出るのだけど、appengine-web.xmlの設定をコピーしてもダメだった。

appengine-web.xmlのbefore afterは以下の通り。
before



sin4sandbox


1


true





















true


true


after



sin4sandbox


201204191949


true


















true


threadsafeがtrueになっていたからかとも思ったのだけど、コメントアウトしても変わらず・・・。
デプロイ時に何かがしくじっているだけなのか・・・、他に原因があるのか・・・。
今のところ、分からない・・・。

最後にGsonをnewしているControllerのソース


package org.sinsandbox.controller.urlfetch;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;

import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

/**
* UrlFetchIndexController
*
* @author Sinmetal
*
*/
public class IndexController extends Controller {

/**
* Twitter検索APIをFetchする
*/
@Override
public Navigation run() throws Exception {
final String TWITER_SEARCH_URL =
"http://search.twitter.com/search.json?q=gaeja";
String tweetJson = fetchUrl(TWITER_SEARCH_URL, "UTF-8");

List tweetList = buildeTweetList(tweetJson);

requestScope("tweetList", tweetList);
return forward("index.jsp");
}

/**
* 指定したURLをFetchする
*
* @param urlStr
* @param encoding
* @return
* @throws IOException
*/
private String fetchUrl(String urlStr, String encoding) throws IOException {
URLFetchService service = URLFetchServiceFactory.getURLFetchService();
URL url = new URL(urlStr);
HTTPResponse res = service.fetch(url);
return new String(res.getContent(), encoding);
}

/**
* Twitter検索APIの結果から、TweetListを作る
*
* @param searchJson
* @return
*/
private List buildeTweetList(String searchJson) {
List tweetList = new ArrayList();
try {
Gson gson = new Gson();
JsonElement element = gson.fromJson(searchJson, JsonElement.class);
JsonObject json = element.getAsJsonObject();
JsonArray array = json.getAsJsonArray("results");
for (int i = 0; i < array.size(); i++) {
Object obj = array.get(i);
if (obj instanceof JsonObject) {
tweetList.add((JsonObject) obj);
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return tweetList;
}
}

何かお心当たりがある方は、是非コメントかtwitterで教えてください。

Slim3 Source Code Reading No12 Router

Slim3 Source Code Reading #12 - ChugokuGTUG

に参加してきました!


今回も@ttyokoyama 殿と2人だったので、まったりと進みました。
まぁ、他の方が参加していても、まったり進むのですがw

T.Yokoyamaのブログ: Slim3 Source Code Reading #12


今回はRouterです。
Google Code Archive - Long-term storage for Google Code Project Hosting.

スマートなURLを実現するための機能ですね。
URLマッピング - Slim3 日本語サイト(非公式)


読んでいて気になった点は以下の2つ。

  • 静的ファイルの判定で、拡張子が"s3"で始まらないことという条件がある
  • 全キャッチのプレースホルダー(*xxx)の使い道

1つ目の静的ファイルの判定ですが、RouterImpl#isStatic()にあります。
Google Code Archive - Long-term storage for Google Code Project Hosting.


public boolean isStatic(String path) throws NullPointerException {
if (path == null) {
throw new NullPointerException("The path parameter is null.");
}
if (path.startsWith("/_ah/")) {
return false;
}
String extension = RequestUtil.getExtension(path);
return extension != null && !extension.startsWith("s3");
}

RouterImplはデフォルトのRouterです。URLマッピングをする時は、このクラスを継承して条件を追加します。
if (path.startsWith("/_ah/"))は管理用のページをはじいています。"s3"は"Slim3"の略だと思われるので、拡張子の部分の最初が"s3"になるような機能があるのかな・・・?
ちょっと、拡張子が"s3"で始まるようなパターンは思いつかなかったので、なぞ。


2つ目の全キャッチのプレースホルダーですが、公式の使い方を見ると、こんな感じ。


public class AppRouter extends RouterImpl {

public AppRouter() {
addRouting(
"/{app}/find/*path",
"/{app}/find?path={path}");
}
}

"*"を指定すると、問答無用でそこから最後までの文字列がパラメータになるようです。
どんなパターンの時に使うのか?を考えたのですが、こんな時かな?と出てきたのが以下。

ホテル一覧検索みたいな階層が動的に変わるけど、1つのコントローラでさばきたい時。
URLが"/都道府県","都道府県/市町村"みたいな感じです。
2つぐらいなら、それぞれ書いた方が良さそうだけど、たくさんあると書くのが面倒なので、"*"使った方が楽そうかなと。
他にも必要になるパターンがあるのだろうけど、これぐらいしか思いつかなかったorz


そして、今回でひとまずSlim3 Source Code Readingは完結となりました。Slim3 Source Code Readingのおかげで分からなかったら、とりあえず中身を読むようになり、読む力がそこそこ上がった気がします!また、GAEへの理解度も上がりました。
機会があれば、他のライブラリも読んでみようかなーと思いながら、twitter4jのソースをDLしたりしていますw

Slim3 Source Code Reading No11 Mock

Slim3 Source Code Reading #11 - ChugokuGTUG

に参加してきました!


今回も@ttyokoyama 殿と2人だったので、まったりと進みました。
まぁ、他の方が参加していても、まったり進むのですがw

T.Yokoyamaのブログ: Slim3 Source Code Reading #11


今回はtesterの中にあるMockたちです。
数はそこそこあるのですが、どれもGAEに関係しているわけではなく、ServletのRequestやResponseのMockでした。
その中に、UrlFetchHandlerというインタフェースがありました。
こいつはURLFetchServiceをテストするためのもののようで、responseを自分で作ってやれるようです。


http://code.google.com/intl/ja/appengine/docs/java/urlfetch/
公式のURLフェッチのところには、URLFetchServiceのサンプルコードはないようです。
また、こいつが本気を出すところは、非同期でURLFetchを行う時のようです。
URLFetchService#fetchAsync()を使うとGAE環境が用意しているスレッドで、サーブレットのスレッドとは別に非同期でHTTP通信を行ってくれます。


twitter4jを使う時も有効のようで、山本祐介殿のBlogでも触れられてました。
http://samuraism.jp/diary/2011/07/06/1309912740000.html

また、実際の使い方は以下のBlogに詳しく書いてあります。
今回作成したサンプルも、こちらを参考にしました。
非同期URLFetch、FetchAsync()の使い方【Google App Engine】


それでは、ソースコードです。
IndexController


public class IndexController extends Controller {

/**
* Twitter検索APIをFetchする
*/
@Override
public Navigation run() throws Exception {
final String TWITER_SEARCH_URL =
"http://search.twitter.com/search.json?q=chugokugtug";
String tweetJson = fetchUrl(TWITER_SEARCH_URL, "UTF-8");

List tweetList = buildeTweetList(tweetJson);

requestScope("tweetList", tweetList);
return forward("index.jsp");
}

/**
* 指定したURLをFetchする
*
* @param urlStr
* @param encoding
* @return
* @throws IOException
*/
private String fetchUrl(String urlStr, String encoding) throws IOException {
URLFetchService service = URLFetchServiceFactory.getURLFetchService();
URL url = new URL(urlStr);
HTTPResponse res = service.fetch(url);
return new String(res.getContent(), encoding);
}

/**
* Twitter検索APIの結果から、TweetListを作る
*
* @param searchJson
* @return
*/
private List buildeTweetList(String searchJson) {
List tweetList = new ArrayList();
try {
Gson gson = new Gson();
JsonElement element = gson.fromJson(searchJson, JsonElement.class);
JsonObject json = element.getAsJsonObject();
JsonArray array = json.getAsJsonArray("results");
for (int i = 0; i < array.size(); i++) {
Object obj = array.get(i);
if (obj instanceof JsonObject) {
tweetList.add((JsonObject) obj);
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return tweetList;
}
}

テスト用に、TwitterSearchAPIを呼んでいるのですが、むしろ取得した後のJSONの処理の方が長くなってしまった・・・。
URLFetchServiceを使っているのは、fetchUrl()なので、4行しかありません。


上記のControllerをテストするコードです。
IndexControllerTest


public class IndexControllerTest extends ControllerTestCase {

/**
* TwitterSearchFetchHandlerを使ったテスト
*
* @throws Exception
*/
@Test
public void testUrlFetch() throws Exception {
tester.setUrlFetchHandler(new TwitterSearchFetchHandler());

tester.start("/urlfetch/");
IndexController controller = tester.getController();
List tweetList = tester.requestScope("tweetList");

assertThat(controller, is(notNullValue()));
assertThat(tester.isRedirect(), is(false));
assertThat(tester.getDestinationPath(), is("/urlfetch/index.jsp"));
assertThat(tweetList, is(notNullValue()));

JsonObject jsonObject = tweetList.get(0);
assertThat(jsonObject.get("id").toString(), is("177727132691214338"));
assertThat(jsonObject.get("from_user").toString(), is("\"sinmetal\""));
assertThat(
jsonObject.get("text").toString(),
is("\". @ttyokoyama http://t.co/POuEYdnZ 殿のコードを検証する内容でBlogを書こうとしたが、URLFetchService よりも、jsonと戦っているというw #chugokugtug\""));
}
}

テストのためのデータを、実際のデータからコピーしてきたので、僕のツイートになってますが、こんな感じです。
キモは一番最初にやってるところです。


tester.setUrlFetchHandler(new TwitterSearchFetchHandler());
TwitterSearchFetchHandlerがテスト用に作ったUrlFetchHandlerを実装しているクラスです。
中身はTwitterSearchAPIを呼んだ時と同じ内容のJSONを返しているだけです。
これでUnitTestで毎回同じ値が返ってくるようにできました。


TwitterSearchFetchHandler


public class TwitterSearchFetchHandler implements URLFetchHandler {

public byte[] getContent(URLFetchRequest request) throws IOException {
String json =
"{\"completed_in\":0.084,\"max_id\":177727132691214338,\"max_id_str\":\"177727132691214338\",\"next_page\":\"?page=2&max_id=177727132691214338&q=chugokugtug&rpp=1\",\"page\":1,\"query\":\"chugokugtug\",\"refresh_url\":\"?since_id=177727132691214338&q=chugokugtug\",\"results\":[{\"created_at\":\"Thu, 08 Mar 2012 12:07:05 +0000\",\"from_user\":\"sinmetal\",\"from_user_id\":26903289,\"from_user_id_str\":\"26903289\",\"from_user_name\":\"\u771f\",\"geo\":null,\"id\":177727132691214338,\"id_str\":\"177727132691214338\",\"iso_language_code\":\"ja\",\"metadata\":{\"result_type\":\"recent\"},\"profile_image_url\":\"http://a0.twimg.com/profile_images/1042264867/1277990403846_normal.png\",\"profile_image_url_https\":\"https://si0.twimg.com/profile_images/1042264867/1277990403846_normal.png\",\"source\":\"<a href="http://www.tweetdeck.com" rel="nofollow">TweetDeck</a>\",\"text\":\". @ttyokoyama http://t.co/POuEYdnZ \u6bbf\u306e\u30b3\u30fc\u30c9\u3092\u691c\u8a3c\u3059\u308b\u5185\u5bb9\u3067Blog\u3092\u66f8\u3053\u3046\u3068\u3057\u305f\u304c\u3001URLFetchService \u3088\u308a\u3082\u3001json\u3068\u6226\u3063\u3066\u3044\u308b\u3068\u3044\u3046\uff57 #chugokugtug\",\"to_user\":null,\"to_user_id\":null,\"to_user_id_str\":null,\"to_user_name\":null}],\"results_per_page\":1,\"since_id\":0,\"since_id_str\":\"0\"}";
return json.getBytes("UTF-8");
}

public int getStatusCode(URLFetchRequest request) throws IOException {
return HttpServletResponse.SC_OK;
}

}


他にもErrorが発生した時なんかも作っていけば、ある程度UnitTestできそう!
前回のServletTester#addBlobKey()もマイナーな感じでしたが、今回のURLFetchHandlerもマイナーな感じのようで、ぐぐってもあんまり情報が出てきてくれないですねw


最後に余談だけど、@ttyokoyama 殿にGo言語で作られてるものって何があるんですか?と聞いたら、youtubeで使われてるよ!と以下を教えて下さった。

GitHub - vitessio/vitess: Vitess is a database clustering system for horizontal scaling of MySQL.

先日公開されたばかりで、vitessでぐぐると"もしかしてvitesse"と言われるぐらいだ!
興味がある方は、読んでみると良いのではないでしょうか。


今回の実際のソースはこちら!
IndexController
Google Code Archive - Long-term storage for Google Code Project Hosting.

IndexControllerTest
Google Code Archive - Long-term storage for Google Code Project Hosting.

TwitterSearchFetchHandler
Google Code Archive - Long-term storage for Google Code Project Hosting.

Slim3 Source Code Reading No10 tester

Slim3 Source Code Reading #10 - ChugokuGTUG

に参加してきました!


今回も@ttyokoyama 殿と2人だったので、まったりと進みました。
まぁ、他の方が参加していても、まったり進むのですがw

T.Yokoyamaのブログ: Slim3 Source Code Reading #10


今回も前回に引き続き、testerです。
前回はAppEngineTesterでしたが、今回はControllerTesterです。

ControllerTesterは、ServletTesterを継承しており、ServletTesterはAppEngineTesterを継承しています。
役割も名前の通りです。
また、中身も初期化をしているのと、テスト時に便利なメソッドがあるだけで、何か特殊なことをしている感じには見えませんでした。


しかし、やたらとハマったのが・・・。

ServletTester#addBlobKey()

Blobstoreをテストするためにあるんだろうけど、どうやって使うんだろう?と横山殿と試行錯誤。
で、上記の横山殿のBlogにもある通り、SDK側のソースと型があってなくてエラーになるぞ?と。
実はこの時、僕の方ではエラーにはなってなかったのですが、期待通りの動作にもなっていなかったので、そもそも僕の使い方が違ってるのかな?と思いながら試行錯誤。


その時にtwitterで泣きながら質問したら、ひが殿が教えて下さいました!

教えていただいた通り、デモを見てみると・・・。

Google Code Archive - Long-term storage for Google Code Project Hosting.


public class UploadControllerTest extends ControllerTestCase {

@Test
public void run() throws Exception {
String keyString = "hoge";
tester.addBlobKey("formFile", keyString);
tester.start("/blobstore/upload");
UploadController controller = tester.getController();
assertThat(controller, is(notNullValue()));
assertThat(tester.isRedirect(), is(true));
assertThat(tester.getDestinationPath(), is("/blobstore/"));
Key key = Datastore.createKey(Blobstore.class, keyString);
Datastore.get(key);
}
}

普通に使ってましたね!w
でも、"addBlobKey"でぐぐってもほとんどHitしなかったので、あんまり知れ渡っていないようです。
今、ぐぐると僕と横山殿のBlogが1ページ目に出てきますねw


その後、帰宅後に調べていて分かったのですが、GAEのSDK1.6.2で、型が変わったようです。
横山殿は1.6.2で動かしていたので、エラーになっていたわけですね。
既に1.6.3も出てきていますが、今のところslim3は1.6.0準拠です。
そろそろ、新しいのが出るのかな?と思いながら、待っていますw