ブックマーク登録画面の作成 その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/