blob: d99b3ec9397f0772f5df9d5b3e5e0b932f991ee4 [file] [log] [blame]
/*
* 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;
}
}
}