1////////////////////////////////////////////////////////////////////////////////
2//
3//  ADOBE SYSTEMS INCORPORATED
4//  Copyright 2005-2007 Adobe Systems Incorporated
5//  All Rights Reserved.
6//
7//  NOTICE: Adobe permits you to use, modify, and distribute this file
8//  in accordance with the terms of the license agreement accompanying it.
9//
10////////////////////////////////////////////////////////////////////////////////
11
12package mx.utils
13{
14
15import flash.utils.ByteArray;
16import flash.utils.Dictionary;
17import flash.utils.getQualifiedClassName;
18import flash.utils.describeType;
19import flash.xml.XMLNode;
20
21import mx.collections.IList;
22
23/**
24 *  The RPCObjectUtil class is a subset of ObjectUtil, removing methods
25 *  that create dependency issues when RPC messages are in a bootstrap loader.
26 *
27 *  @langversion 3.0
28 *  @playerversion Flash 9
29 *  @playerversion AIR 1.1
30 *  @productversion Flex 3
31 */
32public class RPCObjectUtil
33{
34    include "../core/Version.as";
35
36    /**
37    *  Array of properties to exclude from debugging output.
38    *
39    *  @langversion 3.0
40    *  @playerversion Flash 9
41    *  @playerversion AIR 1.1
42    *  @productversion Flex 3
43    */
44    private static var defaultToStringExcludes:Array = ["password", "credentials"];
45
46    //--------------------------------------------------------------------------
47    //
48    //  Class methods
49    //
50    //--------------------------------------------------------------------------
51
52
53    /**
54     *  Pretty-prints the specified Object into a String.
55     *  All properties will be in alpha ordering.
56     *  Each object will be assigned an id during printing;
57     *  this value will be displayed next to the object type token
58     *  preceded by a '#', for example:
59     *
60     *  <pre>
61     *  (mx.messaging.messages::AsyncMessage)#2.</pre>
62     *
63     *  <p>This id is used to indicate when a circular reference occurs.
64     *  Properties of an object that are of the <code>Class</code> type will
65     *  appear only as the assigned type.
66     *  For example a custom definition like the following:</p>
67     *
68     *  <pre>
69     *    public class MyCustomClass {
70     *      public var clazz:Class;
71     *    }</pre>
72     *
73     *  <p>With the <code>clazz</code> property assigned to <code>Date</code>
74     *  will display as shown below:</p>
75     *
76     *  <pre>
77     *   (somepackage::MyCustomClass)#0
78     *      clazz = (Date)</pre>
79     *
80     *  @param obj Object to be pretty printed.
81     *
82     *  @param namespaceURIs Array of namespace URIs for properties
83     *  that should be included in the output.
84     *  By default only properties in the public namespace will be included in
85     *  the output.
86     *  To get all properties regardless of namespace pass an array with a
87     *  single element of "*".
88     *
89     *  @param exclude Array of the property names that should be
90     *  excluded from the output.
91     *  Use this to remove data from the formatted string.
92     *
93     *  @return String containing the formatted version
94     *  of the specified object.
95     *
96     *  @example
97     *  <pre>
98     *  // example 1
99     *  var obj:AsyncMessage = new AsyncMessage();
100     *  obj.body = [];
101     *  obj.body.push(new AsyncMessage());
102     *  obj.headers["1"] = { name: "myName", num: 15.3};
103     *  obj.headers["2"] = { name: "myName", num: 15.3};
104     *  obj.headers["10"] = { name: "myName", num: 15.3};
105     *  obj.headers["11"] = { name: "myName", num: 15.3};
106     *  trace(ObjectUtil.toString(obj));
107     *
108     *  // will output to flashlog.txt
109     *  (mx.messaging.messages::AsyncMessage)#0
110     *    body = (Array)#1
111     *      [0] (mx.messaging.messages::AsyncMessage)#2
112     *        body = (Object)#3
113     *        clientId = (Null)
114     *        correlationId = ""
115     *        destination = ""
116     *        headers = (Object)#4
117     *        messageId = "378CE96A-68DB-BC1B-BCF7FFFFFFFFB525"
118     *        sequenceId = (Null)
119     *        sequencePosition = 0
120     *        sequenceSize = 0
121     *        timeToLive = 0
122     *        timestamp = 0
123     *    clientId = (Null)
124     *    correlationId = ""
125     *    destination = ""
126     *    headers = (Object)#5
127     *      1 = (Object)#6
128     *        name = "myName"
129     *        num = 15.3
130     *      10 = (Object)#7
131     *        name = "myName"
132     *        num = 15.3
133     *      11 = (Object)#8
134     *        name = "myName"
135     *        num = 15.3
136     *      2 = (Object)#9
137     *        name = "myName"
138     *        num = 15.3
139     *    messageId = "1D3E6E96-AC2D-BD11-6A39FFFFFFFF517E"
140     *    sequenceId = (Null)
141     *    sequencePosition = 0
142     *    sequenceSize = 0
143     *    timeToLive = 0
144     *    timestamp = 0
145     *
146     *  // example 2 with circular references
147     *  obj = {};
148     *  obj.prop1 = new Date();
149     *  obj.prop2 = [];
150     *  obj.prop2.push(15.2);
151     *  obj.prop2.push("testing");
152     *  obj.prop2.push(true);
153     *  obj.prop3 = {};
154     *  obj.prop3.circular = obj;
155     *  obj.prop3.deeper = new ErrorMessage();
156     *  obj.prop3.deeper.rootCause = obj.prop3.deeper;
157     *  obj.prop3.deeper2 = {};
158     *  obj.prop3.deeper2.deeperStill = {};
159     *  obj.prop3.deeper2.deeperStill.yetDeeper = obj;
160     *  trace(ObjectUtil.toString(obj));
161     *
162     *  // will output to flashlog.txt
163     *  (Object)#0
164     *    prop1 = Tue Apr 26 13:59:17 GMT-0700 2005
165     *    prop2 = (Array)#1
166     *      [0] 15.2
167     *      [1] "testing"
168     *      [2] true
169     *    prop3 = (Object)#2
170     *      circular = (Object)#0
171     *      deeper = (mx.messaging.messages::ErrorMessage)#3
172     *        body = (Object)#4
173     *        clientId = (Null)
174     *        code = (Null)
175     *        correlationId = ""
176     *        destination = ""
177     *        details = (Null)
178     *        headers = (Object)#5
179     *        level = (Null)
180     *        message = (Null)
181     *        messageId = "14039376-2BBA-0D0E-22A3FFFFFFFF140A"
182     *        rootCause = (mx.messaging.messages::ErrorMessage)#3
183     *        sequenceId = (Null)
184     *        sequencePosition = 0
185     *        sequenceSize = 0
186     *        timeToLive = 0
187     *        timestamp = 0
188     *      deeper2 = (Object)#6
189     *        deeperStill = (Object)#7
190     *          yetDeeper = (Object)#0
191     *  </pre>
192     *
193     *
194     *  @langversion 3.0
195     *  @playerversion Flash 9
196     *  @playerversion AIR 1.1
197     *  @productversion Flex 3
198     */
199    public static function toString(value:Object,
200                                    namespaceURIs:Array = null,
201                                    exclude:Array = null):String
202    {
203        if (exclude == null)
204        {
205            exclude = defaultToStringExcludes;
206        }
207
208        refCount = 0;
209        return internalToString(value, 0, null, namespaceURIs, exclude);
210    }
211
212    /**
213     *  This method cleans up all of the additional parameters that show up in AsDoc
214     *  code hinting tools that developers shouldn't ever see.
215     *  @private
216     */
217    private static function internalToString(value:Object,
218                                             indent:int = 0,
219                                             refs:Dictionary= null,
220                                             namespaceURIs:Array = null,
221                                             exclude:Array = null):String
222    {
223        var str:String;
224        var type:String = value == null ? "null" : typeof(value);
225        switch (type)
226        {
227            case "boolean":
228            case "number":
229            {
230                return value.toString();
231            }
232
233            case "string":
234            {
235                return "\"" + value.toString() + "\"";
236            }
237
238            case "object":
239            {
240                if (value is Date)
241                {
242                    return value.toString();
243                }
244                else if (value is XMLNode)
245                {
246                    return value.toString();
247                }
248                else if (value is Class)
249                {
250                    return "(" + getQualifiedClassName(value) + ")";
251                }
252                else
253                {
254                    var classInfo:Object = getClassInfo(value, exclude,
255                        { includeReadOnly: true, uris: namespaceURIs });
256
257                    var properties:Array = classInfo.properties;
258
259                    str = "(" + classInfo.name + ")";
260
261                    // refs help us avoid circular reference infinite recursion.
262                    // Each time an object is encoumtered it is pushed onto the
263                    // refs stack so that we can determine if we have visited
264                    // this object already.
265                    if (refs == null)
266                        refs = new Dictionary(true);
267
268                    // Check to be sure we haven't processed this object before
269                    var id:Object = refs[value];
270                    if (id != null)
271                    {
272                        str += "#" + int(id);
273                        return str;
274                    }
275
276                    if (value != null)
277                    {
278                        str += "#" + refCount.toString();
279                        refs[value] = refCount;
280                        refCount++;
281                    }
282
283                    var isArray:Boolean = value is Array;
284                    var prop:*;
285                    indent += 2;
286
287                    // Print all of the variable values.
288                    for (var j:int = 0; j < properties.length; j++)
289                    {
290                        str = newline(str, indent);
291                        prop = properties[j];
292                        if (isArray)
293                            str += "[";
294                        str += prop.toString();
295                        if (isArray)
296                            str += "] ";
297                        else
298                            str += " = ";
299                        try
300                        {
301                            str += internalToString(
302                                        value[prop], indent, refs, namespaceURIs,
303                                        exclude);
304                        }
305                        catch(e:Error)
306                        {
307                            // value[prop] can cause an RTE
308                            // for certain properties of certain objects.
309                            // For example, accessing the properties
310                            //   actionScriptVersion
311                            //   childAllowsParent
312                            //   frameRate
313                            //   height
314                            //   loader
315                            //   parentAllowsChild
316                            //   sameDomain
317                            //   swfVersion
318                            //   width
319                            // of a Stage's loaderInfo causes
320                            //   Error #2099: The loading object is not
321                            //   sufficiently loaded to provide this information
322                            // In this case, we simply output ? for the value.
323                            str += "?";
324                        }
325                    }
326                    indent -= 2;
327                    return str;
328                }
329                break;
330            }
331
332            case "xml":
333            {
334                return value.toString();
335            }
336
337            default:
338            {
339                return "(" + type + ")";
340            }
341        }
342
343        return "(unknown)";
344    }
345
346    /**
347     *  @private
348     *  This method will append a newline and the specified number of spaces
349     *  to the given string.
350     */
351    private static function newline(str:String, n:int = 0):String
352    {
353        var result:String = str;
354        result += "\n";
355
356        for (var i:int = 0; i < n; i++)
357        {
358            result += " ";
359        }
360        return result;
361    }
362
363
364    /**
365     *  Returns information about the class, and properties of the class, for
366     *  the specified Object.
367     *
368     *  @param obj The Object to inspect.
369     *
370     *  @param exclude Array of Strings specifying the property names that should be
371     *  excluded from the returned result. For example, you could specify
372     *  <code>["currentTarget", "target"]</code> for an Event object since these properties
373     *  can cause the returned result to become large.
374     *
375     *  @param options An Object containing one or more properties
376     *  that control the information returned by this method.
377     *  The properties include the following:
378     *
379     *  <ul>
380     *    <li><code>includeReadOnly</code>: If <code>false</code>,
381     *      exclude Object properties that are read-only.
382     *      The default value is <code>true</code>.</li>
383     *  <li><code>includeTransient</code>: If <code>false</code>,
384     *      exclude Object properties and variables that have <code>[Transient]</code> metadata.
385     *      The default value is <code>true</code>.</li>
386     *  <li><code>uris</code>: Array of Strings of all namespaces that should be included in the output.
387     *      It does allow for a wildcard of "~~".
388     *      By default, it is null, meaning no namespaces should be included.
389     *      For example, you could specify <code>["mx_internal", "mx_object"]</code>
390     *      or <code>["~~"]</code>.</li>
391     *  </ul>
392     *
393     *  @return An Object containing the following properties:
394     *  <ul>
395     *    <li><code>name</code>: String containing the name of the class;</li>
396     *    <li><code>properties</code>: Sorted list of the property names of the specified object.</li>
397     *  </ul>
398    *
399    *  @langversion 3.0
400    *  @playerversion Flash 9
401    *  @playerversion AIR 1.1
402    *  @productversion Flex 3
403    */
404    public static function getClassInfo(obj:Object,
405                                        excludes:Array = null,
406                                        options:Object = null):Object
407    {
408        var n:int;
409        var i:int;
410
411		// this version doesn't handle ObjectProxy
412
413        if (options == null)
414            options = { includeReadOnly: true, uris: null, includeTransient: true };
415
416        var result:Object;
417        var propertyNames:Array = [];
418        var cacheKey:String;
419
420        var className:String;
421        var classAlias:String;
422        var properties:XMLList;
423        var prop:XML;
424        var dynamic:Boolean = false;
425        var metadataInfo:Object;
426
427        if (typeof(obj) == "xml")
428        {
429            className = "XML";
430            properties = obj.text();
431            if (properties.length())
432                propertyNames.push("*");
433            properties = obj.attributes();
434        }
435        else
436        {
437			// don't cache describe type.  Makes it slower, but fewer dependencies
438            var classInfo:XML = describeType(obj);
439            className = classInfo.@name.toString();
440            classAlias = classInfo.@alias.toString();
441            dynamic = (classInfo.@isDynamic.toString() == "true");
442
443            if (options.includeReadOnly)
444                properties = classInfo..accessor.(@access != "writeonly") + classInfo..variable;
445            else
446                properties = classInfo..accessor.(@access == "readwrite") + classInfo..variable;
447
448            var numericIndex:Boolean = false;
449        }
450
451        // If type is not dynamic, check our cache for class info...
452        if (!dynamic)
453        {
454            cacheKey = getCacheKey(obj, excludes, options);
455            result = CLASS_INFO_CACHE[cacheKey];
456            if (result != null)
457                return result;
458        }
459
460        result = {};
461        result["name"] = className;
462        result["alias"] = classAlias;
463        result["properties"] = propertyNames;
464        result["dynamic"] = dynamic;
465        result["metadata"] = metadataInfo = recordMetadata(properties);
466
467        var excludeObject:Object = {};
468        if (excludes)
469        {
470            n = excludes.length;
471            for (i = 0; i < n; i++)
472            {
473                excludeObject[excludes[i]] = 1;
474            }
475        }
476
477        var isArray:Boolean = className == "Array";
478        if (dynamic)
479        {
480            for (var p:String in obj)
481            {
482                if (excludeObject[p] != 1)
483                {
484                    if (isArray)
485                    {
486                         var pi:Number = parseInt(p);
487                         if (isNaN(pi))
488                             propertyNames.push(new QName("", p));
489                         else
490                            propertyNames.push(pi);
491                    }
492                    else
493                    {
494                        propertyNames.push(new QName("", p));
495                    }
496                }
497            }
498            numericIndex = isArray && !isNaN(Number(p));
499        }
500
501        if (className == "Object" || isArray)
502        {
503            // Do nothing since we've already got the dynamic members
504        }
505        else if (className == "XML")
506        {
507            n = properties.length();
508            for (i = 0; i < n; i++)
509            {
510                p = properties[i].name();
511                if (excludeObject[p] != 1)
512                    propertyNames.push(new QName("", "@" + p));
513            }
514        }
515        else
516        {
517            n = properties.length();
518            var uris:Array = options.uris;
519            var uri:String;
520            var qName:QName;
521            for (i = 0; i < n; i++)
522            {
523                prop = properties[i];
524                p = prop.@name.toString();
525                uri = prop.@uri.toString();
526
527                if (excludeObject[p] == 1)
528                    continue;
529
530                if (!options.includeTransient && internalHasMetadata(metadataInfo, p, "Transient"))
531                    continue;
532
533                if (uris != null)
534                {
535                    if (uris.length == 1 && uris[0] == "*")
536                    {
537                        qName = new QName(uri, p);
538                        try
539                        {
540                            obj[qName]; // access the property to ensure it is supported
541                            propertyNames.push();
542                        }
543                        catch(e:Error)
544                        {
545                            // don't keep property name
546                        }
547                    }
548                    else
549                    {
550                        for (var j:int = 0; j < uris.length; j++)
551                        {
552                            uri = uris[j];
553                            if (prop.@uri.toString() == uri)
554                            {
555                                qName = new QName(uri, p);
556                                try
557                                {
558                                    obj[qName];
559                                    propertyNames.push(qName);
560                                }
561                                catch(e:Error)
562                                {
563                                    // don't keep property name
564                                }
565                            }
566                        }
567                    }
568                }
569                else if (uri.length == 0)
570                {
571                    qName = new QName(uri, p);
572                    try
573                    {
574                        obj[qName];
575                        propertyNames.push(qName);
576                    }
577                    catch(e:Error)
578                    {
579                        // don't keep property name
580                    }
581                }
582            }
583        }
584
585        propertyNames.sort(Array.CASEINSENSITIVE |
586                           (numericIndex ? Array.NUMERIC : 0));
587        // remove any duplicates, i.e. any items that can't be distingushed by toString()
588        for (i = 0; i < propertyNames.length - 1; i++)
589        {
590            // the list is sorted so any duplicates should be adjacent
591            // two properties are only equal if both the uri and local name are identical
592            if (propertyNames[i].toString() == propertyNames[i + 1].toString())
593            {
594                propertyNames.splice(i, 1);
595                i--; // back up
596            }
597        }
598
599        // For normal, non-dynamic classes we cache the class info
600        if (!dynamic)
601        {
602            cacheKey = getCacheKey(obj, excludes, options);
603            CLASS_INFO_CACHE[cacheKey] = result;
604        }
605
606        return result;
607    }
608
609    /**
610     *  @private
611     */
612    private static function internalHasMetadata(metadataInfo:Object, propName:String, metadataName:String):Boolean
613    {
614        if (metadataInfo != null)
615        {
616            var metadata:Object = metadataInfo[propName];
617            if (metadata != null)
618            {
619                if (metadata[metadataName] != null)
620                    return true;
621            }
622        }
623        return false;
624    }
625
626    /**
627     *  @private
628     */
629    private static function recordMetadata(properties:XMLList):Object
630    {
631        var result:Object = null;
632
633        try
634        {
635            for each (var prop:XML in properties)
636            {
637                var propName:String = prop.attribute("name").toString();
638                var metadataList:XMLList = prop.metadata;
639
640                if (metadataList.length() > 0)
641                {
642                    if (result == null)
643                        result = {};
644
645                    var metadata:Object = {};
646                    result[propName] = metadata;
647
648                    for each (var md:XML in metadataList)
649                    {
650                        var mdName:String = md.attribute("name").toString();
651
652                        var argsList:XMLList = md.arg;
653                        var value:Object = {};
654
655                        for each (var arg:XML in argsList)
656                        {
657                            var argKey:String = arg.attribute("key").toString();
658                            if (argKey != null)
659                            {
660                                var argValue:String = arg.attribute("value").toString();
661                                value[argKey] = argValue;
662                            }
663                        }
664
665                        var existing:Object = metadata[mdName];
666                        if (existing != null)
667                        {
668                            var existingArray:Array;
669                            if (existing is Array)
670                                existingArray = existing as Array;
671                            else
672                                existingArray = [];
673                            existingArray.push(value);
674                            existing = existingArray;
675                        }
676                        else
677                        {
678                            existing = value;
679                        }
680                        metadata[mdName] = existing;
681                    }
682                }
683            }
684        }
685        catch(e:Error)
686        {
687        }
688
689        return result;
690    }
691
692    /**
693     *  @private
694     */
695    private static function getCacheKey(o:Object, excludes:Array = null, options:Object = null):String
696    {
697        var key:String = getQualifiedClassName(o);
698
699        if (excludes != null)
700        {
701            for (var i:uint = 0; i < excludes.length; i++)
702            {
703                var excl:String = excludes[i] as String;
704                if (excl != null)
705                    key += excl;
706            }
707        }
708
709        if (options != null)
710        {
711            for (var flag:String in options)
712            {
713                key += flag;
714                var value:String = options[flag] as String;
715                if (value != null)
716                    key += value;
717            }
718        }
719        return key;
720    }
721
722
723    /**
724     * @private
725     */
726    private static var refCount:int = 0;
727
728    /**
729     * @private
730     */
731    private static var CLASS_INFO_CACHE:Object = {};
732}
733
734}