Tuesday, February 16, 2010

Serializing parts of your Java Bean to AMF

I found a good example of how to serialize a Java object to AMF using BlazeDS here http://javadevelopmentforthemasses.blogspot.com/2008/08/amf-serialization-from-java-to-flex-and.html

But what happens if I don't want to serialize my model objects?
Suppose my entity has internalId, name and value fields and I only want to send the name and value fields.

According to BlazeDS developer guide I can solve this by setting fields as transient or implementing the Externalizable interface.

I can't set my internalId field as transient, nor do I want to implement Externalizable, which I would have to update every time I change my model object.
This can be solved using the PropertyProxyRegistry in the BlazeDS framework.

Let's define a new annotation - AMFTransient - this annotation will mark which fields should not be serialized.
package amf;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value={ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AmfTransient
{
}

Now write our own new BeanProxy
package amf;

import java.lang.reflect.Field;
import java.util.List;

import flex.messaging.io.BeanProxy;

@SuppressWarnings("serial")
public class TransientAwareBeanProxy extends BeanProxy
{
 @SuppressWarnings("unchecked")
 @Override
 public List getPropertyNames( Object instance )
 {
  List propertyNames = super.getPropertyNames( instance );
  
  // find which fields where marked as transient and remove them
  Class c = instance.getClass();
  for ( Field field : c.getDeclaredFields() )
  {
   if ( field.isAnnotationPresent( AmfTransient.class ) )
   {
    propertyNames.remove( field.getName() );
   }
  }
  
  return propertyNames;
 }
}

Finally we have to register our Proxy in bootstrap code (each application probably does this differently)

PropertyProxyRegistry.getRegistry().register( Object.class, new TransientAwareBeanProxy() );

Finally, let's put it all together with our AMF serializer and Main class

package amf;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import flex.messaging.io.SerializationContext;
import flex.messaging.io.amf.Amf3Input;
import flex.messaging.io.amf.Amf3Output;

public class AmfSerializer
{
 public void toAmf( Object source, OutputStream os ) throws IOException
 {
  Amf3Output amf3Output = new Amf3Output( getSerializationContext() );
  amf3Output.setOutputStream( os );
  amf3Output.writeObject( source );
  amf3Output.flush();
  amf3Output.close();  
 }
 
 @SuppressWarnings("unchecked")
 public  T fromAmf( byte[] amf ) throws ClassNotFoundException, IOException
{
InputStream bIn = new ByteArrayInputStream( amf );
Amf3Input amf3Input = new Amf3Input( getSerializationContext() );
amf3Input.setInputStream( bIn );
return (T) amf3Input.readObject();
}

private SerializationContext getSerializationContext()
{
// Let the framework create the object and set the thread local variable
SerializationContext context = SerializationContext.getSerializationContext();
// set flags on the serialization context here
return context;
}
}

package amf;

import java.io.ByteArrayOutputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import flex.messaging.io.PropertyProxyRegistry;

public class Main
{
 public static void main( String[] args ) throws java.lang.Exception
 {
  // by default send objects to the transient aware proxy
  // you should set this in your bootstrap code
  // ff you don't want certain classes to use this proxy set the BeanProxy explicitly for them
  PropertyProxyRegistry.getRegistry().register( Object.class, new TransientAwareBeanProxy() );

  B toAmf = new B( "bid" );
  toAmf.as.put( "a1", new A( 11, "aid1" ) );
  toAmf.as.put( "a2", new A( 12, "aid2" ) );
  
  ByteArrayOutputStream bout = new ByteArrayOutputStream();
  AmfSerializer serializer = new AmfSerializer();
  serializer.toAmf( toAmf, bout );
  byte[] amf = bout.toByteArray();
  
  B fromAmf = serializer.fromAmf( amf );
  
  System.out.println( "this should be null: " + fromAmf.d );
  System.out.println( "this should be null: " + ((A)(fromAmf.as.values().toArray()[0])).id );
  System.out.println( "this should be a number: " + ((A)(fromAmf.as.values().toArray()[0])).i );
 }
 
 public static class A { 
  @AmfTransient public String id;
  private Integer i;
  
  public A() {} // required for desrializing AMF 

  public A( Integer i, String str ) {
   this.i = i;
   this.id = str;
  }
  public void setI( Integer i ) {
   this.i = i;
  }
  public Integer getI() {
   return i;
  }
  
  public boolean equals( Object obj ) {
   A tmp = (A)obj;
   return id.equals( tmp.id );
  }  
  public int hashCode() {
   return id.hashCode();
  }
 }
 
 public static class B {  
  @AmfTransient public Date d;
  public String id;
  public Map as = new HashMap();

  public B() {} // required for desrializing AMF

  public B( String id ) {
   this.id = id;
   d = new Date();
  }

  public boolean equals( Object obj ) {
   B tmp = (B)obj;
   return id.equals( tmp.id );
  }
  public int hashCode() {
   return id.hashCode();
  }
 }
}