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. Likewise if you only use persistent entities managed and mapped by Pimento this step can also be skipped since Pimento registers these classes automatically for you.

For all other use cases 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 optional 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). If this attribute is omitted the included default templates will be used which are also included in the Cinnamon Jar.
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.3.3 Services managed by Pimento

In case you want to use Cinnamon services with persistent entities as parameters and return types managed by Pimento the setup is slightly different. In Pimento 1.0 you have to setup the services programmatically as described in the Pimento Manual.

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.

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 The ChannelObserver interface

The ChannelObserver interface can be implemented by classes that wish to observe one or more channels. Usually an implementation of this interface registers listeners for all events it is interested in when the addChannel method is called. Cinnamon comes with two implementations of this interface: SessionTimeoutObserver and ChannelActivityObserver, both explained in the following sections.

3.7.1 SessionTimeoutObserver

This observer implementation observes the idle time of one or more channels to fire events if the server-side session is about to expire. One possible usage scenarion with this observer is to set a warningDelay to about 5 minutes less than the server-side session-timeout setting. When the SessionTimeoutEvent.WARNING fires the user may be presented with an alert in a popup window. If he/she confirms the dialog you send a keep-alive message to the server (using SessionTimeoutObserver.keepAlive(). If he/she does not react until the SessionTimeoutEvent.TIMEOUT is fired you automatically return to the login screen of your Flex/Flash application with an additional message that the session has expired. This is a better user experience than just to conitinue with the user interaction and run into remote service invocation errors when the session has expired.

The following example for a Flex application assumes that the server was configured with a session timeout of 30 minutes (= 1800 seconds). The first argument passed to the SessionTimeoutObserver constructor is the timeout in seconds minus 10 seconds to avoid synchronization issues. The second argument is the (optional) time in seconds the observer should dispatch a warning (in this example 5 minutes before the actual timeout).

package example {

import flash.events.Event;

import mx.controls.Alert;
import mx.managers.PopUpManager;

import org.spicefactory.cinnamon.client.ServiceChannel;
import org.spicefactory.cinnamon.client.util.SessionTimeoutEvent;
import org.spicefactory.cinnamon.client.util.SessionTimeoutObserver;


public class SessionTimeoutManager {
	
    private var alert:Alert;
    private var observer:SessionTimeoutObserver;

    public function init (channel:ServiceChannel) : void {
        observer = new SessionTimeoutObserver(1790, 1500);
        observer.addChannel(channel);
        observer.addEventListener(SessionTimeoutEvent.WARNING, onSessionWarning);
        observer.addEventListener(SessionTimeoutEvent.TIMEOUT, onSessionTimeout);
        observer.addEventListener(SessionTimeoutEvent.RESET, onSessionReset);
    }

    private function onSessionWarning (event:Event) : void {
        var message:String = "The session is about to expire";
        alert = Alert.show(message, "Session Timeout", Alert.OK, null, onCloseAlert);
    }

    private function onCloseAlert (event:Event) : void {
        /* The user closed the alert so we know that he/she is still alive.
           We will send a keep-alive signal to the server. */
       observer.keepAlive(); 
    }

    private function onSessionTimeout (event:Event) : void {
        closeAlert();
        returnToLoginScreen();
    }

    private function onSessionReset (event:Event) : void {
        /* There was some activity in one of the channels, so we
           have to remove the alert if it is currently open */
       closeAlert();
    }

    private function closeAlert () : void {
        if (alert != null) {
            PopUpManager.removePopUp(alert);
            alert = null;
        }
    }

    private function returnToLoginScreen () : void {
        /* implementation omittted */
    } 
    
}

} 

We wanted to keep the example simple. Instead of sending the user to the login screen you could alternatively cache the user credentials and silently reconnect in the background if the user continues with the application after a session timeout. But of course this might pose a security risk in some scenarios, so be careful when choosing the appropiate behaviour for your application.

3.7.2 ChannelActivityObserver

This observer implementation dispatches events when the total number of pending service invocations for all registered channels switches from 0 to 1 or from 1 to 0. Other changes in the number of invocations will not cause any events. One possible use case would be to disable the UI during service calls in a Flex application:

var channel:ServiceChannel = ...
var observer:ChannelObserver = new ChannelActivityObserver();
observer.addChannel(channel);
observer.addEventListener(ChannelActivityEvent.ACTIVE, onChannelActive);
observer.addEventListener(ChannelActivityEvent.INACTIVE, onChannelInactive);

private function onChannelActive (event:Event) : void {
    Application(Application.application).enabled = false;
    CursorManager.setBusyCursor();
}

private function onChannelInactive (event:Event) : void {
    Application(Application.application).enabled = true;
    CursorManager.removeBusyCursor();
}

3.8 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.

3.9 The ServiceInvoationHandler interface

This is a rather low-level extension point which will rarely be used by application code. Its main purpose is to offer hooks for other frameworks to extend or modify the core invocation handling in Cinnamon. In fact the main reason it was introduced with version 0.3.0 was Pimento, the Data Management Framework, that needs to process the entities that were sent to and received from the server before handing them out to the application.

The interface is quite simple, it contains two methods:

function beforeInvocation (invocation:ServiceInvocation) : void;	

function afterInvocation (invocation:ServiceInvocation) : void;	

The first one will be invoked before the request is sent to the server. It allows you to modify method parameters or even replace the ServiceRequest instance. The latter will be demonstrated later with an example for transparent caching. The second method will be invoked after the response has been received from the server, but before the event listeners and result handlers will be invoked. It allows you to process, modify or replace the return value from the service invocation.

Like listeners or headers a ServiceInvocationHandler can be registered for different scopes: For the ServiceChannel it will affect all invocations of that channel, for the ServiceProxy it will only affect that single service and if you register a handler for a single ServiceOperation, only that operation is affected. All these objects now contain a new invocationHandler property. If you register a handler for the channel and for a proxy for example, the handler for the proxy will be used, the most specific scope has always precedence. The handler for the channel would still be used for other proxies registered for that channel.

3.9.1 Example: Transparent Caching

To give you a concrete example how you might use this extension point we'll show you how to implement caching "behind the scenes" without the application code having to deal with it explicitly. We intend to use the class above for a single service operation ("load user by name") and want to return a cached instance for each user that was already loaded. This is the signature for the remote method:

function loadUser (name:String) : ServiceRequest;

Next we'll implement the ServiceInvocationHandler interface:

package example {

import flash.utils.Dictionary;

import org.spicefactory.cinnamon.client.ServiceInvocation;
import org.spicefactory.cinnamon.client.ServiceInvocationHandler;
import org.spicefactory.cinnamon.client.ServiceRequest;
import org.spicefactory.cinnamon.client.ServiceResponse;
import org.spicefactory.cinnamon.client.util.CachedServiceRequest;

public class CachingInvocationHandler implements ServiceInvocationHandler {

    private var cache:Dictionary = new Dictionary();
	
    public function beforeInvocation (invocation:ServiceInvocation) : void {
        var username:String = invocation.parameters[0];
        if (cache[username] != undefined) {
            var user:User = cache[username] as User;
            var response:ServiceResponse = new ServiceResponse(user);
            invocation.request 
                = new CachedServiceRequest(invocation.request.operation, [user], response);
        }
    }
	
    public function afterInvocation (invocation:ServiceInvocation) : void {
        if (invocation.result is User) {
            var user:User = invocation.result as User;
            cache[user.name] = user;
        }
    }
}
}

In the beforeInvocation method, we examine the username parameter that was passed to the remote method. If we find a cached User instance for the specified name, we create a new ServiceResponse instance wrapping the cached instance. Then we create an instance of CachedServiceRequest (that was also introduced with version 0.3.0) and pass it the original operation, the method parameter and the response instance we created. We then replace the existing ServiceRequest in the ServiceInvocation instance. The CachedServiceRequest class, like you might expect, will never actually invoke the server. If the application code adds a result handler to such an instance it will be invoked immediately, passing the result that you specified. That way the application code can use the remote method in the usual asynchronous way without having to know whether the result was retrieved from the cache or from the server.

In the afterInvocation method, we first check if the return value is actually an instance of the User class. If the server side implementation threw an Exception, the return value would be an instance of ErrorMessage instead. Also the server side method might be implemented in a way that it just returns null, if no user with the specified name exists. If we did receive an actual User instance we put it into our local cache.

Finally we need to register this handler with the operation:

var channel:ServiceChannel = new NetConnectionServiceChannel();
channel.serviceUrl = "http://your.server.com/webapp/services/";
channel.timeout = 5000;
var userService:UserService = new UserServiceImpl();
var proxy:ServiceProxy = channel.createProxy("users", userService);
var op:ServiceOperation = proxy.getOperation("loadUser");
op.invocationHandler = new CachingInvocationHandler();

Everything except for the last line is the usual programmatic configuration style for Cinnamon.

This was a rather simple example for using this extension point. If you want to examine a complex example you can check the class PimentoServiceInvocationHandler in the Pimento code base. That handler hooks into the invocation logic not only to deal with caching but also to create change sets from modified entities to avoid sending full entities and much more.