Google App Engine を使ってみよう(1) 〜 掲示板を作る

今更ながら、Google App Engine に手を出してみた。
webサービスの入門といえば掲示板だろう、ということでまずは掲示板を作ってみる。
とりあえず最新版ということで、SDK は 1.4.3 、slim3 は 1.0.10 を使うことに。


まずは開発環境を整えるために Slim3 日本語サイト(非公式) を見ながら Eclipse Plugin や環境設定を行う。


開発環境が準備できたら、最初に投稿用ページ(/post) を作る。
build.xml から gen-controller を実行すると、投稿用ページのコントローラが生成される。
とりあえず投稿用ページには投稿内容を /post に送信するフォームだけ用意した。

<form action="/post" method="POST">
	<div>Content:</div>
	<div><textarea name="content" rows="10" cols="30"></textarea></div>
	<div><button type="submit">submit</button></div>
</form>


続いて、gen-model を実行して投稿内容を保持するためのモデル(Posting)を生成する。
生成されたモデルにとりあえず投稿内容と投稿日時を保持するフィールドを追加した。

	private String content;

	private Date postingDate;

	public void setContent(String content) {
		this.content = content;
	}

	public String getContent() {
		return content;
	}

	public void setPostingDate(Date postingDate) {
		this.postingDate = postingDate;
	}

	public Date getPostingDate() {
		return postingDate;
	}


モデルを作ったら、今度はモデルを扱うサービス(PostingService)を作るために gen-service を実行する。
slim3 はテスト用クラスの雛形まで生成してくれるので、TDD が非常にやりやすい。
サービスとそのテストコードは以下のような感じ。


PostingService.java

public class PostingService {

	private PostingMeta pm = new PostingMeta();

	public Posting addNewPosting(String content, Date postingDate) {
		Posting post = new Posting();
		post.setKey(Datastore.allocateId(Posting.class));
		post.setContent(content);
		post.setPostingDate(postingDate);

		Datastore.put(post);

		return post;
	}
        
	public Posting addNewPosting(String content) {
		return addNewPosting(content, new Date());
	}
}


PostingServiceTest.java

public class PostingServiceTest extends AppEngineTestCase {

	private PostingService service = new PostingService();

	@Test
	public void test() throws Exception {
		assertThat(service, is(notNullValue()));
	}

	@Test
	public void addNewPosting() throws Exception {
		String content = "aaa";
		Date postingDate = new Date();

		Posting posting = service.addNewPosting(content, postingDate);
		assertThat(posting, is(notNullValue()));

		Posting stored = Datastore.get(Posting.class, posting.getKey());
		assertThat(stored, is(notNullValue()));
		assertThat(stored.getContent(), is(content));
		assertThat(stored.getPostingDate(), is(postingDate));
	}
}


次に、PostController に投稿処理を追加する。


PostController.java

public class PostController extends Controller {
	private PostingService service = new PostingService();

	@Override
	public Navigation run() throws Exception {
		
		if(isPost() && doPost()) {
			return redirect("/");
		}
		
		return forward("post.jsp");
	}
	
	private boolean doPost() {
		String content = asString("content");
		service.addNewPosting(content);
		
		return true;
	}
}


PostControllerTest.java

public class PostControllerTest extends ControllerTestCase {

	@Test
	public void run() throws Exception {
		tester.param("content", "aaa");
		tester.start("/post");
		PostController controller = tester.getController();
		assertThat(controller, is(notNullValue()));
		assertThat(tester.isRedirect(), is(false));
		assertThat(tester.getDestinationPath(), is("/post.jsp"));

		Posting stored = Datastore.query(Posting.class).asSingle();
		assertThat(stored, is(nullValue()));
	}
	
	@Test
	public void addNewPost() throws Exception {
		String content = "aaa";

		tester.request.setMethod("POST");
		tester.param("content", content);
		tester.start("/post");
		PostController controller = tester.getController();
		assertThat(controller, is(notNullValue()));
		assertThat(tester.isRedirect(), is(true));
		assertThat(tester.getDestinationPath(), is("/"));

		Posting stored = Datastore.query(Posting.class).asSingle();
		assertThat(stored, is(notNullValue()));
		assertThat(stored.getContent(), is(content));
		assertThat((new Date().getTime() - stored.getPostingDate().getTime()) <= 1 * 1000, is(true));
	}
}


ここまでで投稿機能まではひとまず完成。
次に一覧表示用のページ(/)を作る。


ひとまず PostService に一覧取得用メソッドを追加する。


PostingService.java

	// 追加
	public List<Posting> getPostingList() {
		return Datastore.query(pm).sort(pm.postingDate.desc).asList();
	}


PostingServiceTest.java

	// 追加
	@Test
	public void getPostingList() throws Exception {
		String content = "aaa";
		Date postingDate = new Date();

		Posting post = new Posting();
		post.setContent(content);
		post.setPostingDate(postingDate);
		Datastore.put(post);

		List<Posting> postList = service.getPostingList();
		assertThat(postList.size(), is(1));
		assertThat(postList.get(0).getContent(), is(content));
		assertThat(postList.get(0).getPostingDate(), is(postingDate));
	}


続いてコントローラを生成して、一覧表示するコードを追加する。


index.jsp

<c:forEach var="e" items="${postingList}">
<p>
<div>
${f:h(e.content)}
</div>
<span class="small">${f:h(e.postingDate)}</span>
</p>
<hr />
</c:forEach>

IndexContoroller.java

public class IndexController extends Controller {

	private PostingService service = new PostingService();

	@Override
	public Navigation run() throws Exception {

		List<Posting> postingList = service.getPostingList();
		requestScope("postingList", postingList);

		return forward("index.jsp");
	}
}


IndexContorollerTest.java

public class IndexControllerTest extends ControllerTestCase {

	@Test
	public void run() throws Exception {
		tester.start("/");
		IndexController controller = tester.getController();
		assertThat(controller, is(notNullValue()));
		assertThat(tester.isRedirect(), is(false));
		assertThat(tester.getDestinationPath(), is("/index.jsp"));

		assertThat(tester.requestScope("postingList"), is(notNullValue()));
	}
}

後は google app engine にデプロイすれば完成。
もっとも、実際に使うためにはまだまだ機能を追加する必要があるので、今後はその辺を拡張してきたいなぁ。


ソースコードGitHub に上げているので参考にどうぞ。
(今回のソースコードタグ v1 で参照可)