ラベル GoogleAppEngine の投稿を表示しています。 すべての投稿を表示
ラベル GoogleAppEngine の投稿を表示しています。 すべての投稿を表示

2010年9月2日木曜日

XMLのシリアライズ




「魔王なんて!」にネットワーク機能を付けようと思い必要な機能を洗い出したのですが、ログインや友達リスト等ゲームの処理に入る前にユーザー管理周りでも通信種類って結構多いのですよね。(メッセージ送信とかいろいろありますよね。)

相変わらず独りで実装しているので通信種類が増えすぎるとプログラム量がどんどん増えて管理しきれなくなるのですよね。個人でプログラムを作る場合はプログラムや処理種類を減らす工夫も大事なので、まずオブジェクトのシリアライズを検討する事にしました。これはサーバーとクライアントで同じクラスを使いたいためです。Android以外の端末にも将来展開したいので、バイナリデータで通信せずXMLが良いかなと思います。

そこで何か使えるライブラリを調べてみたのですが、どうも定義ファイルが必要なものが多いのですよね。で、実装がシンプルなものも少ないですし、ドキュメントは英語ですし。

なんだか英語のドキュメント読んでいる時間で実装できそうな気がしたので実装してみました。

XMLReaderがデシリアライズ。XMLWriterがシリアライズを行います。
key, valueで表しつつ、内部クラスも表現できる最小セットの実装かなとおもいます。


package com.cosmicdragon.darkgreen.xml;

public interface XMLDefine {

public static final String ELEMENT_ROOT = "d";
public static final String ELEMENT_ROW = "r";
public static final String ELEMENT_CLASS = "c";

public static final String ATTR_NAME = "n";
public static final String ATTR_VERSION = "v";
public static final String ATTR_KEY = "k";
public static final String ATTR_TYPE = "t";

public static final String TYPE_INT = "i";
public static final String TYPE_BOOL = "b";
public static final String TYPE_LONG = "l";
public static final String TYPE_STRING = "s";
public static final String TYPE_DATE = "d";

}




package com.cosmicdragon.darkgreen.xml;

import java.util.Date;

public class XMLReadListener {
public void readStart(String version){}
public void readEnd(){}
public void readClassStart(String name){}
public void readClassEnd(){}
public void readRowInt(String key, int value){}
public void readRowLong(String key, long value){}
public void readRowBoolean(String key, boolean value){}
public void readRowString(String key, String value){}
public void readRowDate(String key, Date value){}
}



package com.cosmicdragon.darkgreen.xml;

import java.io.IOException;
import java.util.Date;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

public class XMLReader implements XMLDefine {
private SAXParser parser = null;
private Handler hdl = new Handler();

public void readXML(XMLReadListener listener, InputSource is)
throws ParserConfigurationException, SAXParseException, SAXException, IOException{
hdl.reset(listener);

if(parser == null){
parser = SAXParserFactory.newInstance().newSAXParser();
}

parser.parse(is, hdl);
if(hdl.getException() != null)throw hdl.getException();

hdl.reset(null);
}
}

class Handler extends DefaultHandler {
private SAXParseException exception;
private XMLReadListener listener;

private boolean rowFlag = false;
private String rowType = "";
private String rowKey = "";
private String rowValue= "";

public void reset(XMLReadListener listener){
this.listener = listener;
this.exception = null;
}

public SAXParseException getException(){
return this.exception;
}

public void startElement(String uri, String localName, String qName, Attributes atts)
throws SAXException {
rowFlag = false;
if(qName.equals(XMLDefine.ELEMENT_ROOT)){
listener.readStart(atts.getValue(XMLDefine.ATTR_VERSION));
}else if(qName.equals(XMLDefine.ELEMENT_CLASS)){
listener.readClassStart(atts.getValue(XMLDefine.ATTR_NAME));
}else if(qName.equals(XMLDefine.ELEMENT_ROW)){
rowFlag = true;
rowValue = "";
rowType = atts.getValue(XMLDefine.ATTR_TYPE);
rowKey = atts.getValue(XMLDefine.ATTR_KEY);
System.out.println("startElement() : " + rowType + " : " + rowKey);
}
}

public void endElement(String uri, String localName, String qName)
throws SAXException {
if(qName == XMLDefine.ELEMENT_ROOT){
listener.readEnd();
}else if(qName == XMLDefine.ELEMENT_CLASS){
listener.readClassEnd();
}else if(qName == XMLDefine.ELEMENT_ROW){
if(rowType.equals(XMLDefine.TYPE_INT))
listener.readRowInt(rowKey, Integer.parseInt(rowValue));
else if (rowType.equals(XMLDefine.TYPE_LONG))
listener.readRowLong(rowKey, Long.parseLong(rowValue));
else if (rowType.equals(XMLDefine.TYPE_BOOL))
listener.readRowBoolean(rowKey, Boolean.parseBoolean(rowValue));
else if (rowType.equals(XMLDefine.TYPE_STRING))
listener.readRowString(rowKey, rowValue);
else if (rowType.equals(XMLDefine.TYPE_DATE))
listener.readRowDate(rowKey, new Date(Long.parseLong(rowValue)));
}
}

public void characters(char[] ch, int start, int length)
throws SAXException {
if(rowFlag)
rowValue += String.copyValueOf(ch, start, length);
}

public void error(SAXParseException e) {
exception = e;
}

public void fatalError(SAXParseException e) {
exception = e;
}

}




package com.cosmicdragon.darkgreen.xml;

import java.io.PrintStream;
import java.util.Date;

public class XMLWriter implements XMLDefine {

private int tabNum;

//---------------------------------------- write APIs.
public final void writeStartDocument(PrintStream out){
writeStartDocument(out, "UTF-8", "1.0");
}

public final void writeStartDocument(PrintStream out, String encode, String version){
tabNum = 0;
out.print("<?xml version=\"1.0\" encoding=\""
+ encode +"\"?>");
out.print("\n");
out.print("<" + ELEMENT_ROOT
+ " " + ATTR_VERSION + "=\"" + version + "\""
+ ">");
out.print("\n");
tabNum = 1;
}

public final void writeEndDocument(PrintStream out){
out.print("</" + ELEMENT_ROOT + ">");
}

public final void writeStartClass(PrintStream out, String name){
writeTab(out);
out.print("<" + ELEMENT_CLASS
+ " " + ATTR_NAME + "=\"" + name + "\""
+ ">");
out.print("\n");
tabNum++;
}

public final void writeEndClass(PrintStream out){
tabNum--;
writeTab(out);
out.print("</" + ELEMENT_CLASS + ">");
out.print("\n");
}

public final void writeInt(PrintStream out, String key, int v) {
writeRowStart(out, key, TYPE_INT);
out.print(v);
writeRowEnd(out);
}

public final void writeBoolean(PrintStream out, String key, boolean v){
writeRowStart(out, key, TYPE_BOOL);
out.print(v);
writeRowEnd(out);
}

public final void writeLong(PrintStream out, String key, long v){
writeRowStart(out, key, TYPE_LONG);
out.print(v);
writeRowEnd(out);
}

public final void writeString(PrintStream out, String key, String v){
writeRowStart(out, key, TYPE_STRING);
out.print(v);
writeRowEnd(out);
}

public final void writeDate(PrintStream out, String key, Date v){
writeRowStart(out, key, TYPE_DATE);
out.print(v.getTime());
writeRowEnd(out);
}

private final void writeRowStart(PrintStream out, String key, String type){
writeTab(out);
out.print("<" + ELEMENT_ROW
+ " " + ATTR_KEY + "=\"" + key + "\""
+ " " + ATTR_TYPE + "=\"" + type + "\""
+ ">");
}

private final void writeRowEnd(PrintStream out){
out.print("</" + ELEMENT_ROW + ">");
out.print("\n");
}

private final void writeTab(PrintStream out){
for(int i=0; i<this.tabNum; i++) out.print('\t');
}
}




テストプログラムを簡単ですが書いてみました。PersonクラスをいったんXMLに出力して、XML文字列からクラスを復元しています。


import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.io.PrintStream;

import java.util.Date;

import javax.xml.parsers.ParserConfigurationException;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import junit.framework.TestCase;

import com.cosmicdragon.darkgreen.xml.XMLReader;
import com.cosmicdragon.darkgreen.xml.XMLWriter;
import com.cosmicdragon.darkgreen.xml.XMLReadListener;

public class Test01 extends TestCase {

public void testA(){
ByteArrayOutputStream buff = new ByteArrayOutputStream();
PrintStream print = new PrintStream(buff);
PersonSerializer serial = new PersonSerializer();

Person p1 = new Person("hero", 18, true);

try{Thread.sleep(2000);}catch(Exception e){}

Person p2 = new Person("lynn", 17, false);


System.out.println("p1: " + p1);
System.out.println("p2: " + p2);

serial.writeXML(print, p1);
System.out.println(buff.toString());

try{
serial.readXML(buff.toByteArray(), p2);
}catch(Exception e){
e.printStackTrace();
}

System.out.println("p1: " + p1);
System.out.println("p2: " + p2);

super.assertEquals(p1.age, p2.age);
super.assertEquals(p1.name, p2.name);
super.assertEquals(p1.flag, p2.flag);
super.assertEquals(p1.date.getTime(), p2.date.getTime());
}
}

class Person extends XMLWriter {
int age;
String name;
boolean flag;
Date date;

public Person(String name, int age, boolean flag){
this.name = name;
this.age = age;
this.flag = flag;
this.date = new Date();
}

public String toString(){
return "Person : "
+ "age:" + age
+ ":name:" + name
+ ":flag:" + flag
+ ":date:" + date
;
}
}

class PersonSerializer extends XMLReadListener {
Person p;

public void writeXML(PrintStream out, Person p){
XMLWriter writer = new XMLWriter();
writer.writeStartDocument(out);
writer.writeStartClass(out, "Person");
writer.writeString(out, "name", p.name);
writer.writeInt(out, "age", p.age);
writer.writeBoolean(out, "flag", p.flag);
writer.writeDate(out, "date", p.date);
writer.writeEndClass(out);
writer.writeEndDocument(out);
}

public void readXML(byte[] in, Person p)
throws ParserConfigurationException, SAXException, IOException{
this.p = p;
InputSource src = new InputSource(new ByteArrayInputStream(in));
XMLReader reader = new XMLReader();
reader.readXML(this, src);
}

public void readRowInt(String key, int value){
p.age = value;
}

public void readRowBoolean(String key, boolean value){
p.flag = value;
}

public void readRowString(String key, String value){
p.name = value;
}

public void readRowDate(String key, Date value){
p.date = value;
}
}


下記は実行結果です。p1をシリアライズしてp2にデシリアライズをしています。


p1: Person : age:18:name:hero:flag:true:date:Thu Sep 02 01:15:23 JST 2010
p2: Person : age:17:name:lynn:flag:false:date:Thu Sep 02 01:15:25 JST 2010
<?xml version="1.0" encoding="UTF-8"?>
<d v="1.0">
<c n="Person">
<r k="name" t="s">hero</r>
<r k="age" t="i">18</r>
<r k="flag" t="b">true</r>
<r k="date" t="d">1283357723441</r>
</c>
</d>
p1: Person : age:18:name:hero:flag:true:date:Thu Sep 02 01:15:23 JST 2010
p2: Person : age:18:name:hero:flag:true:date:Thu Sep 02 01:15:23 JST 2010

2010年4月11日日曜日

GoogleAppEngine-RPCの使い方


前回のJDOQLクエリフィルタに引き続き、RPCによるGWTとサーバーの通信を実装します。RPCとは簡単に言うと、クライアントとサーバーでJavaのクラスをそのままやりとりするための通信方式です。

通常AJAXでの通信はXMLもしくはJSOPで行いますが、通信毎にプロトコルの仕様を決めなければなりませんでした。しかしGoogleAppEngineJavaはクライアント画面のGWTとサーバーの両方で使えるJavaのクラスを定義すれば簡単にデータのやりとりができます。

RPCでの通信を行うにはいくつか準備する必要があります。

・通信でやりとりされるオブジェクト
・RemoteServiceインターフェースとサーバー側の実装
・通信結果を受け取る非同期インターフェース

今回はつぶやき情報をサーバーに送る機能を実装してみようかと思います。

TweetPutParam.java
---------------------------

package com.devtter.client.rpc;
import com.google.gwt.user.client.rpc.IsSerializable;

public class TweetPutParam implements IsSerializable {
public String tweet;
public double x;
public double y;
public double z;
}

このクラスはつぶやきと、つぶやいた位置情報を保持しています。ここでのポイントはcom.google.gwt.user.client.rpc.IsSerializableクラスをimplemntsしていることです。このインターフェースを使うだけでコンパイル時にシリアライズ処理が実装されるようです。

TweetService.java
---------------------------

package com.devtter.client.rpc;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

@RemoteServiceRelativePath("tweet")
public interface TweetService extends RemoteService {
public String put(TweetPutParam param) throws IllegalArgumentException;
}


TweetServiceAsync.java
---------------------------

package com.devtter.client.rpc;

import com.google.gwt.user.client.rpc.AsyncCallback;

public interface TweetServiceAsync {
void put(TweetPutParam param, AsyncCallback callback)
throws IllegalArgumentException;
}


ここまでがインターフェースの定義です。サーバー側では定義されたTweetServiceをサーブレットとして実装します。具体的にはRPCを処理するcom.google.gwt.user.server.rpc.RemoteServiceServletを実装します。
今回のサンプルではクライアントでつぶやかれた内容をJDOに格納します。

TweetServiceImpl.java
---------------------------

package com.devtter.server.rpc;

import com.devtter.server.model.Tweet;
import com.devtter.server.model.TweetDAO;
import com.devtter.client.rpc.TweetGetParam;
import com.devtter.client.rpc.TweetPutParam;
import com.devtter.client.rpc.TweetResponse;
import com.devtter.client.rpc.TweetService;
import com.devtter.shared.FieldVerifier;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import java.util.logging.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;

@SuppressWarnings("serial")
public class TweetServiceImpl extends RemoteServiceServlet implements TweetService {
private static final Logger log = Logger.getLogger(TweetServiceImpl.class.getName());

public String put(TweetPutParam param) throws IllegalArgumentException{
if (!FieldVerifier.isValidTweet(param.tweet)) {
throw new IllegalArgumentException(
"Tweet must be at less 140 characters long");
}
log.info("put : " + param.tweet);
TweetDAO.tweetAnonymous(param.tweet);
return "";
}
}


次にクライアント側で、つぶやきを送る処理を実装します。実装はちょっと難解な書き方をしますが、まずTweetServiceAsyncをGWTクラスから生成します。これで呼び出す準備ができます。次に実際にコールするときはパラメータとレスポンスを指定することで呼び出すことができます。

TweetPanel.java
---------------------------

private final TweetServiceAsync tweetService = GWT
.create(TweetService.class);

public void sendTweetToServer() {
TweetPutParam putParam = new TweetPutParam();
putParam.tweet = inputPanel.inputBox.getText();
if (!FieldVerifier.isValidTweet(putParam.tweet)) {
return;
}

tweetService.put(putParam,
new AsyncCallback() {
public void onFailure(Throwable caught) {
}

public void onSuccess(String result) {
}
});
}


以上でRPCによるサーバークライアント通信の処理は完了です。慣れるまで敷居が高いですが、一度作ってしまえば、通信内容を変更する時は通信クラスのパラメータをかえるだけで対応出きるのでとても便利です。

なお、今回作ったプログラムは下記に置いてあります
http://code.google.com/p/devtter/source/browse/#svn/trunk/devtter/src/com/devtter/client/rpc
http://code.google.com/p/devtter/source/browse/#svn/trunk/devtter/src/com/devtter/server/rpc
http://code.google.com/p/devtter/source/browse/#svn/trunk/devtter/src/com/devtter/client/ui/tweet

2010年4月10日土曜日

GoogleAppEngine-JDOQLクエリフィルタの使い方

前回のGoogleAppEngine-GWTの使い方の続きです

今回はGWT UIとサーバーを連携させる前に、JDOQLの高度なフィルタ機能を少し説明します。

JDOQLはSQLのような構文でGoogleのストレージを操作することができます。基本的には文字列で条件文を記述すれば操作はできますが、JDOQLにJavaのクラスを混ぜたいときがあります。例えば、オブジェクト内にDateクラスが含まれていて、ある日時以降のデータを検索したい場合です。
そういったときに、JDOQLのクエリフィルタAPIを使用し、高度な条件を指定します。今回は、「指定した位置から指定個数検索する」と「指定時間以降のデータを指定個数検索する」の二つの機能を作ってみます。


Tweet.java
----------------------------------


package com.devtter.server.model;

import java.util.Date;
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.users.User;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Tweet {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long id;

@Persistent
private User googleId;

@Persistent
private String content;

@Persistent
private Date date;
}


TweetDAO.java
----------------------------------

package com.devtter.server.model;

import java.util.List;
import java.util.Date;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;


public class TweetDAO {

/**
* 指定した位置から指定個数のつぶやきを検索します
*
*
* @param start 開始位置
* @param num 個数
* @return Tweetクラスのリスト
*/
public static List find(int start, int num) {
PersistenceManager pm = PMF.get();
String query = "select from "
+ Tweet.class.getName()
+ " order by date desc range "
+ start + "," + num;
List tweets = (List) pm.newQuery(query).execute();
pm.detachCopyAll(tweets);
pm.close();
return tweets;
}

/**
* 指定時間以降につぶやかれたTweetを検索します。
*
* @param starttime 開始時間
* @param num 個数
* @param desc 降順にするかどうか。
* @return Tweetクラスのリスト
*/
public static List findBeforeStartTime(long starttime, int num, boolean desc) {
PersistenceManager pm = PMF.get();
Date dateStartTime = new Date(starttime);
Query query = pm.newQuery(Tweet.class);
query.declareImports("import java.util.Date");
query.declareParameters("Date ParamStartDate");
query.setFilter("date > ParamStartDate");
if(desc)
query.setOrdering("date desc");
else
query.setOrdering("date asc");

query.setRange(0,num);

List tweets
= (List) pm.newQuery(query).execute(dateStartTime);
pm.detachCopyAll(tweets);
pm.close();
return tweets;
}
}


一つ目のfind関数では、クエリフィルタAPI使わず文字列で検索文を作成し、実効しています。この処理はおおよそ下記のような文字列になります。

select from com.devtter.server.model.Tweet order by date desc range 0, 5

次にfindBeforeStartTime関数ではTweetクラスのDateオブジェクトが指定した日時以降かを条件に検索しています。この処理をするにはまず、declareImports関数でDate型をimportします。これによりQueryクラス内でjava.util.Dateクラスが使えるようになります。次にdeclareParameters関数で変数を宣言します。そして、setFilter関数で条件判定を指示しています。ここで指定しているParamStartDate変数に値を代入するには、検索実行時のexecute関数の引数に変数を代入します。

Twitterのようなアプリケーションを実際に作り込むと、必ず高度な条件での検索が必要になるのでクエリフィルタは重要な機能ですね。
なお今回のプログラムはおおよそ下記にあります。
http://code.google.com/p/devtter/source/browse/trunk/devtter/src/com/devtter/server/model/TweetDAO.java

GoogleAppEngine-GWTの使い方


前回のJDOに引き続き今回はGWTをGoogleAppEngine上で取り扱ってみます。

GWTとは簡単にいえば、JavaのAWTににたライブラリでアプレットのように画面を開発すると、それがJavaScriptに自動的に置き換わる開発キットのことです。
C++やJavaなどで組み込み系やWindows環境でGUIを開発してきたエンジニアにとって、HTMLやJavaScriptは開発スタイルの毛色が異なってやり辛いのですが、こういったクロスコンパイラがあれば今までのやり方でリッチなWEB画面が作れるので、GWTはとても魅力的に感じます。

では早速画面を構成するクラスを提示してみます。今回も引き続きTwitterのようなサイトを作るので、「メインパネルにテキストボックス、送信ボタン、つぶやきリスト」が出てくる部品を作ります。なお、このサンプルはEclipseのGAEプラグインが入っていて、「Web Application Project」というプロジェクトテンプレートを使った開発を想定しています。





TweetPanel.java 画面の部品がすべて乗ってくるパネル
-----------------------------

package com.devtter.client.ui.tweet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Label;

public class TweetPanel extends VerticalPanel {
public TweetInputPanel inputPanel;
public TweetListPanel listPanel;

public TweetPanel(){
this.inputPanel = new TweetInputPanel();
this.listPanel = new TweetListPanel();
super.add(inputPanel);
super.add(listPanel);

MyHandler handler = new MyHandler();
inputPanel.sendButton.addClickHandler(handler);
inputPanel.inputBox.addKeyUpHandler(handler);
}

class MyHandler implements ClickHandler, KeyUpHandler {
public void onClick(ClickEvent event) {
}

public void onKeyUp(KeyUpEvent event) {
if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
}
}

private void sendTweetToServer() {
}
}
}


TweetPanelクラスのMyHandlerと言うインナクラスはパネルに対するイベント処理を行います。ここではキーイベントとクリックイベントのリスナが用意されており、送信ボタンが押されたときなどはここにサーバーへの通信処理を記述します。


TweetInputPanel.java 入力用のテキストボックスと送信ボタン
-----------------------------

package com.devtter.client.ui.tweet;

import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.TextBox;

public class TweetInputPanel extends HorizontalPanel {
public TextBox inputBox;
public Button sendButton;

public TweetInputPanel() {
inputBox = new TextBox();
sendButton = new Button("Send");
sendButton.addStyleName("sendButton");
super.add(inputBox);
super.add(sendButton);
inputBox.setFocus(true);
inputBox.selectAll();
}
}


TweetListPanel.java つぶやきリスト。ラベルを乗せる
-----------------------------

package com.devtter.client.ui.tweet;
import com.google.gwt.user.client.ui.VerticalPanel;

public class TweetListPanel extends VerticalPanel {
public TweetListPanel(){
}

public void putItem(String content){
TweetListItemPanel item = new TweetListItemPanel();
item.setText(content);
super.insert(item, 0);
}
}


TweetListItemPanel.java 一つ一つのつぶやき。最初はlabelで実装しておく
-----------------------------

package com.devtter.client.ui.tweet;

import com.google.gwt.user.client.ui.Label;;

public class TweetListItemPanel extends Label{
public TweetListItemPanel(){
this("");
}
public TweetListItemPanel(String label){
super(label);
}
}

-----------------------------

これで最低限の画面構成クラスはできました。これを表示するにはメインで使われるHTMLテンプレートにコンテナを記述するのと、そのコンテナにaddするプログラムを記述します。まず、メインが面に対応したHTMLファイルにmainContainerというidを作ります。これにより、この位置にパネルを配置できるようになります。

war/Devtter.html

<h1>devtter</h1>

<table align="center">
<tr>
<td colspan="2" style="font-weight:bold;">What's Happening?</td>
</tr>
<tr>
<td id="mainContainer"></td>
</tr>
<tr>
<td colspan="2" style="color:red;" id="errorLabelContainer"></td>
</tr>
</table>

---------------------------

次にEntoryPointクラスの初期処理に、UIを乗せる記述をします。なおcom.google.gwt.core.client.EntryPointは画面が最初に呼び出されるエントリーポイントを表すクラスです。通常HTMLのテンプレートとセットで扱います。

Devtter.java 画面表示時に最初に呼び出されるクラス

package com.devtter.client;

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

public class Devtter implements EntryPoint {
public void onModuleLoad() {
final TweetPanel tweetPanel = new TweetPanel();
RootPanel.get("mainContainer").add(tweetPanel);
}
}


これでUIのベースができました。次回はサーバーと連携するプログラムを付け足します
今回作成したプログラムはおおよそ下記にあります

http://code.google.com/p/devtter/source/browse/#svn/trunk/devtter/src/com/devtter/client/ui/tweet

GoogleAppEngine-JDOの使い方

GoogleAppEngineの練習としてTwitterのようなサイトを作ってみようと思います。

JDOとは簡単にいえば、Googleの高性能ストレージにデータを保存するためのインターフェースです。今回はJDOを使ってつぶやきデータの保存が出きるような機能を用意してみます。

最初に定義するのはTweetクラスとPositionクラスです。Tweetクラスはつぶやきを表しPositionクラスは発言時の位置情報を表します。ポイントとしてはJDO用のオブジェクトが親子関係になる場合のアノテーションの書き方です。子クラスの方はimport com.google.appengine.api.datastore.Keyをプライマリーキーに使います。

Tweet.java

package com.devtter.server.model;

import java.util.Date;
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.users.User;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Tweet {

@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long id;

@Persistent
private User author;

@Persistent
private String content;

@Persistent
private Date date;

@Persistent
private Position pos;

public Tweet(User author, String content, Date date) {
this.author = author;
this.content = content;
this.date = date;
}

public Long getId() {
return id;
}

public User getAuthor() {
return author;
}

public String getContent() {
return content;
}

public Date getDate() {
return date;
}

public void setAuthor(User author) {
this.author = author;
}

public void setContent(String content) {
this.content = content;
}

public void setDate(Date date) {
this.date = date;
}

public Position getPotision() {
return pos;
}

public void setPosition(Position pos) {
this.pos = pos;
}
}


Position.java


package com.devtter.server.model;

import java.util.Date;
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;


@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Position {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Key key;

@Persistent
public double x;
@Persistent
public double y;
@Persistent
public double z;
@Persistent
private Date date;

public Key getKey(){
return key;
}

public Date getDate() {
return date;
}

public void updateDate() {
this.date = new Date();;
}
}


次に、オブジェクトを操作するクラスです。ポイントはdetachCopyAll関数を使って、PersistenceManagerをクローズ後もJDOオブジェクトに参照出きるよう、オブジェクトをデタッチしているところです。簡単に言えばオブジェクトのインスタンスのコピー、Java風に言えばクローンを作成します。これをしないでクローズ後に参照しようとすると下記のエラーが発生します。

org.datanucleus.exceptions.NucleusUserException: Object Manager has been closed

JDOがORマッピングのような作りになっているので、実際にデータをストレージに反映
するタイミングとメモリ上のインスタンスの差分の関係を解決するためにこういった設計に
なっていると思われます。

TweetDAO.java


package com.devtter.server.model;

import java.util.List;
import java.util.Date;
import javax.jdo.PersistenceManager;

public class TweetDAO {

public static void tweetAnonymous(String comment){
Tweet t = new Tweet(null, comment, new Date());
save(t);
}

public static void save(Tweet t) {
PersistenceManager pm = PMF.get();
pm.makePersistent(t);
pm.close();
}

public static List find(int start, int num) {
PersistenceManager pm = PMF.get();
String query = "select from "
+ Tweet.class.getName()
+ " order by date desc range "
+ start + "," + num;
List tweets = (List) pm.newQuery(query).execute();
pm.detachCopyAll(tweets);
pm.close();
return tweets;
}
}



この作りでデータを格納していくと、もしこれがMySQLなどであれば一つのテーブルにすべてのつぶやきが入ってしまうことになります。しかしGoogleAppEngineの説明を読む限り、GAE上でJDOを使う限りは負荷分散は自動化されるようです。この作りで大量にデータを投入し、パフォーマンスがどうなるか実験してみようかと思います。


引用:
Google App Engine なら、そのようなことを気にする必要はありません。
App Engine のインフラは、シンプルな API を使って、データの配布、
複製、負荷分散を一手に引き受けます。ユーザーは強力なクエリ エンジン
とトランザクションのメリットを享受できます。



今回使ったプログラムはおおよそ下記にあります。

http://code.google.com/p/devtter/source/browse/trunk/devtter/src/com/devtter/server/model/

2010年4月6日火曜日

初めてのDeploy




何とかアプリケーションアカウントを設定後、Eclipseプラグインから
シンプルなスケルトンを作成し、deployボタンを押しつつメールアドレスやら
パスワードを設定したらなんと!
動きました!!

http://devtter.appspot.com/

この環境はすごい。Eclipseと完全に連動している。完全に時代は変わった。

GoogleAppEngineアカウント取得!


GoogleAppEngineのアカウントが急に取得できました!
諦めてアマゾン使おうかと落ち込んでいたら急にgmailにメールが
きてちょっと感動!
問い合わせフォームから書き込んだのがよかったのかな?

GoogleAppEngineのダッシュボードが何だか作戦会議室見たいでかっこいいです。


-
Hello,
You're receiving this email because you indicated you'd had some problems verifying your Google App Engine account using SMS. We just wanted to let you know we've enabled your account--you should be able to create applications now!
To start creating applications with Google App Engine, simply follow this link:
https://appengine.google.com/
Thanks!
The Google App Engine Team


俄然やる気が出てきました!

2010年4月4日日曜日

GoogleAppEngineのSMSメールが来ない

Google App Engine はローカル環境でも十分開発できるのだが、やはり
Google側にアカウントを用意しておきたい。作ったものが公開できる
保証がないと落ち着かないので。

しかし一晩待っても携帯電話にGoogle App EngineのSMS verification
messageメールがこないので下記のFAQを読んでみた。

http://code.google.com/intl/ja/appengine/kb/sms.html


-----------------------------------
現在問題が発生している携帯サービス会社

現在、次の携帯サービス会社への SMS メッセージ送信には問題があります。

* 日本 - DoCoMo
* 日本 - KDDI

確認コードを記載した SMS メッセージを受信できない場合は、SMS の問題のフォームに記入してください。
-----------------------------------


どうも携帯電話のメール送信が完全ではないようだ。
しょうがないので下記の申告フォームに調べてもらえるように書いてみる。

http://appengine.google.com/waitlist/sms_issues

とりあえずしばらくまた待つことにする。

路線変更

GoogleCodeを使っていたら、GoogleAppEngineが便利そうなことに
気づいたのでこちらを試すことに。

試しに下記から開発用のアカウントをとろうかと思ったのですが

https://appengine.google.com/start

携帯電話のアドレスを入れてもエラーがでるばかりでよくわからない
明日またチャレンジ。