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プログラミングガイド
- 作者: 伊藤千光
- 出版社/メーカー: インプレス
- 発売日: 2010/12/10
- メディア: 単行本(ソフトカバー)
- 購入: 4人 クリック: 183回
- この商品を含むブログ (15件) を見る
僕の応用力が足りないため、なかなか思いつかず・・・。
テストしようとしている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プログラミングガイド
- 作者: 伊藤千光
- 出版社/メーカー: インプレス
- 発売日: 2010/12/10
- メディア: 単行本(ソフトカバー)
- 購入: 4人 クリック: 183回
- この商品を含むブログ (15件) を見る
とりあえず、ClosureLibraryの初期設定は、やはり伊藤殿のブログを見ていただくとして・・・w
Closure Libraryによるアプリ開発のはじめ方 - WebOS Goodies
実際にSlim3で使う時に、やりたくなるのは以下の点。
とりあえず、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
//Production用と書いてあるファイルだけが、本番環境で実際に読み込むファイルです。
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
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 ListbuildeTweetList(String searchJson) {
ListtweetList = 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 ListbuildeTweetList(String searchJson) {
ListtweetList = 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();
ListtweetList = 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\""));
}
}
テストのためのデータを、実際のデータからコピーしてきたので、僕のツイートになってますが、こんな感じです。
キモは一番最初にやってるところです。
TwitterSearchFetchHandlerがテスト用に作ったUrlFetchHandlerを実装しているクラスです。
tester.setUrlFetchHandler(new TwitterSearchFetchHandler());
中身は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