//////////////////////////////////////////////////////////////////////////////// // // 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. // //////////////////////////////////////////////////////////////////////////////// /* * The Bridge class, responsible for navigating JS instances */ package bridge { /* * imports */ import flash.external.ExternalInterface; import flash.utils.Timer; import flash.events.*; import flash.display.DisplayObject; import flash.system.ApplicationDomain; import flash.utils.Dictionary; import flash.utils.setTimeout; import mx.collections.errors.ItemPendingError; import mx.core.IMXMLObject; import flash.utils.getQualifiedClassName; import flash.utils.describeType; import flash.events.TimerEvent; /** * The FABridge class, responsible for proxying AS objects into javascript */ public class FABridge extends EventDispatcher implements IMXMLObject { //holds a list of stuff to call later, to break the recurrence of the js <> as calls //you must use the full class name, as returned by the getQualifiedClassName() function public static const MethodsToCallLater:Object = new Object(); MethodsToCallLater["mx.collections::ArrayCollection"]="refresh,removeItemAt"; public static const EventsToCallLater:Object = new Object(); EventsToCallLater["mx.data.events::UnresolvedConflictsEvent"]="true"; EventsToCallLater["mx.events::PropertyChangeEvent"]="true"; public static const INITIALIZED:String = "bridgeInitialized"; // constructor public function FABridge() { super(); initializeCallbacks(); } // private vars /** * stores a cache of descriptions of AS types suitable for sending to JS */ private var localTypeMap:Dictionary = new Dictionary(); /** * stores an id-referenced dictionary of objects exported to JS */ private var localInstanceMap:Dictionary = new Dictionary(); /** * stores an id-referenced dictionary of functions exported to JS */ private var localFunctionMap:Dictionary = new Dictionary(); /** * stores an id-referenced dictionary of proxy functions imported from JS */ private var remoteFunctionCache:Dictionary = new Dictionary(); /** * stores a list of custom serialization functions */ private var customSerializersMap:Dictionary = new Dictionary(); /** * stores a map of object ID's and their reference count */ private var refMap:Dictionary = new Dictionary(); /** * a local counter for generating unique IDs */ private var nextID:Number = 0; private var lastRef:int; /* values that can't be serialized natively across the bridge are packed and identified by type. These constants represent different serialization types */ public static const TYPE_ASINSTANCE:uint = 1; public static const TYPE_ASFUNCTION:uint = 2; public static const TYPE_JSFUNCTION:uint = 3; public static const TYPE_ANONYMOUS:uint = 4; private var _initChecked:Boolean = false; // properties //getters and setters for the main component in the swf - the root public function get rootObject():DisplayObject {return _rootObject;} public function set rootObject(value:DisplayObject):void { _rootObject = value; checkInitialized(); } /** * the bridge name */ public var bridgeName:String; private var _registerComplete:Boolean = false; /** * increment the reference count for an object being passed over the bridge */ public function incRef(objId:int):void { if(refMap[objId] == null) { //the object is being created; we now add it to the map and set its refCount = 1 refMap[objId] = 1; } else { refMap[objId] = refMap[objId] +1; } } /** * when an object has been completely passed to JS its reference count is decreased with 1 */ public function releaseRef(objId:int):void { if(refMap[objId] != null) { var newRefVal:int = refMap[objId] - 1; // if the object exists in the referenceMap and its count equals or has dropped under 0 we clean it up if(refMap[objId] != null && newRefVal <= 0) { delete refMap[objId]; delete localInstanceMap[objId]; } else { refMap[objId] = newRefVal; } } } /** * attaches the callbacks to external interface */ public function initializeCallbacks():void { if (ExternalInterface.available == false) { return; } ExternalInterface.addCallback("getRoot", js_getRoot); ExternalInterface.addCallback("getPropFromAS", js_getPropFromAS); ExternalInterface.addCallback("setPropInAS", js_setPropertyInAS); ExternalInterface.addCallback("invokeASMethod", js_invokeMethod); ExternalInterface.addCallback("invokeASFunction", js_invokeFunction); ExternalInterface.addCallback("releaseASObjects", js_releaseASObjects); ExternalInterface.addCallback("create", js_create); ExternalInterface.addCallback("releaseNamedASObject",js_releaseNamedASObject); ExternalInterface.addCallback("incRef", incRef); ExternalInterface.addCallback("releaseRef", releaseRef); } private var _rootObject:DisplayObject; private var _document:DisplayObject; /** * called to check whether the bridge has been initialized for the specified document/id pairs */ public function initialized(document:Object, id:String):void { _document = (document as DisplayObject); if (_document != null) { checkInitialized(); } } private function get baseObject():DisplayObject { return (rootObject == null)? _document:rootObject; } private function checkInitialized():void { if(_initChecked== true) { return; } _initChecked = true; // oops! timing error. Player team is working on it. var t:Timer = new Timer(200,1); t.addEventListener(TimerEvent.TIMER,auxCheckInitialized); t.start(); } /** * auxiliary initialization check that is called after the timing has occurred */ private function auxCheckInitialized(e:Event):void { var bCanGetParams:Boolean = true; try { var params:Object = baseObject.root.loaderInfo.parameters; } catch (e:Error) { bCanGetParams = false; } if (bCanGetParams == false) { var t:Timer = new Timer(100); var timerFunc:Function = function(e:TimerEvent):void { if(baseObject.root != null) { try { bCanGetParams = true; var params:Object = baseObject.root.loaderInfo.parameters; } catch (err:Error) { bCanGetParams = false; } if (bCanGetParams) { t.removeEventListener(TimerEvent.TIMER, timerFunc); t.stop(); dispatchInit(); } } } t.addEventListener(TimerEvent.TIMER, timerFunc); t.start(); } else { dispatchInit(); } } /** * call into JS to annunce that the bridge is ready to be used */ private function dispatchInit(e:Event = null):void { if(_registerComplete == true) { return; } if (ExternalInterface.available == false) { return; } if (bridgeName == null) { bridgeName = baseObject.root.loaderInfo.parameters["bridgeName"]; if(bridgeName == null) { bridgeName = "flash"; } } _registerComplete = ExternalInterface.call("FABridge__bridgeInitialized", [bridgeName]); dispatchEvent(new Event(FABridge.INITIALIZED)); } // serialization/deserialization /** serializes a value for transfer across the bridge. primitive types are left as is. Arrays are left as arrays, but individual * values in the array are serialized according to their type. Functions and class instances are inserted into a hash table and sent * across as keys into the table. * * For class instances, if the instance has been sent before, only its id is passed. If This is the first time the instance has been sent, * a ref descriptor is sent associating the id with a type string. If this is the first time any instance of that type has been sent * across, a descriptor indicating methods, properties, and variables of the type is also sent across */ public function serialize(value:*, keep_refs:Boolean=false):* { var result:* = {}; result.newTypes = []; result.newRefs = {}; if (value is Number || value is Boolean || value is String || value == null || value == undefined || value is int || value is uint) { result = value; } else if (value is Array) { result = []; for(var i:int = 0; i < value.length; i++) { result[i] = serialize(value[i], keep_refs); } } else if (value is Function) { // serialize a class result.type = TYPE_ASFUNCTION; result.value = getFunctionID(value, true); } else if (getQualifiedClassName(value) == "Object") { result.type = TYPE_ANONYMOUS; result.value = value; } else { // serialize a class result.type = TYPE_ASINSTANCE; // make sure the type info is available var className:String = getQualifiedClassName(value); var serializer:Function = customSerializersMap[className]; // try looking up the serializer under an alternate name if (serializer == null) { if (className.indexOf('$') > 0) { var split:int = className.lastIndexOf(':'); if (split > 0) { var alternate:String = className.substring(split+1); serializer = customSerializersMap[alternate]; } } } if (serializer != null) { return serializer.apply(null, [value, keep_refs]); } else { if (retrieveCachedTypeDescription(className, false) == null) { try { result.newTypes.push(retrieveCachedTypeDescription(className, true)); } catch(err:Error) { var interfaceInfo:XMLList = describeType(value).implementsInterface; for each (var interf:XML in interfaceInfo) { className = interf.@type.toString(); if (retrieveCachedTypeDescription(className, false) == null){ result.newTypes.push(retrieveCachedTypeDescription(className, true)); } //end if push new data type } //end for going through interfaces var baseClass:String = describeType(value).@base.toString(); if (retrieveCachedTypeDescription(baseClass, false) == null){ result.newTypes.push(retrieveCachedTypeDescription(baseClass, true)); } //end if push new data type } } // make sure the reference is known var objRef:Number = getRef(value, false); var should_keep_ref:Boolean = false; if (isNaN(objRef)) { //create the reference if necessary objRef = getRef(value, true); should_keep_ref = true; } result.newRefs[objRef] = className; //trace("serializing new reference: " + className + " with value" + value); //the result is a getProperty / invokeMethod call. How can we know how much you will need the object ? if (keep_refs && should_keep_ref) { incRef(objRef); } result.value = objRef; } } return result; } /** * deserializes a value passed in from javascript. See serialize for details on how values are packed and * unpacked for transfer across the bridge. */ public function deserialize(valuePackage:*):* { var result:*; if (valuePackage is Number || valuePackage is Boolean || valuePackage is String || valuePackage === null || valuePackage === undefined || valuePackage is int || valuePackage is uint) { result = valuePackage; } else if(valuePackage is Array) { result = []; for (var i:int = 0; i < valuePackage.length; i++) { result[i] = deserialize(valuePackage[i]); } } else if (valuePackage.type == FABridge.TYPE_JSFUNCTION) { result = getRemoteFunctionProxy(valuePackage.value, true); } else if (valuePackage.type == FABridge.TYPE_ASFUNCTION) { throw new Error("as functions can't be passed back to as yet"); } else if (valuePackage.type == FABridge.TYPE_ASINSTANCE) { result = resolveRef(valuePackage.value); } else if (valuePackage.type == FABridge.TYPE_ANONYMOUS) { result = valuePackage.value; } return result; } public function addCustomSerialization(className:String, serializationFunction:Function):void { customSerializersMap[className] = serializationFunction; } // type management /** * retrieves a type description for the type indicated by className, building one and caching it if necessary */ public function retrieveCachedTypeDescription(className:String, createifNecessary:Boolean):Object { if(localTypeMap[className] == null && createifNecessary == true) { localTypeMap[className] = buildTypeDescription(className); } return localTypeMap[className]; } public function addCachedTypeDescription(className:String, desc:Object):Object { if (localTypeMap[className] == null) { localTypeMap[className] = desc; } return localTypeMap[className]; } /** * builds a type description for the type indiciated by className */ public function buildTypeDescription(className:String):Object { var desc:Object = {}; className = className.replace(/::/,"."); var objClass:Class = Class(ApplicationDomain.currentDomain.getDefinition(className)); var xData:XML = describeType(objClass); desc.name = xData.@name.toString(); var methods:Array = []; var xMethods:XMLList = xData.factory.method; for (var i:int = 0; i < xMethods.length(); i++) { methods.push(xMethods[i].@name.toString()); } desc.methods = methods; var accessors:Array = []; var xAcc:XMLList = xData.factory.accessor; for (i = 0; i < xAcc.length(); i++) { accessors.push(xAcc[i].@name.toString()); } xAcc = xData.factory.variable; for (i = 0; i < xAcc.length(); i++) { accessors.push(xAcc[i].@name.toString()); } desc.accessors = accessors; return desc; } // instance mgmt /** * resolves an instance id passed from JS to an instance previously cached for representing in JS */ private function resolveRef(objRef:Number):Object { try { return (objRef == -1)? baseObject : localInstanceMap[objRef]; } catch(e:Error) { return serialize("__FLASHERROR__"+"||"+e.message); } return (objRef == -1)? baseObject : localInstanceMap[objRef]; } /** * returns an id associated with the object provided for passing across the bridge to JS */ public function getRef(obj:Object, createIfNecessary:Boolean):Number { try { var ref:Number; if (createIfNecessary) { var newRef:Number = nextID++; localInstanceMap[newRef] = obj; ref = newRef; } else { for (var key:* in localInstanceMap) { if (localInstanceMap[key] === obj) { ref = key; break; } } } } catch(e:Error) { return serialize("__FLASHERROR__"+"||"+e.message) } return ref; } // function management /** * resolves a function ID passed from JS to a local function previously cached for representation in JS */ private function resolveFunctionID(funcID:Number):Function { return localFunctionMap[funcID]; } /** * associates a unique ID with a local function suitable for passing across the bridge to proxy in Javascript */ public function getFunctionID(f:Function, createIfNecessary:Boolean):Number { var ref:Number; if (createIfNecessary) { var newID:Number = nextID++; localFunctionMap[newID] = f; ref = newID; } else { for (var key:* in localFunctionMap) { if (localFunctionMap[key] === f) { ref = key; } break; } } return ref; } /** * returns a proxy function that represents a function defined in javascript. This function can be called syncrhonously, and will * return any values returned by the JS function */ public function getRemoteFunctionProxy(functionID:Number, createIfNecessary:Boolean):Function { try { if (remoteFunctionCache[functionID] == null) { remoteFunctionCache[functionID] = function(...args):* { var externalArgs:Array = args.concat(); externalArgs.unshift(functionID); var serializedArgs:* = serialize(externalArgs, true); if(checkToThrowLater(serializedArgs[1])) { setTimeout(function a():* { try { var retVal:* = ExternalInterface.call("FABridge__invokeJSFunction", serializedArgs); for(var i:int = 0; i