-DataSqueeze

土曜日、Tapestryの技術記事を求める声がすくなからずありましたので、鋭意がんばります。さて唐突ですが。。。
DataSqueezeアダプターを作成することは、ステートレスTapestryワールドにおいて、重要な役割を果たします。サービスパラメータでオブジェクトをページにシリアライズダンプしてやりとりする方法を以前に示しましたが、さすがにオブジェクトをそのままダンプすると効率が悪かったりします。特にGETリクエストを発生させる<a>タグリンクでは、全長が特定バイト数(WEBサーバーもしくはWEBブラウザの設定・仕様上限。大抵は2048バイト)を超えると切り捨てられてしまうという問題もあります。DataSqueezeのSerializeダンプが選択される前に、もっと効率のよいダンプ方法をカスタムアダプターを用意することが可能です。アダプターはorg.apache.tapestry.util.io.ISqueezeAdapterを実装します。

public interface ISqueezeAdaptor {
  public String squeeze(DataSqueezer squeezer, Object data) throws IOException;
  public Object unsqueeze(DataSqueezer squeezer, String string) throws IOException;
  public void register(DataSqueezer squeezer);
}

squeezeが、オブジェクトからリンクに埋め込まれる文字列(次のリクエストのパラメータ値)に変えるもので、unsqueezeがリクエストパラメータ値からオブジェクトに戻す際に呼び出されるメソッドです。実装例は以下のとおりです。

package sample.valuepagelink;
import java.io.IOException;
import org.apache.tapestry.util.io.DataSqueezer;
import org.apache.tapestry.util.io.ISqueezeAdaptor;
public class PersonAdapter implements ISqueezeAdaptor {
  private static final String PREFIX = "P";
  private static final String SEPARATER = ":";
  public void register(DataSqueezer squeezer) {
    squeezer.register(PREFIX, Person.class, this);
  }
  public String squeeze(DataSqueezer squeezer, Object data) throws IOException {
    Person person = (Person)data;
    return PREFIX + person.getName() + SEPARATER + person.getAge(); 
  }
  public Object unsqueeze(DataSqueezer squeezer, String string) throws IOException {
    int pos = string.indexOf(SEPARATER);
    String name = string.substring(1, pos);
    int age = Integer.parseInt(string.substring(pos + 1));
    return new Person(name, age);
  }
}

DataSqueeze#register(String,Class,ISqueezeAdapter)によって、アダプターが登録されます。登録は適切なISqueezeAdapterを、squeeze時およびunsqueeze時に見つけられるように、unsqueeze時のためにString、squeeze時のためにClassをキーとして登録します。String型のキーは、DataSqueeze結果の頭1バイトの文字で、!とzの間の文字から重複しないように任意に選んだ文字となります。例示のPersonAdapterでは、PREFIX定数で定義された「P」をキーとしました。Class型のキーは、アダプターに処理させたいクラスの型を指定します。例ではPerson.classとしています。この値には、インターフェイスを指定することもできます。処理させたいクラスが実装するインターフェイスを指定すると、DataSqueezeのほうで処理するオブジェクトインスタンスの型をしらべて該当するアダプターが見つからなかったときに、実装インターフェイス、スーパークラスと探していきます。Serializableを実装しているとObjectOutputStreamを利用してダンプされるのは、クラスを調べている途中でSerializableが見つかったためです。
ここでユニークなのは、String型のキー登録は一文字に限りません。たとえば、組み込みのBooleanAdapterでは、「TF」と2文字登録されています。また、IntegerAdapterは「-0123456789」と登録されています。unsqueezeする際に、リクエストパラメータ値の先頭一文字が、BooleanAdapterでは「T」もしくは「F」の場合に処理を行いますし、IntegerAdapterの場合は「-」および「0」〜「9」ではじまる値を処理するのです。組み込みアダプターですでに以下のキーが利用されているので、カスタムアダプターではそれ以外の値を選択しなければなりません。

  • BooleanAdapter
    • T,F
  • ByteAdapter
    • b
  • CharacterAdaptor
    • c
  • ComponentAddressAdaptor
    • A
  • DoubleAdaptor
    • d
  • EnumAdaptor
    • E
  • FloatAdaptor
    • f
  • IntegerAdaptor
    • 0,1,2,3,4,5,6,7,8,9,-
  • LongAdaptor
    • l(アルファベット小文字)
  • SerializableAdaptor
    • O (アルファベット大文字)
  • ShortAdaptor
    • s
  • StringAdapter
    • S

まとめると、A,E,F,O,S,T,b,c,d,f,l,s,0,1,2,3,4,5,6,7,8,9,- がすでに使われています。
処理対象のPersonクラスは以下のとおりです。

public class Person implements Serializable {
  private String name;
  private int age;
  public Person(String name, int age) {
    this.name = name;
    this.age = age;
  }
  public String getName() {
    return name;
  }
  public int getAge() {
    return age;
  }
}

Personを復元するには、メンバーのnameおよびageの値があれば十分なので、PersonAdapter#unsqueeze()では、リクエストパラメータ値を、「キー値」+「String」+「セパレータ」+「int」に分解して、new Person(String,int)とコンストラクタコールをしています。PersonAdapter#squeeze()ではPersonオブジェクトインスタンスのプロパティから「P」+「Person#getName()」+「:」+「Person#getAge()」と文字列を作っています。
アダプターができたら実際に動作するDataSqueezeに登録しないといけません。この方法は様々ありますが、最もシンプルな方法は、Tapestryアプリケーションで用いているEngineのcreateDataSqueeze()メソッドをオーバーライドする方法です。このメソッドはDataSqueezeのファクトリメソッドで、Tapestryアプリケーションで用いるDataSqueezeは常にこのメソッド経由で生成されています。オーバーライドの例示は以下のとおりです。

package sample.valuepagelink;
import org.apache.tapestry.engine.BaseEngine;
import org.apache.tapestry.util.io.DataSqueezer;
public class PersonAdapterEngine extends BaseEngine {
  public DataSqueezer createDataSqueezer() {
    DataSqueezer squeezer = super.createDataSqueezer();
    new PersonAdapter().register(squeezer);
    return squeezer;
  }
}

このオーバーライドしたエンジンを、アプリケーションスペックXMLの「engine-class」属性の設定によって登録します。例は以下のとおりです。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE application
  PUBLIC "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
  "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
<application name="ValuePageLink"
    engine-class="sample.valuepagelink.PersonAdapterEngine">
  <page name="Home" specification-path="Home.page"/>
  <page name="Next" specification-path="Next.page"/>
  <service name="valuePage" class="sample.valuepagelink.ValuePageService"/>
</application>

これで実行した際のリンクに埋め込まれた様子は以下のようになります。該当箇所の先頭文字は当然「P」です。

/app?service=valuePage/Next/person&sp=PMasataka%3A31

ちなみに、SerializableAdapterでダンプした例は以下のようになります(適宜改行をいれていますが、実際は1行です)。該当箇所の先頭文字がSeralizableAdapterのキーである「O」であることに注意して下さい。

/app?service=valuePage/Next/person&sp=
OH4sIAAAAAAAAAFvzloG1uIhBujgxtyAnVa8sMac0tSAxPTUnMy9bLyC1q
Dg_796ixFsOe9mimBiYPBmYgXI-DCx5ibmpJQxCPlmJZYn6OYl56frBJUWZeenWFQUMDAzyJQwcv
onFiSWJ2YkAru8CKWEAAAA.

カスタムアダプターを作成することによって、効率がよくなっていることが見て取れるでしょう。次回はより実践的なカスタムアダプターについての考察を示した上で、DataSqueezerがTapestryステートレス機構上で重要な役目を示しているという説明を行い、その後隠れ大物コンポーネント、ListEditの解説をゴールとした連載に入ります。このコンポーネントは難解な上に超重要。説明にも前提知識多数の難物ですが、まあ気負わずにゆるゆるとやっていきましょう。
今回のサンプルソースコードは、http://www.kjps.net/user/kurihara/ からダウンロードできます。「ValuePage sample [2004/06/07]」です。これは今回のカスタムアダプターおよび、前回のカスタムサービスのコード一式です。参考にしてみてください。