All Downloads are FREE. Search and download functionalities are using the official Maven repository.

flex.messaging.io.amfx.AmfxOutput Maven / Gradle / Ivy

/*
 * 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.
 * 

* XML is formatted using using UTF-8 encoding. *

* * @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; // ... 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; // 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 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); // else writeUTF(FALSE_TAG); // if (isDebug) trace.write(b); } /** * */ protected void writeAMFDouble(double d) throws IOException { int buflen = 40; // ... 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; // ... 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; // number of bytes * 2 for encoding 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; // ... 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; // 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; // ... + 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. &#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("&"); } else if (c == '<') { temp.append("<"); } 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; // 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. *

* Primitives include the following: * boolean, char, double, float, long, int, short, byte *

* * @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; // 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; // 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; // 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 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; // 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); } }