ブックマーク登録画面の作成 その1
Slim3の「Getting Started」を例にプロジェクトを作成。プロジェクト名は「com.appspot.simplebookmarks」とした。パッケージも同様。
まずブラウザから http://localhost:8888/bookmark/ をアクセス。プロジェクトの作成以外は何もしていなので当然エラー画面が表示。さっそくコントローラーを作ります。gen-controllerを実行、「/bookmark/」を指定。OKボタンでさくっと作成。再度アクセスすると「Hello bookmark Index!!」と表示されました。よしよし。この画面は登録されたブックマークの一覧を表示するメイン画面とします。さっそく実装していきたいところですが、データは何もない状態。なのでまずはデータを登録する画面から作る事にします。
ブックマーク登録画面の作成
まず登録画面のモックアップをhtmlで作っていきます。ソースは以下の通り。
create.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Simple Bookmarks - Create bookmark</title> <link rel="icon" href="../favicon.ico" type="image/x-icon"> <link rel="shortcut icon" href="../favicon.ico" type="image/x-icon"> <link rel="stylesheet" type="text/css" media="all" href="../css/form.css"> <script type="text/javascript" src="../js/form.js"></script> </head> <body> <p id="author">tsutomu.uchima@gmail.com</p> <form id="aform" action="/bookmark/add" method="post"> <label>title:</label><br/> <input type="text" id="t" name="t" value="" class="" style="width: 40em;"> <br/> <label>url:</label><br/> <input type="text" id="u" name="u" value="" class="" style="width: 40em;"> <br/> <label>collection:</label><br/> <select name="c"> <option value="">HOME</option> </select><br/> <label>new collection:</label><br/> <input type="text" name="n" value="" style="width: 40em;"><br/> <br/> <input type="submit" name="doAdd"/> <input type="button" value="Cancel" onclick="javascript: history.back(); return false;"/> </form> </body> </html>
form.css
* { margin : 0px; padding : 0px; } body { background-color : #dae0e7; } #author { background-color : black; color : white; padding : 0.3em; } #aform { padding : 0.3em; } input.err { border-color : red; }
form.js
checkRequired = function() { var t = document.getElementById('t'); var u = document.getElementById('u'); if (t.value == '' || u.value == '') { window.alert('input title and url.'); return false; } } init = function() { document.getElementById('aform').onsubmit=checkRequired; } onload = init;
こんな感じで。続いてコントローラーを作ります。
gen-controllerを実行し「/bookmark/create」を指定。登録画面を作成します。ブラウザから http://localhost:8888/bookmark/create をアクセスして問題がないのを確認。うんうん、問題なし。
先ほど作成したhtmlを元にjspを書いていきます。
create.jsp
<%@page pageEncoding="UTF-8" isELIgnored="false" session="false"%> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%> <%@taglib prefix="f" uri="http://www.slim3.org/functions"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Simple Bookmarks - Create bookmark</title> <link rel="icon" href="../favicon.ico" type="image/x-icon"> <link rel="shortcut icon" href="../favicon.ico" type="image/x-icon"> <link rel="stylesheet" type="text/css" media="all" href="../css/form.css"> <script type="text/javascript" src="../js/form.js"></script> </head> <body> <p id="author">${f:h(author)}</p> <form id="aform" action="${f:url('add')}" method="post"> <label>title:</label><br/> <input type="text" id="t" ${f:text("t")} class="${f:errorClass('t', 'err')}" style="width: 40em;"> ${f:h(errors.t)}<br/> <label>url:</label><br/> <input type="text" id="u" ${f:text("u")} class="${f:errorClass('u', 'err')}" style="width: 40em;"> ${f:h(errors.u)}<br/> <label>collection:</label><br/> <select name="c"> <c:forEach var="e" items="${collections}"> <option ${f:select("c", f:h(e.key))}>${f:h(e.name)}</option> </c:forEach> </select><br/> <label>new collection:</label><br/> <input type="text" ${f:text("n")} style="width: 40em;"><br/> <br/> <input type="submit" name="doAdd"/> <input type="button" value="Cancel" onclick="javascript: history.back(); return false;"/> </form> </body> </html>
jspが書けたらブラウザでアクセスしモックアップと同様に表示されるのを確認します。うん、問題ありません。
jspが書けたので次はコントローラーを書いていきます。コントローラーの機能内容は
- ログインチェック
- 登録するブックマークのグループ分けにコレクションを指定出来るよう選択肢を設ける
という感じで。
それでは最初のログインチェックを実装していきます。GAEにはGoogleアカウントを使った認証機能があるので今回はそれを利用します。それではソースをさくっと書いてみましょう。
CreateController.java
package com.appspot.simplebookmarks.controller.bookmark; import org.slim3.controller.Controller; import org.slim3.controller.Navigation; import com.google.appengine.api.users.User; import com.google.appengine.api.users.UserService; import com.google.appengine.api.users.UserServiceFactory; public class CreateController extends Controller { @Override public Navigation run() throws Exception { //ユーザー情報取得とログインチェック UserService userService = UserServiceFactory.getUserService(); User user = userService.getCurrentUser(); if (user == null) { return redirect(userService.createLoginURL("/bookmark/create")); } return forward("create.jsp"); } }
5行程で認証機能が書けました。楽でいいですねー。それではこの認証が正しく機能するかテストしてみましょう。ブラウザ操作で確認してもいいのですが、Slim3を使っているのでテストで確認する事にします。
コントローラー作成時にテストクラス CreateControllerTest も作成されているのでそれを使います。まずは何も変更を加えず実行してみます。実行すると
java.lang.AssertionError: Expected: is <false> got: <true> 〜
となりました。認証が完了していない状態なのでコントローラーのNavigationはリダイレクトを返し、期待していたフォワードではなかったのでエラーとなります。それでは認証を完了した状態でテストするにはどうすればよいでしょうか?Slim3には認証済みの状態を簡単に作り出すTestEnvironmentが実装されているのでそれを利用します。ソースは以下の通り。
CreateControllerTest.java
package com.appspot.simplebookmarks.controller.bookmark; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import org.junit.Test; import org.slim3.tester.ControllerTestCase; import org.slim3.tester.TestEnvironment; import com.google.apphosting.api.ApiProxy; public class CreateControllerTest extends ControllerTestCase { @Test public void run() throws Exception { ApiProxy.setEnvironmentForCurrentThread(new TestEnvironment( "tsutomu.uchima@gmail.com")); tester.start("/bookmark/create"); CreateController controller = tester.getController(); assertThat(controller, is(notNullValue())); assertThat(tester.isRedirect(), is(false)); assertThat(tester.getDestinationPath(), is("/bookmark/create.jsp")); } }
TestEnvironmentのnew時に引数でメールアドレスを渡します。そしてApiProxy.setEnvironmentForCurrentThread()にセットし実行する事で認証済みの状態となりテストを行う事が出来ます。それではテストを実行してみます。期待通り認証チェックがtrueとなりテストに成功しました。テストが通るのは気持ちがいいですねー。
ログインチェックの実装が終わったので次はコレクションを指定出来るようにします。コレクションのデータはBigtableに登録したデータを取ってくるようにします。結論からいうとこんな感じのソースになります。
CreateController.java
package com.appspot.simplebookmarks.controller.bookmark; import java.util.List; import org.slim3.controller.Controller; import org.slim3.controller.Navigation; import com.appspot.simplebookmarks.model.Collection; import com.appspot.simplebookmarks.service.CollectionService; import com.google.appengine.api.users.User; import com.google.appengine.api.users.UserService; import com.google.appengine.api.users.UserServiceFactory; public class CreateController extends Controller { //Collectionを操作するサービス private CollectionService service = new CollectionService(); @Override public Navigation run() throws Exception { //ユーザー情報取得とログインチェック UserService userService = UserServiceFactory.getUserService(); User user = userService.getCurrentUser(); if (user == null) { return redirect(userService.createLoginURL("/bookmark/create")); } //コレクション取得 List<Collection> collections = service.getList(user.getEmail()); //jspへ値を渡す requestScope("author", user.getEmail()); requestScope("collections", collections); return forward("create.jsp"); } }
新しく Collection と CollectionService が出て来ました。Collection はコレクションデータのモデルクラス、CollectionService は Collection を操作するサービスクラスです。まだ定義されていませんのでこのコントローラーのソースはエラーになります。早速モデルとサービスクラスを作成していきましょう。
gen-model を実行し Collection クラスを作成します。ソースは以下の通り。
Collection.java
package com.appspot.simplebookmarks.model; import java.io.Serializable; import java.util.Date; import javax.jdo.annotations.Persistent; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import org.slim3.datastore.Attribute; import org.slim3.datastore.Model; @Model(schemaVersion = 1) public class Collection implements Serializable { private static final long serialVersionUID = 1L; @Attribute(primaryKey = true) private Key key; @Attribute(version = true) private Long version; @Persistent private String name; @Persistent private String author; @Persistent private Integer displayOrder; @Persistent private Date createDate = new Date(); @Persistent private Date updateDate = new Date(); public Collection() { } public Collection(String name, String author, Integer displayOrder) { this.name = name; this.author = author; this.displayOrder = displayOrder; } @Override public String toString() { StringBuffer sb = new StringBuffer("["); sb.append(KeyFactory.keyToString((Key) key)).append(","); sb.append(name).append(","); sb.append(author).append(","); sb.append(displayOrder).append(","); sb.append(createDate).append(","); sb.append(updateDate).append(","); sb.append(version).append("]"); return sb.toString(); } 〜 accessor、hashCode()、equals() は省略 〜 }
自動生成されたプロパティ以外に
- name /*コレクション名*/
- author /*作成者*/
- displayOrder /*表示順*/
- createDate /*作成日*/
- updateDate /*更新日*/
を設けました。モデルクラスは以上です。続いてモデルを操作するサービスクラスを定義します。
gen-service を実行し CollectionService を作成します。作成が終わったらメソッドを追加します。先ほどのコントローラーで使用する getList() メソッドが書かれていますのでそのメソッドを追加します。ソースは以下の通り。
CollectionService.java
package com.appspot.simplebookmarks.service; import java.util.List; import org.slim3.datastore.Datastore; import com.appspot.simplebookmarks.meta.CollectionMeta; import com.appspot.simplebookmarks.model.Collection; import com.google.appengine.api.datastore.Transaction; public class CollectionService { public List<Collection> getList(String author) { CollectionMeta e = CollectionMeta.get(); List<Collection> list = Datastore.query(e) .filter(e.author.equal(author)) .sort(e.displayOrder.asc) .asList(); if (list.size() == 0) { Collection collection = new Collection(); collection.setName("HOME"); collection.setAuthor(author); collection.setDisplayOrder(1); Transaction tx = Datastore.beginTransaction(); Datastore.put(collection); tx.commit(); list.add(collection); } return list; } }
authorにヒットする Collection を取得します。どれもヒットしなかった場合はデフォルトコレクションとして「HOME」を作成し、それを取得するようにします。このサービスが期待通り動くようテストします。テストの内容は以下の通りです。
CollectionServiceTest.java
package com.appspot.simplebookmarks.service; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import java.util.List; import org.junit.Test; import org.slim3.tester.AppEngineTestCase; import com.appspot.simplebookmarks.model.Collection; public class CollectionServiceTest extends AppEngineTestCase { private CollectionService service = new CollectionService(); @Test public void test() throws Exception { assertThat(service, is(notNullValue())); } @Test public void getList1() throws Exception { List<Collection> list = service.getList("tsutomu.uchima@gmail.com"); assertThat(list, is(notNullValue())); assertThat(list.size(), is(1)); Collection collection = list.get(0); assertThat(collection.getName(), is("HOME")); assertThat(collection.getAuthor(), is("tsutomu.uchima@gmail.com")); assertThat(collection.getDisplayOrder(), is(1)); } }
実行してみます。問題なくテスト完了出来ました。うんうん。
モデル、サービスの実装が出来たのでコントローラーからエラーがなくなり、実行も可能になりました。コントローラーが期待通り動作するかテストをします。テスト内容は以下の通り。
CreateControllerTest.java
package com.appspot.simplebookmarks.controller.bookmark; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import org.junit.Test; import org.slim3.tester.ControllerTestCase; import org.slim3.tester.TestEnvironment; import com.google.apphosting.api.ApiProxy; public class CreateControllerTest extends ControllerTestCase { @Test public void run() throws Exception { ApiProxy.setEnvironmentForCurrentThread(new TestEnvironment( "tsutomu.uchima@gmail.com")); tester.start("/bookmark/create"); CreateController controller = tester.getController(); assertThat(controller, is(notNullValue())); assertThat(tester.isRedirect(), is(false)); assertThat(tester.getDestinationPath(), is("/bookmark/create.jsp")); assertThat(tester.requestScope("author"), is(notNullValue())); assertThat(tester.requestScope("collections"), is(notNullValue())); } }
テスト内容としてコントローラーから jsp へ引き渡す値が存在するかを確認しています。それでは実行してみましょう。問題なくてストが完了しました。素晴らしいね!
ブラウザからも動作を確認してみます。 http://localhost:8888/bookmark/create へアクセス!コレクションの選択肢に HOME があれば正しく動作しています。画面上にログイン時に指定したメールアドレスが表示されているのも確認。問題ありません。イェーイ。
今日はここまで。次はフォームに入力されたブックマーク情報を Bigtable へ登録します。お楽しみにー。
ソースコードはこちらに置いてます。
http://code.google.com/p/tsutomuuchima/
自作アプリをslim3で再構築
slim3がリリースされたのでピュアservletとJDOで書いていた自作アプリケーションをslim3で書き直す事にしました。常にspin downしている自作アプリはJDOを使っているので起動が遅くて厳しいー。cronで回して常に起動させていたけどGoogleは「そんなセコい事しても後々損するよ」って言ってるし。毎日使うアプリだから快適にしたい!のでslim3で書き直しです。
せっかくなので書き直しの作業を綴っていきます。slim3で作る簡単なサンプルの解説にもなれば。
自作のアプリケーションはシンプルなオンラインブックマークアプリ。どのブラウザからもブックマークを呼び出す事が出来る、登録が出来るようになっています。自宅や会社のブックマークを同期させるツールは Xmarks とかあるけど Opera をサポートしていないのが痛い。あとIE6も。最近は「IE6の“葬儀”行われる Microsoftから献花も」とか「YouTube、3月13日にIE6など旧版ブラウザのサポート終了」とか言われてもうIE6は対応しなくていいよねーって感じだけど自分はまだIE6を使用している環境だし、そんな訳でいろんなブラウザでブックマークを共有したいから作りました。
それでは早速書き直してみます。
メイン画面
まず最初は画面周りから。メイン画面はこんな感じ。
画面右側はコレクション一覧でリンクを纏めたものです。画面右は登録したリンク一覧。コレクションの中にコレクションを作る事は出来ません。階層を持とうかどうか迷ったけどシンプルにって事なので階層は持っていません。GAE/Jで全文検索がサポートされたら検索機能を入れたいところ。
管理画面
続きまして管理画面。登録したブックマークの編集、削除等はこちらから行います。
ブックマーク編集画面
続きましてブックマーク編集画面。イメージは新規登録と一緒。
■
かれこれ20年以上言われ続けている「Macにはゲームが少ない」問題に転換期が訪れそうです。後頭部にバルブを付けたおじさんロゴでお馴染みValve社が、ゲーム配信プラットフォーム Steam…
http://japanese.engadget.com/2010/03/09/mac-steam-pc/
Mac版 Steam 正式発表、PCと対戦・ライセンス共有・データ共用も対応
米国時間の2月24日、グーグルが提供するGoogle App Engineのデータストアが障害により停止するという事故が発生しました。この事故の状況については、そのときの記事「Google App Engineが昨夜ダウン。障害がごく一部に残り、対応チームは現在も作業中」」で詳しく報じました。 それから2週間、グーグルが原因と対策について書いたドキュメントをGoogle…
http://www.publickey.jp/blog/10/google_app_engine_1.html
Google App Engineダウンの原因はデータセンターの電源故障。さらに復旧手順のミスが重なった
http://www.gizmodo.jp/2010/03/psppspaditunes.html
ウォークマンで破れた借りは返す! ポシャったのかと思われていた「PSPケータイ」を今年中に発売し、あのアップル期待のiPadさえギャフンと泣かせる「PSPad」なる新ガジェットのリリースを宣言しているとも伝えられたソニーですが、どうやら真の狙いは本丸のiTunesをも凌駕する新サービス「Sony Online Service」に懸けるところが大きいようですね。…
ソニー、PSPケータイとPSPadでアップル撃破! iTunesを超える新サービス投入へ
今年後半にも正式版が登場予定の Google Chrome OS について、いくつか新情報が出てきました。Googleのソフトウェア・セキュリティ・エンジニア Will Drewry氏が暗号屋さん向けイベント RSAカンファレンスで語ったものです。
http://japanese.engadget.com/2010/03/09/google-chrome-os/
Googleが Chrome OSのセキュリティを解説、「開発者スイッチ」も搭載
http://japanese.engadget.com/2010/03/08/eee-pc-1005pr-hd-crystal-hd-11/
Asusからまたまたまた新しいEee PCが公開されています。新モデル Eee PC 1005PRは、国内でも今年発売された10.1型・Atom N450 採用モデル 1005PE の画面を1366 x 768 高解像度に、またBroadcom のCrystal HD アクセラレータ (BCM 70015)…
Eee PC 1005PR は HD液晶&Crystal HDデコーダ採用、11時間駆動
いまベテランのITエンジニアとして活躍している方々の中には、子どもの頃にBasic言語で初めてプログラミングを覚えた、あるいは駆け出しエンジニアの頃に最初に仕事で使った言語がVisual Basicだった、という方も多いのではないでしょうか? Small Basic 実は僕も、最初に使ったプログラミング言語はPC-8001のN-BASICでした。もう30年くらい前のことですね
http://www.publickey.jp/blog/10/ms_small_basic.html
超簡単プログラミング「MS Small Basic」が正式版で無料公開、サンデープログラミングにどうですか?
各所で話題のCSS3をいじりながら学べるサイトがあったのでご紹介。その名もCSS3 Please!w。
↑ コードをいじると右上のエレメントが動的に変化します。
↑ たとえば角丸をいじると・・・。
それほど多くがあるわけではないですが、「CSS3って何ができるの?」を体感するにはいいのではないでしょうかね。
http://www.ideaxidea.com/archives/2010/03/css3_please.html
話題のCSS3をインタラクティブに学習できる『CSS3 Please!』
今日は、ノウハウというよりは、豆知識を。「URL」という呼び方と「URI」という呼び方がありますが、どう違うのか、あなたはご存じですか
http://web-tan.forum.impressrd.jp/e/2010/03/09/7539
URLとURIは何が違うの? どちらが正しい呼び方? | Web担当者Forum
MuseScoreは、同社の配布するクロスプラットフォームの無償楽譜作成ソフトウェアの最新ベータ版「MuseScore 0.9.6 beta 2」を発表した。
http://journal.mycom.co.jp/news/2010/03/10/001/index.html
MuseScore、無償の楽譜作成ソフト「MuseScore」の最新ベータ版登場
AppleがMac OS X開発者向けに提供するサポートプログラムが変更され、装い新たに「Mac Developer Program」となりました。これまでのADCは、個人開発者向けの「Select」、専門的なサポートを必要とする法人向けの「Premier」、学生や研究者向けの「Student」という有償メンバーシップと、「Online」と呼ばれる無償メンバーシップが用意されていましたが、これからはMac Developer Programに一本化されます。
http://builder.japan.zdnet.com/member/u48681/blog/2010/03/09/entry_27038030/?ref=rss
新しい「Mac Developer Program」に見るAppleの思惑
2010年03月09日15:00カテゴリ「タイムセールなう」でRT6431回=無印良品がTwitterで大成功湯川鶴章(tsuruaki)無印良品ブランドの株式会社良品計画が、Twitterを使ったソーシャルメディアマーケティングで成果を上げ始めた。
http://techwave.jp/archives/51411971.html
「タイムセールなう」でRT6431回=無印良品がTwitterで大成功
RESTが簡単に書けたのでRestControllerを書いてみた。
slim3のコントローラーは isGet(), isPost(), isPut(), isDelete() が用意されているのでRESTが簡単に書ける。便利だなぁ。なのでREST用のコントローラーを抽象化して RestController ってのを書いてみた。
ソースは以下の通り。
RestController.java
package tutorial.cool.controller; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import javax.servlet.http.HttpServletResponse; import org.slim3.controller.Controller; import org.slim3.controller.Navigation; import com.google.appengine.repackaged.com.google.io.base.IORuntimeException; public abstract class RestController extends Controller { //200 public static final int SC_OK = HttpServletResponse.SC_OK; public static final int SC_CREATED = HttpServletResponse.SC_CREATED; public static final int SC_NO_CONTENT = HttpServletResponse.SC_NO_CONTENT; //400 public static final int SC_BAD_REQUEST = HttpServletResponse.SC_BAD_REQUEST; public static final int SC_NOT_FOUND = HttpServletResponse.SC_NOT_FOUND; public static final int SC_CONFLICT = HttpServletResponse.SC_CONFLICT; //500 public static final int SC_INTERNAL_SERVER_ERROR = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; public static final int SC_NOT_IMPLEMENTED = HttpServletResponse.SC_NOT_IMPLEMENTED; /** * メインメソッド。HTTP method の内容に対応するメソッドを呼び出し実行する。 */ @Override public Navigation run() { if (isGet()) { doGet(); return null; } else if (isPost()) { doPost(); return null; } else if (isPut()) { doPut(); return null; } else if (isDelete()) { doDelete(); return null; } try { response.sendError(SC_NOT_IMPLEMENTED); } catch (IOException e) { e.printStackTrace(); } return null; } /** * HTTP method が DELETE の場合に実行されるメソッド。 */ public void doDelete() { try { response.sendError(SC_NOT_IMPLEMENTED); } catch (IOException e) { e.printStackTrace(); } return; } /** * HTTP method が PUT の場合に実行されるメソッド。 */ public void doPut() { try { response.sendError(SC_NOT_IMPLEMENTED); } catch (IOException e) { e.printStackTrace(); } return; } /** * HTTP method が POST の場合に実行されるメソッド。 */ public void doPost() { try { response.sendError(SC_NOT_IMPLEMENTED); } catch (IOException e) { e.printStackTrace(); } return; } /** * HTTP method が GET の場合に実行されるメソッド。 */ public void doGet() { try { response.sendError(SC_NOT_IMPLEMENTED); } catch (IOException e) { e.printStackTrace(); } return; } /** * 指定した str のリクエストパラメーターが null か確認するメソッド。 * * @param str * リクエストパラメーターのキー * @return * true: null である。 * false: null でない。 */ public boolean isNull(String str) { if (asString(str) == null) { try { response.sendError(SC_BAD_REQUEST); } catch (IOException e) { e.printStackTrace(); } return true; } return false; } /** * レスポンスにテキストを書き込みます。 * * @param text * テキスト */ public void responseWriter(String text) { responseWriter(text, null, null); } /** * レスポンスにテキストを書き込みます * * @param text * テキスト * @param contentType * コンテントタイプ。 デフォルトはtext/plain。 */ public void responseWriter(String text, String contentType) { responseWriter(text, contentType, null); } /** * レスポンスにテキストを書き込みます。 * * @param text * テキスト * @param contentType * コンテントタイプ。 デフォルトはtext/plain。 * @param encoding * エンコーディング。 指定しなかった場合は、リクエストのcharsetEncodingが設定される。 * リクエストのcharsetEncodingも指定がない場合は、UTF-8。 */ public void responseWriter(String text, String contentType, String encoding) { responseWriter(SC_OK, text, contentType, encoding); } /** * レスポンスにテキストを書き込みます。 * * @param status * ステータスコード */ public void responseWriter(int status) { responseWriter(status, null, null, null); } /** * レスポンスにテキストを書き込みます。 * * @param status * ステータスコード * @param text * テキスト */ public void responseWriter(int status, String text) { responseWriter(status, text, null, null); } /** * レスポンスにテキストを書き込みます * * @param status * ステータスコード * @param text * テキスト * @param contentType * コンテントタイプ。 デフォルトはtext/plain。 */ public void responseWriter(int status, String text, String contentType) { responseWriter(status, text, contentType, null); } /** * レスポンスにテキストを書き込みます。 * * @param status * ステータスコード * @param text * テキスト * @param contentType * コンテントタイプ。 デフォルトはtext/plain。 * @param encoding * エンコーディング。 指定しなかった場合は、リクエストのcharsetEncodingが設定される。 * リクエストのcharsetEncodingも指定がない場合は、UTF-8。 */ public void responseWriter(int status, String text, String contentType, String encoding) { // if (status == 0) { status = SC_OK; } if (!(status == SC_OK || status == SC_CREATED || status == SC_NO_CONTENT)) { try { response.sendError(status); } catch (IOException e) { e.printStackTrace(); } return; } // if (contentType == null) { contentType = "text/plain"; } if (encoding == null) { encoding = request.getCharacterEncoding(); if (encoding == null) { encoding = "UTF-8"; } } response.setStatus(status); response.setContentType(contentType + "; charset=" + encoding); try { PrintWriter out = null; try { out = new PrintWriter(new OutputStreamWriter(response .getOutputStream(), encoding)); out.print(text); } finally { if (out != null) { out.close(); } } } catch (IOException e) { throw new IORuntimeException(e); } } }
responseWriter は SAStruts の ResponsUtil#write() をパクって改変しました <(_ _)> 。
レスポンスのステータスはきちんと煮詰めるべきか。もう少し勉強してみよう。
使い方は RestController を継承して以下のように書く。
各 HTTP method に対応したメソッド doGet(), doPost(), doPut(), doDelete() をオーバーライドして定義すれば OK。
OrdersController.java
package tutorial.controller.rest; import tutorial.cool.controller.RestController; public class OrdersController extends RestController { private boolean condition = true; @Override public void doDelete() { if (isNull("key")) return; if (condition) { responseWriter(createXML("DELETE"), "text/xml"); } else { responseWriter(SC_CONFLICT); } } @Override public void doPut() { if (isNull("key")) return; if (condition) { responseWriter(createXML("PUT"), "text/xml"); } else { responseWriter(SC_CONFLICT); } } @Override public void doPost() { if (condition) { responseWriter(createXML("POST"), "text/xml"); } else { responseWriter(SC_CONFLICT); } } @Override public void doGet() { if (condition) { responseWriter(createXML("GET"), "text/xml"); } else { responseWriter(SC_NO_CONTENT, null, "text/html"); } } protected String createXML(String methodType) { StringBuffer sb = new StringBuffer(); 〜〜〜 return (new String(sb)); } }
ルーターは以下の様に定義。
AppRouter.java
package tutorial.controller; import org.slim3.controller.router.RouterImpl; public class AppRouter extends RouterImpl { public AppRouter() { addRouting("/rest/orders/{key}", "/rest/orders?key={key}"); } }
テストはこんな感じで。
OrdersControllerTest.java
package tutorial.controller.rest; import org.slim3.tester.ControllerTestCase; import org.junit.Test; import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; public class OrdersControllerTest extends ControllerTestCase { @Test public void runPost() throws Exception { tester.request.setMethod("post"); tester.start("/rest/orders"); OrdersController controller = tester.getController(); assertThat(controller, is(notNullValue())); assertThat(tester.isRedirect(), is(false)); assertThat(tester.getDestinationPath(), is(nullValue())); assertTrue(tester.response.getOutputAsString().length() > 0); System.out.println(tester.response.getOutputAsString()); } @Test public void runGet() throws Exception { tester.request.setMethod("get"); tester.start("/rest/orders"); OrdersController controller = tester.getController(); assertThat(controller, is(notNullValue())); assertThat(tester.isRedirect(), is(false)); assertThat(tester.getDestinationPath(), is(nullValue())); assertTrue(tester.response.getOutputAsString().length() > 0); System.out.println(tester.response.getOutputAsString()); } @Test public void runPut1() throws Exception { tester.request.setMethod("put"); tester.start("/rest/orders"); OrdersController controller = tester.getController(); assertThat(controller, is(notNullValue())); assertThat(tester.isRedirect(), is(false)); assertThat(tester.getDestinationPath(), is(nullValue())); assertTrue(tester.response.getOutputAsString().length() == 0); } @Test public void runPut2() throws Exception { tester.request.setMethod("put"); tester.param("key", "1"); tester.start("/rest/orders"); OrdersController controller = tester.getController(); assertThat(controller, is(notNullValue())); assertThat(tester.isRedirect(), is(false)); assertThat(tester.getDestinationPath(), is(nullValue())); assertTrue(tester.response.getOutputAsString().length() > 0); System.out.println(tester.response.getOutputAsString()); } @Test public void runDelete1() throws Exception { tester.request.setMethod("delete"); tester.start("/rest/orders"); OrdersController controller = tester.getController(); assertThat(controller, is(notNullValue())); assertThat(tester.isRedirect(), is(false)); assertThat(tester.getDestinationPath(), is(nullValue())); assertTrue(tester.response.getOutputAsString().length() == 0); } @Test public void runDelete2() throws Exception { tester.request.setMethod("delete"); tester.param("key", "1"); tester.start("/rest/orders"); OrdersController controller = tester.getController(); assertThat(controller, is(notNullValue())); assertThat(tester.isRedirect(), is(false)); assertThat(tester.getDestinationPath(), is(nullValue())); assertTrue(tester.response.getOutputAsString().length() > 0); System.out.println(tester.response.getOutputAsString()); } }
テストクラスではなくクライアントプログラムで呼び出してテストするなら jQuery で書くか、「RESTCliant」というクライアントを使えばいい。
RESTClient
足し算プログラム
slim3の勉強を始めてみる。まずは足し算から。
Getting Started に沿ってプロジェクト整備。そしてページとコントローラーを作成。ここでは"/add/"と指定して生成した。
index.jsp
<%@page pageEncoding="UTF-8" isELIgnored="false"%> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%> <%@taglib prefix="f" uri="http://www.slim3.org/functions"%> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>add Index</title> <link rel="stylesheet" type="text/css" href="/css/global.css" /> </head> <body> <p>Hello add Index !!!</p> <form action="${f:url('calculate')}" method="post"> <table> <tr> <td></td><td><input type="text" ${f:text("arg1")} title="INPUT1" class="${f:errorClass('arg1', 'err')}"/></td> <td>${f:h(errors.arg1)}</td> </tr> <tr> <td> + </td> <td><input type="text" ${f:text("arg2")} title="INPUT2" class="${f:errorClass('arg2', 'err')}"/></td> <td>${f:h(errors.arg2)}</td> </tr> <tr> <td> = </td> <td>${f:h(result)}</td> </tr> </table> <input type="submit" name="doCalculate" value="calculate"/> <input type="submit" name="doReset" value="reset"/> </form> <a href="${f:h('/')}">Go Index.</a> </body> </html>
GAE/Jでセッションを有効にすると_ah_SESSIONがどんどん膨れ上がる件
GAE/Jでセッションを有効にすると_ah_SESSIONがどんどん膨れ上がっていきます。無駄なデータは容量の圧迫にもなるので定期的に消さなければなりません。
_ah_SESSIONを削除するにはGoogle app engineのコンソールから手動で削除するか、用意されている削除用サーブレットを定期的に実行すればOK。
Provide a servlet for session cleanup - Google App Engine for Java | Google Groups
http://groups.google.com/group/google-appengine-java/browse_thread/thread/4f0d9af1c633d39a?pli=1
しかし_ah_sessioncleanupサーブレットで削除される件数は一度に100件まで。それを一日一回実行してあげれば事足りる場合があるのでしょうが、私が作成したアプリの場合は通常のサーブレットで扱うセッション方法で気軽にセッションを使用したせいか一日一回100件の削除では全然足りない事が分かりました。削除回数を増やせばいいのかもしれないけど、セッション情報を一気に消すようなアプリは如何なものかと思い始めました。
GAE/Jではセッションを有効にして使用するのではなく、状態を独自で保持、保存する方法を取った方が何かと都合が良さそうです。状態を保存するようなコストはGAE/Jでは屁みたいなものでしょう。
後でセッションを無効にしようっと。
テーブルのヘッダを固定してエクセル風に表示する Super Tables
大量のデータを使用するテーブルをHTMLで書くと画面をスクロールする事になりますが、その際にヘッダも一緒にスクロールされてしまい現在見ている項目が何なのか確認する事が出来ない時があります。
そんな時は Super Tables を使えばエクセルのようにヘッダを固定して表示する事が出来ます。縦横のヘッダを同時に固定させる事も可能です。サンプルを作ってみました。ついでにデータ入力可能にしてみました。
エクセル風入力可能なテーブル
http://www.geocities.jp/uchblog/supertables/mySuperTablesDemo01.html
ソースは以下のような感じで。
<div class="fakeContainer"> <table id="demoTable" class="demoTable"> <tr> <th>Account</th> <th>First Name</th> <th>Last Name</th> <th>Age</th> <th>State</th> <th>Email Address</th> <th>Favorite Color</th> <th>Favorite Season</th> </tr> <tr> <td>account0001</td> <td>Jim</td> <td>Bo</td> <td>25</td> <td>Delaware</td> <td>Jim.Bo@gmail.com</td> <td>Blue</td> <td>Winter</td> </tr> 〜 </table> </div> <div id="testDiv" style="position:absolute;top:0px;right:0px;display:block;border:1px solid #adadad;width:300px;height:80px;padding:8px;background-color:#f6f6f6;"></div> <script type="text/javascript" src="javascripts/superTables.js"></script> <script type="text/javascript"> //<![CDATA[ (function() { var mySt = new superTable("demoTable", { cssSkin : "sSky", fixedCols : 1, headerRows : 1, onStart : function () { this.start = new Date(); }, onFinish : function () { document.getElementById("testDiv").innerHTML += "Finished...<br>" + ((new Date()) - this.start) + "ms.<br>"; } }); })(); //]]> </script>
通常のテーブルを書いてsuperTables.jsを読み込み数行の初期設定を書けばOK。テーブルを書くのに特別な事をしなくて良いのが嬉しいですね。