EQUIP2 ActionScript Client

Chris Greenhalgh, 2007-06-05; last updated 2007-06-05

Introduction

EQUIP2_Remote_Dataspace_Protocol.html describes the EQUIP2 remote dataspace protocol. As noted there, the ECMAScript marshalling is designed to be particularly easy to use from Adode/Macromedia Flash ActionScript. This document describes how to do this, in particular how to make use of the provided action script client code in a Flash application to create an interface to an EQUIP2-based application.

The flash-related files are in the top-level directory flash/.

Overview

The connection to the dataspace server is made via an XMLSocket. Custom ActionScript functions are used to marshall and unmarshall ActionScript objects to/from the text encoding used by the EQUIP2 protocol identified as 'text/x-actionscript':
A marshalled message is send using the function sendMessage(str), which will queue messages during the initial connection/registration process. Particular requests can be send using the functions:

Note that the marshalling requires that the old/new values be Objects, with the property _classname set to the full name of the corresponding Java class, e.g. "equip2.net.test.MyDataType" when interworking with the remote dataspace tuple test. Local instances can be created using the '{'...'}' object literal notation in action script, for example (adding a single object):

sendSessionBegin();
changes = new Array();
changes[0] = makeAddRequest({_classname:"equip2.net.test.MyDataType",x:50,y:50,name:'flash'});
sendSessionEnd(changes);

A listener is regestered (and unregistered) using:

Events from the dataspace matching a registered listener call the event handler:

Dataspace events are polled automatically (hopefully) using setInterval, by default every ?100ms.

ActionScript

The current most complete test version is in ../flash/examples/xmlsocket3.py. The relevant action script (2) code is included below.

Note that the XMLSocket connect will typically fail silently (due to a security check, at least in later versions of Flash) unless the connection is to the same host as the movie was loaded from. Similarly, a movie loaded directly from a local file typically CANNOT connect even to the local host - instead you must load the movie from a local web server (e.g. example.HttpServer in javatrans) so that it can connect to the local host.

// reusable stuff...
var onStatusChange = null;
var socketStatus = 0;

// xml socket
sock = new XMLSocket();

status.text = "Register handlers...";

function handleOnClose () {
socketStatus = -1;
onStatusChange(socketStatus, "closed");
}
sock.onClose = handleOnClose;

function handleOnConnect (success) {
if (success) {
socketStatus = 2;
onStatusChange(socketStatus, "negotiating protocol");
sock.send("text/x-actionscript\n");
}
}
sock.onConnect = handleOnConnect;

function newUnmarshallInfo(textin, posin, parentin) {
var ui = new Object();
ui.text = textin;
ui.pos = posin;
ui.parent = parentin;
return ui;
}
// (UnmarshallInfo):String
function unmarshallName(ui) {
var value = "";
while (true) {
c = ui.text.charAt(ui.pos);
if ((c.charCodeAt(0)>=String('a').charCodeAt(0) &&
c.charCodeAt(0)<=String('z').charCodeAt(0)) ||
(c.charCodeAt(0)>=String('A').charCodeAt(0) &&
c.charCodeAt(0)<=String('Z').charCodeAt(0)) ||
c=='_' || c=='$')
{
value = value+c;
ui.pos = ui.pos+1;
}
else
break;
}
return value;
}
// (UnmarshallInfo):Object
function unmarshallUi(ui) {
var c = ui.text.charAt(ui.pos);
var value = null;
//status.text = status.text+",unmarshall "+c+"@"+ui.pos+")";
if (c=='{') {
value = new Object();
//status.text = status.text+",object";
var expectComma = false;
var expectEnd = true;
ui.pos = ui.pos+1;
while (true) {
c = ui.text.charAt(ui.pos);
if (expectEnd && c=='}') {
// done
ui.pos = ui.pos+1;
break;
}
if (expectComma && c==',') {
ui.pos = ui.pos+1;
expectComma = false;
expectEnd = false;
continue;
}
var name = unmarshallName(ui);
c = ui.text.charAt(ui.pos);
if (c!=':')
// error
throw new Error("No : after presumed name "+name);
ui.pos = ui.pos+1;
var elvalue = unmarshallUi(ui);
expectEnd = true;
expectComma = true;
value[name] = elvalue;
}
} else if (c=='[') {
//status.text = status.text+",array";
value = new Array();
var expectComma = false;
var expectEnd = true;
var i = 0;
ui.pos = ui.pos+1;
while (true) {
c = ui.text.charAt(ui.pos);
if (expectEnd && c==']') {
// done
ui.pos = ui.pos+1;
break;
}
if (expectComma && c==',') {
ui.pos = ui.pos+1;
expectComma = false;
expectEnd = false;
continue;
}
var elvalue = unmarshallUi(ui);
expectEnd = true;
expectComma = true;
value[i] = elvalue;
i = i+1;
}
} else if (c=="'" || c=='"') {
value = "";
var quote = c;
ui.pos = ui.pos+1;
var escaped = false;
while(true) {
c = ui.text.charAt(ui.pos);
ui.pos = ui.pos+1;
if (escaped) {
value = value+c;
escaped = false;
}
else if (c.charCodeAt(0)=="\\\\") {
escaped = true;
}
else if (c==quote) {
break;
} else {
value = value+c;
}
}
//status.text = status.text+",string'"+value+"'";
} else if (c.charCodeAt(0)>=String('0').charCodeAt(0) &&
c.charCodeAt(0)<=String('9').charCodeAt(0)) {
// number
value = new String();
while(true) {
c = ui.text.charAt(ui.pos);
if ((c.charCodeAt(0)>=String('0').charCodeAt(0) &&
c.charCodeAt(0)<=String('9').charCodeAt(0)) || c=='.')
value = value+c;
else
break;
ui.pos = ui.pos+1;
}
value = Number(value);
//status.text = status.text+",number "+value;
} else {
value = unmarshallName(ui);
if (value=='true')
value = true;
else if (value=='false')
value = false;
else if (value=='null')
value = null;
else {
//status.text = status.text+",unknown "+value;
throw new Error("Unknown token '"+value+"'");
}
//status.text = status.text+",token "+value;
}
return value;
}
// (String):Object
function unmarshall(s) {
ui = newUnmarshallInfo(s, 0, null);
return unmarshallUi(ui);
}
// (sofar:String,Object):String
function marshall(obj) {
var str = '';
if (obj==null)
{
str = str+'null';
return str;
}
var type = typeof(obj);
if (type=='number' || type=='boolean')
{
str = str+String(obj);
return str;
}
if (type=='string') {
str = str+'"';
var i;
for (i=0; i<obj.length; i++) {
c = obj.charAt(i);
if (c=='"' || c=='\\\\')
str = str+'\\\\';
str = str+c;
}
str = str+'"';
return str;
}
if (obj instanceof Array) {
str = str+'[';
var i;
for (i=0; i<obj.length; i++) {
if (i>0)
str = str+ ',';
str = str+marshall(obj[i]);
}
str = str+']';
return str;
}
// object
str = str+'{';
var first = true;
if (obj.hasOwnProperty('_classname')) {
str = str+'_classname:'+marshall(obj._classname);
first = false;
}
var pname;
for (pname in obj) {
if (pname!='_classname') {
if (first)
first = false;
else
str = str+',';
str = str+pname+":"+marshall(obj[pname]);
}
}
str = str+'}';
return str;
}

var msgCount = 0;
var messagesToSend = new Array();

function sendMessage(str) {
if (socketStatus==4)
sock.send(str);
else if (socketStatus>=0 && messagesToSend!=null)
// queue
messagesToSend[messagesToSend.length] = str;
else
trace("cannot send message (state "+socketStatus+")");
}

function sendPollListeners() {
sendMessage("{_classname:'equip2.core.marshall.MethodCallRequest',method:'pollListeners'}");
}

function sendSessionBegin(readOnly) {
sendMessage('{_classname:"equip2.core.marshall.MethodCallRequest",arguments:["s1",'+getSessionType(readOnly)+'],method:"sessionBegin"}');
}
function getSessionType(readOnly) {
if (readOnly!=undefined && readOnly==true)
return 2;
return 1;
}
function sendSessionEnd(changes) {
if (changes!=null && !(changes instanceof Array)) {
// single change -> array
var nc = new Array();
nc[0] = changes;
changes = nc;
}
sendMessage('{_classname:"equip2.core.marshall.MethodCallRequest",arguments:["s1",'+marshall(changes)+'],method:"sessionEnd"}');
}
function makeUpdateRequest(oldValue, newValue)
{
var smo = new Object();
smo._classname = "equip2.core.impl.SessionManagedObject";
smo.userObject = newValue;
smo.systemObject = oldValue;
if (newValue==null)
smo.status = 2; // removed
else if (oldValue==null)
smo.status = 1; // added
else
smo.status = 4; // modified
return smo;
}
function makeAddRequest(newValue) {
return makeUpdateRequest(null,newValue);
}
function makeRemoveRequest(oldValue) {
return makeUpdateRequest(oldValue, null);
}

var nextLocalListenerId = 1;
function addObjectsListener(obj) {
var localListenerId = 'listener'+nextLocalListenerId;
nextLocalListenerId = nextLocalListenerId+1;
// 240 = 0x0f0 (NOT listener removed)
sendMessage("{_classname:'equip2.core.marshall.MethodCallRequest',method:'doAddObjectsListener',arguments:["+marshall(localListenerId)+","+marshall(obj)+",240]}");
return localListenerId;
}
var onDataspaceObjectEvent = null;

function handleOnData (src) {
//status.text = "Status "+socketStatus+", text: "+src;
if (socketStatus==2) {
// response to negotiate protocol
if (src!="text/x-actionscript\n") {
socketStatus = -1;
onStatusChange(socketStatus,"invalid protocol response: "+src);
sock.close();
return;
}
socketStatus = 3;
onStatusChange(socketStatus, "registering");
sock.send("{_classname:'equip2.core.marshall.MethodCallRequest',method:'newClient',arguments:['ds1']}");
} else if (socketStatus==3) {
// response to register client
var value = unmarshall(src);
//status.text = status.text + " = "+value;
if (value.successful) {
socketStatus = 4;
onStatusChange(socketStatus,"registered as "+value.value);
for (i=0; i<messagesToSend.length; i++)
sock.send(messagesToSend[i]);
messagesToSend = null;
// start to poll - every 100ms?! 1s for testing
setInterval(sendPollListeners, 1000); //100
} else {
socketStatus = -1;
onStatusChange(socketStatus,"Error registering");
sock.close();
return;
}
}
else if (socketStatus==4) {
// some other response
var value = unmarshall(src);
msgCount = msgCount+1;
//status.text = "received("+msgCount+"): "+marshall(value);
if (value.successful && value.value instanceof Array) {
for (j=0; j<value.value.length; j++) {
if (value.value[j].events instanceof Array) {
for (i=0; i<value.value[j].events.length; i++)
if (value.value[j].events[i]._classname=="equip2.net.equip2.RemoteDataspaceObjectEvent")
// callback
onDataspaceObjectEvent(value.value[j].events[i], value.value[i].localListenerId);
}
}
}
}
}
sock.onData = handleOnData;

// host null to connect to web server that this was download from (safest option)
function connect(host,port) {
socketStatus = 1;
onStatusChange(socketStatus,"connecting to "+host+":"+port);
sock.connect(host, port);
}

// just for debug
_root.createTextField("status",1,100,100,300,400);
status.multiline = true;
status.wordWrap = true;
status.border = false;
status.text = "status...";
myformat = new TextFormat();
myformat.font = "_sans";//Courier";
status.setTextFormat(myformat);
_root.createTextField("eventStatus",2,100,200,300,400);
eventStatus.multiline = true;
eventStatus.wordWrap = true;
eventStatus.border = false;
eventStatus.text = "event...";
eventStatus.setTextFormat(myformat);

function debugStatusChange(stat, msg) {
status.text = "state="+stat+": "+msg;
}
var onStatusChange = debugStatusChange;

// connect
connect(null, 9123);


// testing
// default/test
function testingListener(event,listener) {
eventStatus.text = "Event for "+listener+": "+marshall(event.oldValue)+" -> "+marshall(event.newValue);
}
onDataspaceObjectEvent = testingListener;

//listen for anything
addObjectsListener(null);

// try an add
sendSessionBegin();
changes = new Array();
changes[0] = makeAddRequest({_classname:"equip2.net.test.MyDataType",x:50,y:50,name:'flash'});
sendSessionEnd(changes);

Change Log

2007-06-05