Google App Engine(Java)のDWRアプリでテキストファイルを読み込んでみる
先日のログ「Google App EngineにDWR2を乗せてみた」で、Google Apps Engine(GAE)にDWR2を乗せて簡単なサンプルを動かしてみた。
GAEに関する記事などを読むと、GAEではファイルIOに制限があるとのこと。「(GAEのプラットフォームを)ファイルシステムと見なして使うことは出来ない」という意味のはずだが(実際に、「JRE クラスのホワイトリスト」を見ると、java.io関連のクラスがたくさん含まれている)、「テキストファイルが全く読み込めない」のでは困る。特に、コンテキストパス配下にテキストを置くことはよくあるので、試しておこうと思う。
以前に「DWR: Javaオブジェクトを画面の要素にマップする」というログで紹介したサンプルが、ちょうどいい実験材料になりそうなので、GAEプロジェクトに移植してみた。このサンプルでは、コンテキストパス配下の以下のようなcsvファイルをデータとして読み込む仕様となっている。
hogehoge,Hawaii,1000.50 やまだ,北海道,1000.50
以下の作業は、GAEプラグインがインストールされたEclipse(3.4:Ganymede)で行った。
プロジェクトの作成
前回のログ「Google Apps Engineでcommons-logging+Log4jでロギングする」で作成したプロジェクトをコピーし、GaeDWRTest2というプロジェクトを作成する。必要なjarファイルは、これで揃う。
トップ画面(edit_table_demo.html)の作成
以前のログで作成したedit_table_demo.htmlを、war/WEB-INF下に置く。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>DWRのサンプリング</title> <meta http-equiv="content-type" content="text/html; charset=utf-8"> <link rel="shortcut icon" href="../images/egp-favicon.ico"> <style type="text/css" id="defaultstyle"> body { margin:0; padding:0; } #container { margin: 2px; padding: 3px; line-height:1.5em; width: 500px; height: auto; border:1px dashed #999999; } </style> <!-- People.jsはDWRによって自動生成される。 dwr/はドキュメントルートとの相対参照。 --> <script src='dwr/interface/People.js'></script> <!-- engine.jsは必須 --> <script src='dwr/engine.js'></script> <!-- util.jsは必須 --> <script src='dwr/util.js'></script> <script type="text/javascript"> function init() { fillTable(); } var peopleCache = { }; var viewed = -1; function fillTable() { // PeopleオブジェクトのgetAllPeopleの呼び出し People.getAllPeople( // getAllPeopleのコールバック function(people) { // peoplebodyの全ての行を一旦削除する。 // オプションとなるオブジェクトにfilterを指定し、 // id != "pattern"の行(tr)を削除する。 dwr.util.removeAllRows("peoplebody", { filter:function(tr) { return (tr.id != "pattern"); } } ); // peopleCacheをクリア peopleCache = { }; // peopleオブジェクトにsortの関数を追加定義。 people.sort( function(p1, p2) { return p1.name.localeCompare(p2.name); } ); // 作表 var person, id; for (var i = 0; i < people.length; i++) { person = people[i]; id = person.id; // id=patternの行のcloneを作る // clone作成時のid属性をidSuffixとして指定。 // tr属性ではid=""+person.idとなる。 // trの子要素でid属性を持つものも同様。 dwr.util.cloneNode("pattern", { idSuffix:id }); dwr.util.setValue("tableName" + id, person.name); dwr.util.setValue("tableSalary" + id, person.salary); dwr.util.setValue("tableAddress" + id, person.address); // $("pattern"+id)でDOMを指定。 $("pattern" + id).style.display = "table-row"; // テーブルに読み込んだpersonをpeopleCacheに入れる。 peopleCache[id] = person; } } ); } function editClicked(eleid) { // editボタンは、id="edit"+person.idとなっているので、 // 4桁目からのsubstring(つまり、id)をもとに // PersonオブジェクトをCacheからとってくる。 var person = peopleCache[eleid.substring(4)]; // Personオブジェクトをフォームにマップする。 dwr.util.setValues(person); } function deleteClicked(eleid) { // deleteボタンは、id="delete"+person.idとなっているので、 // 6桁目からのsubstring(つまり、id)をもとに // PersonオブジェクトをCacheからとってくる。 var person = peopleCache[eleid.substring(6)]; if (confirm(person.name + " さんを削除してよいですか?")) { // サーバーの処理をまとめる(start)。 // 処理が速くなるらしい。 // http://directwebremoting.org/dwr/browser/engine/batch dwr.engine.beginBatch(); // 削除 People.deletePerson(eleid.substring(6)); fillTable(); dwr.engine.endBatch(); // サーバーの処理をまとめる(end)。 } } function errh(msg) { alert(msg); } function writePerson() { var person = { id:viewed, name:null, address:null, salary:null }; // Formから値を取得して、Personオブジェクトを生成する。 dwr.util.getValues(person); // サーバーの処理をまとめる(start)。 // 処理が速くなるらしい。 dwr.engine.beginBatch(); // 登録 People.setPerson(person); fillTable(); dwr.engine.endBatch({ errorHandler:function(errorString, exception) { alert("エラーが発生しました。"); } }); // サーバーの処理をまとめる(end)。 } function clearPerson() { viewed = -1; dwr.util.setValues({ id:-1, name:null, address:null, salary:null }); } </script> </head> <body onload="init()"> <div id="container"> <p> DWRのデモです。<br> サーバーにある平文のデータを読み込み、セッションスコープでデータの 登録、更新、削除ができます。 </p> <h3>All People;データ</h3> <table border="1" class="rowed grey"> <thead> <tr> <th>名前</th> <th>給料</th> <th>アクション</th> </tr> </thead> <tbody id="peoplebody"> <tr id="pattern" style="display:none;"> <td> <span id="tableName">名前</span> <br> <small> <span id="tableAddress">住所</span> </small> </td> <td>¥<span id="tableSalary">給料</span></td> <td> <input id="edit" type="button" value="編集" onclick="editClicked(this.id)"/> <input id="delete" type="button" value="削除" onclick="deleteClicked(this.id)"/> </td> </tr> </tbody> </table> <h3>Personの編集</h3> <table class="plain"> <tr> <td>名前:</td> <td><input id="name" type="text" size="30"/></td> </tr> <tr> <td>給料:</td> <td><input id="salary" type="text" size="20"/></td> </tr> <tr> <td>住所:</td> <td><input type="text" id="address" size="40"/></td> </tr> <tr> <td colspan="2" align="right"> <small>(ID=<span id="id">-1</span>)</small> <input type="button" value="Save" onclick="writePerson()"/> <input type="button" value="Clear" onclick="clearPerson()"/> </td> </tr> </table> </div> </body> </html>
これがwelcome pageになるので、web.xmlを修正する。
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5"> <servlet> <servlet-name>dwr-invoker</servlet-name> <display-name>DWR Servlet</display-name> <description>Direct Web Remoter Servlet</description> <servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class> <init-param> <param-name>debug</param-name> <param-value>false</param-value> </init-param> <init-param> <param-name>activeReverseAjaxEnabled</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>initApplicationScopeCreatorsAtStartup</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>maxWaitAfterWrite</param-name> <param-value>-1</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dwr-invoker</servlet-name> <url-pattern>/dwr/*</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>edit_table_demo.html</welcome-file> </welcome-file-list> </web-app>
csvファイルの作成
war配下にdataディレクトリを作成し、people.txtという名前で以下のテキストファイル()を用意する。
hogehoge,Hawaii,1000.50 やまだ,北海道,1000.50
javaクラスの作成
これも、以前に紹介したプログラムとほぼ同じだが、system.formatで標準出力していた箇所をcommons-loggingを使うように変更。
デプロイしてみたところ、java.io.Serializableをimplementsしていない旨のエラーが発生したので、(マーカーインターフェイスであるから)これをimplementsするようにした。
以下の2つのクラスをtestパッケージに配置する。
people.java; このプログラムで上で配置したpeople.txtを読み込む。
package test; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.StringTokenizer; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.directwebremoting.WebContext; import org.directwebremoting.WebContextFactory; /** * Personの集合(HashMap)を表すオブジェクト。 * DWRで利用する。 * */ public class People implements Serializable{ private static final long serialVersionUID = -1738868638418288696L; private static final Log log = LogFactory.getLog(People.class); private Map<Integer,Person> people= new HashMap<Integer,Person>(); // idはPeopleインスタンス生成の際に0から附番 private int ids = 0; /** * クラス <code>People</code> のオブジェクトを構築します。 * */ public People() { super(); createPeople(); } public void createPeople(){ WebContext wctx = WebContextFactory.get(); ServletConfig scfg = wctx.getServletConfig(); ServletContext sc = scfg.getServletContext(); String path = sc.getRealPath("/data/people.txt"); try { FileInputStream fi; fi = new FileInputStream(path); InputStreamReader is = new InputStreamReader(fi, "UTF8"); BufferedReader br = new BufferedReader(is); String tmpStr; int id; while ((tmpStr = br.readLine()) != null) { log.debug("read: "+tmpStr); StringTokenizer st = new StringTokenizer(tmpStr,","); Person np = new Person(); id = ++ids; np.setId(id); int i = 0; while(st.hasMoreTokens()) { String ts = st.nextToken(); switch(i){ case 0: np.setName(ts); break; case 1: np.setAddress(ts); break; case 2: np.setSalary(Float.parseFloat(ts)); break; } i++; } people.put(id, np); log.debug("person created: "+np.toString()); } br.close(); is.close(); fi.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } /* * Peopleのvalueを返却する。 */ public Collection<Person> getAllPeople() { return people.values(); } /* * Idを登録、更新する。 */ public void setPerson(Person person) { // id=-1のpersonは新規登録となる。 if (person.getId() == -1) { person.setId(getNextId()); } people.remove(person.getId()); people.put(person.getId(),person); // コンソールにPersonの内容を表示する。 Iterator<Person> it = people.values().iterator(); Person tmpPer; while(it.hasNext()){ tmpPer = it.next(); log.debug("person: "+tmpPer.toString()); } } /* * Idを削除する。 */ public void deletePerson(int id) { people.remove(id); // コンソールにPersonの内容を表示する。 Iterator<Person> it = people.values().iterator(); Person tmpPer; while(it.hasNext()){ tmpPer = it.next(); log.debug("person: "+tmpPer.toString()); } } /* * Idを採番する。 */ public int getNextId(){ return ++ids; } }
person.java
package test; import java.io.Serializable; /** * Personを表すオブジェクト。 * DWRでconvertでFormにマッピングする。 * */ public class Person implements Serializable{ private static final long serialVersionUID = -8481123990342057803L; private int id; private String name; private String address; private float salary; /** * クラス <code>Person</code> のオブジェクトを構築します。 * */ public Person() { } /** * idのGetter * @return id */ public int getId() { return id; } /** * idのSetter * @param id セットする id */ public void setId(int id) { this.id = id; } /** * nameのGetter * @return name */ public String getName() { return name; } /** * nameのSetter * @param name セットする name */ public void setName(String name) { this.name = name; } /** * addressのGetter * @return address */ public String getAddress() { return address; } /** * addressのSetter * @param address セットする address */ public void setAddress(String address) { this.address = address; } /** * salaryのGetter * @return salary */ public float getSalary() { return salary; } /** * salaryのSetter * @param salary セットする salary */ public void setSalary(float salary) { this.salary = salary; } /** * equalsの定義 * @param person */ public boolean equals(Person person){ if(id==person.getId()) return true; return false; } /** * toStringの定義 */ public String toString(){ return Integer.toString(id)+name+address+ Float.toString(salary); } }
dwr.xmlの変更
以下のように定義する。people.javaの生存期間(スコープ)はsessionとする。
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE dwr PUBLIC "-//GetAhead Limited//DTD Direct Web Remoting 2.0//EN" "http://directwebremoting.org/schema/dwr20.dtd"> <dwr> <allow> <create creator="new" javascript="People" scope="session"> <param name="class" value="test.People"/> </create> <convert match="test.Person" converter="bean"/> </allow> </dwr>