Using BlazeDS with AS2

POSTED IN Actionscript, Flash Platform, Flex | TAGS : , , , , March 18, 2009

A few months ago, the project I was working on came to a little dilemma.  The system was built using AS2 (not my decision of course), and was using a mix of web services(ws) and an XMLSocket to communicate.  The ws calls would be outgoing, and the XMLSocket would be incoming, that way the server could push any changes done on the back-end and the UI would then appropriately display the proper information.

Our dilemma came when the manager of the project then wanted the application to be accessed from behind any firewall as long as http requests are accepted behind that firewall (as in, a corporate firewall).  Since XMLSocket doesn’t do http request, it would of been filtered out and our system wouldn’t work at all.  While looking for the solution, we found BlazeDS, an open source solution from Adobe.  It’s a Java  library that let’s you do remoting and web messaging using various different protocols.   They have a pushing solution that works over port 80 with http request.  Perfect.

Only problem is that AS2 isn’t built to do this kind of thing, it can’t communicate with this technology at all.  Now from this point, I strongly suggested that we rebuild the system using Flex, and they listened this time because we tapped out the limitations of AS2.  However, they still needed the AS2 system to work until we complete development on the Flex version which would take many months.

The idea behind the solution was easy enough; have two swfs on the page, one with our main AS2 app, and a very small flex app who’s only purpose is to send ws calls using the RemoteObject component and receive server pushes with the Consumer component, then have both swfs communicate between each other using LocalConnection.  The only real limitation of the system is because you’d have two swfs (one being a flex app), the overhead would be a bit bigger, and also the data being sent through the LocalConnection cannot be bigger than 40kb.  Since it’s LocalConnection, we’d also need to do a check to make sure the browser has cookies enabled.

Where to start?  Well, with a quick check online, there’s already classes out there made so that AS2 and AS3 can communicate.  The best one I found was Grant Skinner’s SWFBridge. So let’s start with adding our MXML:

AS2Bridge.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="initApp()" >
	<mx:Script source="AS2Bridge.as" />
	<mx:Consumer id="consumer" destination="Topic" message="streamingChannelResult(event);" fault="streamingChannelFault(event);">
		<mx:channelSet>
			<mx:ChannelSet id="consumerChannelSet" result="loginResult(event)" fault="loginFault(event)">
				<mx:channels>
					<mx:StreamingAMFChannel id="consumerAMFChannel" url="http://domain.com/services/messagebroker/streamingamf"/>
				</mx:channels>
			 </mx:ChannelSet>
		</mx:channelSet>
	</mx:Consumer>
	<mx:RemoteObject id="remoteObject" destination="WebService" result="channelResult(event)" fault="channelFault(event)">
		<mx:channelSet>
	        <mx:ChannelSet id="remoteObjectChannelSet">
	            <mx:channels>
	                <mx:AMFChannel uri="http://domain.com/services/messagebroker/amf"/>
	            </mx:channels>
	        </mx:ChannelSet>
	    </mx:channelSet>
	</mx:RemoteObject>
</mx:Application>

This small amount of code is pretty self explanatory.  We added an external script, then created the Consumer and RemoteObject, set the appropriate parameters, then created the channelsets needed.  This part is pretty easy, we’ve set up our connection, now let’s make it functional:

AS2Bridge.as

import com.gskinner.utils.SWFBridgeAS3;
import mx.messaging.*;
import mx.messaging.events.*;
import mx.messaging.errors.*;
import mx.rpc.events.*;
import flash.events.Event;
 
private var bridge:SWFBridgeAS3;
 
/**
* Initializes object after the application has been created
*
* @return void
*/
private function initApp():void
{
	if(this.parent.loaderInfo.parameters.bridgeId)
	{
                // Creates an instance of SWFBridge to communicate to AS2
                // First parameter needed is a unique ID for the bridge which is being sent from
                // the html to both the AS2 and AS3 swfs.  This way we can have multiple tabs of this
                // application without having any conflicts
		this.bridge = new SWFBridgeAS3(this.parent.loaderInfo.parameters.bridgeId, this);
		this.bridge.addEventListener(Event.CONNECT, onBridgeConnect);
	}
}
 
/**
* Calls the specified function on the RemoteObject
*
* @param method:String The function to call on the RemoteObject
* @param arguments:Array An array containing all the arguments to send to the function
* @return void
*/
public function call(method:String, arguments:Array):void
{
	if(consumer.authenticated && consumer.subscribed)
	{
		remoteObject.getOperation(method).send.apply(null, arguments);
	}else{
		this.bridge.send('onResult', '0<end/>');
	}
}
 
/**
* Event call when the bridge is connected. Logs you into the backend.
*
* @param e:Event The event object being sent over
* @return void
*/
private function onBridgeConnect(e:Event):void
{
	 consumerChannelSet.login('username', 'password');
}
 
/**
* Event function called when a message gets pushed from the backend to the Consumer
*
* @param event:MessageEvent The event object being sent over
* @return void
*/
private function streamingChannelResult(event:MessageEvent):void
{
	if(consumer.subscribed)
	{
                // Workaround for LocalConnection limitations
		var str:String = event.message.body.toString() + '<end/>';
		var byteArray:ByteArray = new ByteArray();
		byteArray.writeUTF(str);
		if(byteArray.length < 40000)
		{
			this.bridge.send('onStreamingResult', str);
		}else{
			for(var i:int = 0; i < byteArray.length; i += 40000)
			{
				byteArray.position = i;
				str = byteArray.readUTFBytes(((i + 40000) > byteArray.length?byteArray.length-i:40000));
				this.bridge.send('onStreamingResult', str);
			}
		}
	}
}
 
/**
* Event function called when an error occurs on the Consumer
*
* @param event:MessageFaultEvent The event object being sent over
* @return void
*/
private function streamingChannelFault(event:MessageFaultEvent):void
{
	if(consumer.subscribed)
	{
		this.bridge.send('onStreamingFault',event.faultCode, event.faultString);
	}
}
 
/**
* Event function called when the RemoteObject receives a reply from the web service
*
* @param event:ResultEvent The event object being sent over
* @return void
*/
private function channelResult(event:ResultEvent):void
{
	if(consumer.subscribed)
	{
		var str:String = event.message.body.toString() + '<end/>';
		var byteArray:ByteArray = new ByteArray();
		byteArray.writeUTF(str);
		if(byteArray.length < 40000)
		{
			this.bridge.send('onResult', str);
		}else{
			for(var i:int = 0; i < byteArray.length; i += 40000)
			{
				byteArray.position = i;
				str = byteArray.readUTFBytes(((i + 40000) > byteArray.length?byteArray.length-i:40000));
				this.bridge.send('onResult', str);
			}
		}
	}
}
 
/**
* Event function called when the RemoteObject receives an error from the web service
*
* @param event:FaultEvent The event object being sent over
* @return void
*/
private function channelFault(event:FaultEvent):void
{
	if(consumer.subscribed)
	{
		this.bridge.send('onFault',event.fault.faultCode, event.fault.faultString);
	}
}
 
/**
* Event function called when a successful login has been established on the Consumer
*
* @param event:ResultEvent The event object being sent over
* @return void
*/
private function loginResult(event:ResultEvent):void
{
	if(!consumer.subscribed)
	{
                // Subscribe to the consumer if login is successful and listen for disconnects
		consumer.subscribe();
		consumer.addEventListener(ChannelEvent.DISCONNECT, streamingDisconnect);
	}
	this.bridge.send('onResult', event.message.body.toString()+'<end/>');
}
 
/**
* Event function called when an unsuccessful login occurs
*
* @param event:FaultEvent The event object being sent over
* @return void
*/
private function loginFault(event:FaultEvent):void
{
	this.bridge.send('onResult', 'fail<end/>');
}
 
/**
* Event function called when the Consumer disconnects from the server
*
* @param event:ChannelEvent The event object being sent over
* @return void
*/
private function streamingDisconnect(event:ChannelEvent):void
{
	consumer.removeEventListener(ChannelEvent.DISCONNECT, streamingDisconnect);
	consumerChannelSet.logout();
	consumer.unsubscribe();
	this.bridge.send('onStreamingFault','Server.Disconnect', '');
}

You might have noticed that at some of the result event functions there’s a quick algorithm that seems to count the amount of bytes the message has. This is a work-around for the limitations of LocalConnection. LocalConnection can only send over 40kb in total, so if the string being pushed is more than 40kb (I set it to 39kb, just to be safe), we split it up in separate messages to be pushed. The receiving end (AS2) waits for the ‘<end/>’ tag before concatenating the message and processing it.

So we’ve got our AS3 side of things working. Now to add our AS2 code to handle our bridge.

AS3Bridge.as

/**
* ConnectionState class is a workaround for an enumerator type
*/
class ConnectionState extends String
{
	static public var NOTCONNECTED:ConnectionState = new ConnectionState('NotConnected');
	static public var READY:ConnectionState = new ConnectionState('Ready');
	static public var WAITING:ConnectionState = new ConnectionState('Waiting');
	private function ConnectionState(value:String){ super(value); }
	public function valueOf():String {
		return super.valueOf();
	}
}
 
import mx.utils.Delegate;
import com.gskinner.utils.SWFBridgeAS2;
 
/**
 * Sends and receives data from a bridge that connects to a BlazeDS server
 */
class AS3Bridge
{
	/** The socket object used throughout the class */
	private var bridge:SWFBridgeAS2;
	/** The id of the bridge to connected to */
	private var id:String;
	/** Let's the system know in which state we're in*/
	private var stateData:ConnectionState;
	/** The function to call when xml is received */
	private var callback:Function;
	private var streamCallback:Function;
	/** Queue array to store socket calls to come */
	private var queue:Array;
	private var streamResultCache:String;
	private var resultCache:String;
 
	/**
	*  AS3Bridge class constructor
	*
	* @param _id:String the id of the bridge to connect to
	* @param _streamCallback:Function the function to call when data is being pushed to the frontend
	*/
	public function AS3Bridge(_id:String, _streamCallback:Function)
	{
		this.stateData = ConnectionState.NOTCONNECTED;
		this.queue = new Array();
		this.id = _id;
		this.streamCallback = _streamCallback;
		this.resultCache = this.streamResultCache = '';
		this.bridge = new SWFBridgeAS2(_id, this);
	}
 
	/**
	*  Calls a function through the bridge
	*
	* @param _method the server method to call
	* @param _callback the function to callback after a response is received from the server
	* @param _data the data to be sent to the server
	* @return Void
	*/
	public function call(_method:String, _callback:Function, _data:Array):Void
	{
		switch(this.state)
		{
			case ConnectionState.NOTCONNECTED:
			case ConnectionState.WAITING:
				// Queue call until the bridge is ready
				if(this.queue[0] != {operation:_method, callback:_callback, data:_data})
				{
					this.queue.push({operation:_method, callback:_callback, data:_data});
				}
				break;
			case ConnectionState.READY:
				// Change State
				this.state = ConnectionState.WAITING;
 
				// save callback
				this.callback = _callback;
				this.bridge.send('call', _method, _data);
				break;
		}
	}
 
	/**
	*  Sets the state of the connection
	*
	* @param value:ConnectionState the current state the system is in
	* @return Void
	*/
	public function set state(value:ConnectionState):Void
	{
		if (value != this.stateData)
		{
			this.stateData = value;
			if (this.stateData == ConnectionState.READY)
			{
				// Go through queue
				if(this.queue.length > 0)
				{
					var op:Object = this.queue.shift();
					this.call(op.operation, op.callback, op.data, op.timeout);
				}
			}
		}
	}
 
	/**
	*  Returns the current state
	*
	* @return ConnectionState
	*/
	public function get state():ConnectionState
	{
		return this.stateData;
	}
 
	/**
	*  The return of the RemoteObject
	*
	* @param _result:String the result returned by the RemoteObject
	* @return Void
	*/
	private function onResult(_result:String):Void
	{
		this.resultCache += _result;
		if (_result.lastIndexOf('') == (_result.length - 6))
		{
			this.resultCache = this.resultCache.slice(0, this.resultCache.length - 6);
			this.callback(this.resultCache);
			this.resultCache = '';
 
			// Change state
			if (Core.loginHandler.isAuthenticated()){ this.state = ConnectionState.READY; }
		}
	}
 
	/**
	*  The pushed information from the BlazeDS server
	*
	* @param _result:String the result received by the Consumer
	* @return Void
	*/
	private function onStreamingResult(_result:String):Void
	{
		this.streamResultCache += _result;
		// checks if the message is done
		if (_result.lastIndexOf('') == (_result.length - 6))
		{
			this.streamResultCache = this.streamResultCache.slice(0, (this.streamResultCache.length - 6));
			// Then send it to the callback function
			this.streamCallback(this.streamResultCache);
			this.streamResultCache = '';
		}
	}
 
	/**
	*  RemoteObject error event
	*
	* @param _code:String the fault code
	* @param _fault:String the fault message
	* @return Void
	*/
	private function onFault(_code:String, _fault:String):Void
	{
		// RemoteObject fault, handle it yourself
	}
 
	/**
	*  Consumer error event
	*
	* @param _code:String the fault code
	* @param _fault:String the fault message
	* @return Void
	*/
	private function onStreamingFault(_code:String, _fault:String):Void
	{
		switch(_code)
		{
			case 'Server.Processing':
				// Can be a slew of blazeds problems
				break;
			case 'Server.Disconnect':
				// Server timeout, make your own processing here
				this.state = ConnectionState.NOTCONNECTED;
				break;
			default:
				// Send fault to callback function
				this.callback('FAULT: '+ _code +' - '+ _fault);
				break;
		}
	}
}

Now, from within flash, we just need to instantiate our class using the bridge id being sent over by html (_root.bridgeId) and then add both swfs to an html page using SWFObject like so:

index.html

< !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Bridge Test</title>
<style type="text/css">
	#as3bridge
	{
		height:0px;
		width:0px;
	}
</style>
<script type="text/javascript" src="swfobject.js"></script>
<script type="text/javascript">
function init(){
	document.cookie="check=true";
	if(document.cookie.length != 0){
		< ?php $id = time().rand(0,1000000); ?>
		swfobject.embedSWF(
					"AS2Bridge.swf", 
					"as3bridge", 
					"1px", 
					"1px", 
					"9.0.0", 
					"expressInstall.swf", 
					{bridgeId: 'bridge< ?php echo $id;?>'}
				);
		swfobject.embedSWF(
					"AS3Bridge.swf", 
					"flashcontent", 
					"100%", 
					"100%", 
					"8.0.0", 
					"expressInstall.swf", 
					{bridgeId: 'bridge< ?php echo $id;?>'})
				;
	}else{ 
		alert('Please enable cookies to view this site')
	}
}
</script>
</head>
<body onLoad="init()">
    <div id="as3bridge">
		<a href="http://www.macromedia.com/go/getflashplayer">
			<strong>Please download and install the latest version of flash player</strong>
		</a>
	</div>
	<div id="flashcontent">
		<a href="http://www.macromedia.com/go/getflashplayer">
			<strong>Please download and install the latest version of flash player</strong>
		</a>
	</div>
</body>
</html>

With a bit of php, a unique id can be created and used by the bridge so that the LocalConnection can establish a link between the two swfs without any external interference from other connections. Also added a bit of javascript to check if cookies are enabled. If they aren’t, LocalConnection will not function. That’s about it. I did run into a few quirks here and there along the way, but this code should do 95% of the job and please post if anyone has comments.

Loading