Struts1.3をGAE/Jに乗せてデータ・ストアを使ってみた
前回のログでは、Apache Struts(1.3.10)に付属するサンプルをGoogle App Engine/Java(GAE/J)に乗せてみた。せっかく(制限事項つきだが)乗ることが分かったので、struts-blank.warをもとに、データストアを使う簡単なサンプルを作ってみることにした。
サンプルは非常に簡単なもので、これまでのサンプルで使ってきた、Personエンティティーを登録して、リスト形式で参照するもの。初期画面は以下。
この画面で入力を行って、「登録ボタン」を押すと、データ・ストアにPersonエンティティーを保管して、リスト形式での表示に遷移する。
Personエンティティーは「同じエンティティー・グループ」に属するようにする。このために、DWR2を使ったサンプルと同様に、RootTOエンティティーというRootを規定するエンティティーを用いる。データストアの初期化の方法や、Personエンティティーの保存のやり方は、以前のサンプルと同様である。
以下の作業は、GAE/JプラグインがインストールされたEclipse3.4(Ganymede)で行った。
プロジェクトの作成
新しくWeb Application Projectを作成する。
struts-blank.warをローカル環境にあるTomcat6.0にデプロイする(webappsディレクトリにおくだけ)。TomcatとGAE/Jではデフォルトのディレクトリ構成が違うので、前回のサンプルと同様に、違いに気をつけて、ファイルをコピーする(こちらを参照)。
作成するリソースのディレクトリ構成は以下のようになる。
RootTOエンティティーとPersonエンティティー
RootTOエンティティー;ストアしたいエンティティの属するエンティティーグループを規定する。
package test.entities; import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.IdentityType; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; import com.google.appengine.api.datastore.Key; /********************************* * * エンティティーグループを定義するRootエンティティー * * @author * tetsuya_odaka (EzoGP) <br> *********************************/ @PersistenceCapable(identityType=IdentityType.APPLICATION) public class RootTO { /* * rootエンティティーのキーはKeyか、String */ @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private String name; @Persistent private Long count; /** * コンストラクタ * */ public RootTO(String name, Long count) { this.name = name; this.count = count; } public Key getKey() { return key; } public void setKey(Key key) { this.key = key; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Long getCount() { return count; } public void setCount(Long count) { this.count = count; } }
Personエンティティー;ストアしたいエンティティ
package test.entities; import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.IdentityType; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; import com.google.appengine.api.datastore.Key; /** * Personを表すオブジェクト。 * */ @PersistenceCapable(identityType=IdentityType.APPLICATION) public class Person{ @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private Long number; @Persistent private String name; @Persistent private String address; @Persistent private float salary; /** * クラスPersonのオブジェクトを構築します。 * */ public Person() { } public Person(String name, String address, float salary) { this.name = name; this.address = address; this.salary = salary; } /** * Setter/Getter */ public Key getKey() { return key; } public void setKey(Key key) { this.key = key; } public Long getNumber() { return number; } public void setNumber(Long number) { this.number = number; } public int getIntNumber() { return number.intValue(); } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public float getSalary() { return salary; } public void setSalary(float salary) { this.salary = salary; } /** * プロパティーが同じならtrue */ public boolean equals(Person person){ if(this.name.equals(person.getName()) && this.address.equals(person.getAddress()) && this.salary == person.getSalary()) return true; return false; } /** * プロパティーを文字列に連結して返す */ public String toString(){ return this.key.toString() + this.name + this.address+ Float.toString(this.salary); } }
JSPファイル
以下の2つのJSPを用意した。
personEntry.jsp; Personエンティティーの入力を行う。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %> <%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %> <%@ taglib uri="http://struts.apache.org/tags-logic" prefix="logic" %> <%@ taglib uri="http://struts.apache.org/tags-nested" prefix="nested" %> <html:html> <head> <title><bean:message key="person.entry.title" /></title> <link rel="shortcut icon" href="../images/egp-favicon.ico" > </head> <body> <h2><bean:message key="person.entry.title" /></h2> <html:form action="/entryPerson"> <table> <tr> <td></td> <td><bean:message key="person.name" /></td> <td><bean:message key="person.salary" /></td> <td><bean:message key="person.address" /></td> </tr> <tr> <td><html:hidden property="number" value="-1" /></td> <td><html:text property="name" size="20" /></td> <td><html:text property="salary" size="20" /></td> <td><html:text property="address" size="20" /></td> </tr> </table> <br> <html:errors/><br> <html:submit property="submit"><bean:message key="person.submit.entry" /></html:submit> <html:reset><bean:message key="person.reset.entry" /></html:reset> </html:form> <br> <a href="./listPerson.do">Personのリストへ</a> </body> </html:html>
personEntry.jsp; Personエンティティーの一覧表示を行う。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %> <%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %> <%@ taglib uri="http://struts.apache.org/tags-logic" prefix="logic" %> <%@ taglib uri="http://struts.apache.org/tags-nested" prefix="nested" %> <html:html> <head> <title><bean:message key="person.list.title" /></title> <link rel="shortcut icon" href="../images/egp-favicon.ico" > </head> <style type="text/css" id="defaultstyle"> td { padding: 3px; } </style> <body> <h2><bean:message key="person.list.title" /></h2> <table border="1"> <tr> <th>ID</th> <th><bean:message key="person.name" /></th> <th><bean:message key="person.salary" /></th> <th><bean:message key="person.address" /></th> </tr> <logic:iterate id="per" name="personList" > <tr> <td><bean:write name="per" property="number" /></td> <td><bean:write name="per" property="name" /></td> <td><bean:write name="per" property="salary" /></td> <td><bean:write name="per" property="address" /></td> </tr> </logic:iterate> </table> <br> <a href="./personEntry.jsp">Personの登録へ</a> </body> </html:html>
Personエンティティー用DAOクラス
Personエンティティーをハンドルするために、DAO(Data Accecc Object)を用意した。
StrutsのActionクラスからは、このDAOを通して、Personエンティティーのハンドルをする。
package test; import java.util.ArrayList; import java.util.List; import javax.jdo.PersistenceManager; import javax.jdo.Query; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import test.entities.Person; import test.entities.RootTO; /** * PersonのData Access Object * */ public class PersonDao{ private static final Log log = LogFactory.getLog(PersonDao.class); /** * コンストラクタ */ public PersonDao() { } @SuppressWarnings("unchecked") private List<Person> loadPeople(){ List<Person> people= new ArrayList<Person>(); PersistenceManager pm = PMF.get().getPersistenceManager(); /** * Personエンティティーに対するクエリの実行 */ Query query = pm.newQuery(Person.class); query.setOrdering("number asc"); try{ List<Person> list = (List<Person>)query.execute(); for(Person person : list){ people.add(person); log.debug("person loaded: "+person.toString()); } }finally{ query.closeAll(); pm.close(); } return people; } /* * Personのリストを返却する。 */ public List<Person> getAllPeople() { return this.loadPeople(); } /* * Personを登録、更新する。 */ public List<Person> setPerson(Person person) { log.debug("setPerson person id : " + person.getNumber()); if(person.getNumber().intValue() != -1){ this.updatePerson(person); }else{ this.registerPerson(person); } return this.loadPeople(); } /* * Personを登録する。 */ private void registerPerson(Person person) { PersistenceManager pm = PMF.get().getPersistenceManager(); long count = 0; try{ pm.currentTransaction().begin(); RootTO root = pm.getObjectById(RootTO.class, "key"); count = root.getCount()+1; root.setCount(count); pm.currentTransaction().commit(); log.debug("root count updated to: " + new Long(count).toString()); }finally{ if(pm.currentTransaction().isActive()){ // ロールバック pm.currentTransaction().rollback(); } pm.close(); } if(count > 0){ Person storePer = new Person(person.getName(), person.getAddress(), person.getSalary()); KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "key"); kb.addChild(Person.class.getSimpleName(), Person.class.getSimpleName()+ new Long(count).toString()); Key key = kb.getKey(); storePer.setKey(key); storePer.setNumber(count); pm = PMF.get().getPersistenceManager(); try{ pm.makePersistent(storePer); log.debug("person entered: "+storePer.toString()); }finally{ pm.close(); } } return; } /* * Personを更新する。 */ private void updatePerson(Person person) { PersistenceManager pm = PMF.get().getPersistenceManager(); try{ // キーの生成 KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "key"); kb.addChild(Person.class.getSimpleName(), Person.class.getSimpleName()+person.getNumber().toString()); Key key = kb.getKey(); // オブジェクトの取得 Person storePer = pm.getObjectById(Person.class, key); storePer.setName(person.getName()); storePer.setAddress(person.getAddress()); storePer.setSalary(person.getSalary()); log.debug("person updated: "+storePer.toString()); }finally{ pm.close(); } return; } /** * Personを削除する。 */ public List<Person> deletePerson(Person person) { PersistenceManager pm = PMF.get().getPersistenceManager(); try{ // キーの生成 KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "key"); kb.addChild(Person.class.getSimpleName(), Person.class.getSimpleName()+person.getNumber().toString()); Key key = kb.getKey(); // オブジェクトの取得 Person storePer = pm.getObjectById(Person.class, key); pm.deletePersistent(storePer); log.debug("person deleted: "+key.toString()); }finally{ pm.close(); } return this.loadPeople(); } /** * Personを一括削除する。 */ @SuppressWarnings("unchecked") public List<Person> deleteAllPerson() { PersistenceManager pm = PMF.get().getPersistenceManager(); Query query = pm.newQuery(Person.class); try{ pm.currentTransaction().begin(); List<Person> list = (List<Person>)query.execute(); pm.deletePersistentAll(list); pm.currentTransaction().commit(); log.debug("person all deleted."); }finally{ if(pm.currentTransaction().isActive()){ // ロールバック pm.currentTransaction().rollback(); log.debug("person all-delete failed."); } query.closeAll(); pm.close(); } return this.loadPeople(); } }
ActionクラスとActionFormクラス
Personエンティティーの登録処理
PersonEntryAction.java; Personエンティティー登録のためのActionクラス。処理後には、Personエンティティーのリスト表示に遷移する。
package test; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.struts.action.Action; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import test.entities.Person; public class PersonEntryAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { PersonEntryForm pef = (PersonEntryForm) form; Person person = new Person(); // -1が入ってくる。 person.setNumber(new Long(pef.getNumber())); person.setName(pef.getName()); person.setSalary(new Float(pef.getSalary()).floatValue()); person.setAddress(pef.getAddress()); PersonDao pdao = new PersonDao(); // 登録 List<Person> personList = pdao.setPerson(person); // requestにセット request.setAttribute("personList", personList); return (mapping.findForward("success")); } }
PersonEntryForm.java; personEntry.jspをラップするActionFormBean。validate()メソッドを定義してみた。
package test; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import org.apache.struts.action.ActionErrors; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionMessage; import org.apache.struts.action.ActionMessages; @SuppressWarnings("serial") public final class PersonEntryForm extends ActionForm { private String number; private String name; private String salary; private String address; /** * Setter / Getter */ public String getNumber() { return number; } public void setNumber(String number) { this.number = number; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSalary() { return salary; } public void setSalary(String salary) { this.salary = salary; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public ActionErrors validate(ActionMapping mapping, HttpServletRequest request){ ActionErrors errs = new ActionErrors(); // number if(getNumber()==null || getNumber().equals("")){ errs.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("errors.required","ID")); }else{ Pattern pattern = Pattern.compile("^[-+]?\\d+$"); Matcher matcher = pattern.matcher(getNumber()); if(!matcher.matches()){ errs.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("errors.invalid","ID")); } } // name if(getName()==null || getName().equals("")){ errs.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("errors.required","NAME")); } // Salary if(getSalary()==null || getSalary().equals("")){ errs.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("errors.required","SALARY")); }else{ Pattern pattern = Pattern.compile("^[-+]?\\d*\\.?\\d+$"); Matcher matcher = pattern.matcher(getSalary()); if(!matcher.matches()){ errs.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("errors.invalid","SALARY")); } } // Address if(getAddress()==null || getAddress().equals("")){ errs.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("errors.required","ADDRESS")); } return errs; } }
Personエンティティーのリスト表示
PersonListAction.java; Personエンティティーをデータストアから取得してリスト表示する。
package test; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.struts.action.Action; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import test.entities.Person; public class PersonListAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { PersonDao pdao = new PersonDao(); List<Person> personList = pdao.getAllPeople(); // requestにセット request.setAttribute("personList", personList); return (mapping.findForward("success")); } }
その他
データストアの初期化は、(DWR2でのサンプルと同様に)ContextListnerで行う仕様とした。これを行うのが、CreateRootContextListner.javaとなる。
また、GAE/JのデータストアのためのPersistenceManagerFactoryが、PMF.javaとなる。
これらは、DWR2のサンプルと同じコードを用いた。
MessageResource
ソースフォルダー直下にMessageResourceを用意した。国際化の再確認をしたかったため、struts-blankにバンドルされているものを改変して、日英それぞれのメッセージリソースを用意した。
MessageResource.properties;英語版(デフォルト)
# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # -- standard errors -- errors.header=<UL> errors.prefix=<LI> errors.suffix=</LI> errors.footer=</UL> # -- validator -- errors.invalid={0} is invalid. errors.maxlength={0} can not be greater than {1} characters. errors.minlength={0} can not be less than {1} characters. errors.range={0} is not in the range {1} through {2}. errors.required={0} is required. errors.byte={0} must be an byte. errors.date={0} is not a date. errors.double={0} must be an double. errors.float={0} must be an float. errors.integer={0} must be an integer. errors.long={0} must be an long. errors.short={0} must be an short. errors.creditcard={0} is not a valid credit card number. errors.email={0} is an invalid e-mail address. # -- other -- errors.cancel=Operation cancelled. errors.detail={0} errors.general=The process did not complete. Details should follow. errors.token=Request could not be completed. Operation is not in sequence. # -- welcome -- welcome.title=Struts Blank Application welcome.heading=Welcome! welcome.message=To get started on your own application, copy the struts-blank.war to a new WAR file using the name for your application. Place it in your container's "webapp" folder (or equivalent), and let your container auto-deploy the application. Edit the skeleton configuration files as needed, restart your container, and you are on your way! (You can find the MessageResources.properties file with this message in the /WEB-INF/src folder.) # -- add -- person.list.title=GAE/J Example: List of Person Entities person.entry.title=GAE/J Example: Entry of Person Information person.name=Name person.salary=Salary person.address=Address person.submit.entry=Enter person.reset.entry=Reset
MessageResource_ja.properties;日本語版(追加)。実際には、以下をnaitive2asciiして配置する。
# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. errors.invalid={0} の入力値がただしくありません. errors.required={0} を入力してください. # -- add -- person.list.title=GAE/J サンプル: Personエンティティーのリスト person.entry.title=GAE/J サンプル: Personの登録 person.name=名前 person.salary=給料 person.address=住所 person.submit.entry=登録 person.reset.entry=リセット
struts-config.xml
<?xml version="1.0" encoding="utf-8" ?> <!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.3//EN" "http://struts.apache.org/dtds/struts-config_1_3.dtd"> <struts-config> <!-- ================================================ Form Bean Definitions --> <form-beans> <form-bean name="personEntryForm" type="test.PersonEntryForm" /> </form-beans> <!-- ========================================= Global Exception Definitions --> <global-exceptions> </global-exceptions> <!-- =========================================== Global Forward Definitions --> <global-forwards> </global-forwards> <!-- =========================================== Action Mapping Definitions --> <action-mappings> <action path="/listPerson" type="test.PersonListAction" scope="request"> <forward name="success" path="/personList.jsp" /> </action> <action path="/entryPerson" type="test.PersonEntryAction" name="personEntryForm" validate="true" input="/personEntry.jsp" scope="request"> <forward name="success" path="/personList.jsp" /> </action> </action-mappings> <!-- ======================================== Message Resources Definitions --> <message-resources parameter="MessageResources" /> </struts-config>
web.xml
<?xml version="1.0" encoding="utf-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <display-name>GAE/J Sample Application</display-name> <listener> <listener-class> test.CreateRootContextListner </listener-class> </listener> <!-- Standard Action Servlet Configuration --> <servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!-- Standard Action Servlet Mapping --> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> <!-- The Usual Welcome File List --> <welcome-file-list> <welcome-file>personEntry.jsp</welcome-file> </welcome-file-list> </web-app>
テストとデプロイ
データストアを使うので、必ずローカルサーバーでテストを行ってからGAE/Jにデプロイする。
(注記)日本語メッセージリソース名であるが、GAE/Jの方がローカルサーバーよりも厳密にlocale情報を見ているようだ。ブラウザーの言語設定で「ja」を選択した場合、ローカルサーバーでは「MessageResource_ja_JP.properties」でも、なんとなく日本語化されてしまうのだが、GAE/Jでは「MessageResource_ja.propertiesが無い!」というログが吐き出されて英語になってしまう(ただし、MessageResource_ja.propertiesだけにすると、ローカルサーバーで動かした際に、コンソールに「MessageResource_ja_JP.propertiesが無い!」という逆のWARNログがでる)。