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.
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.
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 | 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.
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.
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.
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. |
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.
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.
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.
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();
}
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.
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.
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.