ゼロから始めるGWT その4
動くまでのメモ書き。試行錯誤で記述が前後してまとめきれなかった…。
- データベース作成
- Seasarとs2jdbc-gen の設定
- UiBinderを使ってHTML表示
- DB処理
その1で環境設定をした後から。
データベースを使えるようにするまで。
フレームワークとしてSeasarを使うことにした。PHPのフレームワークを作るときの参考でマニュアルとサンプルは何度か読んでいたので。
GoogleのWebアプリケーションプロジェクトの新規作成で、名前をGwtDb、パッケージをgwtdbとする。
Google App エンジンを使用するのチェックを外して完了ボタンを押す。
取り敢えず「プロジェクトを右クリック」「実行」「Webアプリケーション」でブラウザでの表示を確認する。
次にDBのテーブルを定義する。PostgreSQLで。
createdb -E UTF-8 gwtdb psql gwtdb create table folder(folder_id serial primary key, folder_name text unique not null); create table item(item_id serial primary key, folder_id int references folder(folder_id) not null, item_name text unique not null); insert into folder (folder_name) values('フォルダ1'); insert into folder (folder_name) values('フォルダ2'); insert into item (folder_id, item_name) values(1, 'アイテム1'); insert into item (folder_id, item_name) values(1, 'アイテム2'); insert into item (folder_id, item_name) values(2, 'アイテム3');
こんな感じ。
ドライバはaptで入れる。
aptitude install libpg-java
次にSeasarのS2Container、S2Tiger、S2JDBC-GenをDLする。
http://s2container.seasar.org/2.4/ja/downloads.html
GwtDbプロジェクトディレクトリの直下にlibディレクトリを作り
S2Containerの s2-framework-*.jar、s2-extension-*.jar、aopalliance-*.jar、commons-logging-*.jar、assist-*.jar、ognl-*.jar、geronimo-jta_1.1_spec-1.0.jar、junit-*.jar(コアファイル郡)、
S2Tigerのs2-tiger-*.jar、geronimo-jpa*.jar(トランザクションなど)、
S2JDBC-Genのfreemaker-*.jar と s2jdbc-gen-*.jarをコピーする。
また、S2JDBC-Gen/resourcesの s2jdbc-gen-build.xml もプロジェクト直下にコピーする。
プロジェクトのツリー表示はF5で更新できるので、追加したファイルをビルドパスに追加する。
s2jdbc-gen-build.xmlを環境に合わせて編集する。
<project name="GwtDb-s2jdbc-gen" default="gen-ddl" basedir="."> <property name="classpathdir" value="war/WEB-INF/classes"/> <property name="rootpackagename" value="gwtdb"/> <property name="entitypackagename" value="entity"/> <property name="entityfilepattern" value="gwtdb/entity/**/*.java"/> <property name="javafiledestdir" value="src"/> <property name="testjavafiledestdir" value="test"/> <property name="javafileencoding" value="UTF-8"/> <property name="version" value="latest"/> <property name="sqlfilepattern" value="META-INF/sql/**/*.sql"/> <property name="applyenvtoversion" value="false"/> <property name="uses2junit4" value="false"/> <property name="env" value="ut"/> <property name="jdbcmanagername" value="jdbcManager"/> <condition property="vmarg.encoding" value="-Dfile.encoding=UTF-8" else=""> <isset property="eclipse.pdebuild.home"/> </condition> <path id="classpath"> <pathelement location="${classpathdir}"/> <fileset dir="war/WEB-INF/lib" /> <fileset dir="lib" /> </path>
新規Diconファイル(EclipseのKijimuraプラグイン)でjdbc.dicon、convention.dicon、s2jdbc.diconファイルをsrc以下に作る。サンプルからコピペするならプラグインは要らないかも。
jdbc.dicon
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd"> <components> <component name="xaDataSource" class="org.seasar.extension.dbcp.impl.XADataSourceImpl"> <property name="driverClassName"> "org.postgresql.Driver" </property> <property name="URL"> "jdbc:postgresql://localhost/gwtdb" </property> <property name="user">"xxx"</property> <property name="password">"xxx"</property> </component> <component name="TransactionManager" class="org.seasar.extension.jta.TransactionManagerImpl" /> <component name="connectionPool" class="org.seasar.extension.dbcp.impl.ConnectionPoolImpl"> <property name="timeout">600</property> <property name="maxPoolSize">10</property> <property name="allowLocalTx">true</property> <property name="transactionManager">TransactionManager</property> <destroyMethod name="close"/> </component> <component name="DataSource" class="org.seasar.extension.dbcp.impl.DataSourceImpl"/> </components>
s2jdbc.dicon
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd"> <components> <include path="jdbc.dicon"/> <include path="s2jdbc-internal.dicon"/> <component name="jdbcManager" class="org.seasar.extension.jdbc.manager.JdbcManagerImpl"> <property name="maxRows">0</property> <property name="fetchSize">0</property> <property name="queryTimeout">0</property> <property name="dialect">postgre81Dialect</property> </component> </components>
convention.dicon
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd"> <components> <component class="org.seasar.framework.convention.impl.NamingConventionImpl"> <initMethod name="addRootPackageName"> <arg>"gwtdb.server"</arg> </initMethod> </component> <component class="org.seasar.framework.convention.impl.PersistenceConventionImpl"/> </components>
ハマりポイント。gwtdb.clientはJavaScript変換用なのでルートパッケージはserverを指定する。
postgresqlのjarファイルはどうすればいいのか分からなかったのでインストールされたディレクトリからコピーした。
cp /usr/share/java/postgresql.jar workspace/GwtDb/lib/
Eclipseの「ウィンドウ」「ビューの表示」でAntを表示させて、そこに s2jdbc-gen-build.xml をドラッグする。それから gen-entity をクリックして緑の実行ボタンを押す。成功したら src/gwtdb/entity などにファイルが生成される。
http://d.hatena.ne.jp/taedium/20081101/p1
ここを参考にDDLを作る。Antビューで gen-ddl をクリックして実行するだけ。
ここまでやって、実感が湧かないので取り敢えずWebページに表示してみようと思う。
S2JDBCのチュートリアルとGWTパネルのマニュアルを見つつ。
「新規」「UiBinder」でパッケージにgwtdb.client、名前にFolderMenuと入力して完了。
GwtDb.javaのonModuleLoad() を変更する。の直下にRootPanel.get().add(new FolderMenu("folderMenu")); を追加する。
<!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" xmlns:app="urn:import:gwtdb.client.widget"> <ui:style> .important { font-weight: bold; } .header { text-align: center; } </ui:style> <g:DockLayoutPanel unit="EM"> <g:north size="10"> <g:Label styleName="{style.header}">ヘッダー</g:Label> </g:north> <g:south size="10"> <g:Label styleName="{style.header}">フッター</g:Label> </g:south> <g:center> <g:Label ui:field="contents">コンテンツ</g:Label> </g:center> <g:west size="20"> <g:HTMLPanel> <g:ListBox ui:field="listBox" visibleItemCount="5" /> <app:Folders ui:field="folders" /> </g:HTMLPanel> </g:west> </g:DockLayoutPanel> </ui:UiBinder>
public class FolderMenu extends Composite { private static FolderMenuUiBinder uiBinder = GWT .create(FolderMenuUiBinder.class); interface FolderMenuUiBinder extends UiBinder<Widget, FolderMenu> { } @UiField ListBox listBox; @UiField Label contents; public FolderMenu(String name) { listBox.addItem(name); initWidget(uiBinder.createAndBindUi(this)); }
元々あるHTMLの余分な箇所は消しておく。
これで一応DBを使わずに画面は出た。
ここからが長い。
DB接続のコードを書く前に、Ajaxでやりとりするためのクラスを作る。
パッケージはgwtdb.sharedで、IsSerializableをimplementsする。
package gwtdb.shared; import com.google.gwt.user.client.rpc.IsSerializable; public class HasId<T> implements IsSerializable { public Integer id; public T value; }
IsSerializableと総称型を一緒に使っていいのかは謎。
サーバーとクライアントのやりとりで、文字列と一緒にデータベースのプライマリキーも欲しいので、IDを持つクラスを作った。
そしてGWTのサーバーとクライアントの非同期通信のためのコードを書く。
package gwtdb.client.server; import gwtdb.shared.HasId; import java.util.List; import com.google.gwt.user.client.rpc.RemoteService; import com.google.gwt.user.client.rpc.RemoteServiceRelativePath; /** * The client side stub for the RPC service. */ @RemoteServiceRelativePath("folders") public interface FolderListService extends RemoteService { List<HasId<String>> getFolders(); }
package gwtdb.client.server; import gwtdb.shared.HasId; import java.util.List; import com.google.gwt.user.client.rpc.AsyncCallback; public interface FolderListServiceAsync { void getFolders(AsyncCallback<List<HasId<String>>> callback); }
HasIdのvalueはString固定で良かったかな?と思いつつそのままいく。
通信用のファイルが増えそうなのでパッケージは分けた。
次はサーバー側の実装。
package gwtdb.server; import java.util.ArrayList; import java.util.List; import org.seasar.extension.jdbc.JdbcManager; import org.seasar.framework.container.SingletonS2Container; import gwtdb.client.server.FolderListService; import gwtdb.entity.Folder; import gwtdb.shared.HasId; import com.google.gwt.user.server.rpc.RemoteServiceServlet; @SuppressWarnings("serial") public class FolderListServiceImpl extends RemoteServiceServlet implements FolderListService { private JdbcManager jdbcManager = SingletonS2Container.getComponent(JdbcManager.class); @Override public List<HasId<String>> getFolders(){ // TODO 自動生成されたメソッド・スタブ List<Folder> folders = jdbcManager.from(Folder.class).getResultList(); List<HasId<String>> list = new ArrayList<HasId<String>>(); for (Folder folder: folders) { HasId<String> hasId = new HasId<String>(); hasId.id = folder.folderId; hasId.value = folder.folderName; list.add(hasId); } return list; } }
同じようにItemエンティティに対してもインターフェースと実装を作る。
jdbcManagerがnullで落ちるのでハマった。このクラス自体がS2Containerで管理されていないから、JdbcManagerの生成は手動じゃないといけない?
app.diconが読み込めていなかったぽいので後でもう一度試す。
S2Strutsのように連携できると思うが、それは後で考える。
S2Containerを使うためにwar/WEB-INF/web.xmlを編集。
<servlet> <servlet-name>s2servlet</servlet-name> <servlet-class>org.seasar.framework.container.servlet.S2ContainerServlet</servlet-class> <init-param> <param-name>configPath</param-name> <param-value>app.dicon</param-value> </init-param> <init-param> <param-name>debug</param-name> <param-value>true</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>
src以下にdiconファイルを作る。
app.dicon
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd"> <components> <include path="convention.dicon"/> <include path="aop.dicon"/> <include path="j2ee.dicon"/> <include path="s2jdbc.dicon"/> </components>
サーブレット用にdiconのファイル4個とlib以下のjarを war/WEB-INF/lib/ 以下にまとめてコピーする。
GwtDb.java にサーバーとクライアントでやり取りするコードを書く。
private final FolderListServiceAsync folderListService = GWT.create(FolderListService.class); private final ItemListServiceAsync itemListService = GWT.create(ItemListService.class); /** * This is the entry point method. */ public void onModuleLoad() { //RootLayoutPanel.get().add(new FolderMenu("folderMenu")); folderListService.getFolders(new AsyncCallback<List<HasId<String>>> (){ @Override public void onFailure(Throwable caught) { // TODO 自動生成されたメソッド・スタブ } @Override public void onSuccess(List<HasId<String>> result) { // TODO 自動生成されたメソッド・スタブ RootLayoutPanel.get().add(new FolderMenu(itemListService, result)); } }); }
LayoutPanelで全体を描画するときは、RootPanelではなくてRootLayoutPanelを使うとうまく表示できた。
選択したFolderのItemを表示するようにFolderMenuを実装する。
package gwtdb.client; import java.util.List; import gwtdb.client.server.ItemListServiceAsync; import gwtdb.client.widget.Folders; import gwtdb.shared.HasId; import com.google.gwt.core.client.GWT; import com.google.gwt.event.logical.shared.SelectionEvent; import com.google.gwt.event.logical.shared.SelectionHandler; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.TreeItem; import com.google.gwt.user.client.ui.Widget; public class FolderMenu extends Composite { private static FolderMenuUiBinder uiBinder = GWT .create(FolderMenuUiBinder.class); interface FolderMenuUiBinder extends UiBinder<Widget, FolderMenu> { } @UiField Folders folders; @UiField Label contents; public FolderMenu(final ItemListServiceAsync proxy, List<HasId<String>> fs) { initWidget(uiBinder.createAndBindUi(this)); for (HasId<String> item: fs){ folders.add(item); } folders.addSelectionHandler(new SelectionHandler<TreeItem> (){ @Override public void onSelection(SelectionEvent<TreeItem> event) { // TODO 自動生成されたメソッド・スタブ Integer i = folders.getSelectedIndex(event.getSelectedItem()); proxy.getItems(i, new AsyncCallback<List<HasId<String>>> (){ @Override public void onFailure(Throwable caught) { // TODO 自動生成されたメソッド・スタブ String val = ""; for (HasId<String> a: result){ val += a.value + ", "; } contents.setText(val); } }); } }); } }
<!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" xmlns:app="urn:import:gwtdb.client.widget"> <ui:style> .important { font-weight: bold; } .header { text-align: center; } </ui:style> <g:DockLayoutPanel unit="EM"> <g:north size="10"> <g:Label styleName="{style.header}">ヘッダー</g:Label> </g:north> <g:south size="10"> <g:Label styleName="{style.header}">フッター</g:Label> </g:south> <g:center> <g:Label ui:field="contents">コンテンツ</g:Label> </g:center> <g:west size="20"> <g:HTMLPanel> <app:Folders ui:field="folders" /> </g:HTMLPanel> </g:west> </g:DockLayoutPanel> </ui:UiBinder>
今回は再利用しないので特に意味はないが、練習のために独自UIで作った。
package gwtdb.client.widget; import java.util.HashMap; import gwtdb.shared.HasId; import com.google.gwt.event.logical.shared.SelectionHandler; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.Tree; import com.google.gwt.user.client.ui.TreeItem; public class Folders extends Composite { private Tree tree; private TreeItem root = new TreeItem ("folders"); private HashMap<TreeItem, Integer> folders = new HashMap<TreeItem, Integer>(); public Folders() { tree = new Tree(); tree.addItem(root); initWidget(tree); } public void add(HasId<String> item) { TreeItem i = new TreeItem(item.value); folders.put(i, item.id); root.addItem(i); root.setState(true); } public Integer getSelectedIndex(TreeItem item) { Integer i = folders.get(item); if (i == null) i = 0; return i; } public void addSelectionHandler(SelectionHandler<TreeItem> handler){ tree.addSelectionHandler(handler); } }
web.xmlで非同期通信のURLを設定する。またS2Containerを使うための設定も追加する。
<servlet> <servlet-name>folderServlet</servlet-name> <servlet-class>gwtdb.server.FolderListServiceImpl</servlet-class> <load-on-startup>2</load-on-startup> </servlet> <servlet> <servlet-name>itemServlet</servlet-name> <servlet-class>gwtdb.server.ItemListServiceImpl</servlet-class> <load-on-startup>2</load-on-startup> </servlet> <servlet> <servlet-name>s2servlet</servlet-name> <servlet-class>org.seasar.framework.container.servlet.S2ContainerServlet</servlet-class> <init-param> <param-name>configPath</param-name> <param-value>app.dicon</param-value> </init-param> <init-param> <param-name>debug</param-name> <param-value>true</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>greetServlet</servlet-name> <url-pattern>/gwtdb/greet</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>folderServlet</servlet-name> <url-pattern>/gwtdb/folders</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>itemServlet</servlet-name> <url-pattern>/gwtdb/items</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>s2servlet</servlet-name> <url-pattern>/gwtdb/s2servlet</url-pattern> </servlet-mapping>
ここでapp.diconを読み込む設定をしたので、src以下にdiconファイルを作る。ここに新規作成すると、(Eclipseの設定によって?)WEB-INF/classes/以下に自動的にコピーされる。
app.dicon
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd"> <components> <include path="convention.dicon"/> <include path="aop.dicon"/> <include path="j2ee.dicon"/> <include path="s2jdbc.dicon"/> </components>
ファイルがないとエラーが出るので、使わないであろうdiconも作成しておく。
creator.dicon
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd"> <components> <include path="convention.dicon"/> <include path="customizer.dicon"/> <component class="org.seasar.framework.container.creator.ActionCreator"/> <component class="org.seasar.framework.container.creator.DaoCreator"/> <component class="org.seasar.framework.container.creator.DtoCreator"/> <component class="org.seasar.framework.container.creator.DxoCreator"/> <component class="org.seasar.framework.container.creator.HelperCreator"/> <component class="org.seasar.framework.container.creator.LogicCreator"/> <component class="org.seasar.framework.container.creator.PageCreator"/> <component class="org.seasar.framework.container.creator.ServiceCreator"/> <component class="org.seasar.framework.container.creator.InterceptorCreator"/> <component class="org.seasar.framework.container.creator.ValidatorCreator"/> <component class="org.seasar.framework.container.creator.ConverterCreator"/> </components>
サンプルから持ってきた。流し読みすると、ActionCreatorはHogeActionをコンテナで管理するため?
多分何も書かなくても大丈夫。
customizer.dicon
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd"> <components> <include path="default-customizer.dicon"/> </components>
s2container.dicon
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd"> <components> <include condition="#ENV == 'ut'" path="warmdeploy.dicon"/> <include condition="#ENV == 'ct'" path="hotdeploy.dicon"/> <include condition="#ENV != 'ut' and #ENV != 'ct'" path="cooldeploy.dicon"/> </components>
これで完成。
動かないようなら localhost/gwtdb/gwtdb/s2servlet?command=list にアクセスしてエラーがないか見てみる。
DebianのTomcatはデフォルトのままだとエラーが出る。
javax.servlet.ServletException: Could not initialize class org.seasar.framework.container.factory.S2ContainerFactory ... java.lang.NoClassDefFoundError: Could not initialize class org.seasar.framework.container.factory.S2ContainerFactory ...
こんな感じの。
これでだいぶハマった。
答えは
http://www.atmarkit.co.jp/fjava/rensai4/safetomcat_06/safetomcat_06_1.html
ここにあった。/etc/default/tomcat5.5 でセキュリティをnoにしたら起動した。
真面目に設定しようと思ったけど「seasar catalina.policy」で検索したら現状14件しかヒットしなかったので取り敢えずオフにしておく。