3 Using the AS3 Client API

The Cinnamon Client API is written in ActionScript 3 and does not depend on the Flex Framework. This was done on purpose, since we wanted to allow Cinnamon to be used with or without Flex and we wanted to offer a different architecture that does not use the RemoteObject API. RemoteObjects have the downside that they are not type-safe and you usually end up with (unnecessary) indirections like BusinessDelegates to hide them. The Cinnamon Client API was designed to support the following architecture instead:

The steps involved in using the Client API will be described in detail in the following sections.

3.1 Registering Class Mappings

If you only use simple data types like String or int for method parameters and return values in your service interfaces you can safely skip this step. Otherwise all class mappings you registered in Cinnamon on the server side also have to be registered in AS3. There are two ways to accomplish this, one that only works in Flex applications and another one that also works in pure AS3 applications.

The next sections assume that you registered the following class mapping in Cinnamons XML configuration:

<bean-mapping alias="model::Person" java-class="com.domain.model.Person"/>

Using the [RemoteClass] tag

package com.domain.model {

    [RemoteClass(alias="model::Person")]	
    public class Person {

        [...]

    }
}

This way of mapping registration will only work in Flex applications. In pure AS3 applications the RemoteClass metadata tag will simply be ignored. The alias attribute of the metadata tag has to match the alias attribute of the bean-mapping tag in the server configuration.

Using registerClassAlias

An alternative way to set up the mapping is to use flash.net.registerClassAlias. You can do that anywhere in your application initialization code (before the first remote call):

flash.net.registerClassAlias("model::Person", Person);

This will work in Flex and Non-Flex applications. Again the first parameter for registerClassAlias has to match the alias attribute of the bean-mapping tag in the server configuration.

3.2 Generating AS3 service proxies

Java service interfaces registered as Cinnamon services can be ported to AS3 automatically with the help of Cinnamons Ant task. But before we demonstrate how to use that task we will show how such a ported interface will look like. Consider the following simple Java service interface:

@CinnamonService
public interface ProductService {

    public void addProduct (Product p);

    public void removeProduct (Product p);

    public List<Product> getAllProducts ();

}

The generated AS3 interface will then look like this:

public interface ProductService {

    function addProduct (p:Product) : ServiceRequest;

    function removeProduct (p:Product) : ServiceRequest;

    function getAllProducts () : ServiceRequest;

}

The following rules apply when Cinnamon generates AS3 interfaces:

3.2.1 Defining the Cinnamon Ant Task

First you need to have the Cinnamon Ant Task defined:

<taskdef 
    name="cinnamonGenerator"
    classname="org.spicefactory.cinnamon.generator.ant.CinnamonGeneratorTask">
    <classpath>
        <fileset dir="${project.dir}/web/WEB-INF/lib" includes="*.jar"/>
        <path location="${project.dir}/bin" />
    </classpath>
</taskdef>

The classpath definition may vary depending on your project setup. The example above was taken from an Eclipse Dynamic Web project. Cinnamon and all its dependencies were included in WEB-INF/lib and the bin folder contained all project classes compiled by the Eclipse builder. In general you just have to make sure that the classpath contains Cinnamon with its dependencies and all your application classes (the services and mappings you declared in Cinnamons configuration) with their dependencies. You can then use the Cinnamon Ant task anywhere in your Ant build file.

3.2.2 Using the Cinnamon Ant Task

An example Cinnamon Ant target including all optional tags and attributes:

<target name="cinnamon_generator">
    <cinnamonGenerator 
        configFile="cinnamon.xml" 
        configMode="cinnamon" 
        templateDir="${project.dir}/generatorTemplates"
        namingStrategy="org.spicefactory.cinnamon.generator.naming.IPrefixNamingStrategy"
        >

        <typeMapping java="example.TestBean" as3="com.foo.example.TestBean"/>
        <packageMapping java="com.domain.service" as3="com.foo.service"/>
	
        <as3SourceGenerator 
            outputDir="${as3project.dir}/src"
        />
        <parsleyConfigGenerator 
            outputFile="${as3project.dir}/config/service-context.xml" 
            serviceUrl="http://localhost:8080/test/service/"
            timeout="15000"
        />
	
    </cinnamonGenerator>
</target>

This example includes the Parsley Configuration generator which can be used in addition to the AS3 source generator. At least one of the tags as3SourceGenerator or parsleyConfigGenerator must be included. The following sections explain all available options.

Attributes of the cinnamonGenerator tag

configFile required The Cinnamon configuration file, either Cinnamons own XML format or a Spring XML file, depending on the value for the configMode attribute.
configMode optional The configuration mode, either cinnamon (the default if this attribute is omitted) which expects a configuration file in Cinnamons own XML format or spring which expects a configuration file with Spring XML bean definitions.
templateDir required The directory containing all template files used by the generator. This directory must contain the files interface.ftl (for the AS3 service interfaces), implementation.ftl (for the AS3 service proxies) and configuration.ftl (for the Parsley XML configuration). You can find default versions of these templates in the generatorTemplates directory of the server project (in the SVN repository and in the download).
namingStrategy optional Specifies the naming strategy to use when generating the name of the service proxy from the corresponding service interface. Cinnamon includes two implementations: ImplPostfixNamingStrategy (the default if this attribute is omitted) if you use ProductService for the interface name (for example) and ProductServiceImpl for the proxy implementing that interface. The other strategy IPrefixNamingStrategy can be used if you use the IProductService (interface) / ProductService (implementation) pattern. Alternatively you could implement the NamingStrategy interface yourself.

Attributes of the as3SourceGenerator tag

outputDir required The output directory for all generated AS3 classes (service interfaces and proxies). Usually points to a source directory of an AS3 project. Existing files will be overwritten.

Attributes of the parsleyConfigGenerator tag

outputFile required The generated Parlsey configuration file. If the file already exists it will be overwritten.
serviceUrl required The URL the AS3 ServiceChannel should use for all of its invocations.
timeout optional The timeout for all operations of the AS3 ServiceChannel.

The packageMapping tag

This tag allows you to optionally map Java packages to AS3 packages. Without explicit mappings AS3 classes will be generated in the same package as their corresponding Java service interface.

The typeMapping tag

This tag allows you to specify mappings from Java types to AS3 types for method parameters and return types. Note that you don't need to use explicit mappings in most cases as Cinnamon will automatically choose the matching AS3 type for all primitive types and for all Class Mappings registered in Cinnamons configuration. There are basically two scenarios where the generator cannot "guess" the correct type: When you specified a Converter for a method parameter or when you registered a ClassMapping but use a supertype or interface implemented by the mapped class to declare the method parameter or return type. In these cases explicit mappings are needed. The generator will throw an Exception if there is a Java type for which it cannot determine the matching AS3 type. So if you are unsure, just try it out without explicit type mappings first and see if it works.

3.3 Setting up client proxies

After the client proxies have been generated we can actually start to use them. If you use Parsley in your project it's recommended to use Parsley XML configuration for client proxy setup and inject those services into the actions that depend on them. If you don't want to use Parsley you can alternatively construct services programmatically.

3.3.1 Programmatic Initialization

First you have to set up a channel that represents an endpoint you want to connect to:

var channel:ServiceChannel = new NetConnectionServiceChannel();
channel.serviceUrl = "http://your.server.com/webapp/services/";
channel.timeout = 5000;

For each service that connects to this endpoint you use that channel to create service proxies:

var productService:ProductService = new ProductServiceImpl();
channel.createProxy("products", productService);

The first parameter you pass to createProxy is the name of the service as it was configured on the server. That's all you have to do to create service instances. In the example above both the ProductService interface and the ProductServiceImpl proxy could be generated with the Cinnamon Ant task.

3.3.2 Parsley Configuration

With Parsley setup is easy, too. You can use the custom configuration namespace for Cinnamon for channel and service setup:

<application-context
    xmlns="http://www.spicefactory.org/parsley/1.0"
    xmlns:cinnamon="http://www.spicefactory.org/parsley/1.0/cinnamon"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.spicefactory.org/parsley/1.0 
        http://www.spicefactory.org/parsley/schema/1.0/parsley-context.xsd
        http://www.spicefactory.org/parsley/1.0/cinnamon
        http://www.spicefactory.org/parsley/schema/1.0/parsley-cinnamon.xsd"
    >
    <factory>
        <cinnamon:service
            id="productService" 
            type="manual.example.ProductServiceImpl">
            channel="mainChannel"
        />
        <cinnamon:channel 
            id="mainChannel"
            url="http://your.server.com/webapp/services/"
            timeout="5000"
        />
    </factory>
</application-context>    

Do not forget to include the declaration for the Cinnamon namespace (highlighted in the example above). The XML configuration can also be generated with Cinnamons Ant task, see 3.2.2 Using the Cinnamon Ant Task for details. Finally you have to add the namespace to the ApplicationContextParser you use to load the context:

var parser:ApplicationContextParser = new ApplicationContextParser("test", true);
parser.addXml(CinnamonNamespaceXml.config);
parser.addFile("config/testContext.xml");
parser.addEventListener(TaskEvent.COMPLETE, onComplete);
parser.addEventListener(ErrorEvent.ERROR, onError);
parser.start();

You can then obtain a service proxy from the ApplicationContext:

var service:ProductService = ProductService(ApplicationContext.root.getObject("productService"));

In this case you don't need to invoke ServiceChannel.createProxy since Parsley already did that for you. You can immediately start using the service.

3.4 Invoking services

Once you have a fully initialized service instance you can start invoking its methods:

public function addProduct () : void {
    var p:Product = new Product();
    p.id = 2765;
    p.name = "Micky Mouse Deluxe";
    p.price = 27.45;
    var request:ServiceRequest = productService.addProduct(p);
    request.addResultHandler(onAddProduct);
}

private function onAddProduct (expectNull:*) : void {
    trace("product added");
}

The result handlers signature always has a single parameter matching the return type of the service method. For methods with a void return type the value passed is always null like in the preceding example. The result handler will only be invoked if the service invocation completed successfully. Otherwise the error handler will be invoked which you can register with ServiceRequest.addErrorHandler. In many cases though you don't want to manually register error handlers for each call individually if all you do is showing a popup message for example. In this case you can register a central event listener on the channel or the service and catch all errors in a single event listener. See 3.5 Adding event listeners for details.

3.5 Adding event listeners

In addition to adding result or error handlers to individual ServiceRequest instances you can optionally register event listeners with ServiceChannels, ServiceProxies or ServiceOperations. The difference is just the scope of operations you observe.

ServiceChannel Represents a connection to an endpoint. One channel handles one or more services. Events fire for each operation of each of its services.
ServiceProxy Represents the proxy for a single service instance. Events fire for each operation of this instance. A proxy instance can be obtained with ServiceProxy.forService(serviceInstance)
ServiceOperation Represents a single operation of one service. Events fire for this particular operation only. An operation instance can be obtained with ServiceProxy.forService(serviceInstance).getOperation(operationName)

The three classes above fire the following events:

ServiceEvent.INVOKE Fires before the actual service invocation. With Event.preventDefault you can prevent the service from being incoked.
ServiceEvent.CANCEL Fires if the request was cancelled. Can only happen if you explicitly called ServiceRequest.cancel()
ServiceEvent.COMPLETE Fires after the service was successfully invoked and the result received. Fires before the actual result handlers for the request will be invoked.
ServiceEvent.ERROR Fires in case of errors. This includes, among others: Client-side errors (e.g. security related), unknown service name or operation name, Exceptions thrown by server-side service methods, timeouts and more.

The following example shows how to implement a class that listens to all events of a ServiceChannel to disable the UI during service calls in a Flex application. For each INVOKE event it increments the internal counter and for each other event it decrements it.

public class UIStateManager {
	
    private var counter:uint = 0;
	
    public function init (channel:ServiceChannel) : void {
        channel.addEventListener(ServiceEvent.INVOKE, addInvocation);	
        channel.addEventListener(ServiceEvent.CANCEL, removeInvocation);	
        channel.addEventListener(ServiceEvent.COMPLETE, removeInvocation);	
        channel.addEventListener(ServiceEvent.ERROR, removeInvocation);	
    }
	
    private function addInvocation (event:ServiceEvent) : void {
        if (counter == 0) {
            switchUI(false);
        }
        counter++; 
    }
	
    private function removeInvocation (event:ServiceEvent) : void {
        counter--;
        if (counter == 0) {
            switchUI(true);
        }
    }
	
    private function switchUI (enabled:Boolean) : void {
        Application(Application.application).enabled = true;
        if (enabled) {
            CursorManager.removeBusyCursor();
        } else {
            CursorManager.setBusyCursor();
        }
    }
}

3.6 Adding message headers

Message headers can be useful when you want to send additional information to the server without adding more method parameters. As with events you can add or remove message headers to channels, proxies and operations, and again the only difference is the scope in which they are applied. For each service invocation the headers for the operation are merged with that for the corresponding proxy and channel.

An example use case would be to add a security token as a header after login and remove it after logout. You can use listeners for a particular operation in this case:

public function initialize (service:AuthenticationService) : void {
    // AuthenticationService contains login and logout methods
    var proxy:ServiceProxy = ServiceProxy.forService(service);
    proxy.getOperation("login").addEventListener(ServiceEvent.COMPLETE, onLogin);
    proxy.getOperation("logout").addEventListener(ServiceEvent.COMPLETE, onLogout);
}

private function onLogin (event:ServiceEvent) : void {
    // login method returns security token:
    var token:String = event.response.result as String;
    var channel:ServiceChannel = ServiceProxy.forService(event.request.service).channel;
    channel.addHeader("token", token);
}

private function onLogout (event:ServiceEvent) : void {
    var channel:ServiceChannel = ServiceProxy.forService(event.request.service).channel;
    channel.removeHeader("token");
}

For an example for how to process that token in an interceptor on the server-side see 2.9 Interceptors.

3.7 Timeouts and error handling

Finally in most cases you should specify a timeout for service invocations. This is necessary because there might be error conditions where you won't receive a regular error callback. The NetConnection class of the Flash Player may bundle multiple invocation into a single AMF "envelope". If a low level AMF parsing error occurs on the server-side, the framework might not be able to fully parse the request and thus might not be able to assign the errors to the individual requests contained in that single AMF envelope. In this case the NetConnection instance will receive a HTTP status of 500 and the individual error handlers will not be invoked. If you specify a timeout you will at least get an error event after that timeout. An example for such a low level error is "No class mapping registered for alias xyz" which fortunately usually only occurs during development.

Timeout values can be specified for single operations or services or a whole channel. Like with listeners and headers the difference is the scope in which they will be applied.