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>&#x00a5;<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>

appengine-web.xmlの変更

sessionスコープを利用するので、appengine-web.xmlに以下の定義をする。

	<sessions-enabled>true</sessions-enabled>

デプロイと実行

ここまでの作業で、プロジェクトのリソースは以下のように配置される。

GaeDWRTest2プロジェクトを選択し、「Google」->「deploy to apps engine」としてGAEにデプロイする。
これを動かした結果が以下の画面となる。people.txtの読み込みも問題なく実行され、また、DWRによるjavaクラスの(画面へ)のマッピングも問題なく動くことが分かった。