blob: b416e5167573879c7020667086ab4d8130a82306 [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.amfx;
import flex.messaging.MessageException;
import flex.messaging.io.amf.ASObject;
import flex.messaging.io.amf.AbstractAmfOutput;
import flex.messaging.io.amf.Amf3Types;
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.amf.TraitsInfo;
import flex.messaging.io.amf.Amf3Output;
import flex.messaging.io.ArrayCollection;
import flex.messaging.io.BeanProxy;
import flex.messaging.util.Hex;
import flex.messaging.util.Trace;
import org.w3c.dom.Document;
import java.io.IOException;
import java.io.Externalizable;
import java.io.ByteArrayOutputStream;
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.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.sql.RowSet;
/**
* Serializes Java types to ActionScript 3 types via AMFX, an XML
* based representation of AMF 3.
* <p>
* XML is formatted using using UTF-8 encoding.
* </p>
*
* @see AmfxMessageSerializer
* @see AmfxInput
*/
public class AmfxOutput extends AbstractAmfOutput implements AmfxTypes
{
/**
* A mapping of object instances to their serialization numbers
* for storing object references on the stream.
*/
protected IdentityHashMap objectTable;
protected HashMap traitsTable;
protected HashMap stringTable;
public AmfxOutput(SerializationContext context)
{
super(context);
objectTable = new IdentityHashMap(64);
traitsTable = new HashMap(10);
stringTable = new HashMap(64);
}
public void reset()
{
super.reset();
objectTable.clear();
traitsTable.clear();
stringTable.clear();
}
/**
* 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 Amf3Output createAMF3Output()
{
return new Amf3Output(context);
}
//
// java.io.ObjectOutput IMPLEMENTATIONS
//
public void writeObject(Object o) throws IOException
{
if (o == null)
{
writeAMFNull();
return;
}
if (!context.legacyExternalizable && o instanceof Externalizable)
{
writeCustomObject(o);
}
else if (o instanceof String || o instanceof Character)
{
String s = o.toString();
writeString(s);
}
else if (o instanceof Number)
{
if (o instanceof Integer || o instanceof Short || o instanceof Byte)
{
int i = ((Number)o).intValue();
writeAMFInt(i);
}
else 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...
writeString(o.toString());
}
else
{
double d = ((Number)o).doubleValue();
writeAMFDouble(d);
}
}
else if (o instanceof Boolean)
{
writeAMFBoolean(((Boolean)o).booleanValue());
}
// We have a complex type...
else if (o instanceof Date)
{
writeDate((Date)o);
}
else if (o instanceof Calendar)
{
writeDate(((Calendar)o).getTime());
}
else if (o instanceof Document)
{
String xml = documentToString(o);
int len = xml.length() + 15; // <xml>...</xml>
StringBuffer sb = new StringBuffer(len);
sb.append(XML_OPEN_TAG);
writeEscapedString(sb, xml);
sb.append(XML_CLOSE_TAG);
writeUTF(sb);
if (isDebug)
trace.writeString(xml);
}
// 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;
writeString(enumValue.name());
}
else
{
//We have an Object or Array type...
Class cls = o.getClass();
if (o instanceof Map && context.legacyMap && !(o instanceof ASObject))
{
writeMapAsECMAArray((Map)o);
}
else if (!context.legacyDictionary && o instanceof Dictionary)
{
writeDictionary((Dictionary)o);
}
else if (o instanceof Collection)
{
if (context.legacyCollection)
writeCollection((Collection)o, null);
else
writeArrayCollection((Collection)o, null);
}
else if (cls.isArray())
{
writeAMFArray(o, cls.getComponentType());
}
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 ti) throws IOException
{
String className = ti.getClassName();
if (className == null || className.length() == 0)
{
writeUTF(OBJECT_OPEN_TAG);
}
else
{
int len = 127; // <object type="...">
StringBuffer sb = new StringBuffer(len);
sb.append("<").append(OBJECT_TYPE).append(" type=\"");
sb.append(className);
sb.append("\">");
writeUTF(sb);
}
if (isDebug)
trace.startAMFObject(className, objectTable.size() - 1);
// We treat an empty anonymous Object as a special case
// of <traits/> and thus do not serialize by reference.
if (ti.length() == 0 && className == null)
{
writeUTF(EMPTY_TRAITS_TAG);
}
else if (!byReference(ti))
{
// We assume all Java objects are not dynamic
//boolean dynamic = ti.isDynamic();
if (ti.isExternalizable())
{
writeUTF(TRAITS_EXTERNALIZALBE_TAG);
}
else
{
int count = ti.getProperties().size();
if (count <= 0)
{
writeUTF(EMPTY_TRAITS_TAG);
}
else
{
writeUTF(TRAITS_OPEN_TAG);
for (int i = 0; i < count; i++)
{
String propName = ti.getProperty(i);
writeString(propName, true);
}
writeUTF(TRAITS_CLOSE_TAG);
}
}
}
}
public void writeObjectProperty(String name, Object value) throws IOException
{
if (isDebug)
trace.namedElement(name);
writeObject(value);
}
public void writeObjectEnd() throws IOException
{
writeUTF(OBJECT_CLOSE_TAG);
if (isDebug)
trace.endAMFObject();
}
//
// java.io.DataOutput IMPLEMENTATIONS
//
public void writeUTF(String s) throws IOException
{
byte[] bytes = s.getBytes(UTF_8);
out.write(bytes);
}
//
// AMF SPECIFIC SERIALIZATION IMPLEMENTATIONS
//
/**
*
*/
protected void writeAMFBoolean(boolean b) throws IOException
{
if (b)
writeUTF(TRUE_TAG); // <true/>
else
writeUTF(FALSE_TAG); // <false/>
if (isDebug)
trace.write(b);
}
/**
*
*/
protected void writeAMFDouble(double d) throws IOException
{
int buflen = 40; // <double>...</double>
StringBuffer sb = new StringBuffer(buflen);
sb.append(DOUBLE_OPEN_TAG);
sb.append(d);
sb.append(DOUBLE_CLOSE_TAG);
writeUTF(sb);
if (isDebug)
trace.write(d);
}
/**
*
*/
protected void writeAMFInt(int i) throws IOException
{
if (i >= Amf3Types.INT28_MIN_VALUE && i <= Amf3Types.INT28_MAX_VALUE)
{
int buflen = 25; // <int>...</int>
StringBuffer sb = new StringBuffer(buflen);
sb.append(INTEGER_OPEN_TAG);
sb.append(i);
sb.append(INTEGER_CLOSE_TAG);
writeUTF(sb);
if (isDebug)
trace.write(i);
}
else
{
// Promote large int to a double; technically not needed for AMFX
// but doing it for consistency with AMF.
writeAMFDouble(i);
}
}
/**
*
*/
protected void writeByteArray(byte[] ba) throws IOException
{
int length = ba.length * 2;
int len = 23 + length; // <bytearray>number of bytes * 2 for encoding</bytearray>
StringBuffer sb = new StringBuffer(len);
sb.append(BYTE_ARRAY_OPEN_TAG);
writeUTF(sb);
Hex.Encoder encoder = new Hex.Encoder(ba.length * 2);
encoder.encode(ba);
String encoded = encoder.drain();
writeUTF(encoded);
writeUTF(BYTE_ARRAY_CLOSE_TAG);
if (isDebug)
trace.startByteArray(objectTable.size() - 1, ba.length);
}
/**
*
*/
protected void writeByteArray(Byte[] ba) throws IOException
{
int length = ba.length;
byte[] bytes = new byte[length];
for (int i = 0; i < length; i++)
{
Byte b = ba[i];
if (b == null)
bytes[i] = 0;
else
bytes[i] = ba[i].byteValue();
}
writeByteArray(bytes);
}
/**
*
*/
public void writeUTF(StringBuffer sb) throws IOException
{
byte[] bytes = sb.toString().getBytes(UTF_8);
out.write(bytes);
}
/**
*
*/
protected void writeDate(Date d) throws IOException
{
if (!byReference(d))
{
int buflen = 30; // <date>...</date>
long time = d.getTime();
StringBuffer sb = new StringBuffer(buflen);
sb.append(DATE_OPEN_TAG);
sb.append(time);
sb.append(DATE_CLOSE_TAG);
writeUTF(sb);
if (isDebug)
trace.write(d);
}
}
protected void writeDictionary(Dictionary dictionary) throws IOException
{
StringBuffer sb = new StringBuffer(25);
sb.append("<").append(DICTIONARY_TYPE).append(" length=\"");
sb.append(dictionary.size());
sb.append("\">");
writeUTF(sb);
if (isDebug) trace.startAMFDictionary(objectTable.size() - 1);
Enumeration keys = dictionary.keys();
while (keys.hasMoreElements())
{
if (isDebug) trace.startDictionaryElement();
Object key = keys.nextElement();
writeObject(key);
if (isDebug) trace.addDictionaryEquals();
Object value = dictionary.get(key);
writeObject(value);
}
writeUTF(DICTIONARY_CLOSE_TAG);
if (isDebug)
trace.endAMFDictionary();
}
/**
*
*/
protected void writeMapAsECMAArray(Map map) throws IOException
{
int len = 20; // <array ecma="true">
StringBuffer sb = new StringBuffer(len);
sb.append("<").append(ARRAY_TYPE).append(" ecma=\"true\">");
writeUTF(sb);
if (isDebug)
trace.startAMFArray(objectTable.size() - 1);
Iterator it = map.keySet().iterator();
while (it.hasNext())
{
Object key = it.next();
if (key != null)
{
String propName = key.toString();
sb = new StringBuffer();
// For now, all keys will be named items
sb.append("<").append(ITEM_TYPE).append(" name=\"").append(propName).append("\">");
writeUTF(sb);
if (isDebug)
trace.namedElement(propName);
writeObject(map.get(key));
writeUTF(ITEM_CLOSE_TAG);
}
}
writeUTF(ARRAY_CLOSE_TAG);
if (isDebug)
trace.endAMFArray();
}
/**
*
*/
protected void writeAMFNull() throws IOException
{
writeUTF(NULL_TAG);
if (isDebug)
trace.writeNull();
}
/**
*
*/
protected void writeString(String s) throws IOException
{
writeString(s, false);
if (isDebug)
trace.writeString(s);
}
//
// PRIVATE SERIALIZATION HELPER METHODS
//
/**
*
*/
protected void writeAMFArray(Object o, Class componentType) throws IOException
{
if (componentType.isPrimitive())
{
writePrimitiveArray(o);
}
else if (componentType.equals(Byte.class))
{
writeByteArray((Byte[])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 (!byReference(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 (!byReference(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;
}
List propertyNames = null;
boolean externalizable = pp.isExternalizable(instance);
if (!externalizable)
{
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))
it.remove();
}
}
}
TraitsInfo ti = new TraitsInfo(pp.getAlias(instance), pp.isDynamic(), externalizable, propertyNames);
writeObjectTraits(ti);
if (externalizable)
{
ByteArrayOutputStream bout = new ByteArrayOutputStream();
Amf3Output objOut = createAMF3Output();
objOut.setOutputStream(bout);
//objOut.setDebugTrace(trace);
((Externalizable)instance).writeExternal(objOut);
writeByteArray(bout.toByteArray());
}
else 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 writeString(String s, boolean isTrait) throws IOException
{
if (s.length() == 0)
{
writeUTF(EMPTY_STRING_TAG);
}
else if (!byReference(s))
{
int len = s.length() + 35; // <string>...</string> + <![CDATA[ ]]>
StringBuffer sb = new StringBuffer(len);
sb.append(STRING_OPEN_TAG);
// Traits won't contain chars that need escaping
if (!isTrait)
writeEscapedString(sb, s);
else
sb.append(s);
sb.append(STRING_CLOSE_TAG);
writeUTF(sb);
}
}
/**
* XML defines the following set as valid characters to appear in a document:
* U+0009, U+000A, U+000D, [U+0020-U+D7FF], [U+E000-U+FFFD], and [U+10000-U+10FFFF].
*
* Java only supports characters up to 0xFFFE so codepoints beyond the BMP are
* not considered.
*
* Characters not in this set will be escaped using a numerical character reference
* in hexadecimal form, i.e. &amp;#xFFFF.
*
* A CDATA section is not used because numerical character references cannot be
* used in such a context.
*
* @param sb The StringBuffer to which the escaped String should be written.
* @param s The source String to escape for XML.
*
*/
protected void writeEscapedString(StringBuffer sb, String s)
{
StringBuffer temp = new StringBuffer(s.length());
char[] chars = s.toCharArray();
for (int i = 0; i < chars.length; i++)
{
char c = chars[i];
if (c >= 0x0020)
{
if (c == '&')
{
temp.append("&amp;");
}
else if (c == '<')
{
temp.append("&lt;");
}
else if (c > 0xD7FF && (c < 0xE000 || c > 0xFFFD))
{
temp.append("&#x").append(Integer.toHexString(c)).append(";");
}
else
{
temp.append(c);
}
}
else if (c == 0x0009 || c == 0x000A || c == 0x000D)
{
temp.append(c);
}
else
{
temp.append("&#x").append(Integer.toHexString(c)).append(";");
}
}
sb.append(temp); //Use temp.toString() if JDK 1.3 or earlier
}
/**
*
*/
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
{
String str = new String(ca);
writeString(str);
}
/**
*
*/
protected void writeCollection(Collection c, SerializationDescriptor descriptor) throws IOException
{
if (!byReference(c))
{
writeObjectArrayDirectly(c.toArray(), descriptor);
}
}
/**
*
*/
protected void writeObjectArray(Object[] values, SerializationDescriptor descriptor) throws IOException
{
if (!byReference(values))
{
writeObjectArrayDirectly(values, descriptor);
}
}
/**
*
*/
protected void writeObjectArrayDirectly(Object[] values, SerializationDescriptor descriptor) throws IOException
{
int len = 25; // <array length="...">
StringBuffer sb = new StringBuffer(len);
sb.append("<").append(ARRAY_TYPE).append(" length=\"");
sb.append(values.length);
sb.append("\">");
writeUTF(sb);
if (isDebug)
trace.startAMFArray(objectTable.size() - 1);
for (int i = 0; i < values.length; ++i)
{
if (isDebug)
trace.arrayElement(i);
writeObject(values[i]);
}
writeUTF(ARRAY_CLOSE_TAG);
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();
if (aType.equals(Character.TYPE))
{
//Treat char[] as a String
char[] c = (char[])obj;
writeCharArrayAsString(c);
}
else if (aType.equals(Byte.TYPE))
{
writeByteArray((byte[])obj);
}
else if (!byReference(obj))
{
int length = Array.getLength(obj);
int buflen = 25; // <array length="...">
StringBuffer sb = new StringBuffer(buflen);
sb.append("<").append(ARRAY_TYPE).append(" length=\"");
sb.append(length);
sb.append("\">");
writeUTF(sb);
if (isDebug)
trace.startAMFArray(objectTable.size() - 1);
if (aType.equals(Boolean.TYPE))
{
boolean[] b = (boolean[])obj;
for (int i = 0; i < b.length; i++)
{
if (isDebug)
trace.arrayElement(i);
writeAMFBoolean(b[i]);
}
}
else if (aType.equals(Integer.TYPE) || aType.equals(Short.TYPE))
{
//We have a primitive number, either an int or short
//We write all of these as Integers...
for (int i = 0; i < length; i++)
{
if (isDebug)
trace.arrayElement(i);
int v = Array.getInt(obj, i);
writeAMFInt(v);
}
}
else
{
//We have a primitive number, either a double, float, or long
//We write all of these as doubles...
for (int i = 0; i < length; i++)
{
if (isDebug)
trace.arrayElement(i);
double v = Array.getDouble(obj, i);
writeAMFDouble(v);
}
}
writeUTF(ARRAY_CLOSE_TAG);
if (isDebug)
trace.endAMFArray();
}
}
/**
* 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 byReference(Object o) throws IOException
{
Object ref = objectTable.get(o);
if (ref != null)
{
try
{
int refNum = ((Integer)ref).intValue();
int len = 20; // <ref id="..."/>
StringBuffer sb = new StringBuffer(len);
sb.append("<").append(REF_TYPE).append(" id=\"");
sb.append(refNum);
sb.append("\"/>");
writeUTF(sb);
if (isDebug)
trace.writeRef(refNum);
}
catch (ClassCastException e)
{
throw new IOException("Object reference is not an Integer");
}
}
else
{
objectTable.put(o, new Integer(objectTable.size()));
}
return (ref != null);
}
/**
*
*/
protected boolean byReference(String s) throws IOException
{
Object ref = stringTable.get(s);
if (ref != null)
{
try
{
int refNum = ((Integer)ref).intValue();
int len = 20; // <string id="..."/>
StringBuffer sb = new StringBuffer(len);
sb.append("<").append(STRING_TYPE).append(" id=\"");
sb.append(refNum);
sb.append("\"/>");
writeUTF(sb);
if (Trace.amf && isDebug)
trace.writeStringRef(refNum);
}
catch (ClassCastException e)
{
throw new IOException("String reference is not an Integer");
}
}
else
{
stringTable.put(s, new Integer(stringTable.size()));
}
return (ref != null);
}
/**
*
*/
protected boolean byReference(TraitsInfo ti) throws IOException
{
// We treat an empty anonymous Object as a special case
// of <traits/> and thus do not serialize by reference.
if (ti.length() == 0 && ti.getClassName() == null)
return false;
Object ref = traitsTable.get(ti);
if (ref != null)
{
try
{
int refNum = ((Integer)ref).intValue();
int len = 20; // <traits id="..."/>
StringBuffer sb = new StringBuffer(len);
sb.append("<").append(TRAITS_TYPE).append(" id=\"");
sb.append(refNum);
sb.append("\"/>");
writeUTF(sb);
if (Trace.amf && isDebug)
trace.writeTraitsInfoRef(refNum);
}
catch (ClassCastException e)
{
throw new IOException("Traits reference is not an Integer");
}
}
else
{
traitsTable.put(ti, new Integer(traitsTable.size()));
}
return (ref != null);
}
}