blob: b04713ac994f140a6c7fb1110fe1e98673e2a3c0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package flex.messaging.io.amf;
import flex.messaging.MessageException;
import flex.messaging.io.ArrayCollection;
import flex.messaging.io.PagedRowSet;
import flex.messaging.io.PropertyProxy;
import flex.messaging.io.PropertyProxyRegistry;
import flex.messaging.io.SerializationContext;
import flex.messaging.io.SerializationDescriptor;
import flex.messaging.io.StatusInfoProxy;
import flex.messaging.io.BeanProxy;
import java.io.IOException;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import javax.sql.RowSet;
/**
* An Amf0 output object.
*
*/
public class Amf0Output extends AbstractAmfOutput implements AmfTypes
{
/**
* 3-byte marker for object end; used for faster serialization
* than a combination of writeUTF("") and writeByte(kObjectEndType).
*
*/
public static final byte[] OBJECT_END_MARKER = {0, 0, kObjectEndType};
/**
* A mapping of object instances to their serialization numbers
* for storing object references on the stream.
*
*/
protected IdentityHashMap serializedObjects;
/**
* Number of serialized objects.
*
*/
protected int serializedObjectCount = 0;
/**
* AVM+ Encoding.
*
*/
protected boolean avmPlus;
/**
*
*/
protected Amf3Output avmPlusOutput;
/**
* Construct a serializer without connecting it to an output stream.
* @param context the context to use
*/
public Amf0Output(SerializationContext context)
{
super(context);
context.supportDatesByReference = false;
serializedObjects = new IdentityHashMap(64);
}
/**
* Set to true if the AMF0 stream should switch to use AMF3 on encountering
* the first complex Object during serialization.
*
* @param a Whether the client is running in AVMPlus and can handle AMF3 encoding.
*/
public void setAvmPlus(boolean a)
{
avmPlus = a;
}
/**
* Reset all object reference information allowing the class to be used to
* write a "new" data structure.
*/
public void reset()
{
super.reset();
serializedObjects.clear();
serializedObjectCount = 0;
if (avmPlusOutput != null)
avmPlusOutput.reset();
}
/**
* Creates a new Amf3Output instance which is initialized with the
* current SerializationContext, OutputStream and debug trace settings
* to switch the version of the AMF protocol mid-stream.
*/
protected void createAMF3Output()
{
avmPlusOutput = new Amf3Output(context);
avmPlusOutput.setOutputStream(out);
avmPlusOutput.setDebugTrace(trace);
}
//
// java.io.ObjectOutput implementations
//
/**
* Serialize an Object using AMF 0.
*/
public void writeObject(Object o) throws IOException
{
if (o == null)
{
writeAMFNull();
return;
}
if (o instanceof String)
{
writeAMFString((String)o);
}
else if (o instanceof Number)
{
if (!context.legacyBigNumbers &&
(o instanceof BigInteger || o instanceof BigDecimal))
{
// Using double to write big numbers such as BigInteger or
// BigDecimal can result in information loss so we write
// them as String by default...
writeAMFString((o).toString());
}
else
{
writeAMFDouble(((Number)o).doubleValue());
}
}
else if (o instanceof Boolean)
{
writeAMFBoolean(((Boolean)o).booleanValue());
}
else if (o instanceof Character)
{
String s = o.toString();
writeAMFString(s);
}
else if (o instanceof Date)
{
// Note that dates are not considered complex types in AMF 0
writeAMFDate((Date)o);
}
else if (o instanceof Calendar)
{
writeAMFDate(((Calendar)o).getTime());
}
// If there is a proxy for this,write it as a custom object so the default
// behavior can be overriden.
else if (o instanceof Enum && PropertyProxyRegistry.getRegistry().getProxy(o.getClass()) == null)
{
Enum<?> enumValue = (Enum<?>)o;
writeAMFString(enumValue.name());
}
else
{
// We have a complex object.
// If we're using AMF 3, delegate to AVM+ encoding format
if (avmPlus)
{
if (avmPlusOutput == null)
{
createAMF3Output();
}
out.writeByte(kAvmPlusObjectType);
avmPlusOutput.writeObject(o);
}
else
{
Class cls = o.getClass();
if (cls.isArray())
{
writeAMFArray(o, cls.getComponentType());
}
else if (o instanceof Map && context.legacyMap && !(o instanceof ASObject))
{
writeMapAsECMAArray((Map)o);
}
else if (o instanceof Collection)
{
if (context.legacyCollection)
writeCollection((Collection)o, null);
else
writeArrayCollection((Collection)o, null);
}
else if (o instanceof org.w3c.dom.Document)
{
out.write(kXMLObjectType);
String xml = documentToString(o);
if (isDebug)
trace.write(xml);
writeUTF(xml, true, false);
}
else
{
// Special Case: wrap RowSet in PageableRowSet for Serialization
if (o instanceof RowSet)
{
o = new PagedRowSet((RowSet)o, Integer.MAX_VALUE, false);
}
else if (o instanceof Throwable && context.legacyThrowable)
{
o = new StatusInfoProxy((Throwable)o);
}
writeCustomObject(o);
}
}
}
}
/**
*
*/
public void writeObjectTraits(TraitsInfo traits) throws IOException
{
String className = null;
if (traits != null)
className = traits.getClassName();
if (isDebug)
trace.startAMFObject(className, serializedObjectCount - 1);
if (className == null || className.length() == 0)
{
out.write(kObjectType);
}
else
{
out.write(kTypedObjectType);
out.writeUTF(className);
}
}
/**
*
*/
public void writeObjectProperty(String name, Object value) throws IOException
{
if (isDebug)
trace.namedElement(name);
out.writeUTF(name);
increaseNestObjectLevel();
writeObject(value);
decreaseNestObjectLevel();
}
/**
*
*/
public void writeObjectEnd() throws IOException
{
out.write(OBJECT_END_MARKER, 0, OBJECT_END_MARKER.length);
if (isDebug)
trace.endAMFObject();
}
//
// AMF SPECIFIC SERIALIZATION METHODS
//
/**
*
*/
protected void writeAMFBoolean(boolean b) throws IOException
{
if (isDebug)
trace.write(b);
out.write(kBooleanType);
out.writeBoolean(b);
}
/**
*
*/
protected void writeAMFDouble(double d) throws IOException
{
if (isDebug)
trace.write(d);
out.write(kNumberType);
out.writeDouble(d);
}
/**
*
*/
protected void writeAMFDate(Date d) throws IOException
{
if (isDebug)
trace.write(d);
out.write(kDateType);
// Write the time as 64bit value in ms
out.writeDouble((double)d.getTime());
int nCurrentTimezoneOffset = TimeZone.getDefault().getRawOffset();
out.writeShort(nCurrentTimezoneOffset / 60000);
}
/**
*
*/
protected void writeAMFArray(Object o, Class componentType) throws IOException
{
if (componentType.isPrimitive())
{
writePrimitiveArray(o);
}
else if (componentType.equals(Character.class))
{
writeCharArrayAsString((Character[])o);
}
else
{
writeObjectArray((Object[])o, null);
}
}
/**
*
*/
protected void writeArrayCollection(Collection col, SerializationDescriptor desc) throws IOException
{
if (!serializeAsReference(col))
{
ArrayCollection ac;
if (col instanceof ArrayCollection)
{
ac = (ArrayCollection)col;
// TODO: QUESTION: Pete ignoring the descriptor here... not sure if
// we should modify the user's AC as that could cause corruption?
}
else
{
// Wrap any Collection in an ArrayCollection
ac = new ArrayCollection(col);
if (desc != null)
ac.setDescriptor(desc);
}
// Then wrap ArrayCollection in PropertyProxy for bean-like serialization
PropertyProxy proxy = PropertyProxyRegistry.getProxy(ac);
writePropertyProxy(proxy, ac);
}
}
/**
*
*/
protected void writeCustomObject(Object o) throws IOException
{
PropertyProxy proxy = null;
if (o instanceof PropertyProxy)
{
proxy = (PropertyProxy)o;
o = proxy.getDefaultInstance();
// The proxy may wrap a null default instance, if so, short circuit here.
if (o == null)
{
writeAMFNull();
return;
}
// HACK: Short circuit and unwrap if PropertyProxy is wrapping an Array
// or Collection type since we don't yet have the ability to proxy multiple
// AMF types. We write an AMF Array directly instead of an AMF Object
else if (o instanceof Collection)
{
if (context.legacyCollection)
writeCollection((Collection)o, proxy.getDescriptor());
else
writeArrayCollection((Collection)o, proxy.getDescriptor());
return;
}
else if (o.getClass().isArray())
{
writeObjectArray((Object[])o, proxy.getDescriptor());
return;
}
else if (context.legacyMap && o instanceof Map && !(o instanceof ASObject))
{
writeMapAsECMAArray((Map)o);
return;
}
}
if (!serializeAsReference(o))
{
if (proxy == null)
{
proxy = PropertyProxyRegistry.getProxyAndRegister(o);
}
writePropertyProxy(proxy, o);
}
}
/**
*
*/
protected void writePropertyProxy(PropertyProxy pp, Object instance) throws IOException
{
/*
* At this point we substitute the instance we want to serialize.
*/
Object newInst = pp.getInstanceToSerialize(instance);
if (newInst != instance)
{
// We can't use writeAMFNull here I think since we already added this object
// to the object table on the server side. The player won't have any way
// of knowing we have this reference mapped to null.
if (newInst == null)
throw new MessageException("PropertyProxy.getInstanceToSerialize class: " + pp.getClass() + " returned null for instance class: " + instance.getClass().getName());
// Grab a new proxy if necessary for the new instance
pp = PropertyProxyRegistry.getProxyAndRegister(newInst);
instance = newInst;
}
//FIXME: Throw exception or use unsupported type for externalizable as it's not supported in AMF 0?
boolean externalizable = false; //sp.isExternalizable(instance);
List propertyNames = pp.getPropertyNames(instance);
// filter write-only properties
if (pp instanceof BeanProxy)
{
BeanProxy bp = (BeanProxy) pp;
Iterator it = propertyNames.iterator();
while (it.hasNext())
{
String propName = (String) it.next();
if (bp.isWriteOnly(instance, propName))
{ // do not serialize this, as we cannot get the value
it.remove();
}
}
}
TraitsInfo ti = new TraitsInfo(pp.getAlias(instance), pp.isDynamic(), externalizable, propertyNames);
writeObjectTraits(ti);
if (propertyNames != null)
{
Iterator it = propertyNames.iterator();
while (it.hasNext())
{
String propName = (String)it.next();
Object value = pp.getValue(instance, propName);
writeObjectProperty(propName, value);
}
}
writeObjectEnd();
}
/**
*
*/
protected void writeMapAsECMAArray(Map m) throws IOException
{
if (!serializeAsReference(m))
{
if (isDebug)
trace.startECMAArray(serializedObjectCount - 1);
out.write(kECMAArrayType);
out.writeInt(0);
Iterator it = m.keySet().iterator();
while (it.hasNext())
{
Object key = it.next();
Object value = m.get(key);
writeObjectProperty(key.toString(), value);
}
writeObjectEnd();
}
}
/**
*
*/
protected void writeAMFNull() throws IOException
{
if (isDebug)
trace.writeNull();
out.write(kNullType);
}
/**
*
*/
protected void writeAMFString(String str) throws IOException
{
if (isDebug)
trace.writeString(str);
writeUTF(str, false, true);
}
/**
*
*/
protected void writeObjectArray(Object[] values, SerializationDescriptor descriptor) throws IOException
{
if (!serializeAsReference(values))
{
if (isDebug)
trace.startAMFArray(serializedObjectCount - 1);
out.write(kStrictArrayType);
out.writeInt(values.length);
for (int i = 0; i < values.length; ++i)
{
if (isDebug)
trace.arrayElement(i);
Object item = values[i];
if (item != null && descriptor != null && !(item instanceof String)
&& !(item instanceof Number) && !(item instanceof Boolean)
&& !(item instanceof Character))
{
PropertyProxy proxy = PropertyProxyRegistry.getProxy(item);
proxy = (PropertyProxy)proxy.clone();
proxy.setDescriptor(descriptor);
item = proxy;
}
increaseNestObjectLevel();
writeObject(item);
decreaseNestObjectLevel();
}
if (isDebug)
trace.endAMFArray();
}
}
/**
* Serialize a Collection.
*
* @param c Collection to be serialized as an array.
* @throws java.io.IOException The exception can be generated by the output stream
*
*/
protected void writeCollection(Collection c, SerializationDescriptor descriptor) throws IOException
{
if (!serializeAsReference(c))
{
if (isDebug)
trace.startAMFArray(serializedObjectCount - 1);
out.write(kStrictArrayType);
out.writeInt(c.size());
Iterator it = c.iterator();
int i = 0;
while (it.hasNext())
{
if (isDebug)
trace.arrayElement(i++);
Object item = it.next();
if (item != null && descriptor != null && !(item instanceof String)
&& !(item instanceof Number) && !(item instanceof Boolean)
&& !(item instanceof Character))
{
PropertyProxy proxy = PropertyProxyRegistry.getProxy(item);
proxy = (PropertyProxy)proxy.clone();
proxy.setDescriptor(descriptor);
item = proxy;
}
increaseNestObjectLevel();
writeObject(item);
decreaseNestObjectLevel();
}
if (isDebug)
trace.endAMFArray();
}
}
/**
* Serialize an array of primitives.
* <p>
* Primitives include the following:
* boolean, char, double, float, long, int, short, byte
* </p>
* @param obj An array of primitives
*
*/
protected void writePrimitiveArray(Object obj) throws IOException
{
Class aType = obj.getClass().getComponentType();
//Treat char[] as a String
if (aType.equals(Character.TYPE))
{
char[] c = (char[])obj;
writeCharArrayAsString(c);
}
else if (!serializeAsReference(obj))
{
if (aType.equals(Boolean.TYPE))
{
out.write(kStrictArrayType);
boolean[] b = (boolean[])obj;
out.writeInt(b.length);
if (isDebug)
{
trace.startAMFArray(serializedObjectCount - 1);
for (int i = 0; i < b.length; i++)
{
trace.arrayElement(i);
writeAMFBoolean(b[i]);
}
trace.endAMFArray();
}
else
{
for (int i = 0; i < b.length; i++)
{
writeAMFBoolean(b[i]);
}
}
}
else
{
//We have a primitive number, either a double, float, long, int, short or byte.
//We write all of these as doubles...
out.write(kStrictArrayType);
int length = Array.getLength(obj);
out.writeInt(length);
if (isDebug)
{
trace.startAMFArray(serializedObjectCount - 1);
for (int i = 0; i < length; i++)
{
trace.arrayElement(i);
double v = Array.getDouble(obj, i);
writeAMFDouble(v);
}
trace.endAMFArray();
}
else
{
for (int i = 0; i < length; i++)
{
double v = Array.getDouble(obj, i);
writeAMFDouble(v);
}
}
}
}
}
/**
*
*/
protected void writeCharArrayAsString(Character[] ca) throws IOException
{
int length = ca.length;
char[] chars = new char[length];
for (int i = 0; i < length; i++)
{
Character c = ca[i];
if (c == null)
chars[i] = 0;
else
chars[i] = ca[i].charValue();
}
writeCharArrayAsString(chars);
}
/**
*
*/
protected void writeCharArrayAsString(char[] ca) throws IOException
{
writeAMFString(new String(ca));
}
/**
*
*/
protected void writeUTF(String str, boolean forceLong, boolean writeType) throws IOException
{
int strlen = str.length();
int utflen = 0;
int c, count = 0;
char[] charr = getTempCharArray(strlen);
str.getChars(0, strlen, charr, 0);
for (int i = 0; i < strlen; i++)
{
c = charr[i];
if (c <= 0x007F)
{
utflen++;
}
else if (c > 0x07FF)
{
utflen += 3;
}
else
{
utflen += 2;
}
}
int type;
if (forceLong)
{
type = kLongStringType;
}
else
{
if (utflen <= 65535)
type = kStringType;
else
type = kLongStringType;
}
byte[] bytearr;
if (writeType)
{
bytearr = getTempByteArray(utflen + (type == kStringType ? 3 : 5));
bytearr[count++] = (byte)(type);
}
else
bytearr = getTempByteArray(utflen + (type == kStringType ? 2 : 4));
if (type == kLongStringType)
{
bytearr[count++] = (byte)((utflen >>> 24) & 0xFF);
bytearr[count++] = (byte)((utflen >>> 16) & 0xFF);
}
bytearr[count++] = (byte)((utflen >>> 8) & 0xFF);
bytearr[count++] = (byte)((utflen) & 0xFF);
for (int i = 0; i < strlen; i++)
{
c = charr[i];
if (c <= 0x007F)
{
bytearr[count++] = (byte)c;
}
else if (c > 0x07FF)
{
bytearr[count++] = (byte)(0xE0 | ((c >> 12) & 0x0F));
bytearr[count++] = (byte)(0x80 | ((c >> 6) & 0x3F));
bytearr[count++] = (byte)(0x80 | ((c) & 0x3F));
}
else
{
bytearr[count++] = (byte)(0xC0 | ((c >> 6) & 0x1F));
bytearr[count++] = (byte)(0x80 | ((c) & 0x3F));
}
}
out.write(bytearr, 0, count);
}
/**
* Remember the object's serialization number so that it can be referred to
* as a reference later. Only complex ojects should be stored as references.
*
*
*/
protected void rememberObjectReference(Object obj)
{
serializedObjects.put(obj, new Integer(serializedObjectCount++));
}
/**
* Attempts to serialize the object as a reference.
* If the object cannot be serialized as a reference, it is stored
* in the reference collection for potential future encounter.
*
* @return Success/failure indicator as to whether the object could be
* serialized as a reference.
*
*/
protected boolean serializeAsReference(Object obj) throws IOException
{
Object ref = serializedObjects.get(obj);
if (ref != null)
{
try
{
int refNum = ((Integer)ref).intValue();
out.write(kReferenceType);
out.writeShort(refNum);
if (isDebug)
trace.writeRef(refNum);
}
catch (ClassCastException e)
{
throw new IOException("Object reference value is not an Integer");
}
}
else
{
rememberObjectReference(obj);
}
return (ref != null);
}
/**
protected void writeUnsupported() throws IOException
{
if (isDebug)
trace.write("UNSUPPORTED");
out.write(kUnsupportedType);
}
*/
}