2009年12月28日月曜日

App Engine JDO Tips

Google App Engineのデータストアを利用する場合、公式にはJDOおよびJPAが提供されています。 過去にJDOからSlim3 Datastoreに乗り換えるようなことも書いていますが、最近のJDOについて少し書いてみたいと思います。

今回は、Google App Engine Blog: JPA/JDO Java Persistence Tips - The Year In Reviewで紹介されている内容をもとに話を進めます。JDOの使い方自体に詳しくなくてもある程度読めるように書いたつもりですが、何かあればコメントください。

相互参照する one-to-many の関係 (episode 1, 3)

JDOを利用すると、相互参照を行うone-to-manyの関係を簡単に記述できます。

下記はParentクラスのオブジェクトが、複数のChildクラスのオブジェクトを保有する例です。

package com.example;

import java.util.*;
import javax.jdo.annotations.*;

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

    @PrimaryKey
    private String keyName;

    // Childクラスのparentプロパティが自身への参照を持つ
    @Persistent(mappedBy = "parent")
    @Element(dependent = "true")
    private List<Child> children = new ArrayList<Child>();

    // ...
}
package com.example;

import javax.jdo.annotations.*;
import com.google.appengine.api.datastore.Key;

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

    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    private String value;

    // Parentへの参照を保持して相互参照させる
    private Parent parent;

    // ...
}

上記のように書いた場合、ParentクラスのchildrenにChildクラスのオブジェクトを設定するだけで、自動的にそれぞれのChildクラスのparentフィールドにはParentクラスのオブジェクトが設定されます。

また、上記のような owned-relationship と呼ばれる関係は、ParentをルートとしたEntity Groupを自動的に構成します。このため、Childクラスのオブジェクトを持つParentはトランザクション処理による変更が可能です。

このEntity GroupはKeyのパスで表現されます。Childクラスのエンティティは親を持つことになるので、@PrimaryKeyの指定を、(String, Longなどではなく)Key型のフィールドに対してする必要があります。

なお現在の実装では、上記のようなParentを保存する際にchildrenというプロパティはデータストア上に作成されません。そのかわり、Childを保存する際にchildren_INTEGER_IDXというプロパティがデータストア上に作成され、Parentから見たChildの位置(children内のインデックス)が保持されます。

このため、過去のParentと同じキーを持つParentオブジェクトを生成し、childrenを操作してしまうと過去のchildrenの内容がマージされるという残念な状況になります。つまり、2つのChildオブジェクトを持つParentを2回作成すると、あとでParentを取得した際に4つのChildオブジェクトを持っていたりします。

そのため、上書きする場合でも一度取得したオブジェクトを利用します。

PersistentManager pm = ...;

Parent parent;
try {
    // すでに存在したら再利用
    parent = pm.getObjectById(Parent.class, "parent");
}
catch (JDOObjectNotFoundException e) {
    // 存在しなければ新しく作る
    parent = new Parent();
    parent.setKeyName("parent");
    pm.makePersistent(parent);
}

Child child1 = new Child();
Child child2 = new Child();

child1.setValue("child-1");
child2.setValue("child-2");

// ChildオブジェクトをParent.childrenに追加
List<Child> children = parent.getChildren();
children.add(child1);
children.add(child2);

JDO(とJPA)はtransparent persistence (透過的な永続化)を提供するためのライブラリです。上記のように新しく作成したオブジェクトはすぐにPersistentManager.makePersistent()によって永続化対象に指定し、そのオブジェクトを使い続けるスタイルのほうがやりやすいかもしれません。

キーのみを取得するクエリ (episode 4)

キーのみを取得するクエリが利用可能になっています。 やりかたは、JDOQLを書く際に"SELECT <キーを保持するフィールド名> FROM ..."のように、@PrimaryKeyの指定をしたフィールドを指定するだけです。

PersistentManager pm = ...;

// SELECT <キーを保持するフィールド名> FROM ... でキーのみを取得
Query query = pm.newQuery("SELECT key FROM " + Hoge.class.getName());
List<Key> keys = (List<Key>) query.execute();
...

App Engineのクエリは、(1)キーの一覧を取得、(2)キーからエンティティを取得、の順で結果を取得しているようなので、キーのみを取り出す場合には上記のように書くことで、(2)の処理をスキップすることができます。

エンティティの取得とモデルへのマッピングはそれなりにコストが高い操作ですので、キーだけが必要である場合には便利です。特に検索のみに利用するエンティティなどは、キーに検索結果としての有効な情報を凝縮することで、不要な情報をロードしなくて済みます。

シリアライズしたオブジェクトの格納 (episode 5)

App Engineのデータストアに利用できるデータの種類は限られていますが、JDOを利用すると自動的にオブジェクトのシリアライズを行ってBlobとして永続化できます (Blob StoreのBlobとは別物です)。

package com.example;

import java.util.Map;

import javax.jdo.annotations.*;

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class ObjectHolder {

    @PrimaryKey
    private String keyName;

    // MapはJDOで扱えないので、serializedを指定する
    @Persistent(serialized = "true")
    private Map<String, String> map;

    // ...
}

@Versionを利用したLong Running Transaction (episode 6)

App Engineのデータストアにはトランザクション処理を行うための仕組みがあらかじめ組み込まれていますが、リクエストごとにコミットまたはロールバックしなければならないため、画面をまたぐような長期間の並行性制御(Long Running Transaction)はソフトウェアで指示する必要があります。 JDOにはこれを自動的に行う仕組みが備わっています。

まず、JDO向けのモデルクラスに@Versionというアノテーションを付与します。

package com.example;

import java.io.Serializable;

import javax.jdo.annotations.*;

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
@Version
public class Versioned implements Serializable {

    private static final long serialVersionUID = 1L;

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

    private String value;

    // ...
}

このとき、detachable = "true"という指定と、implements Serializableを併せて指定しておきました。前者はオブジェクトをJDOの管轄から外してスナップショットを作成する(デタッチ)のための指定で、後者はセッションに同オブジェクトを保持しておくための指定です。

Long Running Transactionを開始する場合には、下記のように@Versionを付与したモデルにPersistenceManager.detachCopy()を実行して記憶しておきます。

PersistenceManager pm = ...;
Versioned data = ...;
data.setValue("Hello!");
pm.makePersistent(data);
// 現在の状態のスナップショットを作成
Versioned snapshot = pm.detachCopy(data);
// snapshotをセッションなどに保存

それぞれの画面で行われた変更は、全て上記のデタッチしたsnapshotに対して行います。 全ての操作が終わったら、最後にデータストアに書き戻します。

PersistenceManager pm = ...;
Versioned snapshot = ...;
pm.makePersistent(snapshot);

上記のように、JDOの@Versionを利用したLong Running Transactionでは、明示的にJDOのトランザクションを開始する必要はありません。というより、明示的にJDOのトランザクションを利用すると、@Versionによる並行性制御が無視されます

これはそれなりに有名なことらしいのですが、知らないと非常に不可解な動きをします。このエントリを書いてる最中に同じようにはまって、様々な方に助けていただきました(いつもありがとうございます)。

このLong Running Transactionは、PersistenceManager.close()を実行した際に実際の反映が行われます。もし、スナップショットを書き戻すまでに外部から(本体の)モデルが変更されている場合、close()が実行されるタイミングでJDOExceptionがスローされていました。

このPersistenceManager.close()の中では、おそらく下記のような操作が実行されています。

PersistenceManager pm = ...;
SomeModel newData = ...;
...
Transaction tx = pm.currentTransaction();
try {
  // 現在のトランザクションを利用して、同じIDのオブジェクトを取り出す
  SomeModel oldData = pm.getObjectById(SomeModel.class, newData.getId());
  // サーバから取り出したバージョンと、手元のバージョンを比較
  // 他の人が変更していたらサーバから取り出したバージョンがずれているはず
  if (oldData.getVersion() != newData.getVersion()) {
    // 実際にはJDOExceptionがスローされる
    throw new ConcurrentModificationException();
  }
  // バージョンチェックと書き込みは同一トランザクションで
  // 処理するのでアトミックに行われる
  pm.makePersistent(newData);
  tx.commit();
}
catch () {
  if (tx.isActive()) {
    tx.rollback();
  }
}

より詳しい考察は、App Engineでバージョンによる楽観的排他制御 - ひがやすを blogなどでも行われています。

はまった点

@Versionの調査をしていてひどくはまっていた点を紹介します。

PersistenceManager pm = ...;
Versioned data = ...;
// (a) 先にmakePersistent()で永続化の対象に指定
pm.makePersistent(data);
// (b) そのあとにモデルを変更
data.setValue("Hello!");

// (c) 現在の状態のスナップショットを作成
Versioned snapshot = pm.detachCopy(data);
...

// (d) 最後にclose()
pm.close()

上記のように、最初にPersistenceManager.makePersistent()を実行(a)し、それ以降にモデルオブジェクトを変更した(b)場合、最後のPersistenceManager.close()の瞬間(d)にモデルオブジェクトのバージョンが変更されます。このため、(b)と(d)の間で行われたスナップショットの作成(c)は、(d)で変更されるバージョンの情報を反映していません(aの時点のバージョンを利用します)。

このようなスナップショットの書き戻しは常に失敗します。これは(d)の時点で変更されたバージョンが「Long Runnning Transactionの外部で変更が行われた」というように見なされてしまうためです。

makePersistent()detachCopy()を同一リクエストで行う場合、最初に紹介したように「すべて変更してからmakePersistent()を実行する」というようにした方がよさそうです。

PersistenceManager pm = ...;
Versioned data = ...;
// 先に変更してから
data.setValue("Hello!");
// makePersistent()で永続化の対象に指定
pm.makePersistent(data);
// すると、detachCopy()はmakePersistent()時点のバージョンと一致
Versioned snapshot = pm.detachCopy(data);

transparent persistence (透過的な永続化)とさっき書いたばかりなのですが、App Engineではどうもそう簡単にいかないようです。

インデックスを作成しないプロパティ (episode 7)

データストアにエンティティを格納した場合、基本的にはそれぞれのプロパティに対するインデックスが作成されます(TextやBlobなどは対象外)。しかしこのインデックス作成はCPUやデータストア領域などのリソースを消費するため、不要の場合には抑制させることもできます。

具体的には、インデックスの作成を抑制したいプロパティに対して、@Extension(vendorName="datanucleus", key="gae.unindexed", value="true")という長い記述の注釈を付与するだけです。

package com.example;

import javax.jdo.annotations.*;

import com.google.appengine.api.datastore.Link;

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class BlogEntry {

    @PrimaryKey
    private String keyName;

    // title はインデックスを作る
    private String title;

    // permalink はインデックスを作らない
    @Persistent
    @Extension(vendorName = "datanucleus", key = "gae.unindexed", value="true")
    private Link permalink;

    // ...
}

上記のモデルに対してpermalinkをフィルタに指定するクエリを実行した場合、クエリは失敗するのではなく単に結果を何も返しません

なお、@Extensionにはのほかにorg.datanucleus.jpa.annotations.Extensionという注釈型も存在します。後者はJPA用なので注意が必要です (Enhanceの際にエラーが表示されます)。

インデックスの作成については、How Entities and Indexes are Stored - Google App Engine - Google Code (英語)が参考になると思います。

はまった点

今回のはまりは下記のような感じです。

package com.example;

import javax.jdo.annotations.*;

import com.google.appengine.api.datastore.Link;

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

    @PrimaryKey
    private String keyName;

    private String title;

    // @Persistent を省略
    @Extension(vendorName = "datanucleus", key = "gae.unindexed", value="true")
    private Link permalink;

    // ...
}

プロパティに@Persistentを省略した場合、永続化対象にはなるけど"unindexed"の対象にならないという残念な挙動をします。このエントリでは基本的に@Persistentを省略していますが、可能な限り明記したほうがいいのかもしれません。

IN, != 演算子の追加 (episode 9)

SDK 1.2.8 Release Notesで語られなかったことでも書きましたが、SDK 1.2.8からはIN, !=という演算子が利用できるようになりました。下記のようにJDOQLの中で指定します。

Query query = pm.newQuery(Employee.class);
// 年齢が20歳でない
query.setFilter("age != 20");
List<Employee> results = (List<Employee>) query.execute();

...
// 特別な年齢の一覧
List<Integer> anniversary = Arrays.asList(20, 40, 60);
Query query = pm.newQuery(Employee.class);
// 年齢が「特別な年齢の一覧」に含まれる
query.setFilter(":anniversary.contains(age)");
List<Employee> results = (List<Employee>) query.execute(anniversary);

...

その他、気付いたこと

Google App Engine Blog: JPA/JDO Java Persistence Tips - The Year In Reviewで紹介されなかったものの、App Engine for Javaのリリース当初から変わっていたことについて、いくつか紹介します。

PersistenceManagerFactoryの初期化が高速化

Slim3 Datastoreに乗り換えるという記事では、JDOをやめた理由の一つに「初期化が遅い」ということを挙げました。 しかし、SDK 1.2.8より利用可能になった<precompilation-enabled>true</precompilation-enabled>という設定により、この初期化のコストをかなり削減できるようです。

このプリコンパイル設定をonにしたりoffにしたりして、それぞれの初回アクセス時の負荷を測定してみました。いずれもPersistenceManagerFactoryを初期化し、pmf.getPersistenceManager()を起動するだけの簡単なプログラムです。

プリコンパイル設定なし プリコンパイル設定あり 削減された時間
1回目 4433cpu_msec 2450cpu_msec 1983cpu_msec
2回目 4394cpu_msec 2741cpu_msec 1653cpu_msec
3回目 4588cpu_msec 2585cpu_msec 2003cpu_msec
4回目 4725cpu_msec 2741cpu_msec 1984cpu_msec
5回目 4510cpu_msec 2605cpu_msec 1905cpu_msec
平均 4530cpu_msec 2624cpu_msec 1906cpu_msec

上記のように測定回数は5回と多少心もとないですが、いずれも2000cpu_msec近い時間がプリコンパイル設定を有効にすることで削減されています。 体感的にも応答に5秒かかっていたものが3秒くらいになっている感じがしますので、上記のプリコンパイル設定は有効にする価値がありそうです。

コアデータタイプがデフォルトフェッチグループに追加

過去のJDOでは、次のようなモデルクラスを作成した際に、linkフィールドやmailsフィールドにはデフォルトフェッチグループの指定が必要でした。 デフォルトフェッチグループとは、特別な指定をせずにエンティティをデータストアから読み出した際に、自動的にフェッチされるプロパティ群のことです。 プリミティブ型、String型、Date型などはJDOの定義でデフォルトフェッチグループに含まれていましたが、App Engine特有のクラス(Key, Link, Email)などはそれらに含まれていなかったため、わざわざフェッチグループの指定をしないとデータストアからロードした際にnullのまま放置されていました。

package com.example;

import java.util.List;
import javax.jdo.annotations.*;
import com.google.appengine.api.datastore.*;

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class CoreDataType {

    @PrimaryKey
    private String keyName;

    private Link link;

    private List<Email> mails;

    // ...
}

SDK 1.2.8 (datanucleus-appengine 1.0.4)からは com.google.appengine.api.datastore.* にある組み込みデータ型や、それらのコレクションについてもについてもデフォルトフェッチグループに追加されるようになったので、上記のモデルクラスはフェッチグループの指定なしに利用できます。

2009年12月18日金曜日

Google AppsアカウントでGAEが利用可能に。

気づくと、GAEのTOPページにこのような記述が増えていました。
Google Apps アカウントで Google App Engine にログインする場合は、次の URL を使用してください。 https://appengine.google.com/a/<YOURDOMAIN.COM>/
Google Appsアカウントで試してみました。 開発者として招待してみると、Googleアカウントで利用する際と違い、招待メールの内容にGoogleアカウントの承認リンクとは別にGoogle Appsアカウント用の承認リンクが追加されています。
To sign in with your Google Account and accept or decline this invitation, simply follow this link:
<Googleアカウント用承認リンク>
OR, if <メールアドレス> is a Google Apps Account, you can sign in and accept or decline this invitation here:
<Google Appsアカウント用承認リンク>
Google Appsを導入していて、これからGAEでの開発やデプロイして利用等には、アカウントの統一という面で利用する価値はありますね。

2009年12月17日木曜日

Google App Engine Downtime Notify でGAEのダウンタイムを知る。

 GAEを利用したアプリケーション開発・保守に関わっていらっしゃる方であれば、GAEへのデプロイ、テスト、運用時の障害というのは気にかかるところです。
 Google App Engine Downtime Notify ではメンテナンスによる事前のスケジュールも含めて、Googleからのダウンタイムの通知が行われており、このグループのメーリングリストに参加すると、Googleからの通知専用のメールが配信されます。
 配信時刻とダウンタイムの時間が太平洋標準時のため、日本時間の16時間前となることと、メンテナンスの予定の通知以外は、ダウン後のダウンタイムの通知になってしまうことについては注意が必要です。
※過去も投稿の時刻を読む限り、サマータイム時についても、そのままの時間で配信されているようです。
 最近では、12月3日の9:33(太平洋標準の12月2日16:33)に「Index definition update issue today」という投稿が配信されています。
「太平洋標準時でおよそ12:55からPM3:55の間に、App EngineのデータストアのIndex definitionsを変更しました。それによるアプリケーションやユーザーへの影響はありませんでしたが、開発者がアプリケーションのデプロイや、Index definitionsを更新しようとするとすると"500 エラー"が発生していたでしょう。この問題は現在、解決され、開発者はアプリケーションのデプロイやIndexの更新が出来る筈です。」
 と、原因や現在の状況をお知らせしてくれますので、何か(無いに越したことはありませんが)と助かるかと思います。GAEのメール受信機能を利用して障害報告のお知らせ画面を作成されている方も居るんじゃないでしょうか。
 また、今まさに障害かな?と感じた場合はGAE/Jの日本海外のフォーラム、またはGAEのフォーラムを覗いてみると、他の開発者が書き込んでいるかもしれません。

2009年12月16日水曜日

GData APIの認証に触れてみる

Webアプリケーションを開発する際に、Googleを始めとした様々なWebのサービスを利用する機能を実装することが多くなってきたのではないでしょうか。その際に必要なAPIが各サービスプロバイダから公開されているケースが多く、手軽に試すことが可能です。
 GoogleのGData APIでは、Java、JavaScript、.Net、PHP、Python、Objective-Cといった様々な言語で開発するアプリケーションから、Googleのサービスのデータを利用出来ます。
http://code.google.com/intl/ja/apis/gdata/docs/client-libraries.html
 業務アプリケーションで、Google Apps上のGoogle Calendarと連携を行うといったケースは十分に考えられますし、Google Docsのデータを用いて最近、話題になっているGoogle Visualization APIを利用しグラフを表示するといった面白そうな事例も今後、増えてくるのではないでしょうか。
 一般にこうしたアプリケーションを開発する際に、各サービスの認証を考慮することは避けては通れません。
 例えば、開発対象のアプリケーションを利用してユーザーがGoogle Calendarの自分の予定を閲覧する、あるいはBloggerの記事を投稿するといったことを実現する場合、ユーザーに自分のアカウントを認証してもらう必要があります。
 GData APIでは認証する際に四つの方法が用意されています。
  • Authentication for Web Applications: AuthSub
  • Authentication for Web Applications: OAuth
  • Authentication for Installed Applications: ClientLogin
  • Authentication for Gadgets
 上記リンク先の利用目的で分類されているように、WebアプリケーションではAuthSub、OAuth、インストールを要するデスクトップアプリケーション等ではClientLogin、ガジェットからはOAuth Proxyを用いて認証を行うことが示されています。
 ClientLoginではアプリケーション上でユーザーにGoogleアカウントの情報(ユーザーIDとパスワード)を登録してもらい、それを利用して認証を行います。
 Webアプリケーションの場合、ClientLoginのように直接、アプリケーション上でユーザーIDとパスワードを登録してもらうのではなく、別の方法が推奨されています。Google独自のAuthSubと、API認可プロトコルの標準仕様てして策定されているOAuthです。
 これらの認証方法はユーザーから見ると、特定の第三者となるアプリケーション側にパスワードを渡さないことから、セキュリティ上のリスクが低くなると考えられます。
 私の場合、Googleのサービスの認証以前に認証技術そのものが、まだまだ判っていないため、ドキュメントやWebの情報を読むのにも一苦労と勘違いの連続です(^^;こちらの記事は大変、参考になりました。
 AuthSubでは、アプリケーションがGoogleアカウントのサイト上へ認証を要求します。ユーザーがGoogleアカウントのサイト上で認証を行い、認証に成功すると、次に、アプリケーションがユーザーのアカウントのデータにアクセスすることを承認するかどうかを確認します。そして、ユーザーが承認した場合、アプリケーションへリダイレクトされます。この際にパラメーターにGoogleアカウントにより承認された認証トークンが付与されており、これを利用することでアプリケーションはユーザーのデータを参照することが出来ます。
 ClientLogin以外の認証を実現する場合、理解もさることながら実装の手順も複雑ですが、Google Code上にサンプルがあるので非常に助かります。
 AuthSubによる認証のサンプルが用意されています。このサンプルではGoogle Calendarのデータを参照します。
 ※サンプルを実行する場合、以下のjarファイルが必要ですので、パスを通す必要があります。
  • gdata-appsforyourdomain-1.0.jar
  • gdata-base-1.0.jar
  • gdata-calendar-1.0.jar
  • gdata-client-1.0.jar
  • gdata-core-1.0.jar
 ダウンロードはこちらから可能です。gdata-samples.java-1.40.1.zip を選択すると、AuthSubのサンプルも含めたファイルとjarファイルが入手出来ます。 gdata-src.java-1.40.1.zip の中身はソースとjarファイルです。
 ちなみにリリースノートによると、1.40からGoogle Translator Tookkit Data API が利用出来るようになったとのことなので、こちらも楽しみです。
 注意点として、このサンプルをEclipse上でGAE/Jのアプリケーションとして試す場合は、セッションを有効にするため、WEB-INF以下の appengine-web.xml に下記の記述を追加する必要があります。
<sessions-enabled>true</sessions-enabled>
 サンプルの実行画面は下記となります。
 サンプルの sample/authsub/web以下にある index.jsp が sample/authsub/src/LoginServlet を呼び出し、認証がされていなければGoogleのログインページへ遷移され、アプリケーションからカレンダーのデータを参照するかどうかの確認画面が表示されます。
 認証が既に行われていれば、sample/authsub/web以下の main.jsp が表示されますので、認証されたアカウントのGoogle Calendarのデータが参照出来る筈です。
 main.jsp 内ではJavaScriptを利用して、Google Calendarのデータを参照しています。JavaによるGoogle Calendarを利用したサンプルもこちらで見ることが出来ます。認証方法がClientLoginになっていますが、カレンダーへのデータの登録、削除、参照が実装されているようなので、GData APIを利用する参考になるかと思います。ダウンロード可能なサンプルは上記を含め、他のサービスの利用例が確認出来ます。
 手前味噌になりますが、AuthSub認証によるGData APIの利用については、弊社の書籍でP.269からP.276にソースと解説が掲載されています。
 
 また、Webアプリケーションで推奨されるGDataへの認証方法として、もう一つOAuthがあります。OAuthではAuthSubより複雑なやり取りになります。
  • アプリケーション側はGoogleアカウントのサイト上へ認証を要求する前に、リクエストトークンと呼ばれるキーを要求します。この時点で、Googleから渡されるリクエストトークンは未承認の状態です。
  • アプリケーションは未承認のリクエストトークンをパラメータに付与し、Googleアカウントのサイト上へ認証を要求します。
  • Googleアカウントのサイト上で、ユーザーがGoogleアカウントのサイト上で認証を行います。
  • アカウントの認証に成功すると、ユーザーはアプリケーションがアカウントのデータにアクセスすることを承認するかどうかをGoogleアカウントのサイト上で決定します。ユーザーが承認した場合、アプリケーションへリダイレクトされます。この際、パラメーターに認証が成功したことを示すGoogleにより承認されたリクエストトークンが付与されます。
  • アプリケーションは承認されたリクエストトークンを用いてアクセストークンというキーと交換することを要求します。Googleにより交換されたアクセストークンが発行されます。
  • アプリケーションは受け取ったアクセストークンを利用することで、ユーザーのデータを参照することが出来ます。
 ※OAuthの解説によると、データを提供するサービスをサービス・プロバイダー、サービスを利用するアプリケーションをコンシューマと位置づけています。gihyo.jpさんのこちらの記事がとても参考になります。
 この手順の中で、アプリケーション毎のコンシューマーキー、コンシューマーシークレットというキーに加えて、アプリケーションからGoogleへのリクエストが署名されている必要があります。こちらに詳細が記されています。
 また、OAuth_Playgroundというサイトでは、Googleのサービスを利用するリクエストトークンの生成、承認、アクセストークンの取得といったプログラム上で行うべき手順を体験出来ますので、ドキュメントと併せて利用すると参考になるかと思います。
 Google Code上ではOAuthによるGData API利用のサンプルがありますので、試してみてはいかがでしょうか。また、GAE/J上でOAuthによるGData API利用のサンプルがこちらでデモされています。ソースはGoogle Code上のこちらとなります。
 最後に追記しますと、GAEで開発を行う場合、特にGoogle Appsとの連携が要件としてある場合は、ユーザーのロールと認証が複数存在するので、頭の中で整理をしておかないと混乱を招くかもしれません。当Blogのこちらの記事を読まれると理解の一助になるのではないかかと思います。
 (12/17 PM3:15 追記) Twitter上でご指摘を頂いた通り、上記のAuthSubのサンプルをGAE/J上で実行される際に、appengine-web.xml に設定を追加する必要があります。こちら、または、こちらをご確認下さい。どうもありがとうございました。

2009年12月15日火曜日

AppEngine1.3.0 Blobstore API入門

AppEngine1.3.0で新しいAPIとして50MBまでのファイルを登録することが出来るBlobstore APIが公開されました。今までは10MBのアップロード、Datastoreには1MBずつしか登録出来なかったので、これは非常に期待がもてるAPIです。折角なので公式ドキュメントを参考に一通り試してみました。

Blobstoreとは?

BlobstoreはDatastoreの厳しいサイズ制限を超えて50MBまでのファイルを登録し、利用することができるサービスです。(Billingを有効にしていないと利用することが出来ません。)このサービスは、Datastoreの様に直接アクセスして利用するのではなく、サービスの受け口に対してWebフォームや、HTTP POSTメソッドによって行われます。従って、アップロード/ダウンロードにかかる処理時間はAppEngineの30秒制限の影響受けることなく利用することが可能なようです。但し、これは検証してないので正確には言えませんが、アプリケーションの中で利用するケースには当てはまらないと思います。

早速試してみる

公式ドキュメントのサンプルコードをそっくりそのまま試してみました。まずはアップロード用のフォームをもったJSPから。
<%@ page language="java" contentType="text/html; charset=UTF-8"
 pageEncoding="UTF-8"%>
<%@ page import="com.google.appengine.api.blobstore.*" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<%
BlobstoreService service = BlobstoreServiceFactory.getBlobstoreService();
String uploadUrl = service.createUploadUrl("/upload");
%>
<form action="<%=uploadUrl%>" method="post" enctype="multipart/form-data">
    <input type="file" name="myFile">
    <input type="submit" value="Submit">
</form>
</body>
</html>
ここではformのactionに対して、BlobstoreService#createUploadUrl(String)を使ってアップロード先となるURLを生成して指定しています。データのアップロードはここで生成されたURLに対して行われます。登録時に必要なその他の処理は、引数で指定している"/upload"にマッピングされるサーブレットなどで行います。それでは次に、"/upload"にマッピングされるサーブレットを見てみましょう。
public class Upload extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        BlobstoreService service = BlobstoreServiceFactory.getBlobstoreService();
        Map<String, BlobKey> blobs = service.getUploadedBlobs(req);
        BlobKey blobKey = blobs.get("myFile");
        if (blobKey == null) {
            resp.sendRedirect("/");
        }
        else {
            resp.sendRedirect("/serve?blob-key=" + blobKey.getKeyString());
        }
    }
}

このサーブレットでは、リクエストからアップロードされたデータに対するキーとなるBlobKeyを取り出し処理を行います。実はこのサーブレットが呼び出される前に既にデータのアップロードは完了しています。従ってここでの処理は登録された後に行われます。アップロードされたファイルの妥当性のチェックを行い、削除するなどの処理を行う必要があります。 最後に、上記のUpload.javaでは""/serve?blob-key="+ blobKey.getKeyString()"に対してアクセスし、アップロードしたファイルをダウンロードするサーブレットにリダイレクトしているので、こちらも見てみましょう。
public class Serve extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
            IOException {
        BlobstoreService service = BlobstoreServiceFactory.getBlobstoreService();
        BlobKey blobKey = new BlobKey(req.getParameter("blob-key"));
        service.serve(blobKey, resp);
    }
}
名前が若干ややこしくて使う際にちょっと疑問に思いましたが、BlobstoreService#serve(BlobKey, HttpServletResponse)メソッドを利用して、レスポンスにBlobデータを"Serve"することが出来るようです。ここは想像なのですが、30秒制限の影響を受けることなく利用するこが出来るように、レスポンスに書き出す処理は直接行うのではなく、アップロード時と同じように間接的に処理されているはずです。

アップロードしたファイル情報の取得

登録されたデータに関する情報は、下記のようなコードで確認することができます。
BlobInfoFactory factory = new BlobInfoFactory();
BlobInfo blobInfo = factory.loadBlobInfo(blobKey);
BlobKey key = blobInfo.getBlobKey();
String contentType = blobInfo.getContentType();
Date creation = blobInfo.getCreation();
String filename = blobInfo.getFilename();
long size = blobInfo.getSize();
BlobInfoFactoryにはこの他にqueryBlobInfos()メソッド、queryBlobInfosAfter(previousBlob)といったメソッドで指定のBlobKey以降のものを取得することが出来るようです。現時点ではBlobInfoFactoryからはその他のプロパティに対するクエリをかけることは出来ない様です。しかし、BlobInfoFactoryの定数にカインド名やプロパティ名が定義されているのでそれらを使って下記のように取得することは可能です。
DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();
for (Entity entity : datastoreService.prepare(new Query(BlobInfoFactory.KIND))
    .asIterable()) {
    BlobInfo info = factory.createBlobInfo(entity);
    // 処理
}

アップロードしたファイルに対する関連

また、エンティティに紐付けて利用するケースもあるかと思います。その際BlobKeyがコアタイプとして利用することができるので、該当のエンティティのプロパティに持たせてあげることによって実現出来ます。試しに登録したほんとに簡単なサンプルが以下になります。
Entity entity = new Entity(KeyFactory.createKey("test", 1));
entity.setProperty("blobKey", new BlobKey("対象のblobKey"));
DatastoreService service = DatastoreServiceFactory.getDatastoreService();
service.put(entity);
このエンティティをDatastore Viewerで見てみると以下のように見えます。

view blobなんていうリンクがあるので、こちらをクリックしてみると、以下のようにイメージが表示されるようです。


まとめ

大容量のファイルを扱えるようになったことは非常に喜ばしいことですが、少なくとも現状ではこれらのデータは静的リソースとしての扱いにとどまる程度と思われます。アプリケーションが利用するファイルとして扱うのは現実的ではありません。(URLFetchする以外に方法はなさそうなので)とはいえ、このBlobstore APIによって動画、写真、音楽ファイルなどの利用が可能になるのは大きいでしょう。

Images APIとの連携(追記)

コメント欄にてtmatsuoさんに教えていただきました。ありがとうございます。
公式のImages APIのドキュメントがこっそり更新されていたようです。これによると、以下の様なコードでBlobstore上の画像ファイルを加工する事が出来るようです。但し、Images APIの制限である1MBまでの制約は残るので用途はドキュメントにもあるようにサムネイルの作成といったところでしょうか。
BlobKey blobKey;  // ...

ImagesService imagesService = ImagesServiceFactory.getImagesService();

Image oldImage = ImagesServiceFactory.makeImageFromBlob(blobKey);
Transform resize = ImagesServiceFactory.makeResize(200, 300);

Image newImage = imagesService.applyTransform(resize, oldImage);

byte[] newImageData = newImage.getImageData();

App Engine SDK 1.3.0 (overview)

先ほどApp Engine SDK 1.3.0がリリースされました。 新機能について簡単に紹介します。

Blobstoreサービス

今回のアップデートの最大の目玉は、Blobstore APIが追加された点です。

これまではApp EngineのDatastoreを利用していましたが、これに格納できるそれぞれのデータは1MBという厳しい上限があったため、巨大なデータを格納する用途には向いていませんでした(巨大なデータを格納するには、データを分割したり、他のサービスと連携したりしていました)。 今回追加されたBlobstoreは巨大なデータ(Binary Large OBject)を格納するためのAPIで、50MBまでのデータを取り扱えるようです。

ただし、APIを見る限りでは、ファイルの内容を直接アプリケーション内で操作する用途ではなく、現時点ではアップロードされたファイルの保存と、そのファイルをそのままクライアントに返す機能だけを提供しているようです。動的に追加削除ができるリソースファイルのような扱いでしょうか。

なお、このBlobstoreサービスは、現時点では実験的なサービスとして提供されています。また、利用にはBillingの設定が必要であるようです。細かい使い方などは、追って紹介していく予定です。

また、この変更に合わせてアプリケーションの管理コンソールに「Blob Viewer」という項目が追加されています。アップロードされたファイル名やファイルサイズ、更新日時などが確認でき、手動で削除することもできるようになっています。

Blobstoreの詳しい使い方については、こちらのエントリを参照してください。

動的言語の高速化

Javaはいくつかの動的言語をサポートしていて、RubyやGroovyなどJava以外の言語で書かれたプログラムをApp Engine上のJava VMで動作させることができます。 これらの動的言語はJavaのリフレクションAPIを多用するため、どうしてもパフォーマンスに多少難があるという問題がありますが、今回のアップデートでこのリフレクション周りの処理が改善されたようです。

公式の発表によると、今回の改善でリフレクションに関する処理が最大で10倍程度速くなるそうです。 もしかすると、いくつかのリフレクションを利用しているフレームワークも高速化されているかもしれません。

システムプロパティの追加

現在稼働しているアプリケーションの状態を知るために、便利なシステムプロパティが追加されています。

// アプリケーションのバージョン (appengine-web.xmlで指定したもの)
String version = System.getProperty("com.google.appengine.runtime.version");

// 実行環境 ("Production", "Development")
String environment = System.getProperty("com.google.appengine.runtime.environment");

これまでは、実行環境によって動作を変えるときには少々トリッキーなコードを書いていましたが、上記のプロパティ情報を確認するだけで判別できるようになりました。

2009年12月14日月曜日

SDK 1.2.8 Release Notesで語られなかったこと

先日App Engine SDK 1.2.8 for Javaがリリースされました公式のリリースノートに記載されていない非常に重要な機能追加がありますので、ここで紹介します。基本的にすべてデータストアのお話です。しかもLow-Level API。

ご指摘いただきまして、ミスリードしそうな個所について修正しました。今後もわかったことがあれば追記していく予定です。

プロダクション環境でカーソルが予想外の動きをしました。このため、開発環境と両方で動くコードに更新しました

カーソルの追加

これまでデータストアでページング処理を行う場合には、ページング用のプロパティを用意したり、ページング用のインデックスを用意したりと、様々な力技が提案されてきました。 しかし、今回のアップデートでCursorという「クエリの現在位置を覚えておくオブジェクト」がひっそりと追加されました。これを利用すると簡単にページング処理が行えますので、簡単に紹介します。

カーソルの取得

現在位置のカーソルを取得するには、PreparedQueryasQueryResult*()というメソッドを利用します。 このメソッドはasIterator(), asIterable(), asList()などと似た動作を行いますが、返されるオブジェクトにはいずれもgetCursor()というメソッドが追加されています。

こんな感じで使います。

Query query = new Query("Hoge");
DatastoreService ds = DatastoreServiceFactory.getDatastoreService();

// asIterator() -> asQueryResultIterator() にして、
// 取得したいエンティティ数 + 1のリミットを指定
QueryResultIterator<Entity> iter =
    ds.prepare(query).asQueryResultIterator(
        FetchOptions.Builder.withLimit(10 + 1));

// 10件分だけ取得
for (int i = 0; i < 10 && iter.hasNext(); i++) {
    Entity entity = iter.next();
    // ...
}

// ここまでの結果をカーソルにする
Cursor cursor = iter.getCursor();
if (cursor == null) {
    // プロダクション環境では、末尾に達しているとnullになる模様
}

// カーソルはBase64エンコードされた文字列にもできる
String page = cursor.toWebSafeString();
// ...

ポイントは、リミットをあらかじめ指定しておくことです。 リミットに達した後に QueryResult*.getCursor()することで、リミットで指定した位置のカーソルを記憶しておいてくれます。 (2009/12/14時点のプロダクション環境では、Limitの末尾に達するとgetCursor()nullを返すようです)。上記のように、「取得したい件数 + 1」をリミットに指定して、イテレータで指定の回数だけフェッチするのがよさそうです。また、Iterator.hasNext()を実行してもカーソルは変化せず、Iterator.next()を実行するとひとつ分だけカーソル位置が変化するようです。イテレータ以外でどのように動くかはまだ追いかけきれていません。

カーソルの復元

先ほど作成したカーソルをもとに、そのカーソルの続きを取り出すことができます。

// Cursor.toWebSafeString()の文字列をカーソルに復元
Cursor cursor = Cursor.fromWebSafeString(page);

// カーソルをもとに、そこから20 + 1件分を取得するオプションを作成
FetchOptions restored = FetchOptions.Builder
    .withLimit(20 + 1)
    .cursor(cursor)
    ;

// 先ほどと同じクエリを再構築して、復元したカーソルを利用
Query query = new Query("Hoge");
DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
QueryResultIterator<Entity> iter =
        ds.prepare(query).asQueryResultIterator(restored);

// 20件分だけ消費
for (int i = 0; i < 20 && iter.hasNext(); i++) {
    Entity entity = iter.next();
    // ...
}

// ここまでの結果もまたカーソルにできる
Cursor next = iter.getCursor();
if (next == null) {
    // ...
}

このように、「指定の件数だけ処理→カーソルを取得→カーソルを復元→指定の件数だけ処理→…」と繰り返すことで、ページング処理が簡単に実装できます。また、カーソルは Cursor.toWebSafeString(), Cursor.fromWebSafeString()を利用することで、クライアント側に安全に返すこともできます。

ただし、このカーソルは次の項で紹介するIN演算子、!=演算子と同時に使えないようです(SDK 1.2.8で確認)。その場合、getCursor()nullを返すようなので、カーソルが取れるか不安なときには戻り値をチェックしておくのがよさそうです。

IN演算子、!=演算子の追加

Python版同様に、Java版にもIN演算子、!= (NOT EQUAL)演算子が追加されました。FilterOperatorから確認できます。

FilterOperator.IN

FilterOperator.INは、「プロパティの値が指定した値のいずれかと一致する」というフィルタです。次のような感じで使います。

Query query = new Query("Hoge")
    .addFilter("value", FilterOperator.IN, Arrays.asList(10, 20, 30));

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
Iterator<Entity> iter = ds.prepare(query).asIterator();
while (iter.hasNext()) {
    Entity entity = iter.next();
    // ...
}

上記のプログラムでは、次のようなエンティティの一覧を検出できます:

  • "Hoge" というカインドで
  • かつ "value" プロパティが 10 または 20 または 30

また、IN演算子はEquality Filterとして解釈されます。そのため、複数のプロパティに対して指定することも可能です。これに関して注意点もありますが、後の項でまとめて紹介します。

FilterOperator.NOT_EQUAL

FilterOperator.NOT_EQUALは、「プロパティの値が指定した値と一致しない」というフィルタです。次のような感じで使います。

// 引数にはIterable<(プロパティの型)>を渡す
Query query = new Query("Hoge")
    .addFilter("value", FilterOperator.NOT_EQUAL, 10);

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
Iterator<Entity> iter = ds.prepare(query).asIterator();
while (iter.hasNext()) {
    Entity entity = iter.next();
    // ...
}

演算子の解釈と注意点

IN演算子と!=演算子は、いずれもデータストアが直接サポートしていません。そのため、アプリケーションサーバ上でいくらかの計算が行われます。実際にどんな計算が行われているのかを簡単に紹介し、その動作から導き出されるいくつかの制約についても簡単に言及します。

IN演算子を利用したクエリについて考えてみます:

... AND a IN (1, 2, 3)

これは、OR演算子を利用して次のように書き換えることができます:

... AND ( a == 1 OR b == 2 OR c == 3 )

これを変形すると、次のようになります

... AND a == 1
OR ... AND a == 2
OR ... AND a == 3

App Engineのデータストアでは、実際にはORという演算子は利用できません。このため、上記のクエリを1行ずつ通常のクエリとして実行し、結果をメモリ状で結合するという操作を Low-Level API で行います。そのため、他のクエリよりもパフォーマンスが悪くなりがちです。

さらに、IN演算子が2個以上含まれているクエリはもっと大変です:

... AND a IN (1, 2, 3) AND b IN (4, 5, 6)

まず、b IN ...の部分を展開します:

... AND a IN (1, 2, 3) AND b == 4
OR ... AND a IN (1, 2, 3) AND b == 5
OR ... AND a IN (1, 2, 3) AND b == 6

さらに、a IN ...の部分を展開します:

... AND a == 1 AND b == 4
OR ... AND a == 1 AND b == 5
OR ... AND a == 1 AND b == 6
OR ... AND a == 2 AND b == 4
OR ... AND a == 2 AND b == 5
OR ... AND a == 2 AND b == 6
OR ... AND a == 3 AND b == 4
OR ... AND a == 3 AND b == 5
OR ... AND a == 3 AND b == 6

といった具合に、aに指定された値の個数×bに指定された値の個数の数だけクエリが展開されてしまいます。あまり多くなりすぎると負荷も高くなるため、App Engineでは展開されたクエリの数が30を超えた場合にはエラーを返すようになっているようです。

次に、!= (NOT EQUAL)演算子はもう少し単純です。

... AND a != 10

上記は、IN演算子の時と同様に、ORを利用して表現されます。

... AND (a < 10 OR a > 10)

同様に二段論理に変換してみましょう:

... AND a < 10
OR ... AND a > 10

上記のように2個のクエリに分割されます。ここで重要なポイントは <, >などの==でない演算子を利用しているという点です。 このため、!=演算子を利用する際には、他のプロパティに対して<, >などの演算子を適用できなくなります。 また、<, >と同様に未定義のプロパティ(None)を検索できません。

もうひとつの制約として、!=同じプロパティに対して同一クエリ内で複数回適用できません。そのような指定をするとIllegalArgumentExceptionがスローされることを確認しました。

2009年12月11日金曜日

Eclipse PluginではじめてのGWT UiBinder

Eclipse のGoogle Plugin の 1.2.0.v200912062003 ではGWT2.0の開発がサポートされています。
 GWT2.0の新機能であるUiBinderを利用した開発支援機能も用意されており、お手軽に試すことが可能です。Eclipse3.5.1(Java6)で実際に動かしている様子を紹介してみたいと思います。
 ※Eclipse3.5でのインストール方法についてはURLのパスを3.5に変更するだけでOKです。また英語のサイトでは情報が更新されています。
 まず、Eclipse上で新規のプロジェクト作成を行います。メニューから「ファイル」→「新規」→「Webアプリケーションプロジェクト」を選択し、「Google Webツールキットを使用します」にチェックを入れて作成します。
 (パッケージ名).clientと(パッケージ名).server以下にGWTのサンプルコードが生成されます。但し、こちらはUiBinderを利用してません。ですので、新たにウイザードを起動します。プロジェクト・エクスプローラー上の(パッケージ名).client上で「ファイル」→「新規」→「UiBinder」を選択します。
 「generate sample contents」にチェックが入った状態のまま、名前を登録して「完了」をクリックすると、下記の画像のように(ファイル名).ui.xmlとファイル(ファイル名).javaの二種類のファイルが生成されます。
 ASP.NetのWeb Formのコードビハインドとなんだか似ていますね。*.aspx と *.aspx.cs(C#の場合) が*.ui.xml と *.javaに対応するようなイメージでしょうか。
 *.ui.xml側でUIの定義を行います。タグ内でCSSライクなスタイルの指定が出来ます。作成される雛形ではボタンが一つだけ定義されています。g:Buttonのタグ内で ui:field="button" と記述されており、これが*.java側の @UiFieldアノテーションのButton button; に対応しています。
 尚、Javaのエディタ上で*.ui.xmlにUIの定義が無いコンポーネントを記述すると下記の画面の様に警告をしてくれる補完機能が備わっているので、単純なスペルミスは回避出来るのではないでしょうか。
 残念ながら、.ui.xmlに定義を追加してもJavaには反映されないようです。*.ui.xml と *.java の2WayなWYSIWYGエディタが欲しいですね。
 また、ui.xmlとJavaのファイルの紐付けは同じパッケージ内の同じファイル名であれば、必要ないようです。もしファイル名が異なる場合は、Javaのソースに定義されているinterface上にアノテーションで @UiTemplate("ファイル名.ui.xml") と定義すれば紐付けされます。
 次に、この雛形をまず実行してみたいので、エントリーポイントとなるJavaのファイル、エントリーポイントを指定するモジュールの*.gwt.xmlファイル、HTMLファイルを作成します。
 こちらもウィザードが用意されています。メニューの「ファイル」→「新規」から「エントリーポイント」、「モジュール」、「HTMLページ」が各々、作成出来ます。
 エントリーポイントの*.javaファイルにはonModuleLoad() の空のメソッドが作成されていますので、UiBinderで作成した*.javaファイルのインスタンスをRootPanelに追加します。この作業は従来のGWTと同じですね。
 次に作成したエントリーポイントの*.javaファイルに対するモジュールを作成します。この際に、Inherited modulesを追加する必要があります。下記の画面の様に com.google.gwt.uibinder.UiBinder を指定して追加します。
 生成された*.gwt.xmlファイルにエントリーポイントの*.javaファイルを記述します。
 次いで、プロジェクト・エクスプローラー上の*.gwt.xmlファイルを選択した状態で対応するHTMLページを追加します。そしてWebアプリケーションの実行を行い、ブラウザ上で確認します。
 Google Pluginの機能が今後、充実してくれば、先行のFlex BuilderやVisual Studioの様にUIとコードが分離した状態での開発が一層、楽になるのではないでしょうか。
 P.S GWT2.0のリリースが asahi.com で紹介されていました。 GoogleのGWT2.0に対する力の入れ具合?が伺えます。

2009年12月10日木曜日

appengine-java-sdk-1.2.8-正式版

appengine-java-sdk-1.2.8が正式版としてリリースされているようです。Eclipse3.5で確認したところ、http://dl.google.com/eclipse/plugin/3.5 を指定してソフトウエア更新を行なうとプラグインとSDKがインストールされます。(こちらの記事にあるGWT.2.0.のSDKもインストールされます。)
追加・変更内容についてはプレリリース時の記事を参照していただくとして、その他に気になった点としては以下の3箇所でしょうか。

- New support for application pre-compilation to reduce the length of loading requests. To enable pre-compilation on your application, add this flag to your appengine-web.xml: <precompilation-enabled>true</precompilation-enabled> If you have trouble deploying your application, you should remove this flag or set it to false.
この設定を有効にすると、アプリケーションのデプロイ時に”Precompiling.”とログに表示されるようになります。 Google App Engine Blog の記事 によれば、この pre-compilation によりGAE上に新しくインスタンスが生成される時やスケールされるときの速度が改善されるようです。ただし、サンプルアプリケーション程度の規模では効果は実感できませんでした。

- Added Quota API (com.google.appengine.api.quota) to match Python API.
現状ではJavaDocと、公式ドキュメントのこちらくらいしか資料が見当たらないのですが、リクエストサイクル中のCPUの占有時間に関する情報が収集できるようです。

- Low-level Memcache API now supports batchIncrement().
インストールされたSDKのディレクトリにあるリリースノートでは batchIncrement() となっていますが、最新版では、 incrementAll() となっています。

GWT2.0リリース

Google Web Toolkit 2.0 のリリースに伴ない、Eclipse Plugin も 1.2.0.v200912062003 になりました。
 Eclipse3.5でインストールしてみたところ、1.7系まではローカル実行用のホスト・ブラウザを利用することでデバッグ中の画面を確認することが出来ましたが、2.0からはIn-Browser Development Modeという通常のブラウザを利用してデバッグする方法が用意されています。
 ブラウザにはプラグインをインストールする必要があり、IE、FireFox、Safari用のプラグインは既に用意されています。Google Chromeも4以降に向けて準備しており、Early Accessが利用出来るようです。
 Eclipseでの実行時にDeelopment Modeのビューが開き、ブラウザへのプラグインのインストールを推奨するメッセージが表示されます。
 メッセージには「このURLをブラウザへコピーして下さい。」とあり、ブラウザ上でプラグインの有無を判断して、下記のFirefoxでの画面例の様にインストールの案内が表示されます。
  試しに、IE8とFirefox3.5.5にインストールしてみました。例えば、Sample001.htmlというページを作成してデバッグを行うと、下記の様なURLで実行されています。
 http://localhost:8888/Sample001.html?gwt.codesvr=(実行しているPCのIPアドレス):9997
 実行中の様子はブラウザ毎にEclipse内のDevelopment Modeのビューで確認することが出来ます。例えば、FirefoxとIEの両方で実行する場合はビュー上に下記の様に表示されていますので、切り替えて別々に動作を確認します。
  • Sample001
  • -Sample001.html - IE
  • Sample001
  • -Sample001.html - FF
 尚、デバッグを停止している状態でブラウザのページにアクセスすると"GWT Code Server Disconnected"とお知らせしてくれて、なかなか?親切なようです。
 また、GWT2.0そのものも様々な新機能がアナウンスされていました。なかでも個人的に面白いと思う機能はUI Binderで、これはXMLでUIを定義出来ます。
 Adpbe FlexのMXML、MicrosoftのXAML を想起しますね。
 Google CodeのGWTのページでUI定義のサンプルが公開されています。Eclipse Plugin ではUI Binderの雛形の(作成したページ名).ui.xml と (作成したページ名).java のファイルが作成されます。
 UI上のコンポーネントを定義する際に、従来はJavaによるコーディングが必要で、面倒だという声もありましたが、UI Binderを利用することでそうした声の不満を解消することが出来るようです。

2009年12月8日火曜日

グルージェントWebページの裏側 (2)


弊社WebサイトはGoogle Bloggerと統合し、bloggerへの記事がマスターデータとしてGAEのデータストアにレプリケーションしています。弊社サイトのメインメニューとフッターメニューは管理画面で編集ができるようになっていますし、GWTのコンポーネントとして独立できるように実装しているのですが、残念ながらbloggerに使うことができていませんでした。

クロスサイトスクリプティングになっちゃうよ問題

弊社サイトが www.gluegent.com、Bloggerが songofcloud.gluegent.com となっているため、Webブラウザのクロスサイトスクリプテイングのセキュリティ制限により、GWT側からblogger側のURLを操作することができなかったのです。
それではと、GAEのモジュールを展開してblogger側に配備することを試してみました。しかしGAEのビルド済みモジュールは複数のHTMLリソースに分割されていて、URL上の同一階層に配置されていることを前提にダイナミックにロードする仕組みになっていました。bloggerは機能として、その構造内にGAEのモジュールなどの任意のリソースを配置できる仕組みにはなっていないため、やはりGAEコンポーネントを配置したwww.gluegent.com内のHTMLページをiframeを使ってガジェットのようにblogger側に取り込まなければならないということになります(※1)。

しかし実は今日から songofcloud.gluegent.comのメインメニューとフッターメニューは www.gluegent.comと同一のGAEコンポーネントを使って構成することが出来るようになりました。これで、管理画面からメニュー構成を変えたら両サイトに反映されるようになったわけです。どのようにして問題を解決したか、苦労した点なども含めて紹介したいと思います。

同じサイトの誰かにモシモシさせる

まず、解決対象を明確にします。クロスサイトスクリプティングの制限が具体的に起こしている不具合は何かということですが、これは、iframe内から親フレーム(またはウィンドウ)のロケーションを変更できないという問題を示します。メインメニューとフッターメニューの目的はその中に並べられたアンカータグから他のページに遷移することです。iframe内に配置してしまえば、只単にiframe内で遷移することになってしまうので、親のロケーションを変更する必要があります。javascriptから以下のように操作することになります。

parent.location.href = "http://....";
しかし同一ホストではないのでブラウザに無視されて実行することはできません。長らくあきらめていたのですが、@ITで、ブラウザでのクロス・ドメイン通信のセキュリティ保護という記事を見つけました。親が別サイトなら孫を親と同じサイトにしてやりとりをさせれば良いという方法です。なるほど納得です!


IE,Firefox,Safari,Opera,Chromeで確認しましたが全て正しく確認できました。血のつながりのない義理の息子の言うことは聞いてくれないけど孫の言うことは聞いてくれました。孫というか本人同士でしたが。

基本コンセプトを理解して実装していったのですが、二つほど壁がありましたので紹介します。

onclickが効かないよ問題

メニューのアンカーはGWTから動的に作成していましたが、その際に不可解な現象に悩まされました。aタグに入れたonclickイベントが効かないという問題です。iframe化しているので、onclickからbloggerから見て孫となるiframeを動的に生成しなければならないので軽く危機です。静的に書かれているアンカーからは問題ないし、Safari,Chromeでは問題ないし、そもそもonclickイベントに入ってこないのでスクリプトの問題でもないし、大分困惑しましたが、原因は hrefの書き方でした。hrefには "#" だったり、実際のURLを見せて onclick内で return false にしたりすることは、createElementで作成したAタグではやってはいけないということでした。これもセキュリティの問題だったようです。hrefに、"javascript:void(0);" と書くことで解決できました。

iframeにURL渡してもロードされないよ問題

アンカータグからのonclickでは、動的にiframeを作り、「bloggerURL+"#" + 移動したいURL」 を渡さなければならなかったのですが、これまた Safari、Chromeでは移動できたのにIE,Firefoxでは移動しません。やっていたのは以下のようなことです。

Frame frame = new Frame(url);
Document.get().getBody().appendChild(frame.getElement());
FirefoxのFindbugプラグインや、IE8の開発者ツールから見ても、ちゃんとタグが出力されています。
<iframe src="http://..."></iframe>
にもかかわらず、読み込まれないのです。これもしばらく調査して解決に至りました。ロケーションを変更するには、document.location.href = "http://..."; と同じように、iframeのdocument.location.hrefを変更する必要があったのです。さてしかし、GWTではframeオブジェクトからdocumentオブジェクトを参照できるものの、locationメンバがありませんでしたので以下のようにJSNIで対応しました。
Anchor result = new Anchor(menu.getTitle(), url);
if (crossDomainUrl != null) {
  final String encodedUrl = encodeUriComponent(url);
  result.setHref("JavaScript:void(0);");
  result.setTitle(url);
  
  result.addClickHandler(new ClickHandler() {
    
    @Override
    public void onClick(ClickEvent event) {
      String iframeUrl = crossDomainUrl + "#go_" + encodedUrl;
      Frame frame = new Frame(iframeUrl);
      frame.setStyleName("hidden_frame");
      FrameElement frameElement = (FrameElement)frame.getElement().cast();
      frameElement.setAttribute("frameborder", "0");
      Document.get().getBody().appendChild(frame.getElement());
      openDocument(frameElement.getContentDocument(), iframeUrl);
    }
    
    private native void openDocument(Document contentDocument, String iframeUrl) /*-{
      contentDocument.location.href = iframeUrl;
    }-*/;
  });
}
もしかしたらGWTで別の方法が用意されているのに見落としているかもしれませんけどね。

とまあ、いろいろハマったりもしましたが、作成したGWT部品をサイトを越えて流用したいという要件を満たすことはできました。同じような機会が出てきたときにこの記事を思い出してくだされば幸いです。でわでわ。


  • ※1: blogger側に取り込む方法としてはレイアウトのテンプレート編集になります。bloggerのテンプレートは、Google Sitesやはてなダイヤリーなどと比べて自由にHTMLやJavaScriptが書けるようになっています。

XMPPを使ったログの通知

AppEngineで開発している時に開発環境ではなく、実環境上でデバッグしたい事がよくあります。しかし、実環境上では当然ログのtailなど出来るわけはなく、ログを見るためにログページへ行って毎回リロードなんてことをしなければならず、これが非常に苦痛です。そこで巷では見捨てられた感もあるXMPPServiceを使ってログを通知出来る仕組みを作ってみました。利用したロギングライブラリは、sl4jとlogback-classicです。

まず、以下のようなXMPPServiceにログを出力する為のAppenderを作成します。

public class XmppAppender extends AppenderBase<ILoggingEvent> {

    private XMPPService service = XMPPServiceFactory.getXMPPService();

    private String notifyTo;

    public String getNotifyTo() {
        return notifyTo;
    }

    public void setNotifyTo(String notifyTo) {
        if (notifyTo == null || notifyTo.isEmpty()) {
            throw new IllegalArgumentException();
        }
        this.notifyTo = notifyTo;
    }

    @Override
    protected void append(ILoggingEvent eventObject) {
        JID jid = new JID(notifyTo);
        Message message = new MessageBuilder()
            .withMessageType(MessageType.CHAT)
            .withRecipientJids(jid)
            .withBody(getLayout().doLayout(eventObject))
            .build();
        switch (service.sendMessage(message).getStatusMap().get(jid)) {
        case SUCCESS:
            return;
        case INVALID_ID:
            throw new LogbackException("failed to append log event for invalid id:" + jid);
        case OTHER_ERROR:
            throw new LogbackException("failed to append log event for unknown error:" + jid);
        default:
            throw new AssertionError();
        }
    }

}

次に、logback.xmlにappenderの設定を書きます。ここで先程作成したXmppAppenderを設定し、notifyToに通知先にするGoogleAccountを設定します。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
 <appender name="XMPP" class="com.gluegent.tools.appengine.logging.XmppAppender">
   <notifyTo>someoneあっとgmail.com</notifyTo>
   <layout class="ch.qos.logback.classic.PatternLayout">
     <pattern>%-5p %d [%t] %m%n</pattern>
   </layout>
 </appender>
 <root>
   <level value="TRACE" />
   <appender-ref ref="XMPP" />
 </root>
</configuration>

ここまでで、プロジェクトに対する設定は終わりです。

最後に、プロジェクトのデプロイ先からの通知を受けられるようにする為に、GTalkクライアントであっとappspot.comを追加します。以上ですべての設定が終わり、プロジェクトをデプロイすればログがGTalkクライアントに通知されてきます。

これで今までのように毎回画面更新をする必要もなくなり、Eclipseでコンソールを見ながら開発しているような感覚で本番環境上でのデバッグが行えるようになります。ただ一点悲しいのが、XMPPServiceの利用はdefault versionになっていないと出来ない為、同一AppId上では複数の開発者が自分の為だけに利用する事が出来ません。Cron、TaskQueueなどについても言えることなので、そのうちversion別の対応がされて欲しいですね。それでは皆様快適?なログライフを~!

2009年12月2日水曜日

Google Analyticsの解析対象から特定アクセスを除外する

Google Analyticsには特定のIPアドレスやドメインからのトラフィックを除外するといった、フィルタを設定することができます。
ということで、グルージェントからのアクセスを除外するようにしました。
フィルタは Analyticsページ右下のリンク「フィルタマネージャ」より設定できます。
設定済みのフィルタが表示されますが、初回は何もありません。
「+フィルタを追加」から新規作成できます。
フィルタ種類で「カスタムフィルタ」を選択することで、どのような設定が可能か確認することができます。
ここでは、社内からのアクセスを除外するので「既定のフィルタ」で「ドメインからのトラフィックを除外」するようにします。
除外するドメインに"gluegent.com"を指定します。
そして、使用できるプロファイルからpirkar.ashikunep.orgを選択、追加して保存します。
これで完了です。

GAE+GWT+Twitter の検索ガジェットの習作

 twitterのトップページの機能と同じですが、発言の中にURLが含まれていると、その直後にiframe内に展開するというものです。Twitter4jを利用しています。

 また、ガジェット領域はそもそも幅が狭いので、IE以外で長いURLなどが改行されないと激しい横スクロールをしてしまいます。この回避のために発言内のタグではない文字全てにwbrタグを入れるようにしました。また、表示できる幅に応じて吹き出し位置を変えるようにしています。

 ガジェットでGWTを使えるようにするポイントはiframe経由にすることです。GWTは動的に自身のドメイン内のリソースを相対パス指定でダイナミックにロードするので、ドメインをまたいで直接配置することが出来ません。そこさえクリアすればGWTでどんどん作れちゃいます。

09.12.3追記  youtubeのURLをインラインのプレイヤーに置き換えてみました。ガジェット内の検索ボックスに「youtube」で検索してみると確認できますよ。

09.12.4追記  Google VideoのURLをインラインのプレイヤーに置き換えてみました。ガジェット内の検索ボックスに「videoplay」で検索してみると引っかかって確認できますよ。

10.10.7追記

  • 発言日時からtwitterにリンクできるようにしました。これにより、対象ツイートへの返信やリツイートが行えるようになります。
  • Twitter APIの利用をサーバーサイドからクライアントサイドで行うようにしました。twitter4jを辞めてgwtのjsonpを使っています。これにより、レスポンスの高速化と、twitter APIリミットによる不具合が解消されました。リミットとは、同一要求元からのAPIコールが1時間150回までしか行えないというtwitterの制限です。これまではGAE一箇所からの要求であったため、本サイトの閲覧者が多いとすぐにパンクしていました。

Google Analytics入れてみました

入れてみたのはPirka'rサイトです。
http://pirkar.ashikunep.org/
Google Analyticsとは、Googleが提供している無料のアクセス解析です。
いろいろ、有用な情報が得られます。
(個人的に見てて面白い。。。)
http://www.google.com/intl/ja_ALL/analytics/
以下、入れてみた手順を記します。
まずは申し込みです。
上記のAnalyticsサイト右上のリンクから申し込んでください。
Googleアカウントを持っていれば、ログインしてAnalyticsに申し込んでください。
入力するのは以下の内容だけです。
1.対象になるWebの情報
2.連絡先氏名
埋め込むスクリプト(トラッキングコード)が指示されるので、対象のサイトの"</body>"タグの直前に追加します。
Pirka'rでは、↓になっています。
<html>
<head>~</head>
<body>
~中略~
<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
try {
var pageTracker = _gat._getTracker("UA-11851321-1");
pageTracker._trackPageview();</script>
</body>
</html>
以上で準備は完了です。
データ表示まで最大24時間かかるようなので、気長に待ってみましょう。
Analyticsページでステータスのアイコンが緑のチェックマークになればOKです。