dimanche 17 janvier 2010

Hibernate, BlazeDS, Java 5 enum and Flex custom collection (HashMap)

Context and problem

Here is the current situation:
  • On the server side, I am using Java and Hibernate for data storage. I am also using Java 5 enums.
  • The client is built in Flex. As mentioned in a previous post, I have gotten rid of "java-like enums", and now all constants are modeled with Strings
  • BlazeDS is in charge of the communication between the two.
 Concerning the data, on the Flex side I have a Farm that contains resources:

    [RemoteClass(alias="org.liveboardgames.agricola.domain.farm.Farm")]
    [Bindable]
    public class Farm implements IValueObject, IExternalizable
    {
        ...
        public var resources:HashCollection;
        ...
    }

And the HashCollection itself is mapped to the PersistentMap that is sent back by Hibernate:

    [RemoteClass(alias="org.hibernate.collection.PersistentMap")]
    public class HashCollection extends ArrayCollection implements IMap

On the server, everything is rather straightfoward: I have a Farm.java pojo, which contains a Map<ResourceTypeEnum, Integer> resources.

And here is the problem:
  • An enum value is stored in the data base (e.g. FOOD)
  • It is properly loaded , and the object to be sent via BlazeDS looks fine (there is a PersistentMap that contains a ResourceTypeEnum.FOOD key, with a 3 Integer as the value)
  • While debugging the message sent, I get that the FOOD key is properly sent, but not the value (FOOD = null)
  • On the client, the HashCollection that models the resources is null or empty.

Why is the value "null" in the message?

I googled a bit, and found someone who had a very similar issue. It does not work because the BlazeDS framework assumes that only String are used as keys in Maps. So it gets the key properly, then converts it to its String value, and tries and retrieves the value... which obviously fails.

So, how to get around it?
In the post linked to above, the author has created its own MapProxy to handle enums. However, this has impacts a bit everywhere in their code.
The other solutions I had at hand were:
  1. Find a smarter solution
  2. Get rid of enums
The main issue is in the BlazeDS framework itself (flex.messaging.io.amf.Amf3Output):

        if (!externalizable)
            propertyNames = proxy.getPropertyNames(instance);
       ...

        if (externalizable)
        {
           ...
        }
        else if (propertyNames != null)
        {
            Iterator it = propertyNames.iterator();
            while (it.hasNext())
            {
                String propName = (String)it.next();
                Object value = null;
                value = proxy.getValue(instance, propName);
                writeObjectProperty(propName, value);
            }
        }

Since they directly use Strings, I did not see much room for a workaround.
Given the situation, I have decided to simply not use the enums anymore (I had already gotten rid of them on the client).
So with simple Strings instead of enums, I properly get a FOOD = 3 in the message debug. However, still nothing on the client.

Mapping the server's Map to my HashCollection

Adding metadata
By default, BlazeDS maps java.util.Map objets to simple ActionScript Objects (i.e. list of property-values). Understandably, if nothing else is specified, you will get the equivalent of a ClassCastException on the client while deserializing the message - which I got.
I then added the
[RemoteClass(alias="org.hibernate.collection.PersistentMap")]
tag, hoping that it would solve the issue magically, which obviously was not the case. I did not have a ClassCastException anymore, but my HashCollection was simply empty.

Implementing flash.utils.IExternalizable
The reason why it did not work seemed obvious enough to me: the client gets a Map, which has a list of key/value pairs, and I tell the framework to map it to my HashCollection, which is not close to being the same. So adding custom serialization should do the trick.

Custom serialization is explained here. Basically, you need to implement the IExternalizable interface to do what you want with the data you receive, which I did:

        public override function readExternal(input:IDataInput):void
        {
            var map:Object = input.readObject();
            if (map != null)
            {
                for (var p:String in map)
                {
                    this.put(p, map[p]);
                }
            }
        }

        public override function writeExternal(output:IDataOutput):void
        {
            trace("write external on Flex HashCollection");
        } 

However, I never got the readExternal() piece of code to ever be called.

More googling and various experiments, it appeared that the class being sent had to implement java.io.Externalizable for the custom serialization to work.
Now, that"s more of an issue - PersistentMap is created by Hibernate, and I have no control on it. How can I have it implement Externalizable?

Specifying custom hibernate collections
Fortunately, I came accross this article that explained pretty everything I needed to know:
  1. Create a custom Map that is Externalizable
  2. Create a custom UserCollectionType to instantiate this new Map
  3. Modify the mapping file to use this new collection instead of Hibernate's default
 Here you go then with the ExternalMap:

public class ExternalMap extends PersistentMap implements Externalizable {
   ...
      public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(this.map);
      }
   }

Pretty simple - in fact, I don't want it to do anything out of the ordinary - I simply needed a map that implemented Externalizable.

The UserCollectionType is pretty much a copy-paste of the one described in the article, so I won't put it here again (you can always find it on Agricola Online google code).

Finally, the mapping for the resources have been modified

map name="resources" table="FARM_RESOURCES" cascade="all-delete-orphan" 
   lazy="false" collection-type="org.liveboardgames.common.util.ExternalMapType"

Last point to do:  update the RemoteClass tag of the HashCollection to target the new ExternalMap, and here we go!

Limitations

I have just finished this implementation. It works (the data is properly sent back from the server and populated on the client), but I have not tested the other way around yet.
Moreover, it forces me to get rid of enums, which I do not really like. So, if anyone has advices or a better solution, I'd gladly hear it :)

Aucun commentaire:

Enregistrer un commentaire