annotation-style in AspectJ

AspectJでは、pointcutやadviceを記述する際にJavaを拡張した記法を用いますが、AspectJ5からはAnnotationを用いてpointcutを定義し、advice側でもAnnotationを用いてpointcutを指定することができます。具体的には、以下のようなコードになります。

@Aspect
public abstract class ModelMetadataAspect {

  // Annotationでpointcutを定義する
  @Pointcut("call(net.wrap_trap.bitro.model.Model+.new())")
  public void metadataCollectPointcut(){};

  // adviceではAnnotationでpointcut名を指定する
  @After("metadataCollectPointcut()")
  public void collectMetadata(ProceedingJoinPoint joinPoint){
    ApplicationContainer container = ApplicationContainer.getContainer();
    MetadataCollector collector = 
      (MetadataCollector)container.getComponent(MetadataCollector.class, Scope.APPLICATION);
    collector.collectMetadata(joinPoint);
  }
}

Annotationをpointcutとするようなアスペクトを、上記のような記法(annotation-style)で定義して動かしてきたのですが、一部のpointcut(またはそれを対象とするadvice)はうまく動かないことがあるようです。今回は上記コードのnew(コンストラクタに関するpointcut)に対するadviceが起動しません(AJDT2.0.1)。AJDTを使うと定義したpointcutにmatchする処理が1つもない場合はwarningが出るのですが、それも出ておらず、起動もしないという困った状況になりました。
通常のアスペクトの記法で書き直してみたところ、期待通りの挙動になりました。

public aspect ModelMetadataCollectAspect {

  pointcut modelMetadataCollectPointcut() : call(net.wrap_trap.bitro.model.Model+.new());
  
  after() : modelMetadataCollectPointcut(){
    ApplicationContainer container = ApplicationContainer.getContainer();
    MetadataCollector collector = 
      (MetadataCollector)container.getComponent(MetadataCollector.class, Scope.APPLICATION);
    collector.collectMetadata((ProceedingJoinPoint)thisJoinPoint);
  }
}

そのうちAJDTのバージョンを上げて再度トライしてみます。

Asynchronus Method Call Using AspectJ

非同期と継続と私 - 技術日記@kiwanamiを読んでいて、関数呼び出しを非同期化するのに、(funcall cc n)を(deferred:call cc n)に変更するだけで簡単にできるのが羨ましかったのでちょっと作ってみました(というだけです…)。

実装
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Async {
    Class<? extends Callback> callbackClass() default NullCallback.class;
}
@Aspect
public class AsyncSupport {

    @Pointcut("call(@net.wrap_trap.bitro.annotation.Async * *.*(..))")
    public void asyncCallPointcut(){};

    @Around("asyncCallPointcut()")
    public Object doAsyncMethodCall(final ProceedingJoinPoint thisJoinPoint) throws Throwable{
        final Callback callback = getCallback(thisJoinPoint);
        
        new Thread(new Runnable(){

            @Override
            public void run() {
                try{
                    Object ret = thisJoinPoint.proceed();
                    if(callback != null){
                        callback.doCallback(ret);
                    }
                }catch(Throwable t){
                    throw new RuntimeException(t);
                }
            }
        }).start();
        return null;
    }

    private Callback getCallback(final ProceedingJoinPoint thisJoinPoint) {
        MethodSignature signature = (MethodSignature)thisJoinPoint.getSignature();
        Async concurrent = signature.getMethod().getAnnotation(Async.class);
        return (Callback)ApplicationContainer.getContainer().getComponent(
            concurrent.callbackClass(), Scope.APPLICATION
        );
    }
}
実行
public class AsyncMethodCallTest {

    @Before
    public void setUp(){
        ApplicationContainer.init();
        ApplicationContainer.getContainer().registerComponent(
            DisplayResultCallback.class,
            new DisplayResultCallback(),
            Scope.APPLICATION
        );
    }
    
    @Test
    public void testAsyncMethodCall() throws InterruptedException{
        Target target = new Target();
        target.add(1, 2);
        System.out.println("after call target.add(1,2).");
    }
    
    static class Target{
        @Async(callbackClass = DisplayResultCallback.class)
        public int add(int a, int b) throws InterruptedException{
            Thread.sleep(10);
            System.out.println("called Target.add");
            return a+b;
        }
    }
    
    static class DisplayResultCallback implements Callback{
        @Override
        public void doCallback(Object result) {
            System.out.println("result: " + result);
        }
    }
}
結果

after call target.add(1,2).
called Target.add
result: 3

メソッドに@Asyncアノテーションを付けると非同期呼び出しになります。

RestletでJSONを扱う

GAE/JでRestlet(server-side)を使い、JSONをやり取りするようにしてみました。使用したRestletのバージョンは、Version 2.1 Snapshot (testing)です。

jarファイル

RestletにはGAE/J版が用意されているので、それをダウンロードし、配備します。配布されているアーカイブには、Restlet本体の他にorg.restlet.ext.*パッケージが含まれているのですが、今回はその中から以下のjarファイルを使用しています。

  • org.restlet.jar
  • org.restlet.ext.servlet.jar
  • org.restlet.ext.json.jar
web.xml

ServletはRestletのServerServletを使うようにします。RestletにはApplicationのサブクラスを作ってルーティングを設定し、各ServerResourceを呼び出すようにするのが基本的な流れになります。

    	<servlet>
    		<servlet-name>RestletServlet</servlet-name>
    		<servlet-class>org.restlet.ext.servlet.ServerServlet</servlet-class>
        	<init-param>
			<param-name>org.restlet.application</param-name>
			<param-value>net.wrap_trap.lifecycle.rest.LifecycleRestApplication</param-value>
		</init-param>
	</servlet>
	<servlet-mapping>
		<servlet-name>RestletServlet</servlet-name>
		<url-pattern>/*</url-pattern>
	</servlet-mapping>
Application

ルーティング等を設定します。

public class LifecycleRestApplication extends Application {
 
    @Override
    public Restlet createInboundRoot() {
        Router router = new Router(getContext());
        getConnectorService().getClientProtocols().add(Protocol.FILE);
 
        Directory dir = new Directory(getContext(), LocalReference.createFileReference(new File("war/")));
        router.attachDefault(dir);
        router.attach("/events/{year}/{month}/{day}", DailyLogResource.class);
 
        return router;
    }
}

上記コードは以下の2点を設定しています。

  • ルーティングのデフォルト(どのルーティング定義にも合致しない場合)の振る舞いを設定
  • リクエスURIが「/events/yyyy/MM/dd」であれば、DailyLogResourceにルーティングする
    • URIに指定された{year},{month},{day}の各値は、UniformResource#getRequestAttributesで取得することができる
ServerResource

ServerResourceのサブクラスを作成し、任意のリソースを管理します。今回は以下の振る舞いを行うServerResourceを作成しました。

  • GET
    • yyyy/MM/ddに対応するリソースをBigTableから取り出し、JSONに変換してクライアントに返す
  • PUT
    • リクエストボディのJSONを受け取り、Javaのオブジェクトに変換し、BigTableに書き込む
    • @Put("json")
      • リクエストのContent-Typeがapplication/jsonの時のみ対応する
public class DailyLogResource extends ServerResource {

	public DailyLogResource() {
		Set<Method> allowedMethods = new HashSet<Method>();
		allowedMethods.add(Method.GET);
		allowedMethods.add(Method.PUT);
		setAllowedMethods(allowedMethods);
	}
	
	@Get
	@Transaction
	public JsonRepresentation getDailyLog(){
		Date date = getDate();
		Collection<TimeLog> timeLogList = null;
		DailyLog dailyLog = DailyLog.getDailyLog(date);
		if(dailyLog != null && dailyLog.getTimeLogs() != null && dailyLog.getTimeLogs().size() > 0){
			timeLogList = DailyLog.detachCopyAll(dailyLog.getTimeLogs());
		}else{
			timeLogList = Collections.EMPTY_LIST;
		}
		JSONArray jsonArray = new JSONArray();
		for(TimeLog timeLog : timeLogList){
			jsonArray.put(timeLog.toJSON());
		}
		return new JsonRepresentation(jsonArray);
	}
	
	@Put("json")
	@Transaction
	public JsonRepresentation saveOrUpdateDailyLog(Representation rep) throws JSONException, IOException{
		JsonRepresentation jsonRep = new JsonRepresentation(rep.getText());
		Date date = getDate();
		DailyLog dailyLog = DailyLog.getDailyLog(date);
		if(dailyLog == null){
			dailyLog = DailyLog.createDailyLog(date);
			dailyLog.save();
		}
		List<TimeLog> timeLogs = dailyLog.getTimeLogs();		
		
		JSONArray jsonArray = jsonRep.getJsonArray();
		for(int i = 0; i < jsonArray.length(); i++){
			JSONObject calEvent = jsonArray.getJSONObject(i);
			TimeLog timeLog = null;
			if(calEvent.isNull("encodedKey")){
				// insert
				timeLog = new TimeLog();
				timeLog.setDailyLog(dailyLog);
				timeLogs.add(timeLog);
			}else{
				// update
				timeLog = TimeLog.getObjectById(TimeLog.class, calEvent.get("encodedKey"));
			}
			timeLog.setStart(calEvent.getLong("startLong"));
			timeLog.setEnd(calEvent.getLong("endLong"));
			timeLog.setName(calEvent.getString("title"));
			timeLog.setOrder(0);
			timeLog.save();
			calEvent.put("encodedKey", timeLog.getEncodedKey());
		}
		return new JsonRepresentation(jsonArray);
	}
	
	protected Date getDate(){
		Map<String, Object> attrMap = getRequestAttributes();
		String year = (String)attrMap.get("year");
		String month = (String)attrMap.get("month");
		String day = (String)attrMap.get("day");
		
		Calendar cal = Calendar.getInstance();
		cal.set(Integer.parseInt(year), Integer.parseInt(month), Integer.parseInt(day));
		return cal.getTime();
	}
}

ServerResourceのサブクラスのインスタンスはリクエスト毎に作成されるので、インスタンスフィールドを使うことができます。GET/PUTに関わらず、クライアントにデータを返す場合は、各メソッドの戻り値をRepresentationにします。本来であればMediaTypeを見て適切なRepresentationのサブクラスを選ぶのが正しいのですが、今回はJSONしか扱わないので所々でJSON決め打ちのコードとなっています。
ちょっと惜しいのが、JsonRepresentationを生成する際に複数のオブジェクトが格納されたListを指定すると、JSONとして出力されるのはListのプロパティ(empty)のみになってしまうこと。今回はListに対応するJSONArrayを作成し、Listに格納されているJavaオブジェクトを自前でJSONObjectに変換してから詰めるようにすることで、JSONで返せるようにしています。RestletでガッツリJSONを扱うのであれば、指定したJavaオブジェクトを再帰的にJSONObjectに変換してくれるようなRepresentationのサブクラスを作った方が良いでしょう。

まとめ

RestletでJSONを扱ってみました。Restletは思ったよりも情報が少なく、目的どおりに動かすのが少し難しいかもしれません。しかし、ルーティングの設定やRestletの導入自体は簡単なので、一般的なWebアプリケーションで使うというのも面白いと思います。

How to use of changeVersion その2

前回のエントリで、以下のように記述しましたが、NGでした。

  • openDatabaseにpostVerを指定し、changeVersion(preVer, postVer, callback)を呼ぶと、currentVersionと引数が違う、というエラーが出る
    • current version of the database and `oldVersion` argument do not match

後者は、changeVersionの第4引数を以下のようにしてスルーすれば回避できるでしょう。

		function(error){
			window.console.log("(initDb)db error:" + error.message);
			callback_success();
		},

理由は、changeVersionの第4引数で指定したエラーハンドラ内でさらにtransactionを実行するとエラーになります。上記コードの場合、callback_success()の中でtransactionを実行するとNGです。試したのはChrome(6.0.472.62)のみです。
openDatabase関数の戻り値であるdatabaseオブジェクトには「version」というプロパティがあるので、それを見てバージョンアップが必要な場合だけchangeVersionを実行するようにすれば問題ありませんでした。

function initDb(callback_success){
	var db = getDbConnection();
	if(db.version == "1.0"){
		db.changeVersion("1.0", "1.1",
			function(t){
				t.executeSql('CREATE TABLE cal_events_new(id INTEGER PRIMARY KEY, server_key TEXT, year INTEGER, month INTEGER, day INTEGER, start TEXT, end TEXT, title TEXT, body TEXT)');
				//t.executeSql('INSERT INTO cal_events_new(id, year, month, day, title) SELECT id, year, month, day, title FROM cal_events');
				t.executeSql('DROP TABLE IF EXISTS cal_events');
				t.executeSql('ALTER TABLE cal_events_new RENAME TO cal_events');
			},
			function(error){
				alert("(initDb)db error:" + error.message);
			},
			callback_success
		);
	}else{
		callback_success();
	}
}

How to use of changeVersion

HTML5のclient-side databaseはバージョン管理されています。Asynchronous database APIでは、このバージョンが変わった際に実行するハンドラを登録する仕組みが提供されています。

The changeVersion() method allows scripts to atomically verify the version number and change it at the same time as doing a schema update. When the method is invoked, it must immediately return, and then asynchronously run the transaction steps with the transaction callback being the third argument, the error callback being the fourth argument, the success callback being the fifth argument, the preflight operation being the following:

1. Check that the value of the first argument to the changeVersion() method exactly matches the database's actual version. If it does not, then the preflight operation fails.

...the postflight operation being the following:

1. Change the database's actual version to the value of the second argument to the changeVersion() method.
2. Change the Database object's expected version to the value of the second argument to the changeVersion() method.

...and the mode being read/write.

If any of the optional arguments are omitted, then they must be treated as if they were null.

http://dev.w3.org/html5/webdatabase/#asynchronous-database-api

ということで試してみました。

function initDb(callback_success){
	getDbConnection().changeVersion("1.0", "1.1",
		function(t){
			t.executeSql('CREATE TABLE cal_events_new(id INTEGER PRIMARY KEY, serverKey TEXT, year INTEGER, month INTEGER, day INTEGER, title TEXT)');
			t.executeSql('INSERT INTO cal_events_new(id, year, month, day, title) SELECT id, year, month, day, title FROM cal_events');
			t.executeSql('DROP TABLE cal_events');
			t.executeSql('ALTER TABLE cal_events_new RENAME TO cal_events');
		},
		function(error){
			window.console.log("(initDb)db error:" + error.message);
		},
		function(){
			callback_success();
		}
	);
}
function getDbConnection(){
	return openDatabase(dbName, '1.0', dbName, 5*1024*1024);
}

実行環境はChrome(6.0.472.62)です。バージョンアップ前のバージョンをpreVer、バージョンアップ後のバージョンをpostVerとします。
まず、changeVersionを呼び出す際のdatabaseオブジェクトはperVer、postVerのどちらを指定してopenDatabaseしたものを使用するか、という疑問がありましたが、preVerを指定するのが正しいようです。postVerでは取得できませんでした。postVerのDBが存在していないからでしょう。
バージョンアップとその際のハンドラの実行はうまくいったのですが、その後次の問題が出ました。

  • openDatabaseする際に、preVerを指定するとエラーとなる
    • INVALID_STATE_ERR: DOM Exception 11
  • openDatabaseにpostVerを指定し、changeVersion(preVer, postVer, callback)を呼ぶと、currentVersionと引数が違う、というエラーが出る
    • current version of the database and `oldVersion` argument do not match

後者は、changeVersionの第4引数を以下のようにしてスルーすれば回避できるでしょう。

		function(error){
			window.console.log("(initDb)db error:" + error.message);
			callback_success();
		},

前者の対応をどうするべきか考え中です。openDatabaseの呼び出すをtry-cacheで囲んで、複数のバージョンでトライしてみて、エラーにならなかったものを返す、という方式ならばできなくもないですが。

function getDbConnection(){
	try{
		return openDatabase(dbVer, '1.0', dbVer, 5*1024*1024);
	}catch(ignore){}
	
	try{
		return openDatabase(dbVer, '1.1', dbVer, 5*1024*1024);
	}catch(ignore){}
}

追記:

後者は、changeVersionの第4引数を以下のようにしてスルーすれば回避できるでしょう。

上記はNGでした。理由は次のエントリで記述します。

test for asynchronous code with QUnit

以前のエントリで書きましたが、SQL-based database APIの一部のメソッドの実行は非同期となります。非同期のメソッドをテストする為に、QUnitを使ってみました。

QUnit

QUnitは、jQuery自体のコードやプラグインのテストに使われている、JavaScript用のテストフレームワークです。jQueryを使っていないJavaScriptや、サーバサイドJavaScriptもテスト可能とのこと。

QUnit is a powerful, easy-to-use, JavaScript test suite. It's used by the jQuery project to test its code and plugins but is capable of testing any generic JavaScript code (and even capable of testing JavaScript code on the server-side).

http://docs.jquery.com/QUnit

そして、QUnitは非同期で実行されるコードをテストする為の仕組みを提供しています。

QUnit is similar to other unit testing frameworks like JUnit, but makes use of the features JavaScript provides and helps with testing code in the browser, eg. with it's stop/start facilities for testing asynchronous code.

http://docs.jquery.com/QUnit
準備

テストを実行する為のHTMLを用意します。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <script src="http://code.jquery.com/jquery-latest.js"></script>
  <link rel="stylesheet" href="http://github.com/jquery/qunit/raw/master/qunit/qunit.css" type="text/css" media="screen" />
<script type="text/javascript" src="http://github.com/jquery/qunit/raw/master/qunit/qunit.js"></script>
<script type="text/javascript" src="./js/db.js"></script>

  <script>
  $(document).ready(function(){
    
module("Module Connection");

test("db test1", function() {
	ok( getDbConnection(), "getDbConnection()");
});

  });

function fail(msg){
	ok(false, msg);
}
  </script>
  
</head>
<body>
  <h1 id="qunit-header">QUnit example</h1>
 <h2 id="qunit-banner"></h2>
 <h2 id="qunit-userAgent"></h2>
 <ol id="qunit-tests"></ol>
 <div id="qunit-fixture">test markup, will be hidden</div>
</body>
</html>

テスト対象となるJavaScriptのコードを用意します。

function getDbConnection(){
	return openDatabase('lifecycle1', '1.0', 'lifecycle1', 5*1024);
}

function startTransaction(executeSql, error, success){
	getDbConnection().transaction(executeSql, error, success);
}
テストコード

用意したHTMLの中で、$(document).readyの中にテストコードを書いていきます。テストメソッドは、test関数を用いて実行します。テストメソッドのグルーピングは、moduleという要素を使って行います。JUnitで言うassert*等のメソッドに該当する「Assertions」は、ok、equals、sameの3つしか用意されていません。このあたりの詳細については、API documentationを参照してください。

module("Module Connection");

test("db test1", function() {
	ok( getDbConnection(), "getDbConnection()");
});
実行

ブラウザでHTMLファイルを開くとテストを実行し、結果を表示します。
テストが成功すれば、以下のようにgreenになります。

失敗すれば、以下のようにredになります。

非同期コードのテスト

SQL-based database APIを使用し、非同期のコードをテストします。

test("db test2_sync", function() {
	startTransaction( // 以下、非同期で実行される関数
		function(t){
			t.executeSql('CREATE TABLE IF NOT EXISTS for_tests(id INTEGER PRIMARY KEY, note TEXT)');
			t.executeSql("SELECT * FROM for_tests", [], function(tx, rx){});
		},
		function(e){
			fail("must not failed.");
		},
		function(){
			ok(true, "success to access for_tests.");
		}
	);
});

function fail(msg){
	ok(false, msg);
}

このテストコードを実行すると、以下のようになります。

テストが実施されていません。テストコードのAssertionsの呼び出しはtest関数のコンテキストに対して非同期に実行されますが、test関数はその非同期処理の終了を待たずに終わってしまう為、テスト結果が反映されません。
そこで、test関数ではなく、asyncTest関数を使ってテストします。

asyncTest("db test2_async", 1, function() {
	startTransaction(
		function(t){
			t.executeSql('CREATE TABLE IF NOT EXISTS for_tests(id INTEGER PRIMARY KEY, note TEXT)');
			t.executeSql("SELECT * FROM for_tests", [], function(tx, rx){});
		},
		function(e){
			fail("must not failed.");
			start();
		},
		function(){
			ok(true, "success to access for_tests.");
			start();
		}
	);
});

test関数→asyncTest関数に切り替え、テスト完了後にstart関数を呼び出すようにしています。start関数を呼び出さないとテストが完了しないので、注意が必要です。
このテストコードを実行すると、

非同期で実行されたAssertionsの結果が反映されます。
上記はSQLの実行が正常に終了するケースをテストしました。SQLの実行が失敗するケースをテストしたい場合は、以下のようになります。

asyncTest("db test3", 1, function() {
	startTransaction(
		function(t){
			t.executeSql("SELECT * FROM dummy", [], function(tx, rx){});
		},
		function(e){
			ok(e, e.message);
			start();
		},
		function(){
			fail("must not succeed.");
			start();
		}
	);
});
まとめ

今回はQUnitを使って非同期で実行される関数(SQL-based database API)のテストコードを記述しテストしました。Web Workersについても、同じような形式でテストできると思います。

iPhone/iPod TouchのSafariでオフラインアプリケーション

以前iPod Touchを買ったのですが、普段ほとんど外に持ち出さず、自宅内のWebブラウザとして使ってました。が、ふと(今作っている)時間簿Webアプリケーションの入力インターフェースとして使えるのではないかと思い立ち、どうせやるならということでHTML5APIを使ってオフラインで動くようにできるか試してみました。

時間簿アプリケーションは単にその日の行動を時間ベースで記録してくだけのシンプルなものです。元々携帯向けのWebアプリケーションとして作るつもりだったのですが、最新に近い機種で動かしてみても通信が大分重く、直接スケジュール帳に記載する方が楽だったので、アプリケーションの方はちょっと放置気味でした。

まず、HTML5でオフラインアプリケーションを作成するには、以下のAPIを呼び出すことになると思います。

  • 必須
    • Offline Application Caching API
  • データをブラウザに保存する場合

今回はこれらのAPIの中で「Offline Application Caching API」と「SQL-based database API」の2つを使ってみました。幸いなことに、この2つのAPIiPod TouchSafari上で動作することが確認できました。

Offline Application Caching API

Webアプリケーションは通常、HTMLや画像ファイルをWebサーバからダウンロードします。オフラインで動かす為には、これらをブラウザ内に保持する必要があります。その仕組みが「Offline Application Caching API」で提供されます。

MANIFESTファイルを作成する

ダウンロードするファイルの一覧をMANIFESTファイルに記述します。今回はこのMANIFESTファイルのファイル名を「manifest.mf」とします。

CACHE MANIFEST
# Version 0.1
weekcalendar.html
/js/jquery.weekcalendar.js
/css/jquery.weekcalendar.css
http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/smoothness/jquery-ui.css
http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js
http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js
Webサーバ側で、MANIFESTファイル用のmime typeを設定する

今回はGAE/Jのサーバに配備するので、web.xmlに以下の記述を追加します。

<mime-mapping>
  <extension>mf</extension>
  <mime-type>text/cache-manifest</mime-type>
</mime-mapping>    
MANIFESTファイルを指定する

ダウンロードするHTMLファイルのHTMLタグのmanifest属性に、ブラウザに保存されるファイル一覧が記載されたMANIFESTファイル名を指定します。

<html manifest="manifest.mf">

以上で、初回のダウンロード以降、サーバが死んでいてもHTMLを表示することができます。

SQL-based database API

ブラウザ内のdatabaseにデータを保存し、更新、削除、およびデータの問い合わせを行うことができます。Firefox3.6はこのAPIをサポートしていないので、Chromeで動きを確認しました。

openDatabase

データベース名を指定して、databaseをオープンします。バージョンや容量を指定することができます。(最大5MB)
バージョンを見て、DBのスキーマ定義を更新する、という仕組みもあるようです。(あまりうまく使えなかったので、後日改めて調べてみます)

var db = openDatabase('lifecycle', '1.0', 'lifecycle', 5242880); 
insert

DB操作を行う場合は、transactionメソッドを呼び出します。transactionメソッドには、DB操作、DB操作失敗時の振る舞い、DB操作成功時の振る舞いの3つのハンドラを渡します。注意する点として、これらのハンドラは非同期で実行されます。ただ、所謂GUIスレッドかどうかを気にする必要はないらしく、これらのハンドラ内でwindow#alert等を呼び出すことができます。DML文はtransactionオブジェクトのexecuteSqlメソッドを使って発行します。

function saveEvent(calEvent){
  var db = openDatabase('lifecycle', '2.0', 'lifecycle', 5242880);
  db.transaction(
    function(t){
      var s = calEvent.start;
      t.executeSql('INSERT INTO cal_events VALUES(NULL, ?, ?, ?, ?, ?, ?, ?)', [s.getFullYear(), s.getMonth(), s.getDate(), s, calEvent.end, calEvent.title, calEvent.body], function(tx, rs){
        calEvent.id = rs.insertId;
      });
    },
    function(error){
      // DB操作失敗時の振る舞い
      alert("(saveEvent)db error:" + error);
    },
    function(){
      // DB操作成功時の振る舞い
      $('#calendar').weekCalendar("updateEvent", calEvent);
      $('#calendar').weekCalendar("removeUnsavedEvents");
    }
  );
}
update

発行するのがupdate文ということ以外、insertと同じです。

function updateEvent(calEvent){
  var db = openDatabase('lifecycle', '2.0', 'lifecycle', 5242880); 
  db.transaction(
    function(t){
      t.executeSql('UPDATE cal_events set start=?, end=?, title=?, body=? WHERE id=?', [calEvent.start, calEvent.end, calEvent.title, calEvent.body, calEvent.id]);
    },
    function(error){
      alert("(updateEvent)db error:" + error.message);
    },
    function(){
      $('#calendar').weekCalendar("updateEvent", calEvent);
      $('#calendar').weekCalendar("removeUnsavedEvents");
    }
  );
}
delete

発行するのがdelete文ということ以外、updateと同じです。

function deleteEvent(calEvent){
  var db = openDatabase('lifecycle', '2.0', 'lifecycle', 5242880);
  db.transaction(
    function(t){
      t.executeSql('DELETE FROM cal_events WHERE id=?', [calEvent.id]);
    },
    function(error){
      alert("(deleteEvent)db error:" + error.message);
    },
    function(){
    }
  );
}
select

select文については、問い合わせた結果をオブジェクトに格納する部分が入ってきます。下記のselect文発行も非同期となる為、selectした結果を以って次の処理に移行するには、DB操作成功時の振る舞いのハンドラ(db.transactionメソッド呼び出しの第3引数)に次の処理を記述することになります。

function loadEvents(newDate, handler){
  var db = openDatabase('lifecycle', '2.0', 'lifecycle', 5242880);
  var eventData = new Array();
  db.transaction(
    function(t){
      t.executeSql('SELECT id, year, month, day, start, end, title, body FROM cal_events WHERE year=? and month=? and day=?', [newDate.getFullYear(), newDate.getMonth(), newDate.getDate()], function(tx, rs){
        for (var i = 0; i < rs.rows.length; i++) {
          var row = rs.rows.item(i);
          eventData[i] = {"id": row.id, "year": row.year, "month": row.month, "day": row.day, "start": new Date(row.start), "end": new Date(row.end), "title": row.title, "body": row.body};
        }
      });
    },
    function(error){
      alert("(loadEvents)db error:" + error.message);
    },
    function(){
      handler(eventData);
    }
  );    
}
スキーマ定義

DB操作を行うページの最初に、スキーマ定義を行う必要があります。ただ、この操作も非同期になる為、呼び出すタイミングはちょっと難しいと考えています。今のところ、ページの最初のクエリ発行前に実行するようにしてますが、もっと適切なタイミングがあるんじゃないかな…。

t.executeSql('CREATE TABLE IF NOT EXISTS cal_events(id INTEGER PRIMARY KEY, year INTEGER, month INTEGER, day INTEGER, start TEXT, end TEXT, title TEXT, body TEXT)');

UI

時間簿をつけるインターフェースは、jquery-week-calendarというモジュールを使わせてもらいました。Google Calendarにかなり近いUIを提供してくれます。jQueryおよびjQuery-UIに依存しており、iPod TouchSafariで見ると動きが非常にモッサリしていますが、表示についてはPCのブラウザと同じように見えます。ただ、エントリを登録する際に、まず開始点をmouse downで指定し、その状態のままdrag→mouse upで終点を指定するようになっているのですが、iPod TouchSafariでdragするとスクロールの方が動いてしまう為、2回目のmouse downで終点を指定するようにしています。

http://github.com/masayuki038/jquery-week-calendar/blob/master/jquery.weekcalendar.js

おわりに

今回試しに作ってみた時間簿は、以下にあります(データを受けるサーバ側はありませんが)。ChromeiPhone/iPod Touchでアクセスしてください(iPhoneは持ってないので試してません)。エントリを操作するダイアログがなかなか表示されなかったり、ドロップダウンリストに時刻が設定されるまでに大分時間がかかります。気長に待ってください。また、デバッグが面倒なのでOffline Application Caching APIは使わないようにしています。

http://life-cycle.appspot.com/weekcalendar.html

アクセスするとデータベースを作成してしまうので、iPod Touchであればメニューから「設定」→「Safari」→「データベース」→「編集」で任意のデータベースを削除することができます。Chromeは「オプション」→「高度な設定」タブ→「コンテンツの設定」→「Cookieとその他のサイトのデータを表示」ボタン押下でドメイン毎にデータベースが表示されるので、任意のデータベースを選択して削除します。