S-JIS[2010-09-12/2010-09-23] 変更履歴

Slim3

Slim3は、Google App Engine for Java(GAE/J)用のMVCフレームワーク。
グローバルトランザクションを扱える他、BigtableへのアクセスがJDO/JPAより速いらしい。


Slim3のインストール

Slim3はEclipseで扱える。
が、Eclipseのプラグインのような形式ではないので、“メニューから新規プロジェクトでSlim3のプロジェクトを作成する”のではない。
Slim3のブランクプロジェクトをダウンロードしてきて、Eclipseにインポートする。

参考: Slim3 日本語サイト(非公式)のブランクプロジェクトの入手

  1. ブランクプロジェクトの一覧のページを開く。(一番上が一番新しい)
  2. Slim3のブランクプロジェクトをダウンロードする。(2010-09-12時点でslim3-blank-1.0.5.zipが最新)
  3. Eclipse3.6のワークスペースにインポートする。
    1. メニューバーの「ファイル(F)」→「インポート(I)」で「インポート」ダイアログを開く。
    2. 「一般」⇒「既存プロジェクトをワークスペースへ」を選択して「次へ」
    3. 「アーカイブ・ファイルの選択(A)」を選んで、ダウンロードしたslim3-blank-1.0.5.zipの場所を指定する。
    4. 「完了」ボタンを押すと、プロジェクトがインポートされる。
    5. インポートされたプロジェクト名は「slim3-blank」となっているので、適宜変更する。
  4. Eclipseの設定を行う。(ブランクプロジェクトの入手に書いてある通り)
  5. war/WEB-INF/web.xmlを編集する。
    要素名 編集内容
    context-param <param-name>slim3.rootPackage</param-name> param-valueの値を自分が使用するパッケージ名にする。
    例えば「jp.hishidama.gae.slim3.ex1」
    servlet
    servlet-mapping
    <servlet-name>GWTServiceServlet</servlet-name> コメントアウトを外す。
  6. その他、ブランクプロジェクトの入手に書いてある設定を行う。

プロジェクトを実行(「実行」→「Webアプリケーション」)すると、「Development Mode」というビューが開く。
そこに表示されている「http://127.0.0.1:8888/index.html?gwt.codesvr=127.0.0.1:9997」をブラウザーで開くと、最初に「Google Web Toolkit Developer Plugin」のインストールが要求される。
GwtDevPluginSetup.exeをダウンロードして実行してインストールする。

再度「http://127.0.0.1:8888/index.html?gwt.codesvr=127.0.0.1:9997」を開くと、ブラウザー上は真っ白の画面が表示されるだけ。(GwtDevPluginのActiveXの実行は許可しておく)
Eclipseの「Development Mode」ビューには「Module main has been loaded」というメッセージが表示される。

この時点で「http://127.0.0.1:8888/index.html」にアクセスすると、「Webページからのメッセージ」というタイトルのエラーダイアログが表示される。文章は「GWT module 'main' may need to be (re)compiled」。
生成されたindex.htmlを見るとJavaScriptを実行しているだけのようなので、パラメーター無しで開く想定ではないのだろう。


ユーザーインターフェースの作成

ユーザー向けの画面(表示したり入力してもらったりする)を作成してみる。
(ただしこの方法は、Slim3というよりはGAE/Jの実装方法のような気がする。
 Slim3のMVC、つまりControllerとJSPを使う方法の場合、このユーザーインターフェースは不要かも?)

参考: Slim3 日本語サイト(非公式)のユーザーインターフェイスの作成

  1. メニューバーの「ファイル(F)」→「UiBinder」で「New UiBinder」ダイアログを開く。
  2. 「名前(M)」にMainと入れて「完了」ボタン
  3. すると、「src/指定パッケージ/」の下にMain.javaとMain.ui.xmlが作られる。

生成されたMain.java:

package jp.hishidama.gae.slim3.ex1.client;

import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Widget;

public class Main extends Composite {

	private static MainUiBinder uiBinder = GWT.create(MainUiBinder.class);

	interface MainUiBinder extends UiBinder<Widget, Main> {
	}

	@UiField
	Button button;

	public Main(String firstName) {
		initWidget(uiBinder.createAndBindUi(this));
		button.setText(firstName);
	}

	@UiHandler("button")
	void onClick(ClickEvent e) {
		Window.alert("Hello!");
	}

}

生成されたMain.ui.xml:

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
	xmlns:g="urn:import:com.google.gwt.user.client.ui">
	<ui:style>
		.important {
			font-weight: bold;
		}
	</ui:style>
	<g:HTMLPanel>
		Hello,
		<g:Button styleName="{style.important}" ui:field="button" />
	</g:HTMLPanel>
</ui:UiBinder>

Slim3のユーザーインターフェイスの作成ページに書かれているものとは内容が異なっているが、きっとバージョン違いの所為だろう。


そして、MainEntryPoint.javaを、生成したMainクラスを呼び出すよう修正する。

MainEntryPoint.java:

package jp.hishidama.gae.slim3.ex1.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.RootPanel;

public class MainEntryPoint implements EntryPoint {

	public void onModuleLoad() {
		RootPanel.get().add(new Main("hishidama"));
	}
}

ちなみに「onModuleLoad()」はインターフェースで宣言されているメソッドの実装だが、@Overrideアノテーションは付けない。
なぜなら、Slim3のブランクプロジェクトはJDK1.5準拠になっているから。


ブラウザーで「http://127.0.0.1:8888/index.html?gwt.codesvr=127.0.0.1:9997」を開く(あるいは既に開いていたのだったら更新/リロードする)と、Main.ui.xmlの内容がHTMLに変換されて表示される。

このボタンを押すと、「Hello!」というダイアログが表示される。


Main.javaMain.ui.xmlはお互いに密接な関係にある。

Main.java
public class Main extends Composite {
〜
	@UiField
	Button button;

	// コンストラクター
	public Main(String firstName) {
		initWidget(uiBinder.createAndBindUi(this));
		button.setText(firstName);
	}

	@UiHandler("button")
	void onClick(ClickEvent e) {
		Window.alert("Hello!");
	}
}
Main.ui.xml
	<g:HTMLPanel>
		Hello,
		<g:Button styleName="{style.important}" ui:field="button" />
	</g:HTMLPanel>

Mainクラスの中にbuttonという名前のフィールド(メンバー変数)がある。
Main()コンストラクターの中でbutton.setText()を行うことにより、ボタンのテキストをセットしている。これが画面表示時に表示されている。

Main.ui.xmlの「g:Button」という要素が、ボタンを生成している。
ここの「ui:field」という属性の値は、Main.javaの「@UiField」アノテーションを付けたフィールドの変数名と一致している必要がある。

また、Main#onClick()の「@UiHandler」アノテーションの値も、Main.ui.xmlのui:fieldで付けられた名前と一致している必要がある。

例えば「button」という名前を別の名前に変えたかったら、上記の4箇所全てを同じ名前に変える必要がある。
Mainクラスのフィールド名だけ変えると、「その名前がMain.ui.xmlに無い」という旨のエラーがEclipse上に表示される。
そしてMain.ui.xmlのui:fieldの値を変えると、Main#onClick()に付けている「@UiHandler」アノテーションの値が「Main.ui.xmlに無い」というエラーが発生する。
連動する箇所のエラーチェックがきちんとされているのはすごい。


Controllerによる画面遷移

Slim3では、画面遷移をControllerによって制御する。
ブランクプロジェクトに入っているbuild.xmlによって、Controllerクラスとjspファイルの雛形を生成することが出来る。

build.xmlの「gen-controller」を実行すると、パス(実行時のURLの一部)を入力するダイアログが開く。
ここにパスを入力すると、それに該当するControllerクラスとjspファイルが生成される。
Controllerクラスは、そのJSPを表示する為に使用される(JSPの表示前(JSPへのフォワード前)に呼ばれる)。

パス 生成されるファイル アクセスするURL
ローカル(テスト) インターネット(本番)
/ war/index.jsp
src/〜/server/controller/IndexController.java
test/〜/server/controller/IndexControllerTest.java
http://localhost:8888/
http://localhost:8888/index
http://aid.appspot.com/
http://aid.appspot.com/index
/aaa/ war/aaa/index.jsp
src/〜/server/controller/aaa/IndexController.java
test/〜/server/controller/aaa/IndexControllerTest.java
http://localhost:8888/aaa/
http://localhost:8888/aaa/index
http://aid.appspot.com/aaa/
http://aid.appspot.com/aaa/index
/aaa/bbb war/aaa/bbb.jsp
src/〜/server/controller/aaa/BbbController.java
test/〜/server/controller/aaa/BbbControllerTest.java
http://localhost:8888/aaa/bbb http://aid.appspot.com/aaa/bbb

生成されたIndexController.java:

package jp.hishidama.gae.slim3.ex1.server.controller;

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

public class IndexController extends Controller {

	@Override
	public Navigation run() throws Exception {
		return forward("index.jsp");	←index.jspへフォワードするよう記述されている
	}
}

このindex.jspにアクセスするには、「http://localhost:8888/」をブラウザーで開く。
jspには直接アクセスできないようになっているので、「http://localhost:8888/index.jsp」は403 FORBIDDENになる。

ただしユーザーインターフェースの作成(MainEntryPointの作成)によってindex.htmlを用意していた場合、そちらの方が優先され、今回作ったindex.jspは使用されない。
したがって、index.htmlを削除しておく必要がある。(あるいはリネームしておく)
Main.java・Main.ui.xml、ついでにMainEntryPoint.javaも(つまりclientパッケージごと全部)削除しておいても大丈夫なようだ。
プロジェクトのプロパティーの「Google」→「Webツールキット」の「エントリー・ポイント・モジュール」のMainを削除すれば、Main.gwt.xmlも不要になる。[2010-09-19]
また、war/main/も削除できる。(エントリーポイントが定義されていると、勝手に作られる)


アンカータグ

リンクによって別の画面へ遷移させるには、a要素(アンカータグ)のhref属性で「f:url」関数を記述する。[/2010-09-17]

<a href="${ f:url('/aaa/') }">遷移する</a>

「f」は「<%@taglib prefix="f" uri="http://www.slim3.org/functions"%>」によって宣言されている、Slim3が提供する関数の接頭辞。
f:url」は相対パスのURLをエンコードしてくれる。

遷移元 遷移先 記述例 備考
どこからでも war/aaa/index.jsp
<a href="${ f:url('/aaa/') }">遷移する</a>
<a href="${ f:url('/aaa/index') }">遷移する</a>
「/」から始めると、そのサイト内の絶対パス扱い。
「/」で終わると、indexが省略された扱い。
war/*.jsp war/aaa/index.jsp
<a href="${ f:url('aaa/') }">遷移する</a>
<a href="${ f:url('aaa/index') }">遷移する</a>
「/」から始めないと、相対パス。
war/aaa/*.jsp war/aaa/bbb.jsp
<a href="${ f:url('/aaa/bbb') }">遷移する</a>
<a href="${ f:url('bbb') }">遷移する</a>
同一ディレクトリー内のページへの遷移。
war/aaa/*.jsp war/aaa/index.jsp
<a href="${ f:url('/aaa/index') }">遷移する</a>
<a href="${ f:url('index') }">遷移する</a>
<a href="${ f:url('./') }">遷移する</a>
同一ディレクトリー内のindex.jspへの遷移。
war/aaa/*.jsp war/index.jsp
<a href="${ f:url('/') }">遷移する</a>
<a href="${ f:url('../') }">遷移する</a>
<a href="${ f:url('../index') }">遷移する</a>
ホームページ(トップページ)への遷移。

入力画面

情報を入力(登録)する画面を作るには、form要素をjspファイルに記述する。[2010-09-17]

参考: Slim3 日本語サイト(非公式)のフォームの作成

<form method="post" action="${ f:url('inputAction') }">
<p><input type="text" ${ f:text("foo") }/></p>
<p><input type="submit" value="登録"/></p>
</form>

input要素には、f:textf:hiddenといった、属性の一部を生成するSlim3の関数が用意されている。
ただしtype="submit"は特に何も用意されていないので、そのままHTMLを記述する。
StrutsだとHTMLのタグをそのまま置き換えるカスタムタグが用意されていたが、Slim3はそういう概念ではないようだ)

このままで入力エリアは表示される。
しかしサブミット(登録)ボタンを押すと、404 NOT_FOUNDのエラーになる。formタグのaction属性で指定したURIの処理を作ってないから当然^^;


なお、初期値を与えたい場合は、そのJSPとペアになっているControllerの中で値をセットしてやればいい。

public class InputController extends Controller {

	@Override
	public Navigation run() throws Exception {
		requestScope("foo", "初期値");

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

入力されたフォームを受け取る処理を作成する。

ブランクプロジェクトに入っているbuild.xmlの「gen-controller-without-view」を実行すると、Controllerクラスだけ作られる。(jspファイルは作られない)
aaa/inputAction」というパスにしておくと、「src/〜/server/controller/aaa/InputActionController.java」とテスト用ソースが出来る。

package jp.hishidama.gae.slim3.ex1.server.controller.aaa;

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

public class InputActionController extends Controller {

	@Override
	public Navigation run() throws Exception {
		return null;			←ただ単にnullを返すだけ(遷移先が無い)
	}
}

この親クラスであるControllerには、requestというフィールドが定義されている。
試しにその内容を表示してみよう。

package jp.hishidama.gae.slim3.ex1.server.controller.aaa;

import java.util.logging.Logger;

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

public class InputActionController extends Controller {
	private final static Logger LOG = Logger.getLogger(InputActionController.class.getName());

	@Override
	public Navigation run() throws Exception {
		RequestMap rm = new RequestMap(request);
		LOG.info(rm.toString());

		return null;
	}
}

RequestMapというのは、Slim3で用意されているユーティリティー。HashMapを継承している。
これで中を見てみると、元の画面で入力された値と名前が入っているのが分かる。
(ついでに「slim3.」で始まるプロパティーもいくつか入っているようだ。入力画面ではそういった名前は使用しない方が良さそう)

なお、run()でnullを返すと真っ白い画面が表示されるだけなので、別の画面に遷移するようにしておく。
gen-controllerで登録完了画面(/aaa/complete)を作っておいて、そこへリダイレクトするのが良さそう。

		 return redirect(basePath + "complete");

さて、Slim3の場合、DB登録などの処理は「サービス」クラスで行うようだ。
なので、Serviceクラスを作成し、ControllerからServiceを呼び出すようにする。

ブランクプロジェクトに入っているbuild.xmlの「gen-service」を実行するとクラス名(の一部)を入力するダイアログが開く。
ここにクラス名(の一部)を入力すると、Serviceクラスとそのテストクラスが作られる。
例えば「aaa.MyService」と入力すると「src/〜/server/service/aaa/MyService.java」が作られる。

生成されたMyService.java:

package jp.hishidama.gae.slim3.ex1.server.service.aaa;

public class MyService {

}

中身は何も無いので^^;、自分でメソッドを追加する。

package jp.hishidama.gae.slim3.ex1.server.service.aaa;

import java.util.Map;
import java.util.logging.Logger;

public class MyService {
	private final static Logger LOG = Logger.getLogger(MyService.class.getName());

	public void regist(Map<String, Object> input) {
		LOG.info(input.toString());
	}
}

修正したInputActionController.java

package jp.hishidama.gae.slim3.ex1.server.controller.aaa;

import jp.hishidama.gae.slim3.ex1.server.service.aaa.MyService;

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

public class InputActionController extends Controller {

	private MyService service = new MyService();

	@Override
	public Navigation run() throws Exception {
		service.regist(new RequestMap(request));

		return null;
	}
}

ちなみにControllerのインスタンスはリクエスト毎に作られる(マルチスレッドで呼ばれない)ようなので、フィールドにインスタンスを持たせてもよい。


Slim3で(GAEの)DBにデータを登録(保存)する為には、「モデル」クラスを使用する。
モデルとは、要するにJavaBeans(フィールドに値を保持し、セッター・ゲッターメソッドで設定・取得する)。
モデルのインスタンスにデータをセットし、それをそのままGAEのDBに保存する。
(Modelの説明→Slim3 日本語サイト(非公式)のデータクラスの定義

ブランクプロジェクトに入っているbuild.xmlの「gen-model」を実行するとクラス名(の一部)を入力するダイアログが開く。
ここにクラス名(の一部)を入力すると、Modelクラスとそのテストクラス、それからMetaというクラスが作られる。
例えば「aaa.Data」と入力すると「src/〜/shared/model/aaa/Data.java」「src/〜/server/meta/aaa/DataMeta.java」が作られる。

生成されたData.javaの一部:

package jp.hishidama.gae.slim3.ex1.shared.model.aaa;

import java.io.Serializable;

import com.google.appengine.api.datastore.Key;

import org.slim3.datastore.Attribute;
import org.slim3.datastore.Model;

@Model(schemaVersion = 1)
public class Data implements Serializable {

	private static final long serialVersionUID = 1L;
	@Attribute(primaryKey = true)
	private Key key;

	@Attribute(version = true)
	private Long version;
	/**
	 * Returns the key.
	 *
	 * @return the key
	 */
	public Key getKey() {
		return key;
	}
〜
}

GAEにデータを登録する際には、キーとバージョンが必須のようだ。なので、それに関するフィールドとセッター・ゲッターメソッドが自動で用意されている。

ここに、自分で必要とするフィールドとセッター・ゲッターメソッドを追加する。
その際の名前(セッター・ゲッターメソッドの名前、いわゆるプロパティー名)は、入力元JSP(input要素のname属性)に合わせておく。
そうすることによって、Slim3のユーティリティーを使って値のコピーが簡単に実装できるから。

	private String foo;
〜
	/**
	 * @return foo
	 */
	public String getFoo() {
		return foo;
	}

	/**
	 * @param foo
	 *	セットする foo
	 */
	public void setFoo(String foo) {
		this.foo = foo;
	}

※フィールドやセッター・ゲッターは自分で追加する必要があるが、それによってMetaクラスが自動的に書き換わる。

このモデルクラスを使ってDBに登録するロジックは、Serviceクラスに記述する。
(トランザクションの中で例外が発生すると、自動的にロールバックされるらしい)

import org.slim3.datastore.Datastore;
import org.slim3.util.BeanUtil;
import com.google.appengine.api.datastore.Transaction;

import jp.hishidama.gae.slim3.ex1.shared.model.aaa.Data;

public class MyService {

	public Data regist(Map<String, Object> input) {
		Data data = new Data();
		BeanUtil.copy(input, data);

		Transaction tx = Datastore.beginTransaction();
		Datastore.put(data);
		tx.commit();

		return data;	//値を返しているのは、テスト時にキーを取得したい為
	}
}

修正したMyServiceTest.java:

public class MyServiceTest extends AppEngineTestCase {
〜
	@Test
	public void regist() {
		Map<String, Object> input = new HashMap<String, Object>();
		input.put("foo", "Hello");

		Data data = service.regist(input);

		Data stored = Datastore.get(Data.class, data.getKey());
		assertThat(stored.getFoo(), is("Hello"));
	}
}

表示画面(一覧表示画面)

DBに登録された値の一覧を表示する画面を作ってみる。[2010-09-18]
画面に表示する値は、そのjspを表示する為(JSPへの遷移前)に呼ばれるControllerで用意(requestオブジェクトにセット)する。

参考: Slim3 日本語サイト(非公式)のつぶやきの一覧表示

生成したaaa/list.jspを修正したもの:

<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="f" uri="http://www.slim3.org/functions"%>
〜
<c:forEach var="e" items="${dataList}">
${f:h(e.foo)}<br>
</c:forEach>

生成したListController.javaを修正したもの:

	@Override
	public Navigation run() throws Exception {
		List<Data> dataList = new ArrayList<Data>();
		{
			Data g1 = new Data(); g1.setFoo("abc");    dataList.add(g1);
			Data g2 = new Data(); g2.setFoo("あいう"); dataList.add(g2);
		}
		requestScope("dataList", dataList);

		return forward("list.jsp");
	}

で、実際にはServiceクラスの方にDBからデータを取得するメソッドを用意して、それを呼び出す。


ListController.java:

	private MyService service = new MyService();
〜
	@Override
	public Navigation run() throws Exception {
		List<Data> dataList = service.getList();
		requestScope("dataList", dataList);

		return forward("list.jsp");
	}

修正したMyService.java:

import org.slim3.datastore.Datastore;

import jp.hishidama.gae.slim3.ex1.shared.model.aaa.Data;

public class MyService {

	private DataMeta meta = new DataMeta();public List<Data> getList() {
		return Datastore.query(meta).sort(meta.foo.asc).asList();
	}
}

Datastore.query()はModelQueryというクラスを返す。
ModelQuery#sort()でソート項目を指定する。この指定の仕方は面白いな。
他にもModelQuery#filter()でフィルター条件(SQLで言うWHERE条件)を指定できるようだ。
最後にModelQuery#asList()を呼ぶことによってDBアクセスが行われる。


削除機能

一覧表示画面に削除機能を追加してみる。[2010-09-23]

現在のデータ一覧の右側に削除リンクを置く。そのリンクをクリックすると、その行が削除される。

list.jsp:

<table border="1">
	<tr>
		<th>タイトル</th>
		<th>内容</th>
		<th>作成日</th>
		<th>削除</th>
	</tr>
<c:forEach var="e" items="${dataList}">
	<tr>
		<td>${f:h(e.title)}</td>
		<td>${f:h(e.content)}</td>
		<td><fmt:formatDate value="${e.createDate}" pattern="yyyy-MM-dd" /></td>
		<c:set var="deleteUrl" value="delete?key=${f:h(e.key)}&amp;version=${e.version}" />
		<td><a href="${f:url(deleteUrl)}" onclick="return confirm('削除していいですか?')">削除</a></td>
	</tr>
</c:forEach>
</table>

DeleteController.java:

package jp.hishidama.gae.slim3.ex1.server.controller.aaa;

import jp.hishidama.gae.slim3.ex1.server.meta.aaa.DataMeta;
import jp.hishidama.gae.slim3.ex1.server.service.aaa.MyService;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;
public class DeleteController extends Controller {

	private DataMeta  meta    = DataMeta.get();
	private MyService service = new MyService();

	@Override
	public Navigation run() throws Exception {
		service.delete(asKey(meta.key), asLong(meta.version));

		return redirect("list"); //一覧表示画面に戻る
	}
}

MyService.java:

public class MyService {

	private DataMeta meta = new DataMeta();
〜
	public void delete(Key key, Long version) {
		Transaction tx = Datastore.beginTransaction();
		Data data = Datastore.get(tx, meta, key, version);
		Datastore.delete(tx, data.getKey());
		Datastore.commit(tx);
	}
}

また、controllerパッケージ直下にAppRouterというクラスを作ってURLマッピングを変更しておくと、別のURI形式「delete/キー/バージョン」で削除を指定することが出来る。

list.jsp:

		<c:set var="deleteUrl" value="delete/${f:h(e.key)}/${e.version}" />
		<td><a href="${f:url(deleteUrl)}" onclick="return confirm('削除していいですか?')">削除</a></td>

AppRouter.java:

package jp.hishidama.gae.slim3.ex1.server.controller;

import org.slim3.controller.router.RouterImpl;

public class AppRouter extends RouterImpl {

	/** コンストラクター */
	public AppRouter() {
		addRouting(
			"/{app}/delete/{key}/{version}",
			"/{app}/delete?key={key}&version={version}"
		);
	}
}

「/aaa/delete/キー/バージョン」というリクエストが来ると、「/aaa/delete?key=キー&version=バージョン」という形に置き換えてからControllerが呼び出されるようになる。


日付書式

Slim3(GAE)では日付は標準時で扱われるので、日本時間(JST)とずれている。[2010-09-18]
内部データとしてはそれでいいとしても、表示する際は日本人としては日本時間の方が都合がいいんじゃないかと思う。

JSPで表示する際にDate型の値を変換してやる。 (Slim3やGAEの機能ではなく、JSTLの日付フォーマットを使用する)

<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<fmt:formatDate value="${createDate}" pattern="yyyy-MM-dd HH:mm:ss" timeZone="JST"/>
<fmt:setTimeZone value="JST"/>
<fmt:formatDate value="${createDate}" pattern="yyyy-MM-dd HH:mm:ss z"/>

精査(Validation)

Slim3では、簡単なバリデーションフレームワークが提供されている。[2010-09-18]

参考: Slim3 日本語サイト(非公式)のバリデーション

入力を受け付けるControllerの中で値をチェックし、エラーがあったらエラー画面へ遷移する。

修正したInputActionController.java

〜
import org.slim3.controller.validator.Validators;
〜
public class InputActionController extends Controller {
〜
	@Override
	public Navigation run() throws Exception {
		if(!validate()){
			return forward("input");	//エラーが起きた場合の遷移先。ここでは元の入力画面
		}

		service.regist(new RequestMap(request));

		return redirect(basePath + "complete");
	}

	boolean validate() {
		Validators v = new Validators(request);

		v.add("foo", v.required());
		v.add("bar", v.required(), v.maxlength(100));
		v.add("zzz", v.required(), v.integerType(), v.longRange(1, Integer.MAX_VALUE));

		return v.validate();
	}
}

Validatorsのインスタンスを作る際にrequestを渡しているので、精査ロジックを組み込むのはControllerクラスでいい、と思う。

v.add()の第1引数に入力画面の各項目の名前(<input name="foo">とか)を指定し、第2引数以降には精査内容を指定する。
required()が必須チェック、maxlength()が最大文字数チェック、integerType()は整数チェック、longRange()は範囲チェックと、大変分かり易い。

エラーが発生するとv.validate()がfalseを返す。
また、Controller#errors(v.getErrors()で返されるのと同じインスタンス)の中にエラーメッセージがセットされる。


エラーメッセージを表示するには、以下のように記述する。
(ここでは元の入力画面にエラーメッセージを表示する想定)

修正したinput.jsp:

<head>
〜
<style type="text/css">
input.err { background-color: pink; }
span.err  { color: red; }
</style>
</head>
<%-- エラーメッセージをまとめて表示する例 --%>
<c:if test="${not empty errors}">
<ul>
<c:forEach var="e" items="${f:errors()}">
<li><span class="err">${f:h(e)}</span></li>
</c:forEach>
</ul>
</c:if>
<%-- 個別のエラーメッセージを表示する例 --%>
foo:
<input type="text" ${ f:text("foo") } class="${f:errorClass('foo', 'err')}"/>
<c:if test="${not empty errors.foo}"><span class="err">エラー:${f:h(errors.foo)}</span></c:if>
<br>
bar:
<input type="text" ${ f:text("bar") } class="${f:errorClass('bar', 'err')}"/>
<c:if test="${not empty errors.bar}"><span class="err">エラー:${f:h(errors.bar)}</span></c:if>
<br>
zzz:
<input type="text" ${ f:text("zzz") } class="${f:errorClass('zzz', 'err')}"/>
<c:if test="${not empty errors.zzz}"><span class="err">エラー:${f:h(errors.zzz)}</span></c:if>

これで、foo・bar・zzzが必須チェックエラーになると以下のようなメッセージが表示される。

  • fooは必須です。
  • barは必須です。
  • zzzは必須です。
foo: エラー:fooは必須です。
bar: エラー:barは必須です。
zzz: エラー:zzzは必須です。

エラーメッセージにはエラーの出た項目名が出力されるが、いわばIDが表示されるのはあまり良くない。
src/application_ja.propertiesに「label.項目名」というエントリーを加えると、その名前で表示されるようになる。

src/application_ja.properties:

〜
label.foo=ふー
label.bar=バー
lbael.zzz=すやすや
  • ふーは必須です。
  • バーは必須です。
  • すやすやは必須です。
foo: エラー:ふーは必須です。
bar: エラー:バーは必須です。
zzz: エラー:すやすやは必須です。

また、application_ja.properties内の「validator.」で始まるプロパティーの文言を書き換えれば、エラーメッセージ全般を独自に定義することが出来る。

validator.required={0}は必須です。
validator.byteType={0}はバイトでなければいけません。
validator.shortType={0}は短整数でなければいけません。
validator.integerType={0}は整数でなければいけません。
validator.longType={0}は長整数でなければいけません。
validator.floatType={0}は単精度実数でなければいけません。
validator.doubleType={0}は倍精度実数でなければいけません。
validator.numberType={0}は数値({1})ではありません。
validator.dateType={0}は日付({1})ではありません。
validator.minlength={0}の長さが最小値({1})未満です。
validator.maxlength={0}の長さが最大値({1})を超えています。
validator.range={0}は{1}と{2}の間でなければいけません。
validator.regexp={0}が不正です。

精査ロジックの方で個別にエラーメッセージを指定することも出来る。
(ただし、この場合には{0}や{1}といった引数の置換は使えないので注意)

修正したInputActionController.java

public class InputActionController extends Controller {
〜
	boolean validate() {
		Validators v = new Validators(request);

		v.add("foo", v.required("fooは必須やがな"));
		v.add("bar", v.required("barは必須よ"), v.maxlength(100, "barが100を超えてるわよ"));
		v.add("zzz", v.required("睡眠は必須っす"), v.integerType("zzzが数じゃない…"), v.longRange(1, Integer.MAX_VALUE, "zzzが範囲外"));

		return v.validate();
	}
}
  • fooは必須やがな
  • barは必須よ
  • 睡眠は必須っす
foo: エラー:fooは必須やがな
bar: エラー:barは必須よ
zzz: エラー:睡眠は必須っす

HTTPセッション

GAEでHTTPセッションを使用するには、appengine-web.xmlを設定しておく必要がある。[2010-09-19]

war/WEB-INF/appengine-web.xml:

<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
〜
	<sessions-enabled>true</sessions-enabled>	…デフォルトはfalse
</appengine-web-app>

Slim3の場合、Controllerクラスでは(requestと同様に)sessionScope()というメソッドが用意されているので、値の設定・取得にはそれを使うのが便利。


GAEへ戻る / Javaへ戻る / 技術メモへ戻る
メールの送信先:ひしだま