2010年4月22日木曜日

作業メモ:BlazeDS + Flex 環境の Enum ルール

今回は、BlazeDS + Flex 環境で Enum をどのように扱うかについて考察してみます。

基本事項

基本的な設定やその他基本事項については、以下に準じます。

ActionScript 側の enum 名格納変数について

ActionScript 側の enum 名格納変数を name とします。AMFで通信される情報も java の Enum#name() に準じたものにします。そのため、以下のような独自のEnumProxyを定義します。
package com.objectfanatics.flex.messaging.io;

import java.util.Collections;
import java.util.List;

import flex.messaging.io.AbstractProxy;
import flex.messaging.io.BeanProxy;

public class EnumProxy extends BeanProxy {

    private static final String ACTION_SCRIPT_ENUM_NAME_PROPERTY_NAME = "name";
    private static final List<String> propertyNames = Collections.singletonList(ACTION_SCRIPT_ENUM_NAME_PROPERTY_NAME);

    @SuppressWarnings("unchecked")
    public Object createInstance(String className) {
        return new EnumStub((Class<Enum>) AbstractProxy.getClassFromClassName(className));
    }

    public Object getValue(Object instance, String propertyName) {
        if (propertyName.equals(ACTION_SCRIPT_ENUM_NAME_PROPERTY_NAME)) {
            return Enum.class.cast(instance).name();
        } else {
            throw new IllegalArgumentException("No property named: " + propertyName + " on enum type");
        }
    }

    public void setValue(Object instance, String propertyName, Object value) {
        if (propertyName.equals(ACTION_SCRIPT_ENUM_NAME_PROPERTY_NAME)) {
            EnumStub.class.cast(instance).value = String.class.cast(value);
        } else {
            throw new IllegalArgumentException("no EnumStub property: " + propertyName);
        }
    }

    @SuppressWarnings("unchecked")
    public Object instanceComplete(Object instance) {
        EnumStub es = EnumStub.class.cast(instance);
        return Enum.valueOf((Class) es.cl, es.value);
    }

    @SuppressWarnings("unchecked")
    public List getPropertyNames(Object instance) {
        if (!(instance instanceof Enum)) {
            throw new IllegalArgumentException("getPropertyNames called with non Enum object");
        } else {
            return propertyNames;
        }
    }

    private class EnumStub {
        private Class<?> cl;
        private String value;
        private EnumStub(Class<?> c) {
            cl = c;
        }
    }
}

同時に HttpFlexSession の static ブロックで以下のように定義し、あらゆる Enum 型に対して EnumProxy が適用されるようにします。
// patch for enum serialization
  static {
      PropertyProxyRegistry.getRegistry().register(Enum.class, new EnumProxy());
  }

ActionScript 側の enum ルール

ActionScript 側の enum は、以下のような形式で作成します。
// Sample enum class.
package com.objectfanatics.englishfanatics.dataobject {
    import com.objectfanatics.flex.EnumBase;
    
    // fqcn of java counterpart enum class
    [RemoteClass(alias="com.objectfanatics.sample.SampleEnum")]
    
    // class name should be the same as the name of the java counterpart enum class.
    public class SampleEnum extends EnumBase {
        
        // enums
        //  - const name and the value of the first parameter of the constructor should be the same.
        public static const KUMA_SAN:SampleEnum = new SampleEnum("KUMA_SAN", "くまさん");
        public static const TANUKI_SAN:SampleEnum = new SampleEnum("TANUKI_SAN", "たぬきさん");
        
        // constructor
        //  - default parameter values are required for deserialized enums.
        //  - this must be invoked only for enum constant creation in this class and deserialized enum creation.
        public function SampleEnum(name:String = null, string:String = null) {
            super(name, string);
        }
        
        // returns all enum constants in this class.
        //  - this method is workaround for ActionScript3.0's limitation. 
        //    static methods are not inherited and cannot be overridden in ActionScript 3.0.
        public static function values():Array {
            return superValues();
        }
    }
}

すべての enum は下記のベースクラスを継承します。
package com.objectfanatics.flex {
    import flash.utils.getDefinitionByName;
    import flash.utils.getQualifiedClassName;
    
    /**
     * Base class of enum object.
     */
    public class EnumBase {
        // key = enum class name, value = enum array.
        private static const valuesMap:Object = new Object();
        
        // enum class name
        private var enumClassName:String;
        
        // enum name
        public var name:String;
        
        // string for presentation
        protected var string:String;
        
        public function EnumBase(name:String = null, string:String = null) {
            this.name = name;
            this.string = string;
            enumClassName = getQualifiedClassName(this);
            if (string != null) {
                // for normal enum
                var values:Array = valuesMap[enumClassName];
                if (values == null) values = new Array();
                valuesMap[enumClassName] = values;
                values.push(this);
            } else {
                // for deserialized enum
                return;
            }
        }
        
        // This method name is weird because of ActionScript 3.0's limitation.
        //   - Static methods are not inherited and cannot be overridden in ActionScript 3.0.
        public static function superValues():Array {
            // TODO: check.
            var blocks:Array = new Error().getStackTrace().split("at ");
            var block:String = blocks[2];
            var startIndex:int = block.indexOf("at ");// start of first line
            var endIndex:int = block.indexOf("$");    // end of class name
            var enumClassName:String = block.substring(startIndex + 1, endIndex);
            return valuesMap[enumClassName];
        }
        
        public function toString():String {
            if (string == null) {
                var enumClass:Class = getDefinitionByName(enumClassName) as Class;
                this.string = enumClass[name].string;
            }
            return string;
        }
        
        //use equality instead of identity.
        public function equals(other:EnumBase):Boolean {
            return this.name == other.name;
        }
    }
}

java 側の enum ルール

Java 側の enum は、ActionScript 側の name プロパティと Java 側の Enum#name() の値が対応していれば、どのような形でもかまいません。

以下例。
package com.objectfanatics.sample;

public enum SampleEnum {
    KUMA_SAN, TANUKI_SAN;
}

(´・ω・`) まだまともにに使ってないから、落とし穴がいっぱいありそうな気がする今日この頃。

2010年4月21日水曜日

作業メモ:FlexのRemotingでjavaからにActionScript型をマッピングする方法

前回は、Flex の Remoting で ActionScript から java に型をマッピングする方法について、実際に動作を確認しながら考えてみました。

今回は、その逆の方向ということで、java から ActionScript に型をマッピングする方法について、実際に動作を確認しながら考えてみます。

基本的な事項は作業メモ:FlexのRemotingでActionScriptからjavaに型をマッピングする方法参照のこと。


Private Fields in Java to Public Fields in ActionScript

基本的に動くようです。

以下、JavaBeans規約を勘違いしてやらかした例。(´・ω・`)

前回 services-config.xml ファイルのチャネルの serialization の設定として include-read-only オプションを追加したのですが、うまく動作しないようです。

シリアライズ対象の Java のクラスは以下のフィールドを持っていて、すべてのフィールドは getter を持っています。
private long id;
private String eText;
private PartOfSpeech partOfSpeech;
private String jText;

仕様どおりなら、public な getter があるので正しく動作するはずです。実装的にも Bean の introspection を利用し判断しているのであれば、問題なく動作するはずなのですが、、、。

しかも、気持ち悪いことに、id だけは正しく伝わっていました。

試しに Java 側の private フィールドを public に変更してみたところ、ActionScript 側にデータが正しく伝わりました。

ということで、原因を調査してみることにしました。

結論。JavaBeans規約的に、おいらが間違ってました。
(´・ω・`) 先頭の2文字が大文字の場合、プロパティ名の先頭は小文字に変換されないのです。

JavaBeans API specification(1.01) の 8.8 Capitalization of inferred names にかかれています。
すっかり忘れてました。Sun出身なのにw


Enums in Java to Enum Like Objects in ActionScript

(´・ω・`) TypeError: Error #1034: 強制型変換に失敗しました。

おいらはいままでのところ、作業ログ:BlazeDSのRemotingServiceでJavaのenumを扱う方法で書いたような方法で Enum を扱っていました。そのため、Java 側から String として渡ってくる情報を ActionScript 上で Enum 的オブジェクトに変換しなければなりません。しかし、FlashPlayer 側に手を入れるわけにも行きません。

少し調べてみると、BLZ-17 に java の Enum と AS3 のクラスのマッピングができるようになっているという話を発見。

そちらに書かれている方法どおりにやってみたところ、問題なく動作しました。

初期化用の static ブロックは、web.xml の先頭で listener として読み込まれているという理由で、flex.messaging.HttpFlexSession クラスの先頭に追加しました。以下、追加されたソースです。
// patch for enum serialization
    static {
            PropertyProxyRegistry.getRegistry().register(Enum.class, new EnumProxy());
    }

2010年4月20日火曜日

作業メモ:FlexのRemotingでActionScriptからjavaに型をマッピングする方法

今回は、Flex の Remoting で ActionScript から java に型をマッピングする方法について、実際に動作を確認しながら考えてみます。

基本情報

基本的な情報は以下に書かれています。

以下、各型についてマッピングの実験をしてみます。


ActonScript to Java : Boolean, String of "true" or "false" to java.lang.Boolean

Java side test code.
public boolean asToJavaBooleanTest(boolean asBooleanTrue, boolean asBooleanFalse, boolean stringTrue, boolean stringFalse) {
    return 
        (asBooleanTrue  == true ) && 
        (asBooleanFalse == false) &&
        (stringTrue     == true ) &&
        (stringFalse    == false);
}

Flex side test code.
public static function asToJavaBooleanTest():void {
    remoteObjectWrapper.invoke(true, false, "true", "false");
}


ActonScript to Java : flash.utils.ByteArray to byte []

Java side test code.
public boolean asToJavaByteArrayTest(byte[] bytes) {
    return 
        (bytes[0] == 0 ) && 
        (bytes[1] == 1 ) && 
        (bytes[2] == 2 );
}

Flex side test code.
public static function asToJavaByteArrayTest():void {
    var byteArray:ByteArray = new ByteArray();
    byteArray.writeByte(0);
    byteArray.writeByte(1);
    byteArray.writeByte(2);
    remoteObjectWrapper.invoke(byteArray);
}


ActonScript to Java : Date to java.util.Date

Java side test code.
public boolean asToJavaDateTest(Date date) {
    return date.getTime() == 1271699264375L;
}

Flex side test code.
public static function asToJavaDateTest():void {
    var date:Date = new Date();
    date.time = 1271699264375;
    remoteObjectWrapper.invoke(date);
}


ActonScript to Java : int/uint to java.lang.Integer

Java side test code.
public boolean asToJavaIntTest(int intValue, int uintValue1, int uintValue2) {
    return
        (intValue   == -1        ) &&
        (uintValue1 == 2147483647) && // rounded from 2147483648
        (uintValue2 == 2147483647);   // rounded from 2147483649
}

Flex side test code.
public static function asToJavaIntTest():void {
    var intValue:int = -1;
    // java: Integer.MAX_VALUE = 2147483647.
    var uintValue1:uint = 2147483648; // this will be rounded to 2147483647!
    var uintValue2:uint = 2147483649; // this will be rounded to 2147483647!
    remoteObjectWrapper.invoke(intValue, uintValue1, uintValue2);
}

このテストの結果から、以下のことが分かります。
  • ActionScript 側 の uint は Integer.MAX_VALUE = 2147483647 よりも大きい値を扱うことができる。
  • Java 側は、Integer.MAX_VALUE より大きい値が渡されると Integer.MAX_VALUE に丸めてしまう。
  • このことから、Java との連携が決まっている場合、uint は使わないほうが無難のようです。


ActonScript to Java : Number to java.lang.Double

Java side test code.
public boolean asToJavaNumberTest(double numberValue) {
    return
        (numberValue == -1.5);
}

Flex side test code.
public static function asToJavaNumberTest():void {
    remoteObjectWrapper.invoke(new Number(-1.5));
}


ActonScript to Java : String to java.lang.String

Java side test code.
public boolean asToJavaStringTest(String stringValue1, String stringValue2, String stringValue3) {
    return
        (stringValue1.equals("")) &&
        (stringValue2.equals("foo")) &&
        (stringValue3 == null); 
}

Flex side test code.
public static function asToJavaStringTest():void {
    var stringValue1:String = "";
    var stringValue2:String = "foo";
    var undefinedValue:String;   // Note: undefined is converted to null.
    var nullValue:String = null; // Note: null is converted to null.
    remoteObjectWrapper.invoke(stringValue1, stringValue2, undefinedValue, nullValue);
}

ActionScript の undefined と null は、Java の null に変換されるようです。


ActonScript to Java : Object (generic) to java.util.Map

Java side test code.
public boolean asToJavaGenericObjectTest(Map<String,Object> nullValue, Map<String,Object> simpleObject, Map<String,Object> hasNullProperty, Map<String,Object> hasStringProperty, Map<String,Object> hasDateProperty, Map<String,Object> hasNestedProperty) {
    return
        (nullValue == null) &&
        (simpleObject != null) &&
        (hasNullProperty.get("property") == null) &&
        (hasStringProperty.get("property").equals("foo")) &&
        (Date.class.isInstance(hasDateProperty.get("property"))) &&
        (Date.class.isInstance(Map.class.cast(hasNestedProperty.get("property")).get("nestedProperty")));
}

Flex side test code.
public static function asToJavaGenericObjectTest():void {
    // null
    var nullValue:Object = null;
    
    // simple object
    var simpleObject:Object = new Object();
    
    // has null property
    var hasNullProperty:Object = new Object();
    hasNullProperty.property = null;
    
    // has String object
    var hasStringProperty:Object = new Object();
    hasStringProperty.property = "foo"
        
    // has Data object
    var hasDateProperty:Object = new Object();
    hasDateProperty.property = new Date();
    
    // has nested property
    var hasNestedProperty:Object = new Object();
    hasNestedProperty.property = new Object();
    hasNestedProperty.property.nestedProperty = new Date();
    
    remoteObjectWrapper.invoke(nullValue, simpleObject, hasNullProperty, hasStringProperty, hasDateProperty, hasNestedProperty);
}
ネストされても、特に問題なく動作するようです。


ActonScript to Java : typed Object to typed Object

private コンストラクタのみのJavaクラスを対象にしてみたところ、以下のように怒られてしまいました。
Unable to create a new instance of type 'experiment.SimpleObject'. 
Types must have a public, no arguments constructor.

ObjectFanatics 的方針としては、public のデフォルトコンストラクタの強制など容認できるはずもありません。
ということで、ソースにパッチを当てて対応しました。

引数無しの private コンストラクタが必要になってしまうのと、そのために final の変数の利用がほぼ絶望的になるという点がかなり厳しいのですが、とりあえずは妥協することにしましょう。将来的には、引数のある private コンストラクタに対応しようと思います。(TODO: 引数なしのプライベートコンストラクタを new してから値を格納する方法ではなく、private コンストラクタの引数にシリアライズされたオブジェクトを復元していく方法を実装する。)


お次は、java 側で private field に値を入れることを期待していたのですが、以下のように怒られてしまいました。
Cannot create class of type 'experiment.WithPrivateFileds'
ObjectFanatics 的方針としては、private field が使えない制約など容認できるはずもありません。

ちょっと調べてみると、こんな issue がありました。
BLZ-427 - Add configuration option for BeanProxy#includeReadOnly property.これか!?
とおもいきや、これは Java から ActionScript 側への変換に関してだけみたいですね。
しかし、BeanProxyのフィールドに対する判断基準になる設定なので、必要です。

ということで、services-config.xml ファイルのチャネル定義に serialization の設定として以下のように include-read-only オプションを追加します。
<channels>
  <channel-definition id="amf" class="mx.messaging.channels.AMFChannel">
    <endpoint url="http://{server.name}:{server.port}/{context.root}/messagebroker/amf" class="flex.messaging.endpoints.AMFEndpoint" />
    <properties>
      <serialization>
        <include-read-only>true</include-read-only>
      </serialization>
    </properties>
  </channel-definition>
</channels>


また、ActionScript 側から渡された値を Java 側の private フィールドに格納する処理も必要です。

ということで、ソースにパッチを当てて対応しました。

Java side test code.
public boolean asToJavaTypedObjectTest(SimpleObject nullValue, SimpleObject simpleObject, SimpleObjectWithPrivateConstructor simpleObjectWithPrivateConstructor, SimpleObjectWithProperties simpleObjectWithProperties, NestableObject nestableObject, WithPrivateFiled withPrivateFiled) {
    return
        (nullValue                               == null) &&
        (simpleObject                            != null) &&
        (simpleObjectWithPrivateConstructor      != null) &&
        (simpleObjectWithProperties.intValue     == 10 ) &&
        (simpleObjectWithProperties.stringValue.equals("foo")) &&
        (simpleObjectWithProperties.simpleObject != null) &&
        (nestableObject.nestableObject.nestableObject.nestableObject == null) &&
        (withPrivateFiled.getStringValue().equals("foo"));
}

Flex side test code.
public static function asToJavaTypedObjectTest():void {
    // null
    var nullValue:Object = null;
    
    // simple object
    var simpleObject:Object = new SimpleObject();
    
    // simple object with private constructor
    var simpleObjectWithPrivateConstructor:SimpleObjectWithPrivateConstructor = new SimpleObjectWithPrivateConstructor();
    
    // simple object with properties
    var simpleObjectWithProperties:SimpleObjectWithProperties = new SimpleObjectWithProperties(10, "foo", new SimpleObject());
    
    // nested properties
    var nestableObject:NestableObject = new NestableObject(new NestableObject(new NestableObject(null)));
    
    // java object has private fields
    var withPrivateFiled:WithPrivateFiled = new WithPrivateFiled("foo");
    
    // enum
    var enumObject:EnumObject = EnumObject.KUMA_SAN;

    remoteObjectWrapper.invoke(nullValue, simpleObject, simpleObjectWithPrivateConstructor, simpleObjectWithProperties, nestableObject, withPrivateFiled);
}


ActonScript to Java : Object to enum

基本方針は、Enums in Java to Enum Like Objects in ActionScriptと同じです。

ActionScript側ではpublicなプロパティを1つだけ持ったオブジェクトをEnumとして扱い、明示的に java 側のEnumにマッピングしないようにします。プロパティの値は java 側の Enum の name と同じにしておきます。

Java side test code.
public boolean asToJavaEnumTest(EnumObject enumValue) {
    return
        (enumValue.equals(EnumObject.KUMA_SAN));
}

Flex side test code.
public static function asToJavaEnumTest():void {
    var enumValue:EnumObject = EnumObject.KUMA_SAN;
    remoteObjectWrapper.invoke(enumValue);
}


以下、未検証
  • Array (dense) to java.util.List
  • Array (sparse) to java.util.Map
  • XML to org.w3c.dom.Document
  • XMLDocument(legacy XML type)  to org.w3c.dom.Document


Patch for Private Constructor

変更前
110: Object instance = cls.newInstance();

変更後
try {
    instance = cls.newInstance();
} catch (IllegalAccessException e){
    for (Constructor<? extends Object> constructor : cls.getDeclaredConstructors()) {
        if (constructor.getParameterTypes().length != 0) continue;
        constructor.setAccessible(true);
        instance = constructor.newInstance();
        break;
    }
    if (instance == null) throw e;
}



Patch for Private Field Serialization

flex.messaging.io.BeanProxy#setValue(Object instance, String propertyName, Object value)
public void setValue(Object instance, String propertyName, Object value)
    {
        BeanProperty bp = getBeanProperty(instance, propertyName);

        if (bp != null)
        {
// patch for private field.
//            if (bp.isWrite())
//            {
                try
                {
                    Class desiredPropClass = bp.getType();
                    TypeMarshaller marshaller = TypeMarshallingContext.getTypeMarshaller();
                    value = marshaller.convert(value, desiredPropClass);
                    ClassUtil.validateAssignment(instance, propertyName, value);
                    bp.set(instance, value);
                }
                catch (Exception e)
                {
                    SerializationContext context = getSerializationContext();

                    // Log ignore failed property set errors
                    if (Log.isWarn() && logPropertyErrors(context))
                    {
                        Logger log = Log.getLogger(LOG_CATEGORY);
                        log.warn("Failed to set property {0} on type {1}.",
                                new Object[] {propertyName, getAlias(instance)}, e);
                    }

                    if (!ignorePropertyErrors(context))
                    {
                        // Failed to get property '{propertyName}' on type '{className}'.
                        MessageException ex = new MessageException();
                        ex.setMessage(FAILED_PROPERTY_WRITE_ERROR, new Object[] {propertyName, getAlias(instance)});
                        ex.setRootCause(e);
                        throw ex;
                    }
                }
// patch for private field.
//            }
//            else
//            {
//                SerializationContext context = getSerializationContext();
//
//                if (Log.isWarn() && logPropertyErrors(context))
//                {
//                    Logger log = Log.getLogger(LOG_CATEGORY);
//                    log.warn("Property {0} not writable on class {1}",
//                            new Object[] {propertyName, getAlias(instance)});
//                }
//
//                if (!ignorePropertyErrors(context))
//                {
//                    //Property '{propertyName}' not writable on class '{alias}'.
//                    MessageException ex = new MessageException();
//                    ex.setMessage(NON_WRITABLE_PROPERTY_ERROR, new Object[] {propertyName, getAlias(instance)});
//                    throw ex;
//                }
//            }
        }
        else
        {
            SerializationContext context = getSerializationContext();

            if (Log.isWarn() && logPropertyErrors(context))
            {
                Logger log = Log.getLogger(LOG_CATEGORY);
                log.warn("Ignoring set property {0} for type {1} as a setter could not be found.",
                            new Object[] {propertyName, getAlias(instance)});
            }

            if (!ignorePropertyErrors(context))
            {
                // Property '{propertyName}' not found on class '{alias}'.
                MessageException ex = new MessageException();
                ex.setMessage(UNKNOWN_PROPERTY_ERROR, new Object[] {propertyName, getAlias(instance)});
                throw ex;
            }
        }
    }


Test source code(Java)

experiment/EnumObject.java
package experiment;

public enum EnumObject {
    KUMA_SAN("くまさん"),
    TANUKI_SAN("たぬきさん");
    private final String string;
    private EnumObject(String string) {
        this.string = string;
    }
}

experiment/NestableObject.java
package experiment;

public class NestableObject {
    public NestableObject nestableObject;
    private NestableObject(){}
    public NestableObject(NestableObject nestableObject) {
        this.nestableObject = nestableObject;
    }
}

experiment/SerializationTestService.java
package test {    
    import flash.utils.ByteArray;
    
    import mx.controls.Alert;

    public final class SerializationTestService {
        private static var remoteObjectWrapper:RemoteObjectWrapper = new RemoteObjectWrapper("SerializationTestService");
        
        public static function asToJavaBooleanTest():void {
            remoteObjectWrapper.invoke(true, false, "true", "false");
        }
        
        public static function asToJavaByteArrayTest():void {
            var byteArray:ByteArray = new ByteArray();
            byteArray.writeByte(0);
            byteArray.writeByte(1);
            byteArray.writeByte(2);
            remoteObjectWrapper.invoke(byteArray);
        }
        
        public static function asToJavaDateTest():void {
            var date:Date = new Date();
            date.time = 1271699264375;
            remoteObjectWrapper.invoke(date);
        }
        
        public static function asToJavaIntTest():void {
            var intValue:int = -1;
            // java: Integer.MAX_VALUE = 2147483647.
            var uintValue1:uint = 2147483648; // this will be rounded to 2147483647!
            var uintValue2:uint = 2147483649; // this will be rounded to 2147483647!
            remoteObjectWrapper.invoke(intValue, uintValue1, uintValue2);
        }
        
        public static function asToJavaNumberTest():void {
            remoteObjectWrapper.invoke(new Number(-1.5));
        }
        
        public static function asToJavaStringTest():void {
            var stringValue1:String = "";
            var stringValue2:String = "foo";
            var undefinedValue:String;   // Note: undefined is converted to null.
            var nullValue:String = null; // Note: null is converted to null.
            remoteObjectWrapper.invoke(stringValue1, stringValue2, undefinedValue, nullValue);
        }
        
        public static function asToJavaGenericObjectTest():void {
            // null
            var nullValue:Object = null;
            
            // simple object
            var simpleObject:Object = new Object();
            
            // has null property
            var hasNullProperty:Object = new Object();
            hasNullProperty.property = null;
            
            // has String object
            var hasStringProperty:Object = new Object();
            hasStringProperty.property = "foo"
                
            // has Data object
            var hasDateProperty:Object = new Object();
            hasDateProperty.property = new Date();
            
            // has nested property
            var hasNestedProperty:Object = new Object();
            hasNestedProperty.property = new Object();
            hasNestedProperty.property.nestedProperty = new Date();
            
            remoteObjectWrapper.invoke(nullValue, simpleObject, hasNullProperty, hasStringProperty, hasDateProperty, hasNestedProperty);
        }
        
        public static function asToJavaTypedObjectTest():void {
            // null
            var nullValue:Object = null;
            
            // simple object
            var simpleObject:Object = new SimpleObject();
            
            // simple object with private constructor
            var simpleObjectWithPrivateConstructor:SimpleObjectWithPrivateConstructor = new SimpleObjectWithPrivateConstructor();
            
            // simple object with properties
            var simpleObjectWithProperties:SimpleObjectWithProperties = new SimpleObjectWithProperties(10, "foo", new SimpleObject());
            
            // nested properties
            var nestableObject:NestableObject = new NestableObject(new NestableObject(new NestableObject(null)));
            
            // java object has private fields
            var withPrivateFiled:WithPrivateFiled = new WithPrivateFiled("foo");
            
            // enum
            var enumObject:EnumObject = EnumObject.KUMA_SAN;

            remoteObjectWrapper.invoke(nullValue, simpleObject, simpleObjectWithPrivateConstructor, simpleObjectWithProperties, nestableObject, withPrivateFiled);
        }
        
        public static function asToJavaEnumTest():void {
            var enumValue:EnumObject = EnumObject.KUMA_SAN;
            remoteObjectWrapper.invoke(enumValue);
        }
        
        private static function createResultHandler(methodName:String):Function {
            return function(result:Boolean):void{Alert.show("methodName : " + (result ? "passed" : "failed"))}
        }
    }
}

experiment/SimpleObject.java
package experiment;

public class SimpleObject {
}

experiment/SimpleObjectWithPrivateConstructor.java
package experiment;

public class SimpleObjectWithPrivateConstructor {
    private SimpleObjectWithPrivateConstructor(){};
}

experiment/SimpleObjectWithProperties.java
package experiment;

public class SimpleObjectWithProperties {
    public int intValue;
    public String stringValue;
    public SimpleObject simpleObject;
    private SimpleObjectWithProperties(){}
    public SimpleObjectWithProperties(int intValue, String stringValue, SimpleObject simpleObject) {
        this.intValue = intValue;
        this.stringValue = stringValue;
        this.simpleObject = simpleObject;
    }    
}

experiment/WithPrivateFiled.java
package experiment;

public class WithPrivateFiled {
    private String stringValue;
    private WithPrivateFiled(){}
    public WithPrivateFiled(String stringValue) { this.stringValue = stringValue; }
    public String getStringValue() { return stringValue; }
}


Test source code(Flex)

test/EnumObject.as
package test {
    
    [RemoteClass(alias="experiment.EnumObject")]
    public class EnumObject {
        
        // Enum constants
        public static const KUMA_SAN:EnumObject = new EnumObject("KUMA_SAN", "くまさん");
        public static const TANUKI_SAN:EnumObject = new EnumObject("TANUKI_SAN", "たぬきさん");
        
        // Enum name. This must be public and not static valiable.
        public var val:String;
        
        [Transient]
        public var string:String;
        
        // Constructor
        public function EnumObject(name:String, string:String) {
            this.val = name;
            this.string = string;
        }
        
        // for displaying.
        public function toString():String {
            return string;
        }
        
        // use equality instead of identity because of the limitation of ActionScript.
        public function equals(object:Object):Boolean {
            return this.toString() == object.toString();
        }
    }
}

test/NestableObject.as
package test {
    
    [RemoteClass(alias="experiment.NestableObject")]
    public class NestableObject {
        
        // nested value
        public var nestableObject:NestableObject;
        
        // constructor
        public function NestableObject(nestableObject:NestableObject) {
            this.nestableObject = nestableObject;
        }
    }
}

test/RemoteObjectWrapper.as
package test {
    import mx.controls.Alert;
    import mx.rpc.events.FaultEvent;
    import mx.rpc.events.ResultEvent;
    import mx.rpc.remoting.Operation;
    import mx.rpc.remoting.RemoteObject;

    public class RemoteObjectWrapper {
        private var remoteObject:RemoteObject;
        public function RemoteObjectWrapper(destination:String):void {
            this.remoteObject = new RemoteObject(destination);
        }
        public function invoke(...args):void {
            var mathodName:String = getCallerSimpleName();
            remoteObject[mathodName].addEventListener("result", function (event:ResultEvent):void{Alert.show(mathodName + " : " + (event.result ? "passed" : "failed"))});
            remoteObject.addEventListener(FaultEvent.FAULT, function(faultEvent:FaultEvent):void{Alert.show("Fault : " + faultEvent)});
            var operation:Operation = remoteObject[mathodName];
            operation.arguments = args;
            operation.send();
        }
        private function getCallerSimpleName():String {
            var block:String = new Error().getStackTrace().split("at ")[3];
            var startIndex:int = block.indexOf("at ");// start of first line
            var endIndex:int = block.indexOf("()");   // end of function name
            var fullName:String = block.substring(startIndex + 3, endIndex);
            var callerSimpleName:String = fullName.substring(fullName.indexOf("/") + 1, endIndex);
            return callerSimpleName; // this is caller's caller.
        }
    }
}

test/SerializationTestService.as
package test {    
    import flash.utils.ByteArray;
    
    import mx.controls.Alert;

    public final class SerializationTestService {
        private static var remoteObjectWrapper:RemoteObjectWrapper = new RemoteObjectWrapper("SerializationTestService");
        
        public static function asToJavaBooleanTest():void {
            remoteObjectWrapper.invoke(true, false, "true", "false");
        }
        
        public static function asToJavaByteArrayTest():void {
            var byteArray:ByteArray = new ByteArray();
            byteArray.writeByte(0);
            byteArray.writeByte(1);
            byteArray.writeByte(2);
            remoteObjectWrapper.invoke(byteArray);
        }
        
        public static function asToJavaDateTest():void {
            var date:Date = new Date();
            date.time = 1271699264375;
            remoteObjectWrapper.invoke(date);
        }
        
        public static function asToJavaIntTest():void {
            var intValue:int = -1;
            // java: Integer.MAX_VALUE = 2147483647.
            var uintValue1:uint = 2147483648; // this will be rounded to 2147483647!
            var uintValue2:uint = 2147483649; // this will be rounded to 2147483647!
            remoteObjectWrapper.invoke(intValue, uintValue1, uintValue2);
        }
        
        public static function asToJavaNumberTest():void {
            remoteObjectWrapper.invoke(new Number(-1.5));
        }
        
        public static function asToJavaStringTest():void {
            var stringValue1:String = "";
            var stringValue2:String = "foo";
            var undefinedValue:String;   // Note: undefined is converted to null.
            var nullValue:String = null; // Note: null is converted to null.
            remoteObjectWrapper.invoke(stringValue1, stringValue2, undefinedValue, nullValue);
        }
        
        public static function asToJavaGenericObjectTest():void {
            // null
            var nullValue:Object = null;
            
            // simple object
            var simpleObject:Object = new Object();
            
            // has null property
            var hasNullProperty:Object = new Object();
            hasNullProperty.property = null;
            
            // has String object
            var hasStringProperty:Object = new Object();
            hasStringProperty.property = "foo"
                
            // has Data object
            var hasDateProperty:Object = new Object();
            hasDateProperty.property = new Date();
            
            // has nested property
            var hasNestedProperty:Object = new Object();
            hasNestedProperty.property = new Object();
            hasNestedProperty.property.nestedProperty = new Date();
            
            remoteObjectWrapper.invoke(nullValue, simpleObject, hasNullProperty, hasStringProperty, hasDateProperty, hasNestedProperty);
        }
        
        public static function asToJavaTypedObjectTest():void {
            // null
            var nullValue:Object = null;
            
            // simple object
            var simpleObject:Object = new SimpleObject();
            
            // simple object with private constructor
            var simpleObjectWithPrivateConstructor:SimpleObjectWithPrivateConstructor = new SimpleObjectWithPrivateConstructor();
            
            // simple object with properties
            var simpleObjectWithProperties:SimpleObjectWithProperties = new SimpleObjectWithProperties(10, "foo", new SimpleObject());
            
            // nested properties
            var nestableObject:NestableObject = new NestableObject(new NestableObject(new NestableObject(null)));
            
            // java object has private fields
            var withPrivateFiled:WithPrivateFiled = new WithPrivateFiled("foo");
            
            // enum
            var enumObject:EnumObject = new EnumObject("");

            remoteObjectWrapper.invoke(nullValue, simpleObject, simpleObjectWithPrivateConstructor, simpleObjectWithProperties, nestableObject, withPrivateFiled);
        }
        
        public static function asToJavaEnumTest():void {
            var enumValue:EnumObject = EnumObject.KUMA_SAN;
            remoteObjectWrapper.invoke(enumValue);
        }
        
        private static function createResultHandler(methodName:String):Function {
            return function(result:Boolean):void{Alert.show("methodName : " + (result ? "passed" : "failed"))}
        }
    }
}

test/SerializationTestServiceApp.mxml
<?xml version="1.0" encoding="utf-8"?>
<!-- Application to invoke serialization tests. -->

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
               xmlns:s="library://ns.adobe.com/flex/spark" 
               xmlns:mx="library://ns.adobe.com/flex/mx" width="100%" height="100%">
    <s:layout>
        <s:VerticalLayout horizontalAlign="center" verticalAlign="middle"/>
    </s:layout>
    <fx:Script>
        <![CDATA[
            import test.SerializationTestService;
            protected function test():void {
                SerializationTestService.asToJavaBooleanTest();
                SerializationTestService.asToJavaByteArrayTest();
                SerializationTestService.asToJavaDateTest();
                SerializationTestService.asToJavaIntTest();
                SerializationTestService.asToJavaNumberTest();
                SerializationTestService.asToJavaStringTest();
                SerializationTestService.asToJavaGenericObjectTest();
                SerializationTestService.asToJavaTypedObjectTest();
                SerializationTestService.asToJavaEnumTest();
            }
        ]]>
    </fx:Script>
    <s:Button id="testButton" label="Test" click="test()"/>
</s:Application>

test/SimpleObject.as
package test {
    [RemoteClass(alias="experiment.SimpleObject")]
    public class SimpleObject {
        public function SimpleObject() {}
    }
}

test/SimpleObjectWithPrivateConstructor.as
package test {
    [RemoteClass(alias="experiment.SimpleObjectWithPrivateConstructor")]
    public class SimpleObjectWithPrivateConstructor {
        public function SimpleObjectWithPrivateConstructor() {
        }
    }
}

test/SimpleObjectWithProperties.as
package test {
 [RemoteClass(alias="experiment.SimpleObjectWithProperties")]
 public class SimpleObjectWithProperties {
  
  // properties
  public var intValue:int;
  public var stringValue:String;
  public var simpleObject:SimpleObject;
  
  // constructor
  public function SimpleObjectWithProperties(intValue:int, stringValue:String, simpleObject:SimpleObject):void {
   this.intValue = intValue;
   this.stringValue = stringValue;
   this.simpleObject = simpleObject;
  }
 }
}

test/WithPrivateFiled.as
package test {
    [RemoteClass(alias="experiment.WithPrivateFiled")]
    public class WithPrivateFiled {
        
        // The field of ActionSide is public. Java side of that is private.
        public var stringValue:String;
        
        // constructor
        public function WithPrivateFiled(stringValue:String) {
            this.stringValue = stringValue;
        }
    }
}

2010年4月19日月曜日

作業ログ:BlazeDSのRemotingServiceでJavaのenumを扱う方法

ActionScrip側からAMFで送られた文字列をBlazeDS4側でenumに変換しようとしたところ、
No enum const class 
と怒られてしまい、うまくいきませんでした。

flex 側で IExternalizable を実装して String を投げるようにしてみたのですが、そうすると今度は BlazeDS 側で配列に変更しようとしてエラーで落ちました。

方法が間違っていたのかもしれませんが、どうも解決法が見つけられなかったので、とりあえずはワークアラウンドで対応することにしました。

(´・ω・`) 正規の方法を教えてください、、、

4.21.2010追記:正規の方法がありました。詳細はこちらを参照のこと。


workaround

  • ActionScript 側の Enum (のような)実装では、内部に必ず Enum.valueOf() メソッドの第二引数に対応する文字列を持つpublicなインスタンス変数(or getter/setter)を配置する。
  • ActionScript 側の Enum (のような)実装では、必ず public var name:String を定義し、java 側の Enum#getName() と対応させる。
  • それ以外の変数(or getter/setter)を配置してもよいが、すべてメタタグ [Transient] をつける。
  • BlazeDS4のソースにパッチを当てる。
AMFから受け取った情報が、値がString型のエントリを一つだけ持つASObjectになればOKです。
この方法だと、副作用として flex 側で IExternalizable を実装する必要がないのでラクチンです。


パッチ

対象のソースコードは、flex.messaging.io.amf.translator.decoder.EnumDecoder クラスです。

元のコードは以下のようになっています。

55: Enum value = Enum.valueOf(desiredClass, encodedObject.toString());

encodedObject の型が flex.messaging.io.amf.ASObject 型なので、encodedObject.toString() の値は以下のようなフォーマットの文字列に変換されます。

ASObject(33469162){name=HOGE}

上記のワークアラウンドを適用すると、ASObjectはひとつだけエントリを持つマップとなるはずなので、そのエントリの値を使えば Enum が復元できます。

以下が変更内容です。ASObject以外が来る場合もあるかもしれないので(まだそこまでソースを追ってません)一応、オリジナルのコードも残しておきます。

final Enum value;
if (ASObject.class.isInstance(encodedObject)) {
 // patch for workaround
 value = Enum.valueOf(desiredClass, ASObject.class.cast(encodedObject).values().iterator().next().toString());
} else {
 // original
 value = Enum.valueOf(desiredClass, encodedObject.toString());
}


ActionScript 側のEnum(のようなもの)の例

public final class Hoge {
 public var name:String;
 public function Hoge(name:String) { this.name = name; }
 public static const HOGETAROU:Hoge = new Hoge("HOGETAROU");
 public static const HOGEJIROU:Hoge = new Hoge("HOGEJIROU");
 public static const HOGENOSUKE:Hoge = new Hoge("HOGENOSUKE");
}

ActionScriptファイル内でXMLファイルのembed設定をするサンプル

ActionScript ファイル内で XML ファイルの embed 設定をする方法のサンプルです。


構成
  • sample.xml
    サンプルのXMLファイル。
  • XmlService.as
    サンプルのXMLファイルへのアクセスを提供する ActionScript ファイル。
  • EmbeddedXmlTest.mxml
    上記の利用するサンプルアプリケーション。

各ファイルを sample パッケージ直下に配置すれば動きます。


見た目はこんな感じです。

sample.xml
<words>
 <word id="1" value="cat" />
 <word id="2" value="dog" />
 <word id="3" value="cow" />
 <word id="4" value="lion" />
 <word id="5" value="monkey" />
 <word id="6" value="squirrel" />
 <word id="7" value="elephant" />
 <word id="8" value="hippopotamus" />
 <word id="9" value="rabbit" />
</words>


XmlService.as
package sample  {
 
 // 埋め込まれた xml ファイルを取得するサービス
 public class XmlService {
  
  /**
   * 埋め込まれた xml ファイル
   */
  [Embed(source="sample.xml")]
  private static const _xml:Class;
    
  /**
   * xml ファイルの内容を表す XML 型の変数
   */
  public static const xml:XML = _xml.data;
  
  /**
   * 指定されたインデックスの word 要素を返します。
   */
  public static function getWord(index:uint):XML {
   return xml.word.(@id==index)[0];
  }
 }
}


EmbeddedXmlTest.mxml
<?xml version="1.0" encoding="utf-8"?>

<!-- XMLファイルの内容を TextArea と DataGrid に表示するサンプル -->

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
      xmlns:s="library://ns.adobe.com/flex/spark" 
      xmlns:mx="library://ns.adobe.com/flex/mx" width="100%" height="100%">
 <s:layout>
  <s:VerticalLayout verticalAlign="middle" horizontalAlign="center"/>
 </s:layout>
 <mx:HBox width="200" borderVisible="true" verticalAlign="middle" borderColor="black" borderStyle="solid">
  <mx:Label text="id : {idNs.value}"/>
  <mx:NumericStepper id="idNs" width="20" change="wordLabel.text = XmlService.getWord(idNs.value).@value" minimum="1" maximum="{XmlService.xml.word.length()}"/>
  <mx:Label id="wordLabel" width="100%"/>
 </mx:HBox>
 <mx:DataGrid id="grid" x="200" y="200" dataProvider="{XmlService.xml.word}" selectedIndex="{idNs.value-1}">
  <mx:columns>
   <mx:DataGridColumn headerText="id" dataField="@id"/>
   <mx:DataGridColumn headerText="value" dataField="@value"/>
  </mx:columns>
 </mx:DataGrid>
</s:Application>

しかし、この手のネタって検索エンジンで引っ掛けるのが難しいですよね。flex, embed, XML, ActionScript など、どの検索ワードも一般的すぎる、、、。

2010年4月18日日曜日

作業ログ:login機能使用時の SessionDuplicate 問題

作業ログ:GAE/J + BlazeDS 4 環境について考えてみた。にて、同じ session id を持つ異なる FlexSession のインスタンスを同一とみなすパッチを当てることにより、セッション重複の誤検出をしないように変更しました。

しかし、 login / logout 機能周りでもセッション重複の誤検出が行われるようです。

以下、状況を確認するために仕込んだログ。
// 最初のアクセス(login)
04-17 05:50AM 29.350 MessageBrokerServlet#service() : begin
04-17 05:50AM 29.350 FlexContext#setThreadLocalObjects(): null is set.
04-17 05:50AM 29.350 FlexContext#setThreadLocalSession(): flex.messaging.HttpFlexSession@c1b161 is set. sessionId = BwbsdybycCfB7kDRSLErnw
04-17 05:50AM 29.350 HttpFlexSessionProvider#getOrCreateSession() : HttpFlexSession is retrieved from thread local. id = BwbsdybycCfB7kDRSLErnw
04-17 05:50AM 29.352 flexClient is retrieved from in-memory flexClients repository.
04-17 05:50AM 29.352 AbstractEndpoint#setupFlexClient(C4DB9840-340F-6FC7-5604-28ADF310987A) : flex.messaging.HttpFlexSession@1d952cdf
04-17 05:50AM 29.352 FlexClient#registerFlexSession() : httpClient = flex.messaging.client.FlexClient@d210ab, sessionId = BwbsdybycCfB7kDRSLErnw
04-17 05:50AM 29.359 doAuthentication
04-17 05:50AM 31.199 MessageBrokerServlet#service() : end


// 二回目のアクセス(logout)
04-17 05:50AM 33.584 MessageBrokerServlet#service() : begin
04-17 05:50AM 33.584 FlexContext#setThreadLocalObjects(): null is set.
04-17 05:50AM 33.584 FlexContext#setThreadLocalSession(): flex.messaging.HttpFlexSession@1601a4f is set. sessionId = BwbsdybycCfB7kDRSLErnw
04-17 05:50AM 33.584 HttpFlexSessionProvider#getOrCreateSession() : HttpFlexSession is retrieved from thread local. id = BwbsdybycCfB7kDRSLErnw
04-17 05:50AM 33.585 flexClient is retrieved from in-memory flexClients repository.
04-17 05:50AM 33.585 AbstractEndpoint#setupFlexClient(C4DB9840-340F-6FC7-5604-28ADF310987A) : flex.messaging.HttpFlexSession@1d952cdf
04-17 05:50AM 33.585 FlexClient#registerFlexSession() : httpClient = flex.messaging.client.FlexClient@d210ab, sessionId = BwbsdybycCfB7kDRSLErnw
04-17 05:50AM 33.586 logout
04-17 05:50AM 33.620 sessionDestroyed : id = BwbsdybycCfB7kDRSLErnw
04-17 05:50AM 33.620 attributeRemoved : name = __flexSession, value = flex.messaging.HttpFlexSession@1d952cdf
04-17 05:50AM 33.702 sessionDestroyed : id = BwbsdybycCfB7kDRSLErnw
04-17 05:50AM 33.758 FlexContext#setThreadLocalObjects(): null is set.
04-17 05:50AM 33.760 MessageBrokerServlet#service() : end


// 三回目のアクセス(?)
04-17 05:50AM 34.276 MessageBrokerServlet#service() : begin
04-17 05:50AM 34.276 FlexContext#setThreadLocalObjects(): null is set.
04-17 05:50AM 34.374 sessionCreated : id = KHeEJ_L9FBgQol3Hj15rlw
04-17 05:50AM 34.374 FlexContext#setThreadLocalSession(): flex.messaging.HttpFlexSession@e7b3cf is set. sessionId = KHeEJ_L9FBgQol3Hj15rlw
04-17 05:50AM 34.374 attributeAdded : name = __flexSession, value = flex.messaging.HttpFlexSession@e7b3cf
04-17 05:50AM 34.374 HttpFlexSessionProvider#getOrCreateSession() : HttpFlexSession is created. id = KHeEJ_L9FBgQol3Hj15rlw
04-17 05:50AM 34.375 flexClient is retrieved from in-memory flexClients repository.
04-17 05:50AM 34.375 AbstractEndpoint#setupFlexClient(C4DB9840-340F-6FC7-5604-28ADF310987A) : flex.messaging.HttpFlexSession@b89a50a6
04-17 05:50AM 34.375 FlexClient#registerFlexSession() : httpClient = flex.messaging.client.FlexClient@d210ab, sessionId = KHeEJ_L9FBgQol3Hj15rlw
04-17 05:50AM 34.376 sessions.size() = 2
04-17 05:50AM 34.376 sessionId = BwbsdybycCfB7kDRSLErnw
04-17 05:50AM 34.376 sessionId = KHeEJ_L9FBgQol3Hj15rlw
04-17 05:50AM 34.376 FlexClient#registerFlexSession() : httpClient = flex.messaging.client.FlexClient@d210ab, sessionId = KHeEJ_L9FBgQol3Hj15rlw
04-17 05:50AM 34.406 sessionDestroyed : id = BwbsdybycCfB7kDRSLErnw
04-17 05:50AM 34.420 sessionDestroyed : id = KHeEJ_L9FBgQol3Hj15rlw
04-17 05:50AM 34.420 attributeRemoved : name = __flexSession, value = flex.messaging.HttpFlexSession@b89a50a6
04-17 05:50AM 34.603 sessionCreated : id = tcnt9ZwRj6jAnqib-C9o0A
04-17 05:50AM 34.604 FlexContext#setThreadLocalSession(): flex.messaging.HttpFlexSession@1912a56 is set. sessionId = tcnt9ZwRj6jAnqib-C9o0A
04-17 05:50AM 34.604 attributeAdded : name = __flexSession, value = flex.messaging.HttpFlexSession@1912a56
04-17 05:50AM 34.604 HttpFlexSessionProvider#getOrCreateSession() : HttpFlexSession is created. id = tcnt9ZwRj6jAnqib-C9o0A
04-17 05:50AM 34.604 FlexContext#setThreadLocalObjects(): flex.messaging.HttpFlexSession@b8b7ad0d is set.
04-17 05:50AM 34.627 MessageBrokerServlet#service() : end

  • 一回目のアクセス
    • 問題なく普通にログインできてる感じです。
  • 二回目のアクセス
    • sessionDestroyed が二回呼ばれてますね。実害はなさそうですが。
  • 三回目のアクセス
    • FlexClientに古いsessionが残ってますね。それで、新規のsessionが追加されることによって重複が検出されているみたいです。
    • 二回目のアクセスで削除されたはずの BwbsdybycCfB7kDRSLErnw に sessionDestroyed が呼ばれてますね。
何気に、全アクセスのパスが /messagebroker/amf;jsessionid=BwbsdybycCfB7kDRSLErnw になってるなぁ。これが原因? ということで、そうならない形で再実験。cookie 削除して、flashも新規にロードして、ついでに flash 側でもすべてのハンドラにログ仕込んで、さぁ、どうなるか?

logout 後にもう一度アクセスしに行って、そのときにエラーが返されてますね。上記の3回目のアクセスと同様と思われます。それが原因か?

いくつか気持ち悪い問題がありますね。
  • 二回目のアクセスで削除されたはずの BwbsdybycCfB7kDRSLErnw に対して sessionDestroyed が発行されるという問題。
  • 二回目のアクセスで BwbsdybycCfB7kDRSLErnw のセッションを削除したのに、オンメモリ上の FlexClient からは情報が削除されていない。
  • 開発環境では発生せずGAE環境だけで発生する。
ひとまず、一つ目の問題について、専用のテストプログラムを作成し、開発環境とGAE環境で動作させて見ることにします。

ローカルだと IllegalStateException で落ちますが、sessionDestroyedのハンドラは呼ばれました。


結局

振り返ってみると、調査開始時にはずいぶん勘違いがあったなぁ。
インメモリのリポジトリって何だよwww

という感じで、上記からけっこう激しく試行錯誤した挙句、以下に落ち着きました。

flex.messaging.client.FlexClient クラスの registerFlexSession(FlexSession) メソッドについて、以下のように修正します。
public void registerFlexSession(FlexSession session)
{
    if (sessions.addIfAbsent(session))
    {
        session.addSessionDestroyedListener(this);
        session.registerFlexClient(this);
    }
    // for session serialization
    else if (session.getFlexClients().isEmpty())
    {
        session.addSessionDestroyedListener(this);
        session.registerFlexClient(this);
    }
}


(´・ω・`) しかし、稀に重複検出の例外が起きるのです。

ログを見る限り、サーバ上では、logout メッセージ でセッションを破棄し、, disconnect メッセージで再度セッションを生成しするのですが、次のログイン前の ping で HTTP レベルのセッションが取得できないことがあるようです。

2,3秒間隔で行うとこの問題は発生しません。

GAEではセッション情報を必ずデータストアから取得するようなので、データストア側で遅延があるのであれば、セッションを取得できないのは納得できます(TODO:要調査)。しかし、それだけでは問題にはなりません。なぜ以前のセッション情報として格納されていた FlexClient の情報が復活しているかということです。FlexClient は実際にはシリアライズされるわけではなく毎回復元されるものなので、以前の情報が残っていることはありえません。となると、この仮定が間違っているのか、ほかに見落としている点があるかのどちらかということになります(TODO:要調査)。

まぁ、今のところはログアウト後に速攻でログインしない限り発生しないので、しばらくは放置しようかと思います。

(´・ω・`) GAE環境は調査に時間がかかりすぎるのねん、、、。

2010年4月17日土曜日

GAE 環境につないだ Flex クライアントを FlexBuilder からデバッグする方法

Flash のクライアントを持つアプリケーションを Google App Engine 上で開発する場合、FlashBuilder 上でデバッグモードで動作するクライアントと GAE をつなぎたくなります。

そのような場合は、以下の手順で対応可能です。
  • FlashBuilder のプロジェクト側で、GAE のパスを Root URL に設定する。
  • デバッグモードでクライアントアプリを起動する。
以上。

作業ログ:GAE/J + BlazeDS 4 環境について考えてみた。

以下、GAE/J + BlazeDS 4 環境について考えてみた作業ログ。

方針

以前、GAE 上で普通に remoting や messaging をしようと思ったところGAE/J + BlazeDS 環境の DuplicateSessionDetected 問題に突き当たりました。また、security 機能を利用しようとした際には後GAE/J + BlazeDS3 環境の HttpSessionListener 問題に突き当たりました。

これらの経緯を考えると、今後も多くの問題に突き当たることが考えられます。これより先に進むのであれば、それなりの覚悟が必要になります。しかし、オープンソースのソフトウェアである限り、手詰まりに可能性が低く、最終的に詰むというリスクは低く抑えられています。これはやるしかないでしょう。

となると、どこまでやるかという問題がありますが、自分たちが必要な機能についてはすべて自分たちで理解したうえでコントロールするというところが落としどころかとおもいます。具体的には、GAE 上で動作すればよく、まずはロールなしのセッションレベルの認証ができればいいし、通常の AMF で remoting ができればOKです。今後メッセージングも必要になるかもしれないとか、GAE上なのでストリーミングが必要になることは当面ありえないだろうとか。そんな感じです。少なくともその時点で必要な機能かつ問題のあるソースはすべて把握しておく必要があり、そのためには全体の構成と主要なクラスについては、ソースレベルで完全に把握する必要があります。

(`・ω・´) さて、どこから手をつけようか?


まずは環境整理

これから作業をする上で、クリーンな環境を維持することは重要になります。そのため、環境整備の手順を整理しておく必要があります。

ということで、GAE/J + BlazeDS 4 環境の構築に内容をまとめました。


GAE/J + BlazeDS 環境の DuplicateSessionDetected 問題を再び追いながら情報収集


Servletのバージョン

BlazeDS は Servlet2.3以上(TODO:出典明記)
GAE は Servlet2.4(TODO:出典明記)


Application Lifecycle Event

BlazeDS 4 では web.xml で以下のように flex.messaging.HttpFlexSession を Application Lifecycle Event のリスナーとして設定しています。
<!-- Http Flex Session attribute and binding listener support -->
<listener>
  <listener-class>flex.messaging.HttpFlexSession</listener-class>
</listener>

このクラスは以下のインタフェースを実装しています。
javax.servlet.http.HttpSessionAttributeListener
javax.servlet.http.HttpSessionBindingListener
javax.servlet.http.HttpSessionListener

HttpFlexSession が実装するリスナーのメソッドと、実行内容の概要を以下に示します。
  • void valueBound(HttpSessionBindingEvent event)
    • 何もしません。
  • void valueUnbound(HttpSessionBindingEvent event)
    • HttpSessionの属性が削除された場合に flexSession.superInvalidate() を呼びます。。
    • isHttpSessionListener が true の場合、つまり通常は何もしません。
    • GAE 環境では、呼ばれたら実行されるはずです。(追記:GAE環境でも実行されません)
    • 属性が削除されただけでなぜ flexSession.superInvalidate() を呼ぶ必要があるのか?
  • void sessionCreated(HttpSessionEvent event)
    • isHttpSessionListener フラグを true にします。
  • void sessionDestroyed(HttpSessionEvent event)
    • HttpSessionが削除されたらFlexSessionも削除されるようにします。
    • attributeRemovedイベントよりよりこちらが先に行われる場合の対策として、attributeRemovedこちらからもattributeRemovedを呼びに行っています。
    • void attributeAdded(HttpSessionBindingEvent event)
    • 属性変更イベント名が"__flexSession"の場合のみ、flexSession.notifyAttributeBound(name, value)とnotifyAttributeAdded(name, value)を呼びに行く。
    • 必要な属性だけコピーするということですね。
  • void attributeRemoved(HttpSessionBindingEvent event)
    • 属性変更イベント名が"__flexSession"の場合のみ、flexSession.notifyAttributeUnbound(name, value) と flexSession.notifyAttributeRemoved(name, value) を呼びに行く。
  • void attributeReplaced(HttpSessionBindingEvent event)
    • 属性変更イベント名が"__flexSession"の場合のみ、flexSession.notifyAttributeUnbound(name, value) と flexSession.notifyAttributeReplaced(name, value) と flexSession.notifyAttributeBound(name, newValue) を呼びに行く。

ログを仕込んで、ローカル環境で実行してみました。
// 初回
MessageBrokerServlet#service() : begin
HttpFlexSession#sessionCreated() : sessionId = 13x3l8s1hg56a
HttpFlexSession#valueBound() : name = __flexSession : sessionId = 13x3l8s1hg56a
HttpFlexSession#attributeAdded() : name = __flexSession : sessionId = 13x3l8s1hg56a
MessageBrokerServlet#service() : end
MessageBrokerServlet#service() : begin
EchoService#echo() : sessionId = 13x3l8s1hg56a
MessageBrokerServlet#service() : end

// 二回目
MessageBrokerServlet#service() : begin
EchoService#echo() : sessionId = 1l72bdhrrlfvy
MessageBrokerServlet#service() : end
デバッガでも追ったところ、Sevletコンテナ側から正しく呼び出されていました。(正しく動作するということが、GAE的にはバグなんですね。)

初回は2回serviceが呼ばれてますね。TODO:要調査


今度はサーバ側にデプロイして同様のことをしてみます。
// 一回目のアクセス(例のセッション重複で失敗)
04-15 02:56PM 55.052 MessageBrokerServlet#service() : begin
04-15 02:56PM 55.283 MessageBrokerServlet#service() : end

04-15 02:56PM 55.555 MessageBrokerServlet#service() : begin
04-15 02:56PM 55.587 HttpFlexSession#sessionDestroyed() : sessionId = CRls4b-kT7-DCbJSV6yFbw
04-15 02:56PM 55.587 FlexSession#invalidate()
04-15 02:56PM 55.870 HttpFlexSession#sessionDestroyed() : sessionId = CRls4b-kT7-DCbJSV6yFbw
04-15 02:56PM 55.961 MessageBrokerServlet#service() : end

// 二回目のアクセス(成功)
04-15 03:17PM 18.307 MessageBrokerServlet#service() : begin
04-15 03:17PM 18.526 HttpFlexSession#sessionCreated() : sessionId = V1pWQsjzwKuDKYUS5IlpZg
04-15 03:17PM 18.527 HttpFlexSession#valueBound() : name = __flexSession : sessionId = V1pWQsjzwKuDKYUS5IlpZg
04-15 03:17PM 18.527 HttpFlexSession#attributeAdded() : name = __flexSession : sessionId = V1pWQsjzwKuDKYUS5IlpZg
04-15 03:17PM 19.251 MessageBrokerServlet#service() : end

04-15 03:17PM 19.719 MessageBrokerServlet#service() : begin
04-15 03:17PM 19.790 HttpFlexSession#sessionDestroyed() : sessionId = V1pWQsjzwKuDKYUS5IlpZg
04-15 03:17PM 19.790 FlexSession#invalidate()
04-15 03:17PM 19.790 HttpFlexSession#valueUnbound() : NOP : name = __flexSession : sessionId = V1pWQsjzwKuDKYUS5IlpZg
04-15 03:17PM 19.790 HttpFlexSession#attributeRemoved() : name = __flexSession : sessionId = V1pWQsjzwKuDKYUS5IlpZg
04-15 03:17PM 19.889 HttpFlexSession#sessionDestroyed() : sessionId = V1pWQsjzwKuDKYUS5IlpZg
04-15 03:17PM 19.976 MessageBrokerServlet#service() : end

04-15 03:20PM 00.169 MessageBrokerServlet#service() : begin
04-15 03:20PM 00.465 HttpFlexSession#sessionCreated() : sessionId = 0Gqlzdy3J_I2n281ANfQmQ
04-15 03:20PM 00.466 HttpFlexSession#valueBound() : name = __flexSession : sessionId = 0Gqlzdy3J_I2n281ANfQmQ
04-15 03:20PM 00.466 HttpFlexSession#attributeAdded() : name = __flexSession : sessionId = 0Gqlzdy3J_I2n281ANfQmQ
04-15 03:20PM 02.255 EchoService#echo() : sessionId = 0Gqlzdy3J_I2n281ANfQmQ
04-15 03:20PM 02.375 MessageBrokerServlet#service() : end

おや?

HttpFlexSession#sessionCreated() は、Servletコンテナのライフサイクルイベントとして以外には絶対に呼ばれないということはソースから確認できます。なのに呼ばれているということは、GAE上でもライフサイクルイベントが呼ばれるということですね。

ということで、GAE/J 環境の Application Lifecycle Listener の実験をしてみたところ、やはり GAE 上でもライフサイクルイベントは呼ばれるようです。

ということで、今度は上記テストのリスナーをコンテナに配備して、埋め込んだデバッグ情報を一度ほぼクリアにしてから、再度動作検証をしていくことにします。ブラウザのcookieは事前に削除しておきます。念のため、_ah_SESSION 情報も削除しておきます。関係ないと思いつつもWeb サイトのプライバシー設定パネルから flash のローカルクッキーも削除します。

開発サーバでの実行
// 一回目のアクセス
sessionCreated : id = 1snep0z5nb1pd
attributeAdded : name = __flexSession, value = flex.messaging.HttpFlexSession@14a18d
EchoService#echo() : sessionId = 1snep0z5nb1pd

// 二回目のアクセス
EchoService#echo() : sessionId = 1snep0z5nb1pd

// 三回目のアクセス
EchoService#echo() : sessionId = 1snep0z5nb1pd

本番サーバでの実行
// UIから一回目のアクセス /messagebroker/amf
04-16 05:16AM 04.683 sessionCreated : id = EmVdv905Ogr0kGuoKMfVHA
04-16 05:16AM 04.690 attributeAdded : name = __flexSession, value = flex.messaging.HttpFlexSession@165547d
// UIから一回しかアクセスしていないけどなぜか二回目のアクセスが行われている。/messagebroker/amf;jsessionid=EmVdv905Ogr0kGuoKMfVHA
04-16 05:16AM 05.584 sessionDestroyed : id = EmVdv905Ogr0kGuoKMfVHA
04-16 05:16AM 05.584 attributeRemoved : name = __flexSession, value = flex.messaging.HttpFlexSession@165547d
04-16 05:16AM 05.861 sessionDestroyed : id = EmVdv905Ogr0kGuoKMfVHA
【結果】Detected duplicate HTTP-based FlexSessions で失敗

// UIから二回目のアクセス /messagebroker/amf;jsessionid=EmVdv905Ogr0kGuoKMfVHA
04-16 05:18AM 19.795 sessionCreated : id = uqSuxURP-RXL1hsdcp2c0A
04-16 05:18AM 19.795 attributeAdded : name = __flexSession, value = flex.messaging.HttpFlexSession@34a1c8
04-16 05:18AM 19.830 sessionDestroyed : id = EmVdv905Ogr0kGuoKMfVHA
04-16 05:18AM 19.850 sessionDestroyed : id = uqSuxURP-RXL1hsdcp2c0A
04-16 05:18AM 19.850 attributeRemoved : name = __flexSession, value = flex.messaging.HttpFlexSession@34a1c8
04-16 05:18AM 20.008 sessionCreated : id = SaKtj72k_5tVABGvK7vOAg
04-16 05:18AM 20.008 attributeAdded : name = __flexSession, value = flex.messaging.HttpFlexSession@1e514a9
【結果】Detected duplicate HTTP-based FlexSessions で失敗)

// 三回目のアクセス /messagebroker/amf;jsessionid=SaKtj72k_5tVABGvK7vOAg
04-16 05:20AM 34.078 EchoService#echo() : sessionId = SaKtj72k_5tVABGvK7vOAg 
【結果】成功


現状のデータからの考察

開発環境上の動作は、意図したとおりのものです。しかし、本番環境上の動作は、意図とはかなり異なる動きをしています。

まず flash クライアントからの初回のアクセスが2回に分けて行われています。これ自体理由が分からないので調査したいところですが、何回アクセスがきたところで、セッション管理に影響するというのは問題です。まずは最初のアクセスからソースを追いつつ、仮定を立ててみます。

・仮定1:初回アクセスの2回目のHTTP呼び出しにて、BaseHTTPEndpoint#setupFlexClient() の432行で DuplicateSessionException が throw されている。
・仮定2:上記は同クラス400行にてflexClient.getFlexSessions()のサイズを2として受け取っている。
・仮定3:仮定2の直接の原因は、FlexClient の 1119 行目で複数の FlexSession が登録されてしまうこと。

とりあえず、ここまでを確認してみたところ、仮定1,2は正解。仮定3は可能性ありです。二回目のアクセスで、一回目で生成された FlexClient を使用しているのであれば、仮定3はほぼクロです。

ということで、ログを仕込んで再実行。

開発環境
// 初回(成功)
04-16 08:52AM 37.279 [beyondseeker-mixi-01/1.341297705342737136].: MessagingConfiguration#createFlexClientManager() : FlexClientManager is created with problem!
04-16 08:52AM 38.068 [beyondseeker-mixi-01/1.341297705342737136].: MessageBrokerServlet#service() : begin
04-16 08:52AM 38.322 [beyondseeker-mixi-01/1.341297705342737136].: sessionCreated : id = zpE2j7OXspDhdA1-960ZCg
04-16 08:52AM 38.325 [beyondseeker-mixi-01/1.341297705342737136].: attributeAdded : name = __flexSession, value = flex.messaging.HttpFlexSession@187e184
04-16 08:52AM 38.644 [beyondseeker-mixi-01/1.341297705342737136].: FlexClient#registerFlexSession() : httpClient = flex.messaging.client.FlexClient@92de02
04-16 08:52AM 38.644 [beyondseeker-mixi-01/1.341297705342737136].: FlexClient#registerFlexSession() : httpClient = flex.messaging.client.FlexClient@92de02
04-16 08:52AM 38.659 [beyondseeker-mixi-01/1.341297705342737136].: EchoService#echo() : sessionId = zpE2j7OXspDhdA1-960ZCg
04-16 08:52AM 38.705 [beyondseeker-mixi-01/1.341297705342737136].: MessageBrokerServlet#service() : end

本番環境
// 初回(成功)
04-16 08:52AM 37.279 MessagingConfiguration#createFlexClientManager() : FlexClientManager is created with problem!
04-16 08:52AM 38.068 MessageBrokerServlet#service() : begin
04-16 08:52AM 38.322 sessionCreated : id = zpE2j7OXspDhdA1-960ZCg
04-16 08:52AM 38.325 attributeAdded : name = __flexSession, value = flex.messaging.HttpFlexSession@187e184
04-16 08:52AM 38.644 FlexClient#registerFlexSession() : httpClient = flex.messaging.client.FlexClient@92de02
04-16 08:52AM 38.644 FlexClient#registerFlexSession() : httpClient = flex.messaging.client.FlexClient@92de02
04-16 08:52AM 38.659 EchoService#echo() : sessionId = zpE2j7OXspDhdA1-960ZCg
04-16 08:52AM 38.705 MessageBrokerServlet#service() : end

// 二回目(失敗)
04-16 08:53AM 39.110 MessageBrokerServlet#service() : begin
04-16 08:53AM 39.112 flexClient is retrieved from in-memory flexClients repository.
04-16 08:53AM 39.112 FlexClient#registerFlexSession() : httpClient = flex.messaging.client.FlexClient@92de02
04-16 08:53AM 39.112 sessions.size() = 2
04-16 08:53AM 39.112 sessionId = zpE2j7OXspDhdA1-960ZCg
04-16 08:53AM 39.112 sessionId = zpE2j7OXspDhdA1-960ZCg
04-16 08:53AM 39.112 FlexClient#registerFlexSession() : httpClient = flex.messaging.client.FlexClient@92de02
04-16 08:53AM 39.128 sessionDestroyed : id = zpE2j7OXspDhdA1-960ZCg
04-16 08:53AM 39.129 attributeRemoved : name = __flexSession, value = flex.messaging.HttpFlexSession@187e184
04-16 08:53AM 39.250 sessionDestroyed : id = zpE2j7OXspDhdA1-960ZCg
04-16 08:53AM 39.343 MessageBrokerServlet#service() : end

開発環境、本番環境に関わらず、二回目のアクセスはオンメモリの情報を取得しています。

FlexClientManager では、取得できればオンメモリのものを使用し、できなければサーバ上のものを使用するので、GAE 環境でも問題なさそうです。

そして、問題なのは、同じ sessionId を二箇所登録している部分です。これは以前
GAE/J + BlazeDS 環境の DuplicateSessionDetected 問題で調べたとおりなので、同様に対処します。

本番環境
// 初回(成功)
04-16 09:38AM 51.550 MessagingConfiguration#createFlexClientManager() : FlexClientManager is created with problem!
04-16 09:38AM 52.813 MessageBrokerServlet#service() : begin
04-16 09:38AM 53.228 FlexClient#registerFlexSession() : httpClient = flex.messaging.client.FlexClient@1bb60ad
04-16 09:38AM 53.228 FlexClient#registerFlexSession() : httpClient = flex.messaging.client.FlexClient@1bb60ad
04-16 09:38AM 53.250 EchoService#echo() : sessionId = IHEyAtViGH3J9Et5RT48mg
04-16 09:38AM 53.300 MessageBrokerServlet#service() : end

// 二回目(成功)
04-16 09:39AM 49.765 MessageBrokerServlet#service() : begin
04-16 09:39AM 49.767 flexClient is retrieved from in-memory flexClients repository.
04-16 09:39AM 49.767 FlexClient#registerFlexSession() : httpClient = flex.messaging.client.FlexClient@1bb60ad
04-16 09:39AM 49.767 EchoService#echo() : sessionId = IHEyAtViGH3J9Et5RT48mg
04-16 09:39AM 49.769 MessageBrokerServlet#service() : end

という感じで、GAE/J 上でも最低限使える環境になったかなと思います。

2010年4月16日金曜日

GAE/J 環境の Application Lifecycle Listener の実験

最近、GAE/J が Application Lifecycle Listener に対応していないという話を聞きました。現在 GAE 上で BlazeDS を動かそうとしている身としては、これはぜひともはっきりさせておきたいネタです。

ということで、ひとまず BlazeDS が使用している下記の3つのイベントリスナインタフェースを対象に実験してみました。
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingListener;
import javax.servlet.http.HttpSessionListener;

開発環境上での実行結果
sessionCreated : id = 16es70wx6934m
attributeAdded : name = foo, value = bar
attributeReplaced : name = foo, value = bar
attributeRemoved : name = foo, value = hoge
valueBound : name = fumyo, value = null
attributeAdded : name = fumyo, value = listenertest.ApplicationLifecycleListener@1977b9b
valueUnbound : name = fumyo, value = null
attributeRemoved : name = fumyo, value = listenertest.ApplicationLifecycleListener@1977b9b
sessionDestroyed : id = 16es70wx6934m

本番環境での実行結果
sessionCreated : id = 8xj_3ZWLJhJRl4nrVAzsCw
attributeAdded : name = foo, value = bar
attributeReplaced : name = foo, value = bar
attributeRemoved : name = foo, value = hoge
valueBound : name = fumyo, value = null
attributeAdded : name = fumyo, value = listenertest.ApplicationLifecycleListener@13b625b
valueUnbound : name = fumyo, value = null
attributeRemoved : name = fumyo, value = listenertest.ApplicationLifecycleListener@13b625b
sessionDestroyed : id = 8xj_3ZWLJhJRl4nrVAzsCw

なんと、GAE/J 上で、上記のリスナーは完全に動作しているようです。(`・ω・´)


テスト用サーブレットのソース
package listenertest;

import java.io.IOException;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@SuppressWarnings("serial")
public class ListenertestServlet extends HttpServlet {
 public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
  HttpSession session = req.getSession(true);
  session.setAttribute("foo", "bar");
  session.setAttribute("foo", "hoge");
  session.removeAttribute("foo");
  session.setAttribute("fumyo", new ApplicationLifecycleListener());
  session.removeAttribute("fumyo");
  session.invalidate();
  resp.setContentType("text/plain");
  resp.getWriter().println("Test complete!");
 }
}


テスト用リスナーのソース
package listenertest;

import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

public class ApplicationLifecycleListener implements HttpSessionListener, HttpSessionAttributeListener, HttpSessionBindingListener {

 @Override
 public void sessionCreated(HttpSessionEvent event) {
  System.err.println("sessionCreated : id = " + event.getSession().getId());
 }

 @Override
 public void sessionDestroyed(HttpSessionEvent event) {
  System.err.println("sessionDestroyed : id = " + event.getSession().getId());
 }

 @Override
 public void attributeAdded(HttpSessionBindingEvent event) {
  System.err.println("attributeAdded : name = " + event.getName() + ", value = " + event.getValue());
 }

 @Override
 public void attributeRemoved(HttpSessionBindingEvent event) {
  System.err.println("attributeRemoved : name = " + event.getName() + ", value = " + event.getValue());
 }

 @Override
 public void attributeReplaced(HttpSessionBindingEvent event) {
  System.err.println("attributeReplaced : name = " + event.getName() + ", value = " + event.getValue());
 }

 @Override
 public void valueBound(HttpSessionBindingEvent event) {
  System.err.println("valueBound : name = " + event.getName() + ", value = " + event.getValue());
 }

 @Override
 public void valueUnbound(HttpSessionBindingEvent event) {
  System.err.println("valueUnbound : name = " + event.getName() + ", value = " + event.getValue());
 }
}

GAE/J + BlazeDS 4 環境の構築

今回は GAE/J(Google App Engine Java) と BlazeDS 4 環境の構築方法についてまとめたいと思います。


BlazeDS 3 と同様の環境を構築

基本的な環境は BlazeDS 3 の時とほぼ同じです。そのため、GAE/J + BlazeDS + Flash Builder 環境の構築と同様の手順で作業を行います。

BlazeDS の jar ファイルを持ってくる箇所は行わなくてもかまいません。


BlazeDS 4 のビルド

  • ソースのチェックアウト
    • BlazeDS4系は trunk 以下に配置されています。(4.16.2010現在)
    • ソースは http://opensource.adobe.com/svn/opensource/blazeds/trunk からチェックアウトします。
  • 必要なツール
    • JDK と ant をそろえましょう。
    • ant には Ant-Contrib が必要になるため、http://ant-contrib.sourceforge.net/ から取得します(現在の最新は ant-contrib-1.0b2-bin.zip 等)。それを展開後、ant-contrib.jar を ant の lib フォルダ以下にコピーします。
  • ビルド
    • trunk 直下で ant main を実行するとビルドが開始されます。

BlazeDS 4 の jar ファイルの取得

BlazeDS 4 のビルドが完了したら、冒頭で準備した Web アプリケーションの WEB-INF/lib 以下に、以下のファイルをコピーします。
  • trunk/lib/flex-messaging-core.jar
  • trunk/lib/flex-messaging-common.jar
  • trunk/lib/flex-messaging-remoting.jar
  • trunk/lib/flex-rds-server.jar
  • trunk/lib/xalan.jar
  • trunk/lib/xercesImpl.jar
※必要に応じて必要なファイルをコピーしましょう。


RDS環境の設定

  • web.xml に以下の設定を追加します。
    <servlet>
      <display-name>RDSDispatchServlet</display-name>
      <servlet-name>RDSDispatchServlet</servlet-name>
      <servlet-class>flex.rds.server.servlet.FrontEndServlet</servlet-class>
      <init-param>
        <param-name>useAppserverSecurity</param-name>
        <param-value>false</param-value>
      </init-param>
      <load-on-startup>10</load-on-startup>
    </servlet>
    
    <servlet-mapping id="RDS_DISPATCH_MAPPING">
      <servlet-name>RDSDispatchServlet</servlet-name>
      <url-pattern>/CFIDE/main/ide.cfm</url-pattern>
    </servlet-mapping>

これで、GAE開発環境では正しく動作し、GAE本番環境では 1/3 の確率で動作するアプリが完成(のはず)。この問題は、GAE/J + BlazeDS + Flash Builder 環境の構築に書かれているような感じで FlexSession に equals()メソッドと hashCode() メソッドをつけると回避できます。

以上。

作業ログ:BlazeDS4調査

以下、BlazeDS4調査の作業ログです。

これまで

BlazeDS系について、今まで以下のような作業を行ってきました。

ここまでわかってきたことは、以下のようなことです。
  • BlazeDS3 を GAE 上で運用するにはいろいろと痛い問題がある。
  • 独自に解決したとしても BLZ-444 により、BlazeDS 3系 にバックポートできる可能性はほぼ無い。
  • 逆に、BLZ-444 により、次世代以降の BlazeDS にて GAE をサポートする意思はあると考えられる。

上記からの考察。
  • BlazeDS3系と今後付き合うメリットは低い。
  • BlazeDS4系は未知数だが、BlazeDS3だって現状でソースをほとんど読んでるんだから、大差ない。
  • BlazeDS4系は、開発中なだけに、こちらでの要望が通る可能性が高い。
  • BlazeDS4系は、データ中心開発系でにぎわい始めてきているので、タイミング的にも悪くない。 

結論
  • 将来的な選択肢は、BlazeDS4で行くか、BlazeDSを捨てるか。
  • BlazeDS4系は、BLZ-444 があることと、現在開発中なのでこちらの意見が通りやすいという点から、これに賭けてみることにする。

BlazeDS4系調査

BlazeDS4のコンパイル
  • ソースは、http://opensource.adobe.com/svn/opensource/blazeds/trunk からチェックアウト。300MB以上あるので結構時間がかかります。
  • ビルド用のツールは ant っぽいですが、どのバージョンか不明。ということで、とりあえず 1.8.0 で試してみる。
  • build.xml をかるく見たところ、main というターゲットが全体的なビルドらしいので、実行してみた。見事に失敗。
    [taskdef] Could not load definitions from resource net/sf/antcontrib/antcontrib.properties. It could not be found.
    ということで、antcontrib 入れなきゃだめっぽい。
  • http://ant-contrib.sourceforge.net/ に行って ant-contrib-1.0b2-bin.zip をダウンロードして、ant-contrib.jar を ant の lib フォルダ以下にコピーして、再チャレンジ。今度は成功。

BlazeDS 3.2 のアプリを BlazeDS 4 に対応させる
  • BlazeDS3.2のころの自分のプロジェクトファイルのバックアップを取り、変更を開始。
  • まず、BlazeDS3.2用に追加した jar ファイルをすべて削除。
  • BlazeDS3.2のソースにパッチを当てたものもすべて削除。
  • 具体的にどの jar を追加すればよいかわからないので、とりあえず設定ファイルから先に手をつける。既存の WEB-INF/flex 以下の設定ファイルの名前に.oldをつけ、新規にBlazeDS4のサンプルの定義ファイルをコピー。両者を見比べながら変更しようとおもったが、特に変更する必要はなさそうなので、もとに戻す。
  • カスタム login command のコンパイルのため(AppServerLoginCommand クラス等が必要なため) flex-messaging-core.jar をコピーし、パスを通す。
  • flex.messaging.LocalizedException が無いため flex-messaging-common.jar をコピーする。
  • flex.messaging.services.RemotingService が無いため flex-messaging-remoting.jar をコピーする。
  • これで、ローカルではあっさり動きました。
  • 今度はGAE上にデプロイして試してみる。
  • 見かけ上、BlazeDS 3.2 上で 起きた session の duplication 問題と、logoutをする際の Session 破棄時に HttpSessionListener が働かない問題とほぼ同様の動きをします。
  • ひとまず、基本的なインストールの作業はこれで終了とします。

RDSを試してみる。
  • Using Adobe LiveCycle Data Services を読んでみた。
  • LiveCycle Data Services JAR files と 関連 JAR ファイルを WEB-INF/lib ディレクトリに入れる必要があるらしい。取り合えず flex-rds-server.jar が怪しいのでコピーしておく。
  • WEB-INF/flex 以下の設定ファイルの設定を書くらしい。
    MessageBrokerServlet の設定と session listener の設定を WEB-INF/web.xml に書くらしい。って、GAE だと session listener 使えないんだけど、、、。既にダメポな予感 (´・ω・`)
  • ここまで読んで、これは LiveCycle Data Services 全般の説明だということに気づく。BlazeDS3.2用のドキュメントを一通り読んでいるので、読まなくてもよさそう。ということで、data-centric development にのみフォーカスして情報を収集することにする。
  • Flex 4 Sample Application using a Java Back-End, BlazeDS 4 and Flash Builder 4 Data Wizardsflex-java-testdrive.zip をダウンロードし、設定をパクることにする。
  • services-config.xmlの services 要素には
    <service class="flex.samples.util.DatabaseService" id="db" />
    を追加。
  • web.xml には次のような設定を追加。
    <servlet>
      <display-name>RDSDispatchServlet</display-name>
      <servlet-name>RDSDispatchServlet</servlet-name>
      <servlet-class>flex.rds.server.servlet.FrontEndServlet</servlet-class>
      <init-param>
        <param-name>useAppserverSecurity</param-name>
        <param-value>false</param-value>
      </init-param>
      <load-on-startup>10</load-on-startup>
    </servlet>
    
    <servlet-mapping id="RDS_DISPATCH_MAPPING">
      <servlet-name>RDSDispatchServlet</servlet-name>
      <url-pattern>/CFIDE/main/ide.cfm</url-pattern>
    </servlet-mapping>
    
  • これでコンテナを実行。Flex Builder 4 の Connect to Data/Service のところから接続しに行ってみる。あっさりつなぎこめました。
  • これでいろいろ実験しようとおもった矢先、Flush Builder 4 が突然不調に。デザインモードを開こうとおもうといきなり VM が落ちる状況に(´・ω・`)
  • この件は、FlashBuilder4の再インストールで解決。
これでやっと、BlazeDS4のGAE対応作業の前準備ができました。

    2010年4月15日木曜日

    GAE/J + BlazeDS3 環境の HttpSessionListener 問題

    最近、GAE/J 環境では HttpSessionListener は効かないらしいという話を聞きました。

    http://groups.google.com/group/google-appengine-java/browse_thread/thread/672879dd9a3ba137の下記の発言から、ローカルの環境で動くのがバグであることがわかります。

    Hi Erum. App Engine doesn't support this callback, so when sessionDestroyed
    is triggered by the development server, it's not within an active request
    and hence the exception that you're seeing. In the production environment,
    you would see a similar result if the callback is even triggered at all.
    
    The fact that the development server does trigger sessionDestroyed is a bug
    in the SDK. Please file a new report in our public tracker, and I'm sorry
    for the bad news.
    

    これがもし本当なら、GAE 上で BlazeDS を使うのは結構厳しそうな気がします。

    一例ですが、BlazeDS 3.2 のHttpFlexSession では、以下のようにライフサイクルイベントを前提としたコードがそれなりにあります。どうしたものか (´・ω・`)
    /**
     * Implements HttpSessionListener.
     * When an HttpSession is destroyed, the associated HttpFlexSession is also destroyed.
     * NOTE: This method is not invoked against an HttpFlexSession associated with a request
     * handling thread.
     */
    public void sessionDestroyed(HttpSessionEvent event)
    {
        HttpSession session = event.getSession();
        Map httpSessionToFlexSessionMap = getHttpSessionToFlexSessionMap(session);
        HttpFlexSession flexSession = (HttpFlexSession)httpSessionToFlexSessionMap.remove(session.getId());
        if (flexSession != null)
        {
            // invalidate the flex session
            flexSession.superInvalidate();
    
            // Send notifications to attribute listeners if needed
            // This may send extra notifications if attributeRemoved is called first by the server,
            // but Java servlet 2.4 says session destroy is first, then attributes.
            for (Enumeration e = session.getAttributeNames(); e.hasMoreElements(); )
            {
                String name = (String) e.nextElement();
                if (name.equals(SESSION_ATTRIBUTE))
                    continue;
                Object value = session.getAttribute(name);
                if (value != null)
                {
                    flexSession.notifyAttributeUnbound(name, value);
                    flexSession.notifyAttributeRemoved(name, value);
                }
            }
        }
    
    }
    

    しかし、本当にそうなのか?

    ということで、GAE/J 環境の Application Lifecycle Listener の実験をしてみました。

    JDO の Query を Generics に対応させてみた。

    javax.jdo.Query interface の execute() メソッドは戻り値が java.lang.Object 型なので、以下のようにキャストする必要があります。

    Collection<User> users = (Collection<User>)query.execute();
    

    でも、めんどくさいです。(´・ω・`)

    しかも、キャストのところでコンパイラが警告を出します。

    @SuppressWarning("unchecked") とか書けば警告は消えますが。

    でも、めんどくさいです。(´・ω・`)

    ということで、execute系メソッドの引数として型変数を渡せるようなクラスを作成してみました。

    PersistenceManagerExt.java
    package com.objectfanatics.gae.datastore;
    
    import java.util.Collection;
    import java.util.Date;
    import java.util.EnumSet;
    import java.util.Set;
    
    import javax.jdo.Extent;
    import javax.jdo.FetchGroup;
    import javax.jdo.FetchPlan;
    import javax.jdo.JDOException;
    import javax.jdo.ObjectState;
    import javax.jdo.PersistenceManager;
    import javax.jdo.PersistenceManagerFactory;
    import javax.jdo.Transaction;
    import javax.jdo.datastore.JDOConnection;
    import javax.jdo.datastore.Sequence;
    import javax.jdo.listener.InstanceLifecycleListener;
    
    /**
     * PersistenceManager for QueryExt.
     */
    public class PersistenceManagerExt implements PersistenceManager {
     
     /**
      * PersistenceManager instance to be delegated.
      */
     private final PersistenceManager instance;
     
     /**
      * Constructor.
      * 
      * @param instance instance to be delegated
      */
     public PersistenceManagerExt(PersistenceManager instance) {
      this.instance = instance;
     }
     
     // for generics
     @SuppressWarnings("unchecked")
     @Override
     public QueryExt newNamedQuery(Class cls, String filter) {
      return new QueryExt(instance.newNamedQuery(cls, filter));
     }
     @Override
     public QueryExt newQuery() {
      return new QueryExt(instance.newQuery());
     }
     @SuppressWarnings("unchecked")
     @Override
     public QueryExt newQuery(Class cls, Collection cln, String filter) {
      return new QueryExt(instance.newQuery(cls, cln, filter));
     }
     @SuppressWarnings("unchecked")
     @Override
     public QueryExt newQuery(Class cls, Collection cln) {
      return new QueryExt(instance.newQuery(cls, cln));
     }
     @SuppressWarnings("unchecked")
     @Override
     public QueryExt newQuery(Class cls, String filter) {
      return new QueryExt(instance.newQuery(cls, filter));
     }
     @SuppressWarnings("unchecked")
     @Override
     public QueryExt newQuery(Class cls) {
      return new QueryExt(instance.newQuery(cls));
     }
     @SuppressWarnings("unchecked")
     @Override
     public QueryExt newQuery(Extent cln, String filter) {
      return new QueryExt(instance.newQuery(cln, filter));
     }
     @SuppressWarnings("unchecked")
     @Override
     public QueryExt newQuery(Extent cln) {
      return new QueryExt(instance.newQuery(cln));
     }
     @Override
     public QueryExt newQuery(Object compiled) {
      return new QueryExt(instance.newQuery(compiled));
     }
     @Override
     public QueryExt newQuery(String language, Object query) {
      return new QueryExt(instance.newQuery(language, query));
     }
     @Override
     public QueryExt newQuery(String query) {
      return new QueryExt(instance.newQuery(query));
     }
     
     // delegate methods.
     @SuppressWarnings("unchecked")
     @Override
     public void addInstanceLifecycleListener(InstanceLifecycleListener listener, Class... classes) {
      instance.addInstanceLifecycleListener(listener, classes);
     }
     @Override
     public void checkConsistency() {
      instance.checkConsistency();
     }
     @Override
     public void close() {
      instance.close();
     }
     @Override
     public Transaction currentTransaction() {
      return instance.currentTransaction();
     }
     @Override
     public void deletePersistent(Object pc) {
      instance.deletePersistent(pc);
     }
     @SuppressWarnings("unchecked")
     @Override
     public void deletePersistentAll(Collection pcs) {
      instance.deletePersistentAll(pcs);
     }
     @Override
     public void deletePersistentAll(Object... pcs) {
      instance.deletePersistentAll(pcs);
     }
     @Override
     public <T> T detachCopy(T pc) {
      return instance.detachCopy(pc);
     }
     @Override
     public <T> Collection<T> detachCopyAll(Collection<T> pcs) {
      return instance.detachCopyAll(pcs);
     }
     @Override
     public <T> T[] detachCopyAll(T... pcs) {
      return instance.detachCopyAll(pcs);
     }
     @Override
     public void evict(Object pc) {
      instance.evict(pc);
     }
     @Override
     public void evictAll() {
      instance.evictAll();
     }
     @SuppressWarnings("unchecked")
     @Override
     public void evictAll(boolean subclasses, Class pcClass) {
      instance.evictAll(subclasses, pcClass);
     }
     @SuppressWarnings("unchecked")
     @Override
     public void evictAll(Collection pcs) {
      instance.evictAll(pcs);
     }
     @Override
     public void evictAll(Object... pcs) {
      instance.evictAll(pcs);
     }
     @Override
     public void flush() {
      instance.flush();
     }
     @Override
     public boolean getCopyOnAttach() {
      return instance.getCopyOnAttach();
     }
     @Override
     public JDOConnection getDataStoreConnection() {
      return instance.getDataStoreConnection();
     }
     @Override
     public boolean getDetachAllOnCommit() {
      return instance.getDetachAllOnCommit();
     }
     @Override
     public <T> Extent<T> getExtent(Class<T> persistenceCapableClass, boolean subclasses) {
      return instance.getExtent(persistenceCapableClass, subclasses);
     }
     @Override
     public <T> Extent<T> getExtent(Class<T> persistenceCapableClass) {
      return instance.getExtent(persistenceCapableClass);
     }
     @SuppressWarnings("unchecked")
     @Override
     public FetchGroup getFetchGroup(Class cls, String name) {
      return instance.getFetchGroup(cls, name);
     }
     @Override
     public FetchPlan getFetchPlan() {
      return instance.getFetchPlan();
     }
     @Override
     public boolean getIgnoreCache() {
      return instance.getIgnoreCache();
     }
     @SuppressWarnings("unchecked")
     @Override
     public Set getManagedObjects() {
      return instance.getManagedObjects();
     }
     @SuppressWarnings("unchecked")
     @Override
     public Set getManagedObjects(Class... classes) {
      return instance.getManagedObjects(classes);
     }
     @SuppressWarnings("unchecked")
     @Override
     public Set getManagedObjects(EnumSet<ObjectState> states, Class... classes) {
      return instance.getManagedObjects(states, classes);
     }
     @SuppressWarnings("unchecked")
     @Override
     public Set getManagedObjects(EnumSet<ObjectState> states) {
      return instance.getManagedObjects(states);
     }
     @Override
     public boolean getMultithreaded() {
      return instance.getMultithreaded();
     }
     @Override
     public <T> T getObjectById(Class<T> cls, Object key) {
      return instance.getObjectById(cls, key);
     }
     @Override
     public Object getObjectById(Object oid, boolean validate) {
      return instance.getObjectById(oid, validate);
     }
     @Override
     public Object getObjectById(Object oid) {
      return instance.getObjectById(oid);
     }
     @Override
     public Object getObjectId(Object pc) {
      return instance.getObjectId(pc);
     }
     @SuppressWarnings("unchecked")
     @Override
     public Class getObjectIdClass(Class cls) {
      return instance.getObjectIdClass(cls);
     }
     @Override
     public Object[] getObjectsById(boolean validate, Object... oids) {
      return instance.getObjectsById(validate, oids);
     }
     @SuppressWarnings("unchecked")
     @Override
     public Collection getObjectsById(Collection oids, boolean validate) {
      return instance.getObjectsById(oids, validate);
     }
     @SuppressWarnings("unchecked")
     @Override
     public Collection getObjectsById(Collection oids) {
      return instance.getObjectsById(oids);
     }
     @Override
     public Object[] getObjectsById(Object... oids) {
      return instance.getObjectsById(oids);
     }
     @SuppressWarnings("deprecation")
     @Override
     public Object[] getObjectsById(Object[] oids, boolean validate) {
      return instance.getObjectsById(oids, validate);
     }
     @Override
     public PersistenceManagerFactory getPersistenceManagerFactory() {
      return instance.getPersistenceManagerFactory();
     }
     @Override
     public Integer getQueryTimeoutMillis() {
      return instance.getQueryTimeoutMillis();
     }
     @Override
     public Sequence getSequence(String name) {
      return instance.getSequence(name);
     }
     @Override
     public Date getServerDate() {
      return instance.getServerDate();
     }
     @Override
     public Object getTransactionalObjectId(Object pc) {
      return instance.getTransactionalObjectId(pc);
     }
     @Override
     public Object getUserObject() {
      return instance.getUserObject();
     }
     @Override
     public Object getUserObject(Object key) {
      return instance.getUserObject(key);
     }
     @Override
     public boolean isClosed() {
      return instance.isClosed();
     }
     @Override
     public void makeNontransactional(Object pc) {
      instance.makeNontransactional(pc);
     }
     @SuppressWarnings("unchecked")
     @Override
     public void makeNontransactionalAll(Collection pcs) {
      instance.makeNontransactionalAll(pcs);
     }
     @Override
     public void makeNontransactionalAll(Object... pcs) {
      instance.makeNontransactionalAll(pcs);
     }
     @Override
     public <T> T makePersistent(T pc) {
      return instance.makePersistent(pc);
     }
     @Override
     public <T> Collection<T> makePersistentAll(Collection<T> pcs) {
      return instance.makePersistentAll(pcs);
     }
     @Override
     public <T> T[] makePersistentAll(T... pcs) {
      return instance.makePersistentAll(pcs);
     }
     @Override
     public void makeTransactional(Object pc) {
      instance.makeTransactional(pc);
     }
     @SuppressWarnings("unchecked")
     @Override
     public void makeTransactionalAll(Collection pcs) {
      instance.makeTransactionalAll(pcs);
     }
     @Override
     public void makeTransactionalAll(Object... pcs) {
      instance.makeTransactionalAll(pcs);
     }
     @Override
     public void makeTransient(Object pc, boolean useFetchPlan) {
      instance.makeTransient(pc, useFetchPlan);
     }
     @Override
     public void makeTransient(Object pc) {
      instance.makeTransient(pc);
     }
     @Override
     public void makeTransientAll(boolean useFetchPlan, Object... pcs) {
      instance.makeTransientAll(useFetchPlan, pcs);
     }
     @SuppressWarnings("unchecked")
     @Override
     public void makeTransientAll(Collection pcs, boolean useFetchPlan) {
      instance.makeTransientAll(pcs, useFetchPlan);
     }
     @SuppressWarnings("unchecked")
     @Override
     public void makeTransientAll(Collection pcs) {
      instance.makeTransientAll(pcs);
     }
     @Override
     public void makeTransientAll(Object... pcs) {
      instance.makeTransientAll(pcs);
     }
     @SuppressWarnings("deprecation")
     @Override
     public void makeTransientAll(Object[] pcs, boolean useFetchPlan) {
      instance.makeTransientAll(pcs, useFetchPlan);
     }
     @Override
     public <T> T newInstance(Class<T> pcClass) {
      return instance.newInstance(pcClass);
     }
     @SuppressWarnings("unchecked")
     @Override
     public Object newObjectIdInstance(Class pcClass, Object key) {
      return instance.newObjectIdInstance(pcClass, key);
     }
     @Override
     public Object putUserObject(Object key, Object val) {
      return instance.putUserObject(key, val);
     }
     @Override
     public void refresh(Object pc) {
      instance.refresh(pc);
     }
     @Override
     public void refreshAll() {
      instance.refreshAll();
     }
     @SuppressWarnings("unchecked")
     @Override
     public void refreshAll(Collection pcs) {
      instance.refreshAll(pcs);
     }
     @Override
     public void refreshAll(JDOException jdoe) {
      instance.refreshAll(jdoe);
     }
     @Override
     public void refreshAll(Object... pcs) {
      instance.refreshAll(pcs);
     }
     @Override
     public void removeInstanceLifecycleListener(InstanceLifecycleListener listener) {
      instance.removeInstanceLifecycleListener(listener);
     }
     @Override
     public Object removeUserObject(Object key) {
      return instance.removeUserObject(key);
     }
     @Override
     public void retrieve(Object pc, boolean useFetchPlan) {
      instance.retrieve(pc, useFetchPlan);
     }
     @Override
     public void retrieve(Object pc) {
      instance.retrieve(pc);
     }
     @Override
     public void retrieveAll(boolean useFetchPlan, Object... pcs) {
      instance.retrieveAll(useFetchPlan, pcs);
     }
     @SuppressWarnings("unchecked")
     @Override
     public void retrieveAll(Collection pcs, boolean useFetchPlan) {
      instance.retrieveAll(pcs, useFetchPlan);
     }
     @SuppressWarnings("unchecked")
     @Override
     public void retrieveAll(Collection pcs) {
      instance.retrieveAll(pcs);
     }
     @Override
     public void retrieveAll(Object... pcs) {
      instance.retrieveAll(pcs);
     }
     @SuppressWarnings("deprecation")
     @Override
     public void retrieveAll(Object[] pcs, boolean useFetchPlan) {
      instance.retrieveAll(pcs, useFetchPlan);
     }
     @Override
     public void setCopyOnAttach(boolean flag) {
      instance.setCopyOnAttach(flag);
     }
     @Override
     public void setDetachAllOnCommit(boolean flag) {
      instance.setDetachAllOnCommit(flag);
     }
     @Override
     public void setIgnoreCache(boolean flag) {
      instance.setIgnoreCache(flag);
     }
     @Override
     public void setMultithreaded(boolean flag) {
      instance.setMultithreaded(flag);
     }
     @Override
     public void setQueryTimeoutMillis(Integer interval) {
      instance.setQueryTimeoutMillis(interval);
     }
     @Override
     public void setUserObject(Object o) {
      instance.setUserObject(o);
     }
    }
    


    QueryExt.java
    package com.objectfanatics.gae.datastore;
    
    import java.util.Collection;
    import java.util.Map;
    
    import javax.jdo.Extent;
    import javax.jdo.FetchPlan;
    import javax.jdo.PersistenceManager;
    import javax.jdo.Query;
    
    /**
     * Generics enabled Query extension.
     */
    public class QueryExt implements Query {
     
     /**
      * Query instance to be delegated.
      */
     private final Query instanceToBeDelegated;
     
     /**
      * Constructor.
      * @param instanceToBeDelegated Query instance to be delegated
      */
     public QueryExt(Query instanceToBeDelegated) {
      if (instanceToBeDelegated == null) throw new IllegalArgumentException("instanceToBeDelegated must not be null.");
      this.instanceToBeDelegated = instanceToBeDelegated;
     }
     
     // for generics.
     @SuppressWarnings("unchecked")
     public <T> Collection<T> executeExt(Class<T> t) {
      return (Collection<T>)instanceToBeDelegated.execute();
     }
     @SuppressWarnings("unchecked")
     public <T> Collection<T> executeExt(Class<T> t, Object p1, Object p2, Object p3) {
      return (Collection<T>)instanceToBeDelegated.execute(p1, p2, p3);
     }
     @SuppressWarnings("unchecked")
     public <T> Collection<T> executeExt(Class<T> t, Object p1, Object p2) {
      return (Collection<T>)instanceToBeDelegated.execute(p1, p2);
     }
     @SuppressWarnings("unchecked")
     public <T> Collection<T> executeExt(Class<T> t, Object p1) {
      return (Collection<T>)instanceToBeDelegated.execute(p1);
     }
     @SuppressWarnings("unchecked")
     public <T> Collection<T> executeWithArrayExt(Class<T> t, Object... parameters) {
      return (Collection<T>)instanceToBeDelegated.executeWithArray(parameters);
     }
     @SuppressWarnings("unchecked")
     public <T> Collection<T> executeWithMapExt(Class<T> t, Map parameters) {
      return (Collection<T>)instanceToBeDelegated.executeWithMap(parameters);
     }
     
     // delegate methods.
     @Override
     public void addExtension(String key, Object value) {
      instanceToBeDelegated.addExtension(key, value);
     }
     @SuppressWarnings("unchecked")
     @Override
     public void addSubquery(Query sub, String variableDeclaration, String candidateCollectionExpression, Map parameters) {
      instanceToBeDelegated.addSubquery(sub, variableDeclaration, candidateCollectionExpression, parameters);
     }
     @Override
     public void addSubquery(Query sub, String variableDeclaration, String candidateCollectionExpression, String... parameters) {
      instanceToBeDelegated.addSubquery(sub, variableDeclaration, candidateCollectionExpression, parameters);
     }
     @Override
     public void addSubquery(Query sub, String variableDeclaration, String candidateCollectionExpression, String parameter) {
      instanceToBeDelegated.addSubquery(sub, variableDeclaration, candidateCollectionExpression, parameter);
     }
     @Override
     public void addSubquery(Query sub, String variableDeclaration, String candidateCollectionExpression) {
      instanceToBeDelegated.addSubquery(sub, variableDeclaration, candidateCollectionExpression);
     }
     @Override
     public void cancel(Thread thread) {
      instanceToBeDelegated.cancel(thread);
     }
     @Override
     public void cancelAll() {
      instanceToBeDelegated.cancelAll();
     }
     @Override
     public void close(Object queryResult) {
      instanceToBeDelegated.close(queryResult);
     }
     @Override
     public void closeAll() {
      instanceToBeDelegated.closeAll();
     }
     @Override
     public void compile() {
      instanceToBeDelegated.compile();
     }
     @Override
     public void declareImports(String imports) {
      instanceToBeDelegated.declareImports(imports);
     }
     @Override
     public void declareParameters(String parameters) {
      instanceToBeDelegated.declareParameters(parameters);
     }
     @Override
     public void declareVariables(String variables) {
      instanceToBeDelegated.declareVariables(variables);
     }
     @Override
     public long deletePersistentAll() {
      return instanceToBeDelegated.deletePersistentAll();
     }
     @SuppressWarnings("unchecked")
     @Override
     public long deletePersistentAll(Map parameters) {
      return instanceToBeDelegated.deletePersistentAll(parameters);
     }
     @Override
     public long deletePersistentAll(Object... parameters) {
      return instanceToBeDelegated.deletePersistentAll(parameters);
     }
     @Override
     public Object execute() {
      return instanceToBeDelegated.execute();
     }
     @Override
     public Object execute(Object p1, Object p2, Object p3) {
      return instanceToBeDelegated.execute(p1, p2, p3);
     }
     @Override
     public Object execute(Object p1, Object p2) {
      return instanceToBeDelegated.execute(p1, p2);
     }
     @Override
     public Object execute(Object p1) {
      return instanceToBeDelegated.execute(p1);
     }
     @Override
     public Object executeWithArray(Object... parameters) {
      return instanceToBeDelegated.executeWithArray(parameters);
     }
     @SuppressWarnings("unchecked")
     @Override
     public Object executeWithMap(Map parameters) {
      return instanceToBeDelegated.executeWithMap(parameters);
     }
     @Override
     public FetchPlan getFetchPlan() {
      return instanceToBeDelegated.getFetchPlan();
     }
     @Override
     public boolean getIgnoreCache() {
      return instanceToBeDelegated.getIgnoreCache();
     }
     @Override
     public PersistenceManager getPersistenceManager() {
      return instanceToBeDelegated.getPersistenceManager();
     }
     @Override
     public Boolean getSerializeRead() {
      return instanceToBeDelegated.getSerializeRead();
     }
     @Override
     public Integer getTimeoutMillis() {
      return instanceToBeDelegated.getTimeoutMillis();
     }
     @Override
     public boolean isUnmodifiable() {
      return instanceToBeDelegated.isUnmodifiable();
     }
     @SuppressWarnings("unchecked")
     @Override
     public void setCandidates(Collection pcs) {
      instanceToBeDelegated.setCandidates(pcs);
     }
     @SuppressWarnings("unchecked")
     @Override
     public void setCandidates(Extent pcs) {
      instanceToBeDelegated.setCandidates(pcs);
     }
     @SuppressWarnings("unchecked")
     @Override
     public void setClass(Class cls) {
      instanceToBeDelegated.setClass(cls);
     }
     @SuppressWarnings("unchecked")
     @Override
     public void setExtensions(Map extensions) {
      instanceToBeDelegated.setExtensions(extensions);
     }
     @Override
     public void setFilter(String filter) {
      instanceToBeDelegated.setFilter(filter);
     }
     @Override
     public void setGrouping(String group) {
      instanceToBeDelegated.setGrouping(group);
     }
     @Override
     public void setIgnoreCache(boolean ignoreCache) {
      instanceToBeDelegated.setIgnoreCache(ignoreCache);
     }
     @Override
     public void setOrdering(String ordering) {
      instanceToBeDelegated.setOrdering(ordering);
     }
     @Override
     public void setRange(long fromIncl, long toExcl) {
      instanceToBeDelegated.setRange(fromIncl, toExcl);
     }
     @Override
     public void setRange(String fromInclToExcl) {
      instanceToBeDelegated.setRange(fromInclToExcl);
     }
     @Override
     public void setResult(String data) {
      instanceToBeDelegated.setResult(data);
     }
     @SuppressWarnings("unchecked")
     @Override
     public void setResultClass(Class cls) {
      instanceToBeDelegated.setResultClass(cls);
     }
     @Override
     public void setSerializeRead(Boolean serialize) {
      instanceToBeDelegated.setSerializeRead(serialize);
     }
     @Override
     public void setTimeoutMillis(Integer interval) {
      instanceToBeDelegated.setTimeoutMillis(interval);
     }
     @Override
     public void setUnique(boolean unique) {
      instanceToBeDelegated.setUnique(unique);
     }
     @Override
     public void setUnmodifiable() {
      instanceToBeDelegated.setUnmodifiable();
     }
    }
    

    PersistenceManager のいくつかの newQuery() 系のメソッドは引数でエンティティの型を受け取っているので、それを利用すれば QueryExt<T> のような形でエンティティの型を渡すこともできそうですね。要検討ということで。