2011/02/02

Slim3 JSON機能の説明(非公式)

(2/7更新)
(2/26更新)
(12/22更新 @Jsonはフィールド直接指定になりました)

Slim3 JSON機能のドキュメントを書き始めました。まだ非公式なものですが、ひと通り書き終わったら公式へのマージを提案する予定です。でも、公式は英語なんですよね。どうしたものか。まぁ、いきなり下手な英語で書くより、まずは日本語でちゃんと書いたほうが良いはず。

というわけで、下記ドキュメントの草案です。若干改行が変ですが、evernoteに書きなぐってexportしたものを貼っつけてるためだと思います。これにあとJSON入出力のカスタマイズ方法を書いて一段落とする予定です。(書きました!)

概要
 Slim3のJSON機能は、モデル(@org.slim3.datastore.Modelアノテーションが付加されたクラス)のJSON変換機能を提供します。JSON変換は、org.slim3.datastore.ModelMeta<M>に定義される以下のメソッドを呼び出すことにより行われます。

モデルのJSON文字列への変換:
  • String modelToJson(Object model);
  • String modelToJson(Object model, int maxDepth);
  • String modelsToJson(Object[] models);
  • String modelsToJson(Object[] models, int maxDepth);
JSON文字列のモデルへの変換:

  • M jsonToModel(String json);
  • M jsonToModel(String json, int maxDepth);
  • M[] jsonToModels(String json);
  • M[] jsonToModels(String json, int maxDepth);
 例えばTestModelというモデルに対してJSON変換機能を使用するコードは、次のようになります。

TestModel m = new TestModel();
m.setValue(1);
String json = TestModelMeta.get().modelToJson(m);
System.out.println(json); // {"value": 1}
TestModel m2 = TestModelMeta.get().jsonToModel(json);
Assert.assertEquals(m.getValue(), m2.getValue());


JSON変換の制御
 フィールドにJSONアノテーション(@org.slim3.datastore.json.JSON)を指定することで、JSON変換の振る舞いを変更できます。以下にJSONアノテーションで指定できるパラメータを示します。

パラメータ名 デフォルト 説明
ignore boolean false このアトリビュートを無視します。JSON出力に含まれず、JSON入力時にも読み込まれません。
ignoreNull boolean true アトリビュートがnullの場合、何も出力しません。falseの場合は、"null"が出力されます。
alias String 空文字列 アトリビュートのJSON文字列内での名前を指定します。
coder Class<? extends JsonCoder> Default.class JSON入出力を行うJsonCoderのクラスを指定します。ここに独自のクラスを指定することで、JSON入出力のカスタマイズを行うことができます(後述)。

 例えばプロパティを無視したり、エイリアスを設定したりする場合、以下のように記述します。

@Model
class TestModel{
  ...
  @Json(ignore=true)
  private String ignoredValue;
  @Json(alise="foo")
  private String bar;
}


対応する値型
 Slim3のJSON変換機能は、以下の値型に対応します。また、後述するカスタマイズを行えば、これ以外の型に対応したり、入出力方法を変更する事ができます。

Javaクラス
JSON表現
java.lang.String, com.google.appengine.api.datastore.Text
文字列。cipher=trueの場合、JSON内では暗号化されます。
"text":"hello"
byte[], com.google.appengine.api.datastore.ShortBlob, com.google.appengine.api.datastore.Blob
バイト列のBase64文字列。
"blob":"mMB4qZAgtBKJq0d1LBGTCA=="
boolean, java.lang.Boolean
ブール値(true or false)。
"value":true
short, java.lang.Short, int, java.lang.Integer, long, java.lang.Long
整数。
"value":100
float, java.lang.Float, double, java.lang.Double
小数。
"value":1.0
java.util.Date
整数(Date.getTime()が返す値)。
"value":10233400
java.lang.Enum
文字列(Enum.name()が返す値)。
"value":"MONDAY"
com.google.appengine.api.users.User
ネストしたJSON文字列(User.getEmail()等の値をJSONエンコード)。
"user":{"authDomain":"authDomain","email":"user@test.com"}
com.google.appengine.api.datastore.Key
文字列(KeyFactory.keyToString(Key)の実行結果)。
"key":"aglzbGltMy1nZW5yCwsSBHRlc3QY6AcM"
com.google.appengine.api.datastore.Category
文字列(Category.getCategory()の値)。
"category":"partOfSpeech"
com.google.appengine.api.datastore.Email
文字列(Email.getEmail()の値)。
"mail":"user@domain.tld"
com.google.appengine.api.datastore.GeoPt
ネストしたJSON文字列(GeoPt.getLatitude()及びGeoPt.getLongitude()の値をJSONエンコード)。
"geopt":{"latitude":10.0,"longitude":10.0}
com.google.appengine.api.datastore.IMHandle
ネストしたJSON文字列(IMHandle.getAddress()及びIMHandle.getProtocol()の値をJSONエンコード)。
"handle":{"address":"handle","protocol":"xmpp"}
com.google.appengine.api.datastore.Link
文字列(Link.getValue()の値)。
"link":"linkValue"
com.google.appengine.api.datastore.PhoneNumber
文字列(PhoneNumber.getNumber()の値)。
"phone":"000-000-000"
com.google.appengine.api.datastore.PostalAddress
文字列(PostalAddress.getAddress()の値)。
"address":"Tokyo, Japan"
com.google.appengine.api.datastore.Rating
整数(Rating.getRating()の値)。
"rating":100
com.google.appengine.api.blobstore.BlobKey
文字列(BlobKey.getKeyString()の値)。
"blobkey":"Q3PqkweYlb4iWpp0BVw"
org.slim3.datastore.ModelRef<M>
文字列(モデルのkeyに対してKeyFactory.keyToString(key)した結果)。
"ref":{"key":"lskfo2ijalefkwejfwlke",value:100}

 また上記サポートする型のコレクションにも対応しています。対応するコレクションクラスはjava.util.List, java.util.Set, java.util.SortedSetで、JSON文字列からモデルを作成する際は、それぞれjava.util.ArrayList、java.util.HashSet、java.util.TreeSetをnewして要素を追加したものがセットされます。


ModelRef<M>の展開
 デフォルトではJSONアノテーションのcoderパラメータにorg.slim3.datastore.json.Defaultが指定されたものとして扱われ、このクラスは、ModelRefを参照先のモデルのキーに変換します。参照先のモデルの内容を展開したい場合、org.slim3.datastore.json.Expandedを指定して下さい。このクラスは、参照先のモデルの内容を展開し、そのモデルがさらにModelRefを持っている場合も展開します。展開のネストの深さは、modelToJsonメソッドやjsonToModelメソッドの、maxDepth引数で制限できます。
 以下にコード例を示します。

@Model
class TestModel{
  ...
  @Json(coder=Expanded.class)
  private ModelRef<TestModel> ref = new ModelRef<TestModel>(TestModel.class);
}
...
TestModel m = new TestModel();
TestModelMeta.get().modelToJson(m, 3);  // 3階層まで展開


JSON入出力のカスタマイズ
 JSONアノテーションのcoderパラメータにJSON入出力を行うクラスを指定することにより、JSON変換の振る舞いを変更することができます。デフォルトではorg.slim3.datastore.json.Defaultが指定されており、slim3が対応するデータ型のJSON入出力が行われます。slim3ではもう一つ、org.slim3.datastore.json.Expandedが用意されていて、これをModelRef<M>型のフィールドに対して使用すると、ModelRef<M>が参照しているオブジェクトも展開します。
 slim3が対応していないデータ型を使う場合や入出力方法を変更したい場合は、org.slim3.datastore.json.Defaultから継承したクラスを作成し、encode/decodeメソッドをオーバーライドして下さい。
 下記にjava.awt.Pointの入出力をサポートするクラスの例を示します。

class CustomCoder extends org.slim3.datastore.json.Default{
  public void encode(JsonWriter writer, Object value){
    if(value instanceof java.awt.Point){
      java.awt.Point pt = (java.awt.Point)value;
      writer.beginObject();
      writer.writeValueProperty("x", pt.x);
      writer.writeValueProperty("y", pt.y);
      writer.endObject();
    } else{
      super.encode(writer, value);
  }
  public <T> T decode(JsonReader reader, T defaultValue, Class<T> clazz){
    if(java.awt.Point.isAssignableFrom(clazz)){
      try{
        int x = Integer.parseInt(reader.readProperty("x");
        int y = Integer.parseInt(reader.readProperty("y");
        return clazz.cast(new Point(x, y));
      } catch(NumberFormatException e){
      }
    }
    return defaultValue;
  }
}

 java.awt.PointクラスのフィールドをJSON入出力の対象にするには、上記のCustomCoderをcoderパラメータに指定します。

import java.awt.Point;

@Model
class TestModel{
  public Key getKey(){
    return key;
  }

  public void setKey(Key key){
    this.key = key;
  }

  public Point getPoint(){
    return point;
  }

  public void setPoint(Point point){
    this.point = point;
  }

  @Attribute(primariKey=true)
  private Key key;

  @Attribute(persistent=false)
  @Json(coder=CustomCoder.class)
  private Point point;

}

 上記のように指定すると、java.awt.Point型のpointフィールドのJSON入出力がおこなえるようになります。

TestModel m = new TestModel();
m.setPoint(new Point(10, 20));
String json = TestModelMeta.get().modelToJson(m); // {"point":{"x":10,"y":20}}
TestModel m2 = TestModelMeta.get().jsonToModel(json);
Assert.assertEquals(m.x, m2.x);
Assert.assertEquals(m.y, m2.y); 

2011/01/02

AppEngine Java で絵文字入りメールを送信する

Google AppEngineで絵文字メールを送る方法を紹介します。AppEngineでは送信メールのエンコーディングを指定できない( issueへのリンク)ため、絵文字を送ることはできないと思っていたんですが、 @tmurakamiさんに、送る方法を教えてもらいました。ただし、今のところこの方法で動作確認が出来ているのは、auのW62T, W63H, W64Kのみです。また、AppEngineの振る舞いに依存しているため、今後も動くかどうかの保証はありません。また、メールをWebメール等に転送している場合、Webメール側(au oneとgmailで確認されています)ではメール全体が(絵文字部分だけでなく)正常に表示されないようです(ヘッダにあるエンコーディング情報とメールボディのエンコーディングが違うので無理も無いですが)。

方法は至ってシンプルで、JISエンコード済みのテキストを無理やりStringに入れるというものです。例えばスマイルマーク(ucs: ☺, au: )であれば、auでの文字コードは0x7656なので、以下のようにします。
String text = new String(
   new byte[]{0x1b, '$', 'B', 0x76, 0x56, 0x1b, '(', 'B'}
   , "ISO8859-1");
0x1b, '$', 'B'はJIS漢字を開始するエスケープシーケンス、0x1b, '(', 'B'はアスキー文字を開始するエスケープシーケンスです。こうやって作った文字列を、メールの本文に設定します。
MailService.Message m = new MailService.Message();
 m.setTextBody(text);
 // set sender, recipients and subject
 MailServiceFactory.getMailService().send(m);
ここでは触れませんが、JavaMailApiでは、ByteArrayDataSourceを使うと送れるそうです。

AppEngineはメールを送信する際、含まれているキャラクタを見て、エンコーディングを決定します。日本語が含まれていればISO2022JPを選択しますし、中国語であればBig5、UTF8になる場合もあります。上記のように制御コードが混じっていると、ISO8859-1として、そのまま送信するようです。そして実機ではエンコーディングを無視してISO2022JPとして表示するため、絵文字が表示できる、という仕組みです。ただしこのAppEngineの振る舞いは規定されているものではないので、今後も動き続ける保証はありません。

pos2witではこの方法を使って、絵文字の送信機能を実装しました(今のところauのみ)。ダッシュボードから有効にできるので、au携帯でお使いの方は是非試してみてください。また、今後他社の携帯も調査して、可能であれば対応する予定です。