Consuming webservices in Flash 8

During a partial refactoring process of the Behrloo client system, one of the items on my list was the backend webservice result processing. Without going into a lot of detail how these services are wrapped, it suffices to say that somewhere in the application a couple of webservices are being initialized and utilized through the macromedia webservice classes.

You might be familiar with them, they come in several flavours, for example the WebServiceConnector and the Webservice class. Personally I don’t like to use the WebServiceConnector, mostly since the Webservice class is simple enough to use and tends to give you more control over what is happening.

Basic example

As a simple example of using this Webservice class, paste the following code onto the first frame of the timeline in a new fla document (on a sidenote, REAL applications are not written on a timeline, but for example purposes/quick proof of concepts, this will do just fine):

//example 1
import mx.services.*;

var lLog:Log = new Log (Log.VERBOSE, "myLog");
var lWebService:WebService =
     new WebService ("http://www.flash-mx.com/mm/tips/tips.cfc?wsdl", lLog);

You’ll note some log information passing by in your output window, showing you the progress during the initialization process and such. Somewhere at the end you’ll see a line like:

4/23 13:14:32 [INFO] myLog: Made SOAPCall for operation getTipByProduct

This means the webservice supports an operation called getTipByProduct. Other than that you don’t really know much about it. This is the first step in handling webservices, getting a grip on what your dealing with. Although there are different methods for doing so, I’ll mention two:
1. the webservice panel in flash, this allows you to enter a webservice url, and check the methods including the required parameters and expected return types in flash.
2. WebServiceStudio, a neat little tool. You might need to disable your proxy if it is giving you the same headaches as ours, but other than that, this tool will let you open, inspect and interrogate webservices.

Looking through Flash’s helpfiles you’ll find an example where the getTipByProduct is called with a string argument of “Flash”, so let’s try that one by extending our example.

Webservices are asynchronous

First thing to realize is that, just like most things in flash, a webservice is asynchronous, meaning that code following the instantiation of a webservice will execute before the webservice is actually instantiated. An example that demonstrates this fact extends the previous example:

//example 1
import mx.services.*;

var lLog:Log = new Log (Log.VERBOSE, "myLog");
var lWebService:WebService =
    new WebService ("http://www.flash-mx.com/mm/tips/tips.cfc?wsdl", lLog);

//example 2 addition
trace ("*** You'll see me before the log output has completed ***");

This ofcourse means that if you try to call a method on the webservice before it has been instantiated the call will fail. In other words: we will have to wait till it has been successfully instantiated. Without too much further explanation, we’ll just show the complete process of calling a method on the webservice and showing the results and then continue to the result processing part, since the process itself is explained in enough detail in the Flash Manual:

//example 1
import mx.services.*;

var lLog:Log = new Log (Log.VERBOSE, "myLog");
var lWebService:WebService = new WebService ("http://www.flash-mx.com/mm/tips/tips.cfc?wsdl", lLog);

//example 2 addition
trace ("*** You'll see me before the log output has completed ***");

//example 3 addition
import mx.utils.Delegate;

lWebService.onFault = function () { trace ("WHOOPS!"); }
lWebService.onLoad = Delegate.create (this, _performExampleCall);

function _performExampleCall() {
   trace("\n\nPerforming example call...");
   var lPendingCall:PendingCall = lWebService.getTipByProduct("Flash");
   lPendingCall.onResult = Delegate.create (this, _parseResult);
}

function _parseResult (pResults:Object) {
   trace ("\n\nResults:\n"+pResults);
}

Decoding webservices results

The thing to note in this example is that the result is a simple string. However, and that is were we get to the interesting part of this post: that is not always the case. The result could be an array, a predefined class, or some other complex object. This is were a couple of other settings/flags come into play:

– doDecoding
– doLazyDecoding

The Flash manual has this to say with respect to these two flags:

SOAPCall.doDecoding-description:
Turns decoding of the XML response on (true) or off (false). By default, the XML response is converted (decoded) into ActionScript objects. If you want just the XML, set SOAPCall.doDecoding to false.

SOAPCall.doLazyDecoding-description:
Turns “lazy decoding” of arrays on (true) or off (false). By default, a “lazy decoding” algorithm is used to delay turning SOAP arrays into ActionScript objects until the last moment; this makes functions execute much more quickly when returning large data sets. This means any arrays you receive from the remote location are ArrayProxy objects. Then when you access a particular index (foo[5]), that element is automatically decoded if necessary. You can turn this behavior off (which causes all arrays to be fully decoded) by setting SOAPCall.doLazyDecoding to false.

Let’s look into doDecoding first:

Although the description is pretty clear, the actual results I got when interpreting webservice results in Behrloo (which uses a .Net webservice backend), were kind of puzzling. When I turned decoding off, I still got an xml object as a result (while I was expecting a large string of some sort), and when I turned decoding on, I got an object which consisted of nodes of type String, Boolean, Array but also of XmlNode (so part of the result was still xml).

In the first implementation of the Behrloo backend, I had decoding turned on, and I dealt with both ‘decoded’ nodes, and xml nodes, which I decoded myself using several xml parsing mechanisms. However triggered by the testresults above, I decided to dive a bit deeper into the WebService class source code, and I found that under the hood the Webservice class is already an XML object to execute any calls to a webservice. This means that WHATEVER you do, the result is always already an XML object. With or without decoding.
With decoding turned on, it goes on to try and decode your object, EXCEPT for the nodes with an xsi:type=”…” attribute, which unfortunately most of my nodes had. I found no way to override this behavior, which means that the default decoding mechanism didn’t do a lot to help me.

Disabling the default decoding

By default, the result is decoded. This takes time, and is kind of useless if you are not using this feature anyway. However disabling the decoding cannot be done on a pendingcall since in order to get a reference to a pendingcall, you need to execute it first, so we disable the decoding through:

_myWebService.getCall ("operation name here").doDecoding = false;

If you want to do this automatically for all calls defined on a webservice use something like:

for (var lOperationName:String in _myWebService.stub.activePort) {
	myWebService.getCall (lOperationName).doDecoding = false;
}

So what about doLazyDecoding?

LazyDecoding only kicks in if you have doDecoding enabled, after all if we do not decode anything, setting it to lazy has no effect.

Parsing the webservice result with decoding turned off

Well assuming you still want to use webservice and don’t want to switch to something like remoting, we use a simple XmlUtil class that converts XML objects to complete actionscript objects. In our project we need to interpret the complete result from the webservice, so this is feasible (in other words, we don’t spend time decoding object we don’t use anyway).

/**
 * Converts an Xml document to an object corresponding with the xml structure.
 *
 * Eg
 *
 * <top>
 * <children>
 * <mychild>rocks</mychild>
 * </children>
 * </top>
 *
 * Becomes an object in which you can do: myObj.top.children.mychild (has value rocks)
 *
 * If a certain node has 2 children with the same name, it becomes an array:
 * <top>
 * <children>
 * <mychild>rocks</mychild>
 * <mychild>drums</mychild>
 * </children>
 * </top>
 *
 * trace (myObj.top.children.mychild[0]); //rocks
 * trace (myObj.children.mychild[1]); //drums
 *
 * If a single string valued node has a type attribute, the value is typed as well (possible types are number and boolean):
 * <top type="boolean">
 * <children type="boolean">
 * <mychild type="boolean">true</mychild>
 * <mychild type="boolean">false</mychild>
 * </children>
 * </top>
 *
 * The top and children nodes have one or more children, but neither of them of type string, so the type attribute simply
 * becomes a value of the object (eg top.type traces boolean). The mychild node has only one child and of type string as well,
 * so top.children.mychild[i] will be of type boolean since it will be converted.
 *
 * In general all other attributes becomes properties of the object but name clashes might cause an toArray conversion:
 *
 * <top children="nothanks">
 * <children type="boolean">
 * <mychild type="boolean">true</mychild>
 * <mychild type="boolean">false</mychild>
 * </children>
 * </top>
 *
 * top will now hold a children object of type array, with element 0 = nothanks, and element1 is another array
 */
class XmlUtil {

 public static
 function convert(pXml: XMLNode): Object {
  return _doConversion(pXml);
 }

 private static
 function _doConversion(pXmlTree: XMLNode): Object {
  //no children only attributes possibly
  if (pXmlTree.childNodes.length == 0) {
   var lStore: Object = new Object();
   var lAttribsFound: Boolean = false;
   for (var lAttrib: String in pXmlTree.attributes) {
    lAttribsFound = true;
    lStore[lAttrib] = pXmlTree.attributes[lAttrib];
   }
   if (lAttribsFound) {
    return lStore;
   } else {
    return null;
   }

   //one child and it is a simple value
  } else if (pXmlTree.childNodes.length == 1 && pXmlTree.childNodes[0].nodeName == null) {

   var lStore: Object = pXmlTree.childNodes[0].nodeValue;

   switch (pXmlTree.attributes.type) {
    case "number":
     lStore = new Number(lStore);
     break;
    case "boolean":
     lStore = new Boolean(lStore == "true");
     break;
    default:
     break; //do nothing
   }

   //now test the object
   var lAttribsFound: Boolean = false;
   for (var lAttrib: String in pXmlTree.attributes) {
    if (lAttrib != "type") {
     trace("A simple node cannot have attributes, it’s one or the other:" + pXmlTree.nodeName);
    }
   }

   return lStore;

   //one or more children with nodeNames (nested objects)
   //you cant have two children of which one has a null nodeName, so all nodes are bound to have them
  } else {
   var lStore: Object = new Object();

   //first store attributes for the current node
   //attributes are always simple values
   for (var i: String in pXmlTree.attributes) {
    lStore[i] = pXmlTree.attributes[i];
   }

   //then do nested children themselves
   for (var i: Number = 0; i < pXmlTree.childNodes.length; i++) {

    var lChild: XMLNode = pXmlTree.childNodes[i];

    //first time we found a child with this name
    if (lStore[lChild.nodeName] == null) {
     lStore[lChild.nodeName] = _doConversion(lChild);
    } else {

     //if child was already converted to an array, just add it
     if (lStore[lChild.nodeName] instanceof Array) {
      lStore[lChild.nodeName].push(_doConversion(lChild));
     } else {
      //first convert what was already there
      var currentNode: Object = lStore[lChild.nodeName];
      lStore[lChild.nodeName] = new Array();
      lStore[lChild.nodeName].push(currentNode);

      //now add new one:
      lStore[lChild.nodeName].push(_doConversion(lChild));
     }
    }
   }

   return lStore;
  }
 }

 /**
  * Converts the child with the parent to an array node IF it is not already an array.
  * If it is already an array nothing happens.
  *
  * @param pObject
  * @param pChild
  */
 public static
 function forceArrayNode(pParent: Object, pChild: String): Array {
  if (pParent[pChild] == null) {
   pParent[pChild] = new Array();
  } else if (pParent[pChild] instanceof Array) {
   //already done
  } else {
   //first convert what was already there
   var currentNode: Object = pParent[pChild];
   pParent[pChild] = new Array();
   pParent[pChild].push(currentNode);
  }

  return pParent[pChild];
 }

 public static
 function getNodeAsArray(pPath: Object): Array {
  if (pPath == null) return null;

  if (pPath instanceof Array) return [pPath][0];

  return [pPath];
 }

 /**
  * Checks if the given node is an array, if not, the node is converted to an array through forceArrayNode,
  * then the objects in the array are added to the array as an associative array, based on the field you passed.
  *
  * For example imagine you have:
  * <children>
  * <child id="first" other values….">
  * </children>
  *
  * Now you do createObjectMapFromArrayNode (children, "child", "id");
  * children.child now contains an array with 1 element reachable through child[0] or child["first"]
  */
 public static
 function createObjectMapFromArrayNode(pParent: Object, pChild: String, pMappingField: String): Array {
  var lResult: Array = forceArrayNode(pParent, pChild);

  for (var i: Number = 0; i < lResult.length; i++) {
   //eg the child at i is queried for its value of its id field
   lResult[lResult[i][pMappingField]] = lResult[i];
  }
  return lResult;
 }

 /**
  * @param pXml the xml node to search
  * @param pNodeName the nodename to look for
  * @return first child encountered with given name
  */
 public static
 function findNode(pXml: XMLNode, pNodeName: String): XMLNode {
  if (pXml.nodeName == pNodeName) return pXml;

  var lResult: XMLNode = null;
  for (var i: Number = 0; i < pXml.childNodes.length; i++) {
   lResult = findNode(pXml.childNodes[i], pNodeName);
   if (lResult != null) break;
  }
  return lResult;
 }

 /**
  * Map the names of all children for a given xmlnode to a function within the given scope passing the childnode to that function,
  * storing the result in an array so it can be returned.
  *
  * @param pXml the xmlnode whose childnodes you want to traverse
  * @param pScope the scope for the functions to run in
  * @param pPrefix any prefix you want to add to the node name to come to a functionname eg parse_
  * @return an array with all the results for the executed functions, if no function was found for a node, it will be skipped
  */
 public static
 function mapChildNamesToFunctions(pXml: XMLNode, pScope: Object, pPrefix: String): Array {
  var lResult: Array = new Array();

  for (var i: Number = 0; i < pXml.childNodes.length; i++) {
   var lFunctionName: String = pPrefix + XMLNode(pXml.childNodes[i]).nodeName;
   if (pScope[lFunctionName] == null) continue;
   lResult[i] = pScope[lFunctionName](pXml.childNodes[i]);
  }

  if (lResult.length > 0) {
   return lResult;
  } else {
   return null;
  }
 }

}
image_pdfimage_print
0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *