/* * Copyright (c) 2009-2010 Digital Bazaar, Inc. All rights reserved. * * @author Dave Longley */ package { import flash.display.Sprite; /** * A SocketPool is a flash object that can be embedded in a web page to * allow javascript access to pools of Sockets. * * Javascript can create a pool and then as many Sockets as it desires. Each * Socket will be assigned a unique ID that allows continued javascript * access to it. There is no limit on the number of persistent socket * connections. */ public class SocketPool extends Sprite { import flash.events.Event; import flash.events.EventDispatcher; import flash.errors.IOError; import flash.events.IOErrorEvent; import flash.events.ProgressEvent; import flash.events.SecurityErrorEvent; import flash.events.TextEvent; import flash.external.ExternalInterface; import flash.net.SharedObject; import flash.system.Security; import flash.utils.ByteArray; import mx.utils.Base64Decoder; import mx.utils.Base64Encoder; // a map of ID to Socket private var mSocketMap:Object; // a counter for Socket IDs (Note: assumes there will be no overflow) private var mNextId:uint; // an event dispatcher for sending events to javascript private var mEventDispatcher:EventDispatcher; /** * Creates a new, unitialized SocketPool. * * @throws Error - if no external interface is available to provide * javascript access. */ public function SocketPool() { if(!ExternalInterface.available) { trace("ExternalInterface is not available"); throw new Error( "Flash's ExternalInterface is not available. This is a " + "requirement of SocketPool and therefore, it will be " + "unavailable."); } else { try { // set up javascript access: // initializes/cleans up the SocketPool ExternalInterface.addCallback("init", init); ExternalInterface.addCallback("cleanup", cleanup); // creates/destroys a socket ExternalInterface.addCallback("create", create); ExternalInterface.addCallback("destroy", destroy); // connects/closes a socket ExternalInterface.addCallback("connect", connect); ExternalInterface.addCallback("close", close); // checks for a connection ExternalInterface.addCallback("isConnected", isConnected); // sends/receives data over the socket ExternalInterface.addCallback("send", send); ExternalInterface.addCallback("receive", receive); // gets the number of bytes available on a socket ExternalInterface.addCallback( "getBytesAvailable", getBytesAvailable); // add a callback for subscribing to socket events ExternalInterface.addCallback("subscribe", subscribe); // add callbacks for deflate/inflate ExternalInterface.addCallback("deflate", deflate); ExternalInterface.addCallback("inflate", inflate); // add callbacks for local disk storage ExternalInterface.addCallback("setItem", setItem); ExternalInterface.addCallback("getItem", getItem); ExternalInterface.addCallback("removeItem", removeItem); ExternalInterface.addCallback("clearItems", clearItems); // socket pool is now ready ExternalInterface.call("window.forge.socketPool.ready"); } catch(e:Error) { log("error=" + e.errorID + "," + e.name + "," + e.message); throw e; } log("ready"); } } /** * A debug logging function. * * @param obj the string or error to log. */ CONFIG::debugging private function log(obj:Object):void { if(obj is String) { var str:String = obj as String; ExternalInterface.call("console.log", "SocketPool", str); } else if(obj is Error) { var e:Error = obj as Error; log("error=" + e.errorID + "," + e.name + "," + e.message); } } CONFIG::release private function log(obj:Object):void { // log nothing in release mode } /** * Called by javascript to initialize this SocketPool. * * @param options: * marshallExceptions: true to pass exceptions to and from * javascript. */ private function init(... args):void { log("init()"); // get options from first argument var options:Object = args.length > 0 ? args[0] : null; // create socket map, set next ID, and create event dispatcher mSocketMap = new Object(); mNextId = 1; mEventDispatcher = new EventDispatcher(); // enable marshalling exceptions if appropriate if(options != null && "marshallExceptions" in options && options.marshallExceptions === true) { try { // Note: setting marshallExceptions in IE, even inside of a // try-catch block will terminate flash. Don't set this on IE. ExternalInterface.marshallExceptions = true; } catch(e:Error) { log(e); } } log("init() done"); } /** * Called by javascript to clean up a SocketPool. */ private function cleanup():void { log("cleanup()"); mSocketMap = null; mNextId = 1; mEventDispatcher = null; log("cleanup() done"); } /** * Handles events. * * @param e the event to handle. */ private function handleEvent(e:Event):void { // dispatch socket event var message:String = (e is TextEvent) ? (e as TextEvent).text : null; mEventDispatcher.dispatchEvent( new SocketEvent(e.type, e.target as PooledSocket, message)); } /** * Called by javascript to create a Socket. * * @return the Socket ID. */ private function create():String { log("create()"); // create a Socket var id:String = "" + mNextId++; var s:PooledSocket = new PooledSocket(); s.id = id; s.addEventListener(Event.CONNECT, handleEvent); s.addEventListener(Event.CLOSE, handleEvent); s.addEventListener(ProgressEvent.SOCKET_DATA, handleEvent); s.addEventListener(IOErrorEvent.IO_ERROR, handleEvent); s.addEventListener(SecurityErrorEvent.SECURITY_ERROR, handleEvent); mSocketMap[id] = s; log("socket " + id + " created"); log("create() done"); return id; } /** * Called by javascript to clean up a Socket. * * @param id the ID of the Socket to clean up. */ private function destroy(id:String):void { log("destroy(" + id + ")"); if(id in mSocketMap) { // remove Socket delete mSocketMap[id]; log("socket " + id + " destroyed"); } log("destroy(" + id + ") done"); } /** * Connects the Socket with the given ID to the given host and port, * using the given socket policy port. * * @param id the ID of the Socket. * @param host the host to connect to. * @param port the port to connect to. * @param spPort the security policy port to use, 0 to use a url. * @param spUrl the http URL to the policy file to use, null for default. */ private function connect( id:String, host:String, port:uint, spPort:uint, spUrl:String = null):void { log("connect(" + id + "," + host + "," + port + "," + spPort + "," + spUrl + ")"); if(id in mSocketMap) { // get the Socket var s:PooledSocket = mSocketMap[id]; // load socket policy file // (permits socket access to backend) if(spPort !== 0) { spUrl = "xmlsocket://" + host + ":" + spPort; log("using cross-domain url: " + spUrl); Security.loadPolicyFile(spUrl); } else if(spUrl !== null && typeof(spUrl) !== undefined) { log("using cross-domain url: " + spUrl); Security.loadPolicyFile(spUrl); } else { log("not loading any cross-domain url"); } // connect s.connect(host, port); } else { // no such socket log("socket " + id + " does not exist"); } log("connect(" + id + ") done"); } /** * Closes the Socket with the given ID. * * @param id the ID of the Socket. */ private function close(id:String):void { log("close(" + id + ")"); if(id in mSocketMap) { // close the Socket var s:PooledSocket = mSocketMap[id]; if(s.connected) { s.close(); } } else { // no such socket log("socket " + id + " does not exist"); } log("close(" + id + ") done"); } /** * Determines if the Socket with the given ID is connected or not. * * @param id the ID of the Socket. * * @return true if the socket is connected, false if not. */ private function isConnected(id:String):Boolean { var rval:Boolean = false; log("isConnected(" + id + ")"); if(id in mSocketMap) { // check the Socket var s:PooledSocket = mSocketMap[id]; rval = s.connected; } else { // no such socket log("socket " + id + " does not exist"); } log("isConnected(" + id + ") done"); return rval; } /** * Writes bytes to a Socket. * * @param id the ID of the Socket. * @param bytes the string of base64-encoded bytes to write. * * @return true on success, false on failure. */ private function send(id:String, bytes:String):Boolean { var rval:Boolean = false; log("send(" + id + ")"); if(id in mSocketMap) { // write bytes to socket var s:PooledSocket = mSocketMap[id]; try { var b64:Base64Decoder = new Base64Decoder(); b64.decode(bytes); var b:ByteArray = b64.toByteArray(); s.writeBytes(b, 0, b.length); s.flush(); rval = true; } catch(e:IOError) { log(e); // dispatch IO error event mEventDispatcher.dispatchEvent(new SocketEvent( IOErrorEvent.IO_ERROR, s, e.message)); if(s.connected) { s.close(); } } } else { // no such socket log("socket " + id + " does not exist"); } log("send(" + id + ") done"); return rval; } /** * Receives bytes from a Socket. * * @param id the ID of the Socket. * @param count the maximum number of bytes to receive. * * @return an object with 'rval' set to the received bytes, * base64-encoded, or set to null on error. */ private function receive(id:String, count:uint):Object { var rval:String = null; log("receive(" + id + "," + count + ")"); if(id in mSocketMap) { // only read what is available var s:PooledSocket = mSocketMap[id]; if(count > s.bytesAvailable) { count = s.bytesAvailable; } try { // read bytes from socket var b:ByteArray = new ByteArray(); s.readBytes(b, 0, count); b.position = 0; var b64:Base64Encoder = new Base64Encoder(); b64.insertNewLines = false; b64.encodeBytes(b, 0, b.length); rval = b64.toString(); } catch(e:IOError) { log(e); // dispatch IO error event mEventDispatcher.dispatchEvent(new SocketEvent( IOErrorEvent.IO_ERROR, s, e.message)); if(s.connected) { s.close(); } } } else { // no such socket log("socket " + id + " does not exist"); } log("receive(" + id + "," + count + ") done"); return {rval: rval}; } /** * Gets the number of bytes available from a Socket. * * @param id the ID of the Socket. * * @return the number of available bytes. */ private function getBytesAvailable(id:String):uint { var rval:uint = 0; log("getBytesAvailable(" + id + ")"); if(id in mSocketMap) { var s:PooledSocket = mSocketMap[id]; rval = s.bytesAvailable; } else { // no such socket log("socket " + id + " does not exist"); } log("getBytesAvailable(" + id +") done"); return rval; } /** * Registers a javascript function as a callback for an event. * * @param eventType the type of event (socket event types). * @param callback the name of the callback function. */ private function subscribe(eventType:String, callback:String):void { log("subscribe(" + eventType + "," + callback + ")"); switch(eventType) { case Event.CONNECT: case Event.CLOSE: case IOErrorEvent.IO_ERROR: case SecurityErrorEvent.SECURITY_ERROR: case ProgressEvent.SOCKET_DATA: { log(eventType + " => " + callback); mEventDispatcher.addEventListener( eventType, function(event:SocketEvent):void { log("event dispatched: " + eventType); // build event for javascript var e:Object = new Object(); e.id = event.socket ? event.socket.id : 0; e.type = eventType; if(event.socket && event.socket.connected) { e.bytesAvailable = event.socket.bytesAvailable; } else { e.bytesAvailable = 0; } if(event.message) { e.message = event.message; } // send event to javascript ExternalInterface.call(callback, e); }); break; } default: throw new ArgumentError( "Could not subscribe to event. " + "Invalid event type specified: " + eventType); } log("subscribe(" + eventType + "," + callback + ") done"); } /** * Deflates the given data. * * @param data the base64-encoded data to deflate. * * @return an object with 'rval' set to deflated data, base64-encoded. */ private function deflate(data:String):Object { log("deflate"); var b64d:Base64Decoder = new Base64Decoder(); b64d.decode(data); var b:ByteArray = b64d.toByteArray(); b.compress(); b.position = 0; var b64e:Base64Encoder = new Base64Encoder(); b64e.insertNewLines = false; b64e.encodeBytes(b, 0, b.length); log("deflate done"); return {rval: b64e.toString()}; } /** * Inflates the given data. * * @param data the base64-encoded data to inflate. * * @return an object with 'rval' set to the inflated data, * base64-encoded, null on error. */ private function inflate(data:String):Object { log("inflate"); var rval:Object = {rval: null}; try { var b64d:Base64Decoder = new Base64Decoder(); b64d.decode(data); var b:ByteArray = b64d.toByteArray(); b.uncompress(); b.position = 0; var b64e:Base64Encoder = new Base64Encoder(); b64e.insertNewLines = false; b64e.encodeBytes(b, 0, b.length); rval.rval = b64e.toString(); } catch(e:Error) { log(e); rval.error = { id: e.errorID, name: e.name, message: e.message }; } log("inflate done"); return rval; } /** * Stores an item with a key and arbitrary base64-encoded data on local * disk. * * @param key the key for the item. * @param data the base64-encoded item data. * @param storeId the storage ID to use, defaults to "forge.storage". * * @return an object with rval set to true on success, false on failure * with error included. */ private function setItem( key:String, data:String, storeId:String = "forge.storage"):Object { var rval:Object = {rval: false}; try { var store:SharedObject = SharedObject.getLocal(storeId); if(!('keys' in store.data)) { store.data.keys = {}; } store.data.keys[key] = data; store.flush(); rval.rval = true; } catch(e:Error) { log(e); rval.error = { id: e.errorID, name: e.name, message: e.message }; } return rval; } /** * Gets an item from the local disk. * * @param key the key for the item. * @param storeId the storage ID to use, defaults to "forge.storage". * * @return an object with rval set to the item data (which may be null), * check for error object if null. */ private function getItem( key:String, storeId:String = "forge.storage"):Object { var rval:Object = {rval: null}; try { var store:SharedObject = SharedObject.getLocal(storeId); if('keys' in store.data && key in store.data.keys) { rval.rval = store.data.keys[key]; } } catch(e:Error) { log(e); rval.error = { id: e.errorID, name: e.name, message: e.message }; } return rval; } /** * Removes an item from the local disk. * * @param key the key for the item. * @param storeId the storage ID to use, defaults to "forge.storage". * * @return an object with rval set to true if removed, false if not. */ private function removeItem( key:String, storeId:String = "forge.storage"):Object { var rval:Object = {rval: false}; try { var store:SharedObject = SharedObject.getLocal(storeId); if('keys' in store.data && key in store.data.keys) { delete store.data.keys[key]; // clean up storage entirely if empty var empty:Boolean = true; for(var prop:String in store.data.keys) { empty = false; break; } if(empty) { store.clear(); } rval.rval = true; } } catch(e:Error) { log(e); rval.error = { id: e.errorID, name: e.name, message: e.message }; } return rval; } /** * Clears an entire store of all of its items. * * @param storeId the storage ID to use, defaults to "forge.storage". * * @return an object with rval set to true if cleared, false if not. */ private function clearItems(storeId:String = "forge.storage"):Object { var rval:Object = {rval: false}; try { var store:SharedObject = SharedObject.getLocal(storeId); store.clear(); rval.rval = true; } catch(e:Error) { log(e); rval.error = { id: e.errorID, name: e.name, message: e.message }; } return rval; } } }