blob: 934d2ad51f02178cb9ae45a6d1858ee6256d4257 [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 mx.messaging.channels.amfx
{
import flash.utils.ByteArray;
import flash.utils.Dictionary;
import flash.utils.IExternalizable;
import flash.utils.describeType;
import flash.xml.XMLDocument;
import mx.logging.Log;
import mx.utils.HexEncoder;
import mx.utils.ObjectProxy;
import mx.utils.ObjectUtil;
[ExcludeClass]
/**
* Serializes an arbitrary ActionScript object graph to an XML
* representation that is based on Action Message Format (AMF)
* version 3.
* @private
*/
public class AMFXEncoder
{
public function AMFXEncoder()
{
super();
settings = {};
settings.prettyPrinting = false;
}
public function encode(obj:Object, headers:Array = null):XML
{
XML.setSettings(settings);
var xml:XML = new XML("<amfx />");
xml.setNamespace(NAMESPACE);
xml.@["ver"] = CURRENT_VERSION;
var context:AMFXContext = new AMFXContext();
context.log = Log.getLogger("mx.messaging.channels.amfx.AMFXEncoder");
encodePacket(xml, obj, headers, context);
return xml;
}
private static function encodePacket(xml:XML, obj:Object, headers:Array = null, context:AMFXContext = null):void
{
if (headers)
encodeHeaders(xml, headers, context);
encodeBody(xml, obj, context);
}
private static function encodeHeaders(xml:XML, headers:Array, context:AMFXContext):void
{
for (var i:uint = 0; i < headers.length; i++)
{
var header:Object = headers[i];
var element:XML = <header />;
element.@["name"] = header.name;
element.@["mustUnderstand"] = (header.mustUnderstand == true);
encodeValue(element, header.content, context);
xml.appendChild(element);
}
}
private static function encodeBody(xml:XML, obj:*, context:AMFXContext):void
{
var element:XML = <body />;
//element.@["targetURI"] = ""; //TODO: Support this attribute
encodeValue(element, obj, context);
xml.appendChild(element);
}
public static function encodeValue(xml:XML, obj:*, context:AMFXContext):void
{
if (obj != null)
{
if (obj is String)
{
encodeString(xml, String(obj), context);
}
else if (obj is Number)
{
encodeNumber(xml, Number(obj));
}
else if (obj is Boolean)
{
encodeBoolean(xml, Boolean(obj));
}
else if (obj is ByteArray)
{
encodeByteArray(xml, ByteArray(obj));
}
else if (obj is Array)
{
encodeArray(xml, obj as Array, context);
}
else if (obj is XML || obj is XMLDocument)
{
encodeXML(xml, obj);
}
else if (obj is Date)
{
encodeDate(xml, obj as Date, context);
}
else if (obj is Class)
{
//TODO: Throw errors for unsupported types?
if (context.log)
context.log.warn("Cannot serialize type Class");
}
else if (obj is Dictionary)
{
encodeDictionary(xml, obj as Dictionary, context);
}
else
{
encodeObject(xml, obj, context);
}
}
else if (obj === undefined)
{
xml.appendChild(X_UNDEFINED.copy());
}
else
{
xml.appendChild(X_NULL.copy());
}
}
private static function encodeArray(xml:XML, array:Array, context:AMFXContext):void
{
var ref:int = context.findObject(array);
var element:XML;
if (ref >= 0)
{
element = <ref />
element.@["id"] = String(ref);
}
else
{
rememberObject(context, array);
element = <array />;
var named:Object = {};
var ordinal:Array = [];
var isECMAArray:Boolean = false;
// Separate named and ordinal array members
for (var member:String in array)
{
if (isNaN(Number(member)))
{
named[member] = array[member];
isECMAArray = true;
}
else
{
var num:int = parseInt(member);
ordinal[num] = array[num];
}
}
// Encode named items as early as possible
for (var n:String in named)
{
encodeArrayItem(element, n, named[n], context);
}
var ordinalLength:uint = 0;
var dense:Boolean = true;
for (var i:uint = 0; i < ordinal.length; i++)
{
var o:* = ordinal[i];
// If we have an undefined slot remaining ordinal
// keys will be converted to named keys to preserve dense set
if (o !== undefined)
{
if (dense)
{
encodeValue(element, o, context);
ordinalLength++;
}
else
{
isECMAArray = true;
encodeArrayItem(element, String(i), o, context);
}
}
else
{
dense = false;
}
}
element.@["length"] = String(ordinalLength);
if (isECMAArray)
{
element.@["ecma"] = "true";
}
}
xml.appendChild(element);
}
private static function encodeArrayItem(xml:XML, name:String, value:*, context:AMFXContext):void
{
var item:XML = <item />;
item.@["name"] = name;
encodeValue(item, value, context);
xml.appendChild(item);
}
private static function encodeBoolean(xml:XML, bool:Boolean):void
{
if (bool)
xml.appendChild(X_TRUE.copy());
else
xml.appendChild(X_FALSE.copy());
}
private static function encodeByteArray(xml:XML, obj:ByteArray):void
{
var element:XML = <bytearray/>;
var encoder:HexEncoder = new HexEncoder();
encoder.encode(obj);
var encoded:String = encoder.flush();
element.appendChild(encoded);
xml.appendChild(element);
}
private static function encodeDate(xml:XML, date:Date, context:AMFXContext):void
{
var ref:int = context.findObject(date);
var element:XML;
if (ref >= 0)
{
element = <ref />
element.@["id"] = String(ref);
}
else
{
rememberObject(context, date);
element = <date />;
element.appendChild(new XML(date.getTime().toString()));
}
xml.appendChild(element);
}
private static function encodeNumber(xml:XML, num:Number):void
{
var element:XML = null;
if (num is int || num is uint)
{
element = <int />;
}
else
{
element = <double />;
}
element.appendChild(new XML(num.toString()));
xml.appendChild(element);
}
private static function encodeDictionary(xml:XML, dict:Dictionary, context:AMFXContext):void
{
var ref:int = context.findObject(dict);
var element:XML;
if (ref >= 0)
{
element = <ref />;
element.@["id"] = String(ref);
}
else
{
rememberObject(context, dict);
element = <dictionary />;
var classInfo:Object = ObjectUtil.getClassInfo(dict, null, CLASS_INFO_OPTIONS);
var properties:Array = classInfo.properties;
var count:uint = properties.length;
for (var i:uint = 0; i < count; i++)
{
var prop:Object = properties[i];
encodeValue(element, prop, context);
encodeValue(element, dict[prop], context);
}
}
element.@["length"] = String(count);
xml.appendChild(element);
}
private static function rememberObject(context:AMFXContext, obj:*):void
{
context.addObject(obj);
}
private static function encodeObject(xml:XML, obj:*, context:AMFXContext):void
{
var ref:int = context.findObject(obj);
var element:XML;
if (ref >= 0)
{
element = <ref />
element.@["id"] = String(ref);
}
else
{
rememberObject(context, obj);
element = <object />;
var classInfo:Object = ObjectUtil.getClassInfo(obj, null, CLASS_INFO_OPTIONS);
var className:String = classInfo.name;
var classAlias:String = classInfo.alias;
var properties:Array = classInfo.properties;
var count:uint = properties.length;
// We need to special case ObjectProxy as for serialization we actually need the
// remote alias of ObjectProxy, not the wrapped object.
if (obj is ObjectProxy)
{
var cinfo:XML = describeType(obj);
className = cinfo.@name.toString();
classAlias = cinfo.@alias.toString();
}
var remoteClassName:String = ((classAlias != null) ? classAlias : className);
if (remoteClassName && remoteClassName != "Object" && remoteClassName != "Array")
{
element.@["type"] = remoteClassName.replace(REGEX_CLASSNAME, ".");
}
if (obj is IExternalizable)
{
classInfo.externalizable = true;
encodeTraits(element, classInfo, context);
var ext:IExternalizable = IExternalizable(obj);
var ba:ByteArray = new ByteArray();
ext.writeExternal(ba);
encodeByteArray(element, ba);
}
else
{
classInfo.externalizable = false;
encodeTraits(element, classInfo, context);
for (var i:uint = 0; i < count; i++)
{
var prop:String = properties[i];
encodeValue(element, obj[prop], context);
}
}
}
xml.appendChild(element);
}
private static function encodeString(xml:XML, str:String, context:AMFXContext, isTrait:Boolean = false):void
{
var ref:int = context.findString(str);
var element:XML = <string />;
if (ref >= 0)
{
element.@["id"] = String(ref);
}
else
{
//Remember string
context.addString(str);
if (str.length > 0)
{
// Traits won't contain chars that need escaping
if (!isTrait)
str = escapeXMLString(str);
var x:XML = new XML(str);
element.appendChild(x);
}
}
xml.appendChild(element);
}
private static function encodeTraits(xml:XML, classInfo:Object, context:AMFXContext):void
{
var element:XML = <traits />;
var ref:int = context.findTraitInfo(classInfo);
if (ref >= 0)
{
element.@["id"] = String(ref);
}
else
{
//Remember trait info
context.addTraitInfo(classInfo)
if (classInfo.externalizable)
{
element.@["externalizable"] = "true";
}
else
{
var properties:Array = classInfo.properties;
if (properties != null)
{
var count:uint = properties.length;
for (var i:uint = 0; i < count; i++)
{
var prop:String = properties[i];
encodeString(element, prop, context, true);
}
}
}
}
xml.appendChild(element);
}
private static function encodeXML(xml:XML, xmlObject:Object):void
{
var element:XML = <xml />;
var str:String;
if (xmlObject is XML)
str = XML(xmlObject).toXMLString();
else
str = xmlObject.toString();
if (str.length > 0)
{
str = escapeXMLString(str);
var x:XML = new XML(str);
element.appendChild(x);
}
xml.appendChild(element);
}
private static function escapeXMLString(str:String):String
{
if (str.length > 0)
{
if ((str.indexOf("<") != -1) || (str.indexOf("&") != -1))
{
if (str.indexOf("]]>") != -1)
{
str = str.replace(REGEX_CLOSE_CDATA, "]]&gt;");
}
str = "<![CDATA[" + str + "]]>";
}
}
return str;
}
private var settings:Object;
public static const CURRENT_VERSION:uint = 3;
public static const NAMESPACE_URI:String = "http://www.macromedia.com/2005/amfx";
public static const NAMESPACE:Namespace = new Namespace("", NAMESPACE_URI);
private static const REGEX_CLASSNAME:RegExp = new RegExp("\\:\\:", "g");
private static const REGEX_CLOSE_CDATA:RegExp = new RegExp("]]>", "g");
private static const CLASS_INFO_OPTIONS:Object = {includeReadOnly:false, includeTransient:false};
private static const X_FALSE:XML = <false />;
private static const X_NULL:XML = <null />;
private static const X_TRUE:XML = <true />;
private static const X_UNDEFINED:XML = <undefined />;
}
}