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.
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.
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:
@CinnamonOperation annotation or the operation XML tag
in the server configuration - in these cases the alias will be used as the method name). org.spicefactory.cinnamon.client.ServiceRequest.
This is necessary since the service invocations are asynchronous.
You can then register any result handlers on the ServiceRequest instance. 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.
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.
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.
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.
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.
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.
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:
| | Fires before the actual service invocation.
With Event.preventDefault you can prevent the service from being incoked. |
| | Fires if the request was cancelled.
Can only happen if you explicitly called ServiceRequest.cancel() |
| | Fires after the service was successfully invoked and the result received. Fires before the actual result handlers for the request will be invoked. |
| | 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();
}
}
}
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.
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.