Sunday, March 7, 2010

Serializing Google Protocol Buffer to Blazeds AMF

What are these technologies and how can I use them?
Protocol buffers are a flexible, efficient, automated mechanism for serializing structured data.
You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.
read more

Action Message Format (AMF) is a binary format used to serialize ActionScript objects. It is used primarily to exchange data between an Adobe Flash application and a remote service, usually over the internet.
read more

BlazeDS is the server-based Java remoting and web messaging technology.
read more

OK. I read this and I am intrigued. How can I use these technologies together?

You start by defining your Protocol Buffer data structure and generate the matching Java classes.
Now you need to serialize your data structure to AMF using BlazeDS. Simple as that.

But wait, I read here that
For Java objects that BlazeDS does not handle implicitly, values found in public bean properties with get/set methods and public variables are sent to the client as properties on an Object. Private properties, constants, static properties, and read-only properties, and so on, are not serialized.
My generated data structures don't have setters, they use the Builder pattern. How can I tell BlazeDS what to serialize? I don't want to hack the code generation process and add implement my data structures as Externalizable.

You need to register your own PropertyProxy in the PropertyProxyRegistry.

I wrote a property proxy to help you out:
package amf;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

import com.google.protobuf.Descriptors;
import com.google.protobuf.GeneratedMessage;
import com.google.protobuf.Message;
import com.google.protobuf.Descriptors.EnumValueDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor.JavaType;

import flex.messaging.io.ArrayCollection;
import flex.messaging.io.ArrayList;
import flex.messaging.io.BeanProxy;

/**
 * This is an implementation of the blazeds PropertyProxy for Google Protocol Buffers.
 */
@SuppressWarnings("unchecked")
public class GoogleProtocolBufferProxy extends BeanProxy
{
 private static ConcurrentHashMap propertiesCache = new ConcurrentHashMap();

private static final long serialVersionUID = -2175023450177522134L;

/**
* @see flex.messaging.io.PropertyProxy#getPropertyNames(java.lang.Object)
*/
public List getPropertyNames( Object instance )
{
List propertyNames = propertiesCache.get( instance.getClass() );
if ( propertyNames != null )
{
return propertyNames;
}

if ( instance instanceof Descriptors.EnumValueDescriptor )
{
Descriptors.EnumValueDescriptor enumValueDescriptor = (Descriptors.EnumValueDescriptor)instance;
String enumName = enumValueDescriptor.getType().getName();
return Collections.singletonList( enumName ); // an enum descriptor has a single enum value
}
else if ( instance instanceof GeneratedMessage )
{
// use GPB reflection to figure out the proprety names
GeneratedMessage message = (GeneratedMessage)instance;
List fields = message.getDescriptorForType().getFields();
propertyNames = new ArrayList( fields.size() );
for ( FieldDescriptor field : fields )
{
propertyNames.add( field.getName() );
}

propertiesCache.put( instance.getClass(), propertyNames );

return propertyNames;
}
else
{
throw new IllegalArgumentException( "Can only serialize GPB objects, not " + instance.getClass() );
}

}

/**
* @see flex.messaging.io.BeanProxy#getValue(java.lang.Object, java.lang.String)
*/
public Object getValue( Object instance, String propertyName )
{
if ( instance instanceof Descriptors.EnumValueDescriptor )
{
Descriptors.EnumValueDescriptor enumValueDescriptor = (Descriptors.EnumValueDescriptor)instance;
String enumValue = enumValueDescriptor.getName();
return enumValue;
}
if ( instance instanceof GeneratedMessage )
{
// use GPB reflection to figure out the proprety names
GeneratedMessage message = (GeneratedMessage)instance;
FieldDescriptor field = message.getDescriptorForType().findFieldByName( propertyName );
Object value = message.getField( field );
return value;
}
else
{
throw new IllegalArgumentException( "Can only serialize GPB objects, not " + instance.getClass() );
}
}

/**
* @see flex.messaging.io.AbstractProxy#createInstance(java.lang.String)
*/
public Object createInstance( String className )
{
if ( Descriptors.EnumValueDescriptor.class.getName().equals( className ) )
{
return new StringBuilder(); // use a StringBuilder to hold the value of the enum
}
try
{
Class clz = Class.forName( className );
Method newBuilderMethod = clz.getDeclaredMethod( "newBuilder", new Class[] {} );
newBuilderMethod.setAccessible( true );
Message.Builder builder = (Message.Builder)newBuilderMethod.invoke( null );
return builder;
}
catch ( ClassNotFoundException e )
{
throw new IllegalArgumentException( e );
}
catch ( NoSuchMethodException e )
{
throw new IllegalArgumentException( e );
}
catch ( InvocationTargetException e )
{
throw new IllegalArgumentException( e );
}
catch ( IllegalAccessException e )
{
throw new IllegalArgumentException( e );
}
catch ( SecurityException e )
{
throw new RuntimeException( e );
}
}

/**
* @see flex.messaging.io.BeanProxy#setValue(java.lang.Object, java.lang.String, java.lang.Object)
*/
public void setValue( Object instance, String propertyName, Object value )
{
if ( instance instanceof StringBuilder ) // this instance is an enum
{
StringBuilder builder = (StringBuilder)instance;
builder.append( value ); // save the enum value
}
else if ( instance instanceof Message.Builder ) // note the input is a message builder, not a message
{
Message.Builder builder = (Message.Builder)instance;
FieldDescriptor field = builder.getDescriptorForType().findFieldByName( propertyName );
if ( value instanceof Double )
{
Double dbl = (Double)value;
Object realValue = dbl;
// ActionScript Number is desrialized to Java Double 
// Java Double, Long, Float, are serialized to ActionScript Number
// here is the place to desrialized wisely
if ( field.getJavaType().equals( JavaType.LONG ) )
{
realValue = new Long( dbl.longValue() );
}
else if ( field.getJavaType().equals( JavaType.FLOAT ) )
{
realValue = new Float( dbl.floatValue() );
}
builder.setField( field, realValue );
}
else if ( field.getJavaType().equals( JavaType.ENUM ) && value instanceof StringBuilder ) // if this is an enum for which we saved the value set the correct enum value
{
EnumValueDescriptor enumValueDescriptor = field.getEnumType().findValueByName( String.valueOf( value ) );
builder.setField( field, enumValueDescriptor );
}
else if ( field.getJavaType().equals( JavaType.ENUM ) && value instanceof ArrayCollection ) // if this is an enum for which we saved the value set the correct enum value
{
ArrayCollection enumCollection = (ArrayCollection)value;
Iterator enumValuesIterator = enumCollection.iterator();
while ( enumValuesIterator.hasNext() )
{
StringBuilder enumValueBuilder = (StringBuilder)enumValuesIterator.next();
EnumValueDescriptor enumValueDescriptor = field.getEnumType().findValueByName( String.valueOf( enumValueBuilder ) );
builder.addRepeatedField( field, enumValueDescriptor );
}
}
else
{
builder.setField( field, value );
}
}
else
{
throw new IllegalArgumentException( "Can only serialize GPB objects, not " + instance.getClass() );
}
}

/**
* @see flex.messaging.io.AbstractProxy#instanceComplete(java.lang.Object)
*/
public Object instanceComplete( Object instance )
{
if ( instance instanceof StringBuilder )
{
return instance; // return the same string builder which holds the enum value
}
else if ( instance instanceof Message.Builder ) // note the input is a message builder, not a message
{
Message.Builder builder = (Message.Builder)instance;
return builder.build();
}
else
{
throw new IllegalArgumentException( "Can only serialize GPB objects, not " + instance.getClass() );
}
}

}

In your unit test don't forget to setup the registry. in JUnit it looks like this

@BeforeClass
public static void setup()
{
 PropertyProxyRegistry.getRegistry().register( GeneratedMessage.class, new GoogleProtocolBufferProxy() );
 PropertyProxyRegistry.getRegistry().register( Descriptors.EnumValueDescriptor.class, new GoogleProtocolBufferProxy() );
}

Happy serializing!

2 comments:

  1. Have you had a chance to get any practical experience using this approach? Any pain points you'd like to share?

    ReplyDelete
  2. We didn't get the project off the ground, I don't have any extra information.

    ReplyDelete