////////////////////////////////////////////////////////////////////////////////
//
// 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.utils
{
import flash.utils.ByteArray;
import flash.utils.Dictionary;
import flash.utils.getQualifiedClassName;
import flash.xml.XMLNode;
import mx.collections.IList;
/**
* The ObjectUtil class is an all-static class with methods for
* working with Objects within Flex.
* You do not create instances of ObjectUtil;
* instead you simply call static methods such as the
* ObjectUtil.isSimple()
method.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public class ObjectUtil
{
include "../core/Version.as";
/**
* Array of properties to exclude from debugging output.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
private static var defaultToStringExcludes:Array = ["password", "credentials"];
//--------------------------------------------------------------------------
//
// Class methods
//
//--------------------------------------------------------------------------
/**
* Compares the Objects and returns an integer value
* indicating if the first item is less than greater than or equal to
* the second item.
* This method will recursively compare properties on nested objects and
* will return as soon as a non-zero result is found.
* By default this method will recurse to the deepest level of any property.
* To change the depth for comparison specify a non-negative value for
* the depth
parameter.
*
* @param a Object.
*
* @param b Object.
*
* @param depth Indicates how many levels should be
* recursed when performing the comparison.
* Set this value to 0 for a shallow comparison of only the primitive
* representation of each property.
* For example:
* var a:Object = {name:"Bob", info:[1,2,3]}; * var b:Object = {name:"Alice", info:[5,6,7]}; * var c:int = ObjectUtil.compare(a, b, 0);* *
In the above example the complex properties of a
and
* b
will be flattened by a call to toString()
* when doing the comparison.
* In this case the info
property will be turned into a string
* when performing the comparison.
This method is designed for copying data objects,
* such as elements of a collection. It is not intended for copying
* a UIComponent object, such as a TextInput control. If you want to create copies
* of specific UIComponent objects, you can create a subclass of the component and implement
* a clone()
method, or other method to perform the copy.
This method is designed for cloning data objects,
* such as elements of a collection. It is not intended for cloning
* a UIComponent object, such as a TextInput control. If you want to clone
* specific UIComponent objects, you can create a subclass of the component
* and implement a clone()
method.
true
if the object reference specified
* is a simple data type. The simple data types include the following:
* String
Number
uint
int
Boolean
Date
Array
true
if the object specified
* is one of the types above; false
otherwise.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static function isSimple(value:Object):Boolean
{
var type:String = typeof(value);
switch (type)
{
case "number":
case "string":
case "boolean":
{
return true;
}
case "object":
{
return (value is Date) || (value is Array);
}
}
return false;
}
/**
* Compares two numeric values.
*
* @param a First number.
*
* @param b Second number.
*
* @return 0 is both numbers are NaN.
* 1 if only a
is a NaN.
* -1 if only b
is a NaN.
* -1 if a
is less than b
.
* 1 if a
is greater than b
.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static function numericCompare(a:Number, b:Number):int
{
if (isNaN(a) && isNaN(b))
return 0;
if (isNaN(a))
return 1;
if (isNaN(b))
return -1;
if (a < b)
return -1;
if (a > b)
return 1;
return 0;
}
/**
* Compares two String values.
*
* @param a First String value.
*
* @param b Second String value.
*
* @param caseInsensitive Specifies to perform a case insensitive compare,
* true
, or not, false
.
*
* @return 0 is both Strings are null.
* 1 if only a
is null.
* -1 if only b
is null.
* -1 if a
precedes b
.
* 1 if b
precedes a
.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static function stringCompare(a:String, b:String,
caseInsensitive:Boolean = false):int
{
if (a == null && b == null)
return 0;
if (a == null)
return 1;
if (b == null)
return -1;
// Convert to lowercase if we are case insensitive.
if (caseInsensitive)
{
a = a.toLocaleLowerCase();
b = b.toLocaleLowerCase();
}
var result:int = a.localeCompare(b);
if (result < -1)
result = -1;
else if (result > 1)
result = 1;
return result;
}
/**
* Compares the two Date objects and returns an integer value
* indicating if the first Date object is before, equal to,
* or after the second item.
*
* @param a Date object.
*
* @param b Date object.
*
* @return 0 if a
and b
are equal
* (or both are null
);
* -1 if a
is before b
* (or b
is null
);
* 1 if a
is after b
* (or a
is null
).
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static function dateCompare(a:Date, b:Date):int
{
if (a == null && b == null)
return 0;
if (a == null)
return 1;
if (b == null)
return -1;
var na:Number = a.getTime();
var nb:Number = b.getTime();
if (na < nb)
return -1;
if (na > nb)
return 1;
return 0;
}
/**
* Pretty-prints the specified Object into a String.
* All properties will be in alpha ordering.
* Each object will be assigned an id during printing;
* this value will be displayed next to the object type token
* preceded by a '#', for example:
*
* * (mx.messaging.messages::AsyncMessage)#2.* *
This id is used to indicate when a circular reference occurs.
* Properties of an object that are of the Class
type will
* appear only as the assigned type.
* For example a custom definition like the following:
* public class MyCustomClass { * public var clazz:Class; * }* *
With the clazz
property assigned to Date
* will display as shown below:
* (somepackage::MyCustomClass)#0 * clazz = (Date)* * @param obj Object to be pretty printed. * * @param namespaceURIs Array of namespace URIs for properties * that should be included in the output. * By default only properties in the public namespace will be included in * the output. * To get all properties regardless of namespace pass an array with a * single element of "*". * * @param exclude Array of the property names that should be * excluded from the output. * Use this to remove data from the formatted string. * * @return String containing the formatted version * of the specified object. * * @example *
* // example 1 * var obj:AsyncMessage = new AsyncMessage(); * obj.body = []; * obj.body.push(new AsyncMessage()); * obj.headers["1"] = { name: "myName", num: 15.3}; * obj.headers["2"] = { name: "myName", num: 15.3}; * obj.headers["10"] = { name: "myName", num: 15.3}; * obj.headers["11"] = { name: "myName", num: 15.3}; * trace(ObjectUtil.toString(obj)); * * // will output to flashlog.txt * (mx.messaging.messages::AsyncMessage)#0 * body = (Array)#1 * [0] (mx.messaging.messages::AsyncMessage)#2 * body = (Object)#3 * clientId = (Null) * correlationId = "" * destination = "" * headers = (Object)#4 * messageId = "378CE96A-68DB-BC1B-BCF7FFFFFFFFB525" * sequenceId = (Null) * sequencePosition = 0 * sequenceSize = 0 * timeToLive = 0 * timestamp = 0 * clientId = (Null) * correlationId = "" * destination = "" * headers = (Object)#5 * 1 = (Object)#6 * name = "myName" * num = 15.3 * 10 = (Object)#7 * name = "myName" * num = 15.3 * 11 = (Object)#8 * name = "myName" * num = 15.3 * 2 = (Object)#9 * name = "myName" * num = 15.3 * messageId = "1D3E6E96-AC2D-BD11-6A39FFFFFFFF517E" * sequenceId = (Null) * sequencePosition = 0 * sequenceSize = 0 * timeToLive = 0 * timestamp = 0 * * // example 2 with circular references * obj = {}; * obj.prop1 = new Date(); * obj.prop2 = []; * obj.prop2.push(15.2); * obj.prop2.push("testing"); * obj.prop2.push(true); * obj.prop3 = {}; * obj.prop3.circular = obj; * obj.prop3.deeper = new ErrorMessage(); * obj.prop3.deeper.rootCause = obj.prop3.deeper; * obj.prop3.deeper2 = {}; * obj.prop3.deeper2.deeperStill = {}; * obj.prop3.deeper2.deeperStill.yetDeeper = obj; * trace(ObjectUtil.toString(obj)); * * // will output to flashlog.txt * (Object)#0 * prop1 = Tue Apr 26 13:59:17 GMT-0700 2005 * prop2 = (Array)#1 * [0] 15.2 * [1] "testing" * [2] true * prop3 = (Object)#2 * circular = (Object)#0 * deeper = (mx.messaging.messages::ErrorMessage)#3 * body = (Object)#4 * clientId = (Null) * code = (Null) * correlationId = "" * destination = "" * details = (Null) * headers = (Object)#5 * level = (Null) * message = (Null) * messageId = "14039376-2BBA-0D0E-22A3FFFFFFFF140A" * rootCause = (mx.messaging.messages::ErrorMessage)#3 * sequenceId = (Null) * sequencePosition = 0 * sequenceSize = 0 * timeToLive = 0 * timestamp = 0 * deeper2 = (Object)#6 * deeperStill = (Object)#7 * yetDeeper = (Object)#0 * * // example 3 with Dictionary * var point:Point = new Point(100, 100); * var point2:Point = new Point(100, 100); * var obj:Dictionary = new Dictionary(); * obj[point] = "point"; * obj[point2] = "point2"; * obj["1"] = { name: "one", num: 1}; * obj["two"] = { name: "2", num: 2}; * obj[3] = 3; * trace(ObjectUtil.toString(obj)); * * // will output to flashlog.txt * (flash.utils::Dictionary)#0 * {(flash.geom::Point)#1 * length = 141.4213562373095 * x = 100 * y = 100} = "point2" * {(flash.geom::Point)#2 * length = 141.4213562373095 * x = 100 * y = 100} = "point" * {1} = (Object)#3 * name = "one" * num = 1 * {3} = 3 * {"two"} = (Object)#4 * name = "2" * num = 2 * ** * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public static function toString(value:Object, namespaceURIs:Array = null, exclude:Array = null):String { if (exclude == null) { exclude = defaultToStringExcludes; } refCount = 0; return internalToString(value, 0, null, namespaceURIs, exclude); } /** * This method cleans up all of the additional parameters that show up in AsDoc * code hinting tools that developers shouldn't ever see. * @private */ private static function internalToString(value:Object, indent:int = 0, refs:Dictionary= null, namespaceURIs:Array = null, exclude:Array = null):String { var str:String; var type:String = value == null ? "null" : typeof(value); switch (type) { case "boolean": case "number": { return value.toString(); } case "string": { return "\"" + value.toString() + "\""; } case "object": { if (value is Date) { return value.toString(); } else if (value is XMLNode) { return value.toString(); } else if (value is Class) { return "(" + getQualifiedClassName(value) + ")"; } else { var classInfo:Object = getClassInfo(value, exclude, { includeReadOnly: true, uris: namespaceURIs }); var properties:Array = classInfo.properties; str = "(" + classInfo.name + ")"; // refs help us avoid circular reference infinite recursion. // Each time an object is encoumtered it is pushed onto the // refs stack so that we can determine if we have visited // this object already. if (refs == null) refs = new Dictionary(true); // Check to be sure we haven't processed this object before // Dictionary has some bugs, so we want to work around them as best we can try { var id:Object = refs[value]; if (id != null) { str += "#" + int(id); return str; } } catch (e:Error) { //Since we can't test for infinite loop, we simply return toString. return String(value); } if (value != null) { str += "#" + refCount.toString(); refs[value] = refCount; refCount++; } var isArray:Boolean = value is Array; var isDict:Boolean = value is Dictionary; var prop:*; indent += 2; // Print all of the variable values. for (var j:int = 0; j < properties.length; j++) { str = newline(str, indent); prop = properties[j]; if (isArray) str += "["; else if (isDict) str += "{"; if (isDict) { // in dictionaries, recurse on the key, because it can be a complex object str += internalToString(prop, indent, refs, namespaceURIs, exclude); } else { str += prop.toString(); } if (isArray) str += "] "; else if (isDict) str += "} = "; else str += " = "; try { // print the value str += internalToString(value[prop], indent, refs, namespaceURIs, exclude); } catch(e:Error) { // value[prop] can cause an RTE // for certain properties of certain objects. // For example, accessing the properties // actionScriptVersion // childAllowsParent // frameRate // height // loader // parentAllowsChild // sameDomain // swfVersion // width // of a Stage's loaderInfo causes // Error #2099: The loading object is not // sufficiently loaded to provide this information // In this case, we simply output ? for the value. str += "?"; } } indent -= 2; return str; } break; } case "xml": { return value.toXMLString(); } default: { return "(" + type + ")"; } } return "(unknown)"; } /** * @private * This method will append a newline and the specified number of spaces * to the given string. */ private static function newline(str:String, n:int = 0):String { var result:String = str; result += "\n"; for (var i:int = 0; i < n; i++) { result += " "; } return result; } private static function internalCompare(a:Object, b:Object, currentDepth:int, desiredDepth:int, refs:Dictionary):int { if (a == null && b == null) return 0; if (a == null) return 1; if (b == null) return -1; if (a is ObjectProxy) a = ObjectProxy(a).object_proxy::object; if (b is ObjectProxy) b = ObjectProxy(b).object_proxy::object; var typeOfA:String = typeof(a); var typeOfB:String = typeof(b); var result:int = 0; if (typeOfA == typeOfB) { switch(typeOfA) { case "boolean": { result = numericCompare(Number(a), Number(b)); break; } case "number": { result = numericCompare(a as Number, b as Number); break; } case "string": { result = stringCompare(a as String, b as String); break; } case "object": { var newDepth:int = desiredDepth > 0 ? desiredDepth -1 : desiredDepth; // refs help us avoid circular reference infinite recursion. var aRef:Object = getRef(a,refs); var bRef:Object = getRef(b,refs); if (aRef == bRef) return 0; // the cool thing about our dictionary is that if // we've seen objects and determined that they are inequal, then // we would've already exited out of this compare() call. So the // only info in the dictionary are sets of equal items // let's first define them as equal // this stops an "infinite loop" problem where A.i = B and B.i = A // if we later find that an object (one of the subobjects) is in fact unequal, // then we will return false and quit out of everything. These refs are thrown away // so it doesn't matter if it's correct. refs[bRef] = aRef; if (desiredDepth != -1 && (currentDepth > desiredDepth)) { // once we try to go beyond the desired depth we should // toString() our way out result = stringCompare(a.toString(), b.toString()); } else if ((a is Array) && (b is Array)) { result = arrayCompare(a as Array, b as Array, currentDepth, desiredDepth, refs); } else if ((a is Date) && (b is Date)) { result = dateCompare(a as Date, b as Date); } else if ((a is IList) && (b is IList)) { result = listCompare(a as IList, b as IList, currentDepth, desiredDepth, refs); } else if ((a is ByteArray) && (b is ByteArray)) { result = byteArrayCompare(a as ByteArray, b as ByteArray); } else if (getQualifiedClassName(a) == getQualifiedClassName(b)) { var aProps:Array = getClassInfo(a).properties; var bProps:Array; // if the objects are dynamic they could have different // # of properties and should be treated on that basis first var isDynamicObject:Boolean = isDynamicObject(a); // if it's dynamic, check to see that they have all the same properties if (isDynamicObject) { bProps = getClassInfo(b).properties; result = arrayCompare(aProps, bProps, currentDepth, newDepth, refs); if (result != 0) return result; } // now that we know we have the same properties, let's compare the values var propName:QName; var aProp:Object; var bProp:Object; for (var i:int = 0; i < aProps.length; i++) { propName = aProps[i]; aProp = a[propName]; bProp = b[propName]; result = internalCompare(aProp, bProp, currentDepth+1, newDepth, refs); if (result != 0) { return result; } } } else { // We must be inequal, so return 1 return 1; } break; } } } else // be consistent with the order we return here { return stringCompare(typeOfA, typeOfB); } return result; } /** * Returns information about the class, and properties of the class, for * the specified Object. * * @param obj The Object to inspect. * * @param exclude Array of Strings specifying the property names that should be * excluded from the returned result. For example, you could specify *
["currentTarget", "target"]
for an Event object since these properties
* can cause the returned result to become large.
*
* @param options An Object containing one or more properties
* that control the information returned by this method.
* The properties include the following:
*
* includeReadOnly
: If false
,
* exclude Object properties that are read-only.
* The default value is true
.includeTransient
: If false
,
* exclude Object properties and variables that have [Transient]
metadata.
* The default value is true
.uris
: Array of Strings of all namespaces that should be included in the output.
* It does allow for a wildcard of "~~".
* By default, it is null, meaning no namespaces should be included.
* For example, you could specify ["mx_internal", "mx_object"]
* or ["~~"]
.name
: String containing the name of the class.properties
: Sorted list of the property names of the specified object,
* or references to the original key if the specified object is a Dictionary. The individual
* array elements are QName instances, which contain both the local name of the property as well as the URI.getClassInfo
and examines the metadata information to
* determine whether a property on a given object has the specified
* metadata.
*
* @param obj The object holding the property.
* @param propName The property to check for metadata.
* @param metadataName The name of the metadata to check on the property.
* @param excludes If any properties need to be excluded when generating class info. (Optional)
* @param options If any options flags need to changed when generating class info. (Optional)
* @return true if the property has the specified metadata.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static function hasMetadata(obj:Object,
propName:String,
metadataName:String,
excludes:Array = null,
options:Object = null):Boolean
{
var classInfo:Object = getClassInfo(obj, excludes, options);
var metadataInfo:Object = classInfo["metadata"];
return internalHasMetadata(metadataInfo, propName, metadataName);
}
/**
* Returns true
if the object is an instance of a dynamic class.
*
* @param obj The object.
*
* @return true
if the object is an instance of a dynamic class.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static function isDynamicObject(obj:Object):Boolean
{
try
{
// this test for checking whether an object is dynamic or not is
// pretty hacky, but it assumes that no-one actually has a
// property defined called "wootHackwoot"
obj["wootHackwoot"];
}
catch (e:Error)
{
// our object isn't from a dynamic class
return false;
}
return true;
}
/**
* @private
*/
private static function internalHasMetadata(metadataInfo:Object, propName:String, metadataName:String):Boolean
{
if (metadataInfo != null)
{
var metadata:Object = metadataInfo[propName];
if (metadata != null)
{
if (metadata[metadataName] != null)
return true;
}
}
return false;
}
/**
* @private
*/
private static function recordMetadata(properties:XMLList):Object
{
var result:Object = null;
try
{
for each (var prop:XML in properties)
{
var propName:String = prop.attribute("name").toString();
var metadataList:XMLList = prop.metadata;
if (metadataList.length() > 0)
{
if (result == null)
result = {};
var metadata:Object = {};
result[propName] = metadata;
for each (var md:XML in metadataList)
{
var mdName:String = md.attribute("name").toString();
var argsList:XMLList = md.arg;
var value:Object = {};
for each (var arg:XML in argsList)
{
var argKey:String = arg.attribute("key").toString();
if (argKey != null)
{
var argValue:String = arg.attribute("value").toString();
value[argKey] = argValue;
}
}
var existing:Object = metadata[mdName];
if (existing != null)
{
var existingArray:Array;
if (existing is Array)
existingArray = existing as Array;
else
{
existingArray = [existing];
delete metadata[mdName];
}
existingArray.push(value);
existing = existingArray;
}
else
{
existing = value;
}
metadata[mdName] = existing;
}
}
}
}
catch(e:Error)
{
}
return result;
}
/**
* @private
*/
private static function getCacheKey(o:Object, excludes:Array = null, options:Object = null):String
{
var key:String = getQualifiedClassName(o);
if (excludes != null)
{
for (var i:uint = 0; i < excludes.length; i++)
{
var excl:String = excludes[i] as String;
if (excl != null)
key += excl;
}
}
if (options != null)
{
for (var flag:String in options)
{
key += flag;
var value:String = options[flag] as String;
if (value != null)
key += value;
}
}
return key;
}
/**
* @private
*/
private static function arrayCompare(a:Array, b:Array,
currentDepth:int, desiredDepth:int,
refs:Dictionary):int
{
var result:int = 0;
if (a.length != b.length)
{
if (a.length < b.length)
result = -1;
else
result = 1;
}
else
{
var key:Object;
for (key in a)
{
if (b.hasOwnProperty(key))
{
result = internalCompare(a[key], b[key], currentDepth,
desiredDepth, refs);
if (result != 0)
return result;
}
else
{
return -1;
}
}
for (key in b)
{
if (!a.hasOwnProperty(key))
{
return 1;
}
}
}
return result;
}
/**
* @private
*/
private static function byteArrayCompare(a:ByteArray, b:ByteArray):int
{
var result:int = 0;
if (a == b)
return result;
if (a.length != b.length)
{
if (a.length < b.length)
result = -1;
else
result = 1;
}
else
{
for (var i:int = 0; i < a.length; i++)
{
result = numericCompare(a[i], b[i]);
if (result != 0)
{
i = a.length;
}
}
}
return result;
}
/**
* @private
*/
private static function listCompare(a:IList, b:IList, currentDepth:int,
desiredDepth:int, refs:Dictionary):int
{
var result:int = 0;
if (a.length != b.length)
{
if (a.length < b.length)
result = -1;
else
result = 1;
}
else
{
for (var i:int = 0; i < a.length; i++)
{
result = internalCompare(a.getItemAt(i), b.getItemAt(i),
currentDepth+1, desiredDepth, refs);
if (result != 0)
{
i = a.length;
}
}
}
return result;
}
/**
* @private
* This is the "find" for our union-find algorithm when doing object searches.
* The dictionary keeps track of sets of equal objects
*/
private static function getRef(o:Object, refs:Dictionary):Object
{
var oRef:Object = refs[o];
while (oRef && oRef != refs[oRef])
{
oRef = refs[oRef];
}
if (!oRef)
oRef = o;
if (oRef != refs[o])
refs[o] = oRef;
return oRef
}
/**
* @private
*/
private static var refCount:int = 0;
/**
* @private
*/
private static var CLASS_INFO_CACHE:Object = {};
}
}