| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // 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 |
| * <code>ObjectUtil.isSimple()</code> 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 <code>depth</code> 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:<pre> |
| * 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);</pre> |
| * |
| * <p>In the above example the complex properties of <code>a</code> and |
| * <code>b</code> will be flattened by a call to <code>toString()</code> |
| * when doing the comparison. |
| * In this case the <code>info</code> property will be turned into a string |
| * when performing the comparison.</p> |
| * |
| * @return Return 0 if a and b are null, NaN, or equal. |
| * Return 1 if a is null or greater than b. |
| * Return -1 if b is null or greater than a. |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| public static function compare(a:Object, b:Object, depth:int = -1):int |
| { |
| return internalCompare(a, b, 0, depth, new Dictionary(true)); |
| } |
| |
| /** |
| * Copies the specified Object and returns a reference to the copy. |
| * The copy is made using a native serialization technique. |
| * This means that custom serialization will be respected during the copy. |
| * |
| * <p>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 <code>clone()</code> method, or other method to perform the copy.</p> |
| * |
| * @param value Object that should be copied. |
| * |
| * @return Copy of the specified Object. |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| public static function copy(value:Object):Object |
| { |
| var buffer:ByteArray = new ByteArray(); |
| buffer.writeObject(value); |
| buffer.position = 0; |
| var result:Object = buffer.readObject(); |
| return result; |
| } |
| |
| /** |
| * Clones the specified Object and returns a reference to the clone. |
| * The clone is made using a native serialization technique. |
| * This means that custom serialization will be respected during the |
| * cloning. clone() differs from copy() in that the uid property of |
| * each object instance is retained. |
| * |
| * <p>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 <code>clone()</code> method.</p> |
| * |
| * @param value Object that should be cloned. |
| * |
| * @return Clone of the specified Object. |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 4 |
| */ |
| public static function clone(value:Object):Object |
| { |
| var result:Object = copy(value); |
| cloneInternal(result, value); |
| return result; |
| } |
| |
| /** |
| * Recursive helper used by the public clone method. |
| * @private |
| */ |
| private static function cloneInternal(result:Object, value:Object):void |
| { |
| if (value && value.hasOwnProperty("uid")) |
| result.uid = value.uid; |
| |
| var classInfo:Object = getClassInfo(value); |
| var v:Object; |
| for each (var p:* in classInfo.properties) |
| { |
| v = value[p]; |
| if (v && v.hasOwnProperty("uid")) |
| cloneInternal(result[p], v); |
| } |
| } |
| |
| /** |
| * Returns <code>true</code> if the object reference specified |
| * is a simple data type. The simple data types include the following: |
| * <ul> |
| * <li><code>String</code></li> |
| * <li><code>Number</code></li> |
| * <li><code>uint</code></li> |
| * <li><code>int</code></li> |
| * <li><code>Boolean</code></li> |
| * <li><code>Date</code></li> |
| * <li><code>Array</code></li> |
| * </ul> |
| * |
| * @param value Object inspected. |
| * |
| * @return <code>true</code> if the object specified |
| * is one of the types above; <code>false</code> 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 <code>a</code> is a NaN. |
| * -1 if only <code>b</code> is a NaN. |
| * -1 if <code>a</code> is less than <code>b</code>. |
| * 1 if <code>a</code> is greater than <code>b</code>. |
| * |
| * @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, |
| * <code>true</code>, or not, <code>false</code>. |
| * |
| * @return 0 is both Strings are null. |
| * 1 if only <code>a</code> is null. |
| * -1 if only <code>b</code> is null. |
| * -1 if <code>a</code> precedes <code>b</code>. |
| * 1 if <code>b</code> precedes <code>a</code>. |
| * |
| * @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 <code>a</code> and <code>b</code> are equal |
| * (or both are <code>null</code>); |
| * -1 if <code>a</code> is before <code>b</code> |
| * (or <code>b</code> is <code>null</code>); |
| * 1 if <code>a</code> is after <code>b</code> |
| * (or <code>a</code> is <code>null</code>); |
| * 0 is both dates getTime's are NaN; |
| * 1 if only <code>a</code> getTime is a NaN; |
| * -1 if only <code>b</code> getTime is a NaN. |
| * |
| * @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; |
| |
| if (isNaN(na) && isNaN(nb)) |
| return 0; |
| |
| if (isNaN(na)) |
| return 1; |
| |
| if (isNaN(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: |
| * |
| * <pre> |
| * (mx.messaging.messages::AsyncMessage)#2.</pre> |
| * |
| * <p>This id is used to indicate when a circular reference occurs. |
| * Properties of an object that are of the <code>Class</code> type will |
| * appear only as the assigned type. |
| * For example a custom definition like the following:</p> |
| * |
| * <pre> |
| * public class MyCustomClass { |
| * public var clazz:Class; |
| * }</pre> |
| * |
| * <p>With the <code>clazz</code> property assigned to <code>Date</code> |
| * will display as shown below:</p> |
| * |
| * <pre> |
| * (somepackage::MyCustomClass)#0 |
| * clazz = (Date)</pre> |
| * |
| * @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 |
| * <pre> |
| * // 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 |
| * |
| * </pre> |
| * |
| * @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 |
| * <code>["currentTarget", "target"]</code> 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: |
| * |
| * <ul> |
| * <li><code>includeReadOnly</code>: If <code>false</code>, |
| * exclude Object properties that are read-only. |
| * The default value is <code>true</code>.</li> |
| * <li><code>includeTransient</code>: If <code>false</code>, |
| * exclude Object properties and variables that have <code>[Transient]</code> metadata. |
| * The default value is <code>true</code>.</li> |
| * <li><code>uris</code>: 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 <code>["mx_internal", "mx_object"]</code> |
| * or <code>["~~"]</code>.</li> |
| * </ul> |
| * |
| * @return An Object containing the following properties: |
| * <ul> |
| * <li><code>name</code>: String containing the name of the class.</li> |
| * <li><code>properties</code>: 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.</li> |
| * </ul> |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 9 |
| * @playerversion AIR 1.1 |
| * @productversion Flex 3 |
| */ |
| public static function getClassInfo(obj:Object, |
| excludes:Array = null, |
| options:Object = null):Object |
| { |
| var n:int; |
| var i:int; |
| |
| if (obj is ObjectProxy) |
| obj = ObjectProxy(obj).object_proxy::object; |
| |
| if (options == null) |
| options = { includeReadOnly: true, uris: null, includeTransient: true }; |
| |
| var result:Object; |
| var propertyNames:Array = []; |
| var cacheKey:String; |
| |
| var className:String; |
| var classAlias:String; |
| var properties:XMLList; |
| var prop:XML; |
| var dynamic:Boolean = false; |
| var metadataInfo:Object; |
| |
| if (typeof(obj) == "xml") |
| { |
| className = "XML"; |
| properties = obj.text(); |
| if (properties.length()) |
| propertyNames.push("*"); |
| properties = obj.attributes(); |
| } |
| else |
| { |
| var classInfo:XML = DescribeTypeCache.describeType(obj).typeDescription; |
| className = classInfo.@name.toString(); |
| classAlias = classInfo.@alias.toString(); |
| dynamic = (classInfo.@isDynamic.toString() == "true"); |
| |
| if (options.includeReadOnly) |
| properties = classInfo..accessor.(@access != "writeonly") + classInfo..variable; |
| else |
| properties = classInfo..accessor.(@access == "readwrite") + classInfo..variable; |
| |
| var numericIndex:Boolean = false; |
| } |
| |
| // If type is not dynamic, check our cache for class info... |
| if (!dynamic) |
| { |
| cacheKey = getCacheKey(obj, excludes, options); |
| result = CLASS_INFO_CACHE[cacheKey]; |
| if (result != null) |
| return result; |
| } |
| |
| result = {}; |
| result["name"] = className; |
| result["alias"] = classAlias; |
| result["properties"] = propertyNames; |
| result["dynamic"] = dynamic; |
| result["metadata"] = metadataInfo = recordMetadata(properties); |
| |
| var excludeObject:Object = {}; |
| if (excludes) |
| { |
| n = excludes.length; |
| for (i = 0; i < n; i++) |
| { |
| excludeObject[excludes[i]] = 1; |
| } |
| } |
| |
| var isArray:Boolean = (obj is Array); |
| var isDict:Boolean = (obj is Dictionary); |
| |
| if (isDict) |
| { |
| // dictionaries can have multiple keys of the same type, |
| // (they can index by reference rather than QName, String, or number), |
| // which cannot be looked up by QName, so use references to the actual key |
| for (var key:* in obj) |
| { |
| propertyNames.push(key); |
| } |
| } |
| else if (dynamic) |
| { |
| for (var p:String in obj) |
| { |
| if (excludeObject[p] != 1) |
| { |
| if (isArray) |
| { |
| var pi:Number = parseInt(p); |
| if (isNaN(pi)) |
| propertyNames.push(new QName("", p)); |
| else |
| propertyNames.push(pi); |
| } |
| else |
| { |
| propertyNames.push(new QName("", p)); |
| } |
| } |
| } |
| numericIndex = isArray && !isNaN(Number(p)); |
| } |
| |
| if (isArray || isDict || className == "Object") |
| { |
| // Do nothing since we've already got the dynamic members |
| } |
| else if (className == "XML") |
| { |
| n = properties.length(); |
| for (i = 0; i < n; i++) |
| { |
| p = properties[i].name(); |
| if (excludeObject[p] != 1) |
| propertyNames.push(new QName("", "@" + p)); |
| } |
| } |
| else |
| { |
| n = properties.length(); |
| var uris:Array = options.uris; |
| var uri:String; |
| var qName:QName; |
| for (i = 0; i < n; i++) |
| { |
| prop = properties[i]; |
| p = prop.@name.toString(); |
| uri = prop.@uri.toString(); |
| |
| if (excludeObject[p] == 1) |
| continue; |
| |
| if (!options.includeTransient && internalHasMetadata(metadataInfo, p, "Transient")) |
| continue; |
| |
| if (uris != null) |
| { |
| if (uris.length == 1 && uris[0] == "*") |
| { |
| qName = new QName(uri, p); |
| try |
| { |
| obj[qName]; // access the property to ensure it is supported |
| propertyNames.push(); |
| } |
| catch(e:Error) |
| { |
| // don't keep property name |
| } |
| } |
| else |
| { |
| for (var j:int = 0; j < uris.length; j++) |
| { |
| uri = uris[j]; |
| if (prop.@uri.toString() == uri) |
| { |
| qName = new QName(uri, p); |
| try |
| { |
| obj[qName]; |
| propertyNames.push(qName); |
| } |
| catch(e:Error) |
| { |
| // don't keep property name |
| } |
| } |
| } |
| } |
| } |
| else if (uri.length == 0) |
| { |
| qName = new QName(uri, p); |
| try |
| { |
| obj[qName]; |
| propertyNames.push(qName); |
| } |
| catch(e:Error) |
| { |
| // don't keep property name |
| } |
| } |
| } |
| } |
| |
| propertyNames.sort(Array.CASEINSENSITIVE | |
| (numericIndex ? Array.NUMERIC : 0)); |
| |
| // dictionary keys can be indexed by an object reference |
| // there's a possibility that two keys will have the same toString() |
| // so we don't want to remove dupes |
| if (!isDict) |
| { |
| // for Arrays, etc., on the other hand... |
| // remove any duplicates, i.e. any items that can't be distingushed by toString() |
| for (i = 0; i < propertyNames.length - 1; i++) |
| { |
| // the list is sorted so any duplicates should be adjacent |
| // two properties are only equal if both the uri and local name are identical |
| if (propertyNames[i].toString() == propertyNames[i + 1].toString()) |
| { |
| propertyNames.splice(i, 1); |
| i--; // back up |
| } |
| } |
| } |
| |
| // For normal, non-dynamic classes we cache the class info |
| // Don't cache XML as it can be dynamic |
| if (!dynamic && className != "XML") |
| { |
| cacheKey = getCacheKey(obj, excludes, options); |
| CLASS_INFO_CACHE[cacheKey] = result; |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Uses <code>getClassInfo</code> 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 <code>true</code> if the object is an instance of a dynamic class. |
| * |
| * @param obj The object. |
| * |
| * @return <code>true</code> 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) |
| { |
| var length:int = excludes.length; |
| for (var i:uint = 0; i < 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]; |
| if (value != null) |
| key += value.toString(); |
| } |
| } |
| 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 = {}; |
| } |
| |
| } |