Integration and features stack :
GDS has been designed to be lightweight, robust, fast, and highly extensible.
The main features of GraniteDS are :
The core development team is Franck Wolff and William Draï, two engineers from Granite Data Services. Many people have contributed to GraniteDS by giving ideas, patches or new features. If you feel you should be listed below, please email .
This section introduces:
You need at least the following four free development tools:
/flex_sdk_4
.
You may also read and follow the recommendations in this in order to customize your development environment.
Let's say you have installed a vanilla JBoss 4.2.3.GA and fresh Flex SDK at the root of your main drive (C:\jboss-4.2.3.GA
and
C:\flex_sdk_3
on Windows).
Follow these few steps:
graniteds-***.zip
.
"graniteds"
project as well as all example projects ready to be imported. When you have made
your choice, choose Finish. NOTE: you must import the core graniteds project (it contains the libraries
required by the example projects).
graniteds/examples/README.txt
file in order to setup your environment.
build.xml
file and run the "deploy" target.
http://localhost:8080/graniteds_pojo
)
(see the README.txt
file again).
You may now look at the code of each example and understand how each sample works.
"Hello " + <typed name> + "!"
The result is then displayed in white under the "Hello World Sample" panel.
You may download this example project if you don't want to copy-paste the code in the following sections below.
In order to create, build, and deploy this sample application you need these free tools:
/flex_3_sdk
(for Windows users: C:\flex_3_sdk
).
/apache-tomcat-6.0.18
(for Windows users: C:\apache-tomcat-6.0.18
).
build
folder. Download it .
Creation of the project in Eclipse:
Start Eclipse and create a new Java project named helloworld
. You may just type in helloworld
for
Project name
and accept all other default settings. Because we are going to have two types of sources, Java and Flex MXML, it is
better to rename the default Java source folder src
to java
. You can do this by right-clicking on the
src
source folder and selecting Refactor / Rename.
We are now going to create a new POJO service named HelloWorldService
. Right-click on the java
source folder
and select New / Class, enter org.test
for Package
and HelloWorldService
for Name
in the following dialog, and then click on the Finish button. In the Java source file editor, modify
the code so it is just as follows:
package org.test;
public class HelloWorldService {
public String sayHello(String name) {
return "Hello " + name + "!";
}
}
You should now see something like the following picture under Eclipse:
Now let create the Flex client code. Create a new folder named flex
by right-clicking on the helloworld
project
and selecting New / Folder. Create a new file directly in this new folder and name it HelloWorld.mxml
by right-clicking on the flex
folder and selecting New / File. In the file editor, which may be Flah Builder
or a simple text editor depending on your Eclipse installation, type in the following code:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" backgroundGradientColors="[#0e2e7d, #6479ab]" layout="vertical" verticalAlign="middle"> <mx:Style> .Panel { padding-left: 8px; padding-top: 8px; padding-right: 8px; padding-bottom: 8px; } .Result { font-size: 26px; color: white; } </mx:Style> <mx:RemoteObject id="srv" destination="helloWorldService" /> <mx:Panel styleName="Panel" title="Hello World Sample"> <mx:Label text="Enter your name:"/> <mx:TextInput id="nameInput" /> <mx:Button label="Say Hello" click="srv.sayHello(nameInput.text)"/> </mx:Panel> <mx:Label styleName="Result" text="{srv.sayHello.lastResult}"/> </mx:Application>
You should now see something like the following picture under Eclipse:
Now we have to create the two main GraniteDS configuration files services-config.xml
and web.xml
at the root of the
project. You should now see:
Copy and paste the following code into these files:
<?xml version="1.0" encoding="UTF-8"?>
<services-config>
<services>
<service
id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<destination id="helloWorldService">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<scope>application</scope>
<source>org.test.HelloWorldService</source>
</properties>
</destination>
</service>
</services>
<channels>
<channel-definition id="my-graniteamf" class="mx.messaging.channels.AMFChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
</services-config>
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<!-- general information about this web application -->
<display-name>Hello World</display-name>
<description>Hello World Sample Application</description>
<!-- read services-config.xml file at web application startup -->
<listener>
<listener-class>org.granite.config.GraniteConfigListener</listener-class>
</listener>
<!-- handle AMF requests ([de]serialization) -->
<filter>
<filter-name>AMFMessageFilter</filter-name>
<filter-class>org.granite.messaging.webapp.AMFMessageFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>AMFMessageFilter</filter-name>
<url-pattern>/graniteamf/*</url-pattern>
</filter-mapping>
<!-- handle AMF requests (execution) -->
<servlet>
<servlet-name>AMFMessageServlet</servlet-name>
<servlet-class>org.granite.messaging.webapp.AMFMessageServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>AMFMessageServlet</servlet-name>
<url-pattern>/graniteamf/*</url-pattern>
</servlet-mapping>
<!-- default content for helloworld application -->
<welcome-file-list>
<welcome-file>HelloWorld.swf</welcome-file>
</welcome-file-list>
</web-app>
Put together, those four files (HelloWorldService.java
, HelloWorld.mxml
, services-config.xml
,
and web.xml
), define an entire Flex/GraniteDS application. Here are some highlights (partial/pseudo code):
public String HelloWorldService.sayHello(String name)
<mx:RemoteObject id="srv" destination="helloWorldService" />
<mx:Button label="Say Hello" click="srv.sayHello(nameInput.text)"/>
<mx:Label ... text="{srv.sayHello.lastResult}"/>
<destination id="helloWorldService">
<channel ref="my-graniteamf"/>
<scope>application</scope>
<source>org.test.HelloWorldService</source>
</destination>
<channel-definition id="my-graniteamf" ...>
<endpoint uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf" .../>
</channel-definition>
<url-pattern>/graniteamf/*</url-pattern>
From top to bottom:
HelloWorldService
Java class declares a method sayHello()
that takes a String
argument
and returns another String
.
HelloWorld.mxml
Flex application declares a RemoteObject
named srv
and maps it to a
destination named helloWorldService
.
RemoteObject
triggers a server request that will call a
sayHello()
method with the text typed in the TextInput
named nameInput
as argument:
srv.sayHello(nameInput.text)
.
Label
by binding the text of this component to the
property srv.sayHello.lastResult
that contains the last received value for this method call.
helloWorldService
destination is declared in services-config.xml
and uses a channel named
my-graniteamf
. The Java class (source) used as a service for this destination call is org.test.HelloWorldService
and its scope is application
. The Java class will be created when the service is first accessed, and this same and unique
instance will be used for all subsequent calls; other possible values are request
and session
.
my-graniteamf
, used by the helloWorldService
destination, declares an endpoint whose
URL will be resolved to http://localhost:8080/helloworld/graniteamf/amf
for a local call; it could also be resolved, for example,
to http://www.helloworld.com/helloworld/graniteamf/amf
for remote calls.
AMFMessageFilter
and AMFMessageServlet
are both mapped in web.xml
to the same URL-pattern
/graniteamf/*
. Inside the helloworld
web application, all requests that match this pattern will be go through
this filter and this servlet such as http://localhost:8080/helloworld/graniteamf/amf
.
Here is basic flow chart that summarizes the expected communication between the Flex client application and the Java server:
The last thing to do is to build and deploy the application.
First we have to add the granite.jar
library and create a build file, here using Ant.
Create a new folder named lib
at the root of the project and put granite.jar
into this new folder. Create a new
file named build.xml
at the root of the project. Copy and paste the following content into it; you may have to modify
the properties FLEX_HOME
and TOMCAT_HOME
to reflect your environment:
<?xml version="1.0" encoding="UTF-8"?>
<project name="hello-world" default="deploy">
<!-- Modify FLEX_HOME/TOMCAT_HOME properties to reflect your environment -->
<property name="FLEX_HOME" value="/flex_sdk_3"/>
<property name="TOMCAT_HOME" value="/apache-tomcat-6.0.18"/>
<!-- Declare Flex Ant tasks (such as mxmlc used below) -->
<taskdef resource="flexTasks.tasks" classpath="${FLEX_HOME}/ant/lib/flexTasks.jar" />
<!-- Compile MXML source code to SWF -->
<target name="mxmlc">
<mxmlc
file="flex/HelloWorld.mxml"
output="build/HelloWorld.swf"
services="services-config.xml"
context-root="helloworld">
</mxmlc>
</target>
<!-- Build a war suitable for Tomcat (and other) -->
<target name="war" depends="mxmlc">
<mkdir dir="build"/>
<war destfile="build/helloworld.war" webxml="web.xml">
<zipfileset file="services-config.xml" prefix="WEB-INF/flex" />
<fileset dir="build" includes="*.swf"/>
<lib dir="lib"/>
<classes dir="bin"/>
</war>
</target>
<!-- Deploy the war in Tomcat -->
<target name="deploy" depends="war">
<copy todir="${TOMCAT_HOME}/webapps" file="build/helloworld.war"/>
</target>
</project>
You should see something like the following picture:
You may now right-click on the build.xml
file and select Run As / Ant Build. This will launch the build process,
compile the MXML code to an SWF, create a WAR (Web Archive), and copy it into your Tomcat webapps
directory.
Finally start Tomcat and test the application.
To start Tomcat, go to the directory bin
just under your Tomcat installation directory, /apache-tomcat-6.0.18/bin
for example, and double-click on startup.bat
, or startup.sh
for Unix/Mac users. After a short while, you should
see in the console that Tomcat has started. You may now point your Web browser to
.
The Flex example application should appear and you may start playing with "Hello, world" ... a fascinating game.
If you don't want to follow this tutorial step by step you may download it as a zip archive .
In order to create, build, and deploy this sample application you need these free tools:
/flex_3_sdk
(for Windows users: C:\flex_3_sdk
).
/jboss-4.2.3.GA
(for Windows users: C:\jboss-4.2.3.GA
).
build
folder. Download it .
Creation of the project in Eclipse:
Start Eclipse and create a new Java project named helloworld2
. You may just type in helloworld2
for
Project name
and accept all other default settings. Because we are going to have two types of sources, Java and Flex MXML, it is
better to rename the default Java source folder src
to java
. You can do this by right-clicking on the
src
source folder and selecting Refactor / Rename.
Create a new directory named lib
at the root of this project and put granite.jar
, granite-hibernate.jar
and granite-essentials.swc
into it. Also add these two JBoss jars: ejb3-persistence.jar
and
jboss-ejb3x.jar
, which you will find in the /jboss-4.2.3.GA/server/default/lib
directory. Right-click on those
JBoss jars and select Build Path / Add to Build Path.
You should now see something like the following picture under Eclipse:
Right-click on the java
source folder, select New / Class and fill the New Java Class dialog as follows:
Copy and paste the following code in the Java editor:
package org.test;
import java.io.Serializable;
import javax.persistence.Basic;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Welcome implements Serializable {
private static final long serialVersionUID = 1L;
@Id @GeneratedValue
private Integer id;
@Basic
private String name;
public Welcome() {
}
public Welcome(String name) {
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
This basic JPA entity bean declares a read-only id
field, auto incremented primary key in the database, and a name
field,
the name of the person that was entered in the Flex application when saying hello.
We are now going to create an EJB 3 session bean that will handle the say hello and persistence operations.
Create a new Java interface named HelloWorldService
by right-clicking on the org.test
package and choosing
New / Interface. Then copy and paste the following code:
package org.test;
import java.util.List;
public interface HelloWorldService {
public String sayHello(String name);
public List<Welcome> findWelcomeHistory();
}
Create a new Java class named HelloWorldServiceBean
that implements the HelloWorldService
interface by
right-clicking on the org.test
package and choosing New / Class. Then copy and paste the following code:
package org.test;
import java.util.List;
import javax.ejb.Local;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
@Stateless
@Local(HelloWorldService.class)
public class HelloWorldServiceBean implements HelloWorldService {
@PersistenceContext
protected EntityManager manager;
@Override
public String sayHello(String name) {
manager.persist(new Welcome(name));
return "Hello " + name + "!";
}
@SuppressWarnings("unchecked")
@Override
public List<Welcome> findWelcomeHistory() {
Query query = manager.createQuery("from " + Welcome.class.getName());
return query.getResultList();
}
}
We now have a complete EJB 3 stateless session bean that will persist each name passed to the sayHello()
method and return the list
of all previous welcome operations with the findWelcomeHistory()
method.
Your Java project should look like that after this step:
Now let create the Flex client code. Create a new folder named flex
by right-clicking on the helloworld2
project
and selecting New / Folder. Create a new file directly in this new folder and name it HelloWorld.mxml
by right-clicking on the flex
folder and selecting New / File. In the file editor, which may be Flah Builder
or a simple text editor depending on your Eclipse installation, type in the following code:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" backgroundGradientColors="[#0e2e7d, #6479ab]" layout="vertical" verticalAlign="middle" creationComplete="srv.findWelcomeHistory()"> <mx:Style> .Panel { padding-left: 8px; padding-top: 8px; padding-right: 8px; padding-bottom: 8px; } .Result { font-size: 26px; color: white; } </mx:Style> <mx:RemoteObject id="srv" destination="helloWorldService" /> <mx:Panel styleName="Panel" title="Hello World Sample"> <mx:Label text="Enter your name:"/> <mx:TextInput id="nameInput" width="200" /> <mx:Button label="Say Hello" click="srv.sayHello(nameInput.text);srv.findWelcomeHistory()"/> <mx:Label text="History:"/> <mx:DataGrid dataProvider="{srv.findWelcomeHistory.lastResult}" width="200" height="200"/> </mx:Panel> <mx:Label styleName="Result" text="{srv.sayHello.lastResult}"/> </mx:Application>
Some explanations:
RemoteObject
named srv
that will be bound to the stateless session bean
we have created earlier. The actual binding between the destination named helloWorldService
and the Java service is specified
in the services-config.xml
that we will create later.
creationComplete
event handler, it first calls the
findWelcomeHistory()
method of the Java service. The result of this call is displayed in the DataGrid
, see
the dataProvider="{srv.findWelcomeHistory.lastResult}"
attribute.
TextInput
field, it calls the
sayHello()
method on the server with the supplied name and then the findWelcomeHistory()
method whose result
is used to update the DataGrid
content. See the click="srv.sayHello(nameInput.text);srv.findWelcomeHistory()"
attribute.
String
of the sayHello()
method, "Hello " + name + "!"
in the Java
service, is displayed in a Label
just below the Panel
. See the
text="{srv.sayHello.lastResult}"
attribute.
We are now going to configure the Granite Eclipse Builder that will generate the Welcome
entity bean ActionScript3 equivalent.
First the builder and install it; just drop the jar in your Eclipse
plugin
directory and restart. In your package explorer, right-click on the helloworld2
project and
select Add GraniteDS Nature:
In the configuration wizard, click on the Add Folder button and select the java
source folder:
Select the Excluded subnode and click on the Edit button. In the following dialog, change the
Output Directory to flex
, instead of the default as3
, and add the
**/*Service*.java
exclusion pattern, as we don't want code generation for services, just for entities:
After those short configuration steps, you may accept all other default options and click directly on the Finish button in the wizard.
The generation process starts and produces two files as shown in the following picture:
There is no need to modify those files for our short example but, if you want to add specific methods to your ActionScript 3 bean, you must put it in the
Welcome.as
class and not in the WelcomeBase.as
class that may be overwritten by subsequent generation processes.
If you look at the WelcomeBase.as
class, you will see that the generated code reproduces the Welcome.java
fields and
accesses (read-only id
and read-write name
). It also implements the code required for externalization mechanisms
with lazy loading support. See Externalizers documentation for details.
The rest is only a matter of configuration files. First, create a new file named granite-config.xml
at the root of the
helloworld2
project directory with this content:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC "-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
<class-getter type="org.granite.hibernate.HibernateClassGetter"/>
<externalizers>
<externalizer type="org.granite.hibernate.HibernateExternalizer">
<include annotated-with="javax.persistence.Entity"/>
</externalizer>
</externalizers>
</granite-config>
This configuration instructs GDS to externalize all Java classes annotated with the @Entity
annotation (such as our
Welcome.java
entity bean).
Next we need a Flex services-config.xml
as follows. Create it in at root of the project, as for granite-config.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<services-config>
<services>
<service
id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<destination id="helloWorldService">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>ejbFactory</factory>
</properties>
</destination>
</service>
</services>
<factories>
<factory id="ejbFactory" class="org.granite.messaging.service.EjbServiceFactory">
<properties>
<lookup>helloworld/{capitalized.destination.id}Bean/local</lookup>
</properties>
</factory>
</factories>
<channels>
<channel-definition id="my-graniteamf" class="mx.messaging.channels.AMFChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
</services-config>
This is where the destination id HelloWorldService
is bound to our stateless EJB 3 session bean service. When the
RemoteObject
in HelloWorld.mxml
is called, the destination id is used in the EjbServiceFactory
to lookup the EJB; helloworld/{capitalized.destination.id}Bean/local
is resolved to
helloworld/HelloWorldServiceBean/local
, that is the JNDI name used in JBoss to access the EJB.
We then need three additional configuration files: application.xml
, persistence.xml
and web.xml
.
All are standard J2EE configuration files and you will find detailed documentation on them on the Internet. Here is their contents.
Again, create them at the root of the project:
<?xml version="1.0" encoding="UTF-8"?>
<application>
<display-name>GraniteDS HelloWorld</display-name>
<module>
<web>
<web-uri>helloworld.war</web-uri>
<context-root>/helloworld</context-root>
</web>
</module>
<module>
<ejb>helloworld.jar</ejb>
</module>
</application>
<?xml version="1.0" encoding="UTF-8"?>
<persistence>
<persistence-unit name="ejb3">
<jta-data-source>java:/DefaultDS</jta-data-source>
<properties>
<property name="hibernate.hbm2ddl.auto" value="update"/>
</properties>
</persistence-unit>
</persistence>
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<!-- general information about this web application -->
<display-name>Hello World</display-name>
<description>Hello World Sample Application</description>
<!-- read services-config.xml file at web application startup -->
<listener>
<listener-class>org.granite.config.GraniteConfigListener</listener-class>
</listener>
<!-- handle AMF requests ([de]serialization) -->
<filter>
<filter-name>AMFMessageFilter</filter-name>
<filter-class>org.granite.messaging.webapp.AMFMessageFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>AMFMessageFilter</filter-name>
<url-pattern>/graniteamf/*</url-pattern>
</filter-mapping>
<!-- handle AMF requests (execution) -->
<servlet>
<servlet-name>AMFMessageServlet</servlet-name>
<servlet-class>org.granite.messaging.webapp.AMFMessageServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>AMFMessageServlet</servlet-name>
<url-pattern>/graniteamf/*</url-pattern>
</servlet-mapping>
<!-- default content for helloworld application -->
<welcome-file-list>
<welcome-file>HelloWorld.swf</welcome-file>
</welcome-file-list>
</web-app>
You should now see something like this in your package explorer:
The last thing to do is to build and deploy the application.
Create a new file named build.xml
at the root of the project. Copy and paste the following content into it; you may have to modify
the properties FLEX_HOME
and TOMCAT_HOME
to reflect your environment:
<?xml version="1.0" encoding="UTF-8"?>
<project name="hello-world" default="deploy">
<!-- Modify FLEX_HOME/JBOSS_HOME properties to reflect your environment -->
<property name="FLEX_HOME" value="/flex_sdk_3"/>
<property name="JBOSS_HOME" value="/jboss-4.2.3.GA"/>
<!-- Declare Flex Ant tasks (such as mxmlc used below) -->
<taskdef resource="flexTasks.tasks" classpath="${FLEX_HOME}/ant/lib/flexTasks.jar" />
<!-- Compile MXML source code to SWF -->
<target name="mxmlc">
<mxmlc
file="flex/HelloWorld.mxml"
output="build/HelloWorld.swf"
services="services-config.xml"
context-root="helloworld">
<source-path path-element="flex" />
<!-- Make sure that the Welcome.as class is compiled into
our HelloWorld.swf (otherwise mxmlc doesn't include it
because there are no explicit reference to this class
in HelloWorld.mxml -->
<includes symbol="org.test.Welcome" />
<!-- Make sure that all "essentials" GDS classes are included into
our HelloWorld.swf (otherwise mxmlc doesn't include them
because there is no explicit reference to these classes
in HelloWorld.mxml and Welcome.as -->
<compiler.include-libraries dir="lib" append="true">
<include name="granite-essentials.swc" />
</compiler.include-libraries>
</mxmlc>
</target>
<!-- Build an ear suitable for JBoss -->
<target name="ear" depends="mxmlc">
<mkdir dir="build"/>
<jar destfile="build/helloworld.jar">
<fileset dir="bin" includes="**/*.class"/>
<zipfileset file="persistence.xml" prefix="META-INF" />
</jar>
<war destfile="build/helloworld.war" webxml="web.xml">
<zipfileset file="services-config.xml" prefix="WEB-INF/flex" />
<zipfileset file="granite-config.xml" prefix="WEB-INF/granite" />
<fileset dir="build" includes="*.swf"/>
</war>
<ear destfile="build/helloworld.ear" appxml="application.xml">
<fileset dir="build" includes="*.jar,*.war"/>
<zipfileset dir="lib" includes="granite*.jar" prefix="lib" />
</ear>
</target>
<!-- Deploy the ear in JBoss -->
<target name="deploy" depends="ear">
<copy todir="${JBOSS_HOME}/server/default/deploy" file="build/helloworld.ear"/>
</target>
</project>
Basically, this Ant build compiles our Flex code in a swf and package everything in an ear suitable for JBoss deployment.
Read the comments in the above build.xml
about including AS3 classes/SWC libraries into the compiled SWF!
Missing this point leads to a very common Flex runtime error because of unexpected mxmlc compiler optimizations!
You may now right-click on the build.xml
file and select Run As / Ant Build. This will launch the build process,
compile the MXML code to an SWF, create an ear (Enterprise ARchive) and copy it into your JBoss deploy
directory.
Finally start JBoss and test the application.
To start JBoss, go to the directory bin
just under your JBoss installation directory, /jboss-4.2.3.GA/bin
for example, and double-click on run.bat
, or run.sh
for Unix/Mac users. After a short while, you should
see in the console that JBoss has started. You may now point your Web browser to
.
The main value of GraniteDS is to provide integration with other frameworks, both client and server side, so there really are lots of different possible combinations of deployment types and usage scenarios. This chapter will describe various options, and common combinations of technologies.
On the client there are two main choices :
RemoteObject
API. This is the easiest if you migrate an existing application from BlazeDS/LCDS/whatever AMF provider.
Note however that GraniteDS does not support the standard Consumer
and Producer
Flex messaging API.
It brings its own client implementations of these classes org.granite.gravity.Consumer
and org.granite.gravity.Producer
that provide very similar functionality.
On the server there are mostly two options :
RemoteObject
API, just choose the GraniteDS service factory depending on your server framework.
This will additionally bring you the GraniteDS support for externalization of lazily loaded JPA entities/collections, and support for scalable messaging though Gravity.
This section describes some classic technology stacks used with Flex applications and GraniteDS.
It's also similar to the previous case, but using EJB 3 instead of Spring.
GraniteDS consists in a set of Flex libraries (swcs) and a set of Java libraries (jars). It is designed to be deployed in a Java application server and packaged in a standard Java Web application, either as a WAR file or as an EAR file. The configuration of a GraniteDS project will generally involve the following steps :
WEB-INF/lib
folder of the WAR file or the lib
folder of the EAR file
WEB-INF/web.xml
configuration file
WEB-INF/granite/granite-config.xml
file
WEB-INF/flex/services-config.xml
Depending on which framework and application server you use on the server (Spring, Seam...) and on the client, some of these steps may be completely omitted,
or done differently. For example, when using the Spring framework on the server, almost all the configuration can be defined in the standard Spring context
instead of the granite-config.xml
and services-config.xml
files. GraniteDS tries to be as transparent and integrated
as possible with the application environment, however it can be useful to know how things work at the lower level if you have specific requirements.
The configuration of the various GraniteDS parts is done in the file WEB-INF/granite/granite-config.xml
.
There are many options that can be defined here, you can refer to the configuration reference.
As a starting point, you can create an empty file :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC "-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config/>
Or much easier a configuration that will use class scanning to determine the default setup.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC "-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config scan="true"/>
The last thing to define on the server is the application configuration in WEB-INF/flex/services-config.xml
.
This is for example the place where you will define which elements of your application you will expose to Flex remoting, or the topic for messaging.
You can refer to the configuration reference for more details.
For example a simple configuration for an EJB 3 service would look like :
<services-config>
<services>
<service id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<destination id="example">
<channels>
<channel ref="graniteamf"/>
</channels>
<properties>
<factory>ejbFactory</factory>
</properties>
</destination>
</service>
</services>
<factories>
<factory id="ejbFactory" class="org.granite.messaging.service.EjbServiceFactory">
<properties>
<lookup>myapp/{capitalized.destination.id}ServiceBean/local</lookup>
</properties>
</factory>
</factories>
<channels>
<channel-definition id="graniteamf" class="mx.messaging.channels.AMFChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
</services-config>
This configuration file declares 3 differents things, let's list them in the reverse order :
web.xml
.
Note that the server-name
, server-port
and context-root
are placeholders that are automatically replaced when running the application in the Flash Player.
To run the application on the AIR runtime you will have to specify the real name and port of the server as it cannot be determined from the source url of the swf.
Depending on the kind of framework integration that is used, the services-config.xml
file may not be necessary and can be omitted.
With Spring and Seam for example, everything can be defined in the respective framework configuration files instead of services-config.xml
.
private function init():void { srv = new RemoteObject("myService"); srv.source = "myService"; srv.channelSet = new ChannelSet(); srv.channelSet.addChannel(new AMFChannel("graniteamf", "http://{server.name}:{server.port}/myapp/graniteamf/amf")); srv.showBusyCursor = true; }
The Java dependencies are in the group org.graniteds
.
<dependency>
<groupId>org.graniteds</groupId>
<artifactId>granite-core</artifactId>
<version>${graniteds.version}</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.graniteds</groupId>
<artifactId>granite-hibernate</artifactId>
<version>${graniteds.version}</version>
<type>jar</type>
</dependency>
...
The Flex application can be built using the plugin. Here is a simple project descriptor for a Flex module :
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.myapp</groupId>
<artifactId>myapp-flex</artifactId>
<packaging>swf</packaging>
<version>1.0-SNAPSHOT</version>
<name>My Flex Module</name>
<dependencies>
<dependency>
<groupId>com.adobe.flex.framework</groupId>
<artifactId>flex-framework</artifactId>
<version>${flex.framework.version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.adobe.flexunit</groupId>
<artifactId>flexunit</artifactId>
<version>4.0-rc-1</version>
<type>swc</type>
<scope>test</scope>
</dependency>
<dependency>
<scope>internal</scope>
<groupId>org.graniteds</groupId>
<artifactId>granite-essentials-swc</artifactId>
<version>${graniteds.version}</version>
<type>swc</type>
</dependency>
<dependency>
<groupId>org.graniteds</groupId>
<artifactId>granite-swc</artifactId>
<version>${graniteds.version}</version>
<type>swc</type>
</dependency>
</dependencies>
<build>
<finalName>myapp</finalName>
<sourceDirectory>src/main/flex</sourceDirectory>
<testSourceDirectory>src/test/flex</testSourceDirectory>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.sonatype.flexmojos</groupId>
<artifactId>flexmojos-maven-plugin</artifactId>
<version>${flexmojos.version}</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.sonatype.flexmojos</groupId>
<artifactId>flexmojos-maven-plugin</artifactId>
<version>${flexmojos.version}</version>
<extensions>true</extensions>
<dependencies>
<dependency>
<groupId>com.adobe.flex</groupId>
<artifactId>compiler</artifactId>
<version>${flex.framework.version}</version>
<type>pom</type>
</dependency>
</dependencies>
<configuration>
<contextRoot>/myapp</contextRoot>
<sourceFile>Main.mxml</sourceFile>
<incremental>true</incremental>
<keepAs3Metadatas>
<keepAs3Metadata>Bindable</keepAs3Metadata>
<keepAs3Metadata>Managed</keepAs3Metadata>
<keepAs3Metadata>ChangeEvent</keepAs3Metadata>
<keepAs3Metadata>NonCommittingChangeEvent</keepAs3Metadata>
<keepAs3Metadata>Transient</keepAs3Metadata>
<keepAs3Metadata>Id</keepAs3Metadata>
<keepAs3Metadata>Version</keepAs3Metadata>
<keepAs3Metadata>Lazy</keepAs3Metadata>
<keepAs3Metadata>Name</keepAs3Metadata>
<keepAs3Metadata>In</keepAs3Metadata>
<keepAs3Metadata>Out</keepAs3Metadata>
<keepAs3Metadata>Inject</keepAs3Metadata>
<keepAs3Metadata>Produces</keepAs3Metadata>
<keepAs3Metadata>PostConstruct</keepAs3Metadata>
<keepAs3Metadata>Destroy</keepAs3Metadata>
<keepAs3Metadata>Observer</keepAs3Metadata>
<keepAs3Metadata>ManagedEvent</keepAs3Metadata>
</keepAs3Metadatas>
</configuration>
</plugin>
</plugins>
</build>
</project>
mvn archetype:generate -DarchetypeGroupId=org.graniteds.archetypes -DarchetypeArtifactId=graniteds-tide-spring-jpa-hibernate -DarchetypeVersion=1.1.0.GA -DgroupId=com.myapp -DartifactId=springflexapp -Dversion=1.0-SNAPSHOT
To build the application, just run :
cd springflexapp mvn install
cd webapp mvn jetty:run-war
cd webapp mvn embedded-glassfish:run
cd webapp mvn war:war
You should install the GraniteDS Eclipse Builder plugin (see here) so you can benefit from the automatic Java -> AS3 code generation. In can be installed in a standalone Flex/Flash Builder or in an Eclipse installation with the Flash Builder plugin.
The first step is to create a new Java EE Web project. You can use the Eclipse WTP wizard (File / New / Web / Dynamic Web Project) :
Change the name of the source folder to java
instead of src
to avoir conflicts with the Flex source folder we will add later.
Then copy the necessary GraniteDS libs in the folder WebContent/WEB-INF/lib
. That should be fine for the Java side.
Next add the Flex nature to the project by right-clicking on the project and selecting Add/Change Project Type / Add Flex Project Type....
Then follow the steps on the wizard.
You may want to change the target build folder of Flex to WebContent
so the target swf
will be directly compiled in the
exploded war folder.
You should change the source folder to flex
in the project properties in Flex Build Path and set the target url so the
Flex debugger will connect to the application deployed on the server :
Next copy the GraniteDS client libraries granite.swc
and granite-essentials.swc
to the libs
folder,
and configure the compiler options in the project properties in Flex Compiler
:
Finally we add the GraniteDS nature to the project with right-click / Add GraniteDS Nature.
Remember to change the target folder to flex
. The GraniteDS properties should like this :
If you have configured a target server (Tomcat for example), you now have a complete environment to run your application. All changes to the Flex application will
be automatically deployed to Tomcat thanks to the Eclipse WTP publishing of the WebContent
folder.
Data serialization between a Flex client application and a J2EE server may use three kinds of transfer encoding:
According to all available benchmarks, the last option, AMF3 with RemoteObject
, is the faster and most efficient.
Additionally it allows to work with strongly typed objects in the Flex application and thus is more maintainable.
GraniteDS provides a full implementation of the AMF3 protocol and a set of adapters suitable for remote calls to POJO, EJB 3, Seam, Spring, and Guice services.
However, standard AMF serialization/deserialization does not provide any way, either with LiveCycle Data Services/BlazeDS or with GraniteDS, to transfer private or protected data fields. Only non-static, non-transient public fields, either those with public getter and setter or with a public declaration, are taken into account. This limitation applies to both Java and ActionScript3 classes.
To preserve strong and secure data encapsulation of your beans while serializing their private internal state - such as a version number in entity beans — GraniteDS provides a specific serialization mechanism called externalization. See corresponding section for details.
AMF3 is a very compact binary format for data serialization/deserialization and remote method invocation. A key feature of this format is that it preserves the entire graph of your data without duplicating identical objects (contrary to JSON for example). For example, if A1 and A2 contain a reference to the same B1, the serialization of A1 and A2 does not duplicate B1. The Flash VM will contain exactly the same data graph with only one B1 referenced by one A1 and one A2. Furthermore, there is no risk of infinite recursion if the data graph contains circular references. For example, if B1 contains the set of A# that references B1. AMF3 messages are sent as a part of a AMF0 envelope and body. GraniteDS implements an AMF3 serializer/deserializer and relies on some code borrowed from the project for AMF0 serialization/deserialization. The AMF0 and AMF3 specifications are now public. You may download them . You will need a Macromedia or Adobe account.
RemoteObject
is the standard remoting API of the Flex SDK. It can be use either declaratively in MXML or programmatically in ActionScript.
A RemoteObject
is attached to a server-side destination, generally defined in the services-config.xml
(see the configuration reference).
You can also refer to the about RemoteObject
to get some useful information.
For this example, we'll show a simple POJO destination :
public class HelloService {
public String hello(String name) {
return "Hello " + name;
}
}
<services>
<service
id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<destination id="hello">
<channels>
<channel ref="graniteamf"/>
</channels>
<properties>
<scope>request</scope>
<source>com.myapp.HelloService</source>
</properties>
</destination>
</service>
</services>
<channels>
<channel-definition id="graniteamf" class="mx.messaging.channels.AMFChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
import mx.rpc.events.ResultEvent;
import mx.rpc.events.FaultEvent;
import mx.controls.Alert;
public function resultHandler(event:ResultEvent):void {
// Display received message
outputMessage.text = event.result as String;
}
public function faultHandler(event:FaultEvent):void {
// Show error alert
Alert.show(event.fault.faultString);
}
</mx:Script>
<!-- Connect to a service destination.-->
<mx:RemoteObject id="helloService"
destination="hello"
result="handleResult(event);"
fault="handleFault(event);"/>
<!-- Provide input data for calling the service. -->
<mx:TextInput id="inputName"/>
<!-- Call the web service, use the text in a TextInput control as input data.-->
<mx:Button click="helloService.hello(inputName.text)"/>
<!-- Display results data in the user interface. -->
<mx:Label id="outputMessage"/>
</mx:Application>
<mx:RemoteObject id="helloService"
destination="hello">
<mx:operation name="hello"
result="handleResult(event);"
fault="handleFault(event);"/>
<mx:operation name="..."
result="..."
fault="..."/>
</mx:RemoteObject>
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<!-- Connect to a service destination.-->
<mx:RemoteObject id="helloService" destination="hello"/>
<!-- Provide input data for calling the service. -->
<mx:TextInput id="inputName"/>
<!-- Call the web service, use the text in a TextInput control as input data.-->
<mx:Button click="helloService.hello(inputName.text)"/>
<!-- Display results data in the user interface using binding on the lastResult property of AsyncToken. -->
<mx:Label id="outputMessage" text="{helloService.hello.lastResult}"/>
</mx:Application>
It is possible to use more complex data types as arguments or as result values. It is then necessary to create an equivalent ActionScript 3 class for each Java data class. You can refer to the mapping section to see how to do this in detail. Also see how you can use the Gas3 code generator to do this for you.
package com.myapp.model;
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.myapp.model { [RemoteClass(alias="com.myapp.model.Person")] public class Person { public var name:String; } }
public class PeopleService {
public List<Person> findAll(Person examplePerson) {
...
return list;
}
}
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<!-- Connect to a service destination.-->
<mx:RemoteObject id="peopleService"
destination="people"
result="handleResult(event);"
fault="handleFault(event);"/>
<!-- Provide input data for calling the service. -->
<mx:TextInput id="inputName"/>
<!-- Call the web service, use the text in a TextInput control as input data.-->
<mx:Button click="peopleService.findAll(inputName.text)"/>
<!-- Display results data in the user interface. -->
<mx:DataGrid id="outputGrid" dataProvider="{peopleService.lastResult}"/>
</mx:Application>
Using HTTPS involves two steps :
<services>
...
</services>
<channels>
<channel-definition id="graniteamf" class="mx.messaging.channels.SecureAMFChannel">
<endpoint
uri="https://{server.name}:{server.port}/{context.root}/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
<security-constraint>
<display-name>AMF access</display-name>
<web-resource-collection>
<web-resource-name>Secure AMF remoting</web-resource-name>
<description>Secure AMF Remoting</description>
<url-pattern>/graniteamf/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>role1</role-name>
...
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
This section describes the usage of the Tide API with a standard AMF provider. When the Tide API is used in conjunction with GraniteDS and Tide-enabled server framework adapters, there are some specificities that are described in the chapters concerning each framework integration (EJB3, Spring, Seam 2, CDI).
This example can be cleaned up by using the dependency injection feature of the Tide framework (see here for more details).
Basically you can inject a client proxy for a remote destination with the annotation [In]
.
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="Tide.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.Tide;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;
[In]
public var helloService:Component;
private function hello(name:String):void {
helloService.hello(name, resultHandler, faultHandler);
}
private function resultHandler(event:TideResultEvent):void {
outputMessage.text = event.result as String;
}
private function faultHandler(event:TideFaultEvent):void {
// Handle fault
}
</mx:Script>
<!-- Provide input data for calling the service. -->
<mx:TextInput id="inputName"/>
<!-- Call the web service, use the text in a TextInput control as input data.-->
<mx:Button click="hello(inputName.text)"/>
<!-- Result message. -->
<mx:Label id="outputMessage"/>
</mx:Application>
public function call():void { var responder1:TideResponder = new TideResponder(helloResult, helloFault, "firstCall"); var responder2:TideResponder = new TideResponder(helloResult, helloFault, "secondCall"); tideContext.helloWorld.sayHello("Jimi", responder1); tideContext.helloWorld.sayHello("Jimi", responder2); } private function helloResult(event:TideResultEvent, token:Object):void { if (token == "firstCall") Alert.show(event.result); }
In this case, the Alert
will show up only once for the first call.
private var products:ArrayCollection = new ArrayCollection(); public function call():void { tideContext.productService.findAllProducts( new TideResponder(resultHandler, null, null, products) ); } private function resultHandler(event:TideResultEvent):void { trace("Assert result was merged: " + (event.result === products)); }
<mx:DataGrid dataProvider="{products}">
...
</mx:DataGrid>
public class EntityNotFoundExceptionConverter implements ExceptionConverter {
public static final String ENTITY_NOT_FOUND = "Persistence.EntityNotFound";
public boolean accepts(Throwable t, Throwable finalException) {
return t.getClass().equals(javax.persistence.EntityNotFoundException.class);
}
public ServiceException convert(
Throwable t, String detail, Map<String, Object> extendedData) {
ServiceException se = new ServiceException(
ENTITY_NOT_FOUND, t.getMessage(), detail, t
);
se.getExtendedData().putAll(extendedData);
return se;
}
}
This exception converter has to be declared on the GDS server config :
scan="true"
in granite-config.xml
, ensure that there is a
META-INF/granite-config.properties
file (even empty) in the jar containing the exception converter class
(same principle than the seam.properties
file to specify which jars need to be scanned in JBoss Seam 2.x).
granite-config.xml
:
<exception-converters> <exception-converter type="com.myapp.custom.MyExceptionConverter"/> </exception-converters>
On the Flex side, you then have to define an exception handler class:
public class EntityNotFoundExceptionHandler implements IExceptionHandler { public function accepts(emsg:ErrorMessage):Boolean { return emsg.faultCode == "Persistence.EntityNotFound"; } public function handle(context:BaseContext, emsg:ErrorMessage):void { Alert.show("Entity not found: " + emsg.message); } }
<mx:Application>
<mx:Script>
Tide.getInstance().addExceptionHandler(EntityNotFoundExceptionHandler);
</mx:Script>
</mx:Application>
There are a few other features that are useful when working with remote services :
Tide.showBusyCursor
can enable or disable the busy mouse cursor during execution of remote calls.
Tide.busy
is a bindable property that can be used to determine if there is currently a remote call in progress.
Tide.disconnected
is a bindable property that can be used to determine if the network connection is currently broken.
If becomes false when a network error is detected and set to true after each successful call.
When using typed objects, it's necessary to create an ActionScript 3 class for each Java class that will be marshalled between Flex and Java.
However due to the differences of data types in the ActionScript 3 and Java languages, data conversions are done during serialization/deserialization.
GraniteDS follows the standard conversions specified in the Adobe Flex SDK documentation ,
with an important exception : GDS will neither convert AS3 String
to Java numeric types or boolean
,
nor AS3 numeric types or boolean
to String
.
You must use AS3 numeric types for Java numeric types and AS3 boolean type for Java boolean types; either primitive or boxed boolean.
Starting with GraniteDS 2.2, long
, Long
, BigInteger
and BigDecimal
values
may by converted to their respective ActionScript 3 equivalent (see Big Number Implementations for details).
java.io.Externalizable
manually. You just have to configure which classes should be processed.
writeExternal
and readExternal
methods.
package com.myapp.entity; import java.io.Serializable; import javax.persistence.Basic; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Version; @Entity public class Person implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue private Integer id; @Version private Integer version; @Basic private String firstName; @Basic private String lastName; public Integer getId() { return id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }
/** * Generated by Gas3 v2.2.0 (Granite Data Services). * * WARNING: DO NOT CHANGE THIS FILE. IT MAY BE OVERWRITTEN EACH TIME YOU USE * THE GENERATOR. INSTEAD, EDIT THE INHERITED CLASS (Person.as). */ package com.myapp.entity { import flash.utils.IDataInput; import flash.utils.IDataOutput; import flash.utils.IExternalizable; import org.granite.collections.IPersistentCollection; import org.granite.meta; use namespace meta; [Bindable] public class PersonBase implements IExternalizable { private var __initialized:Boolean = true; private var __detachedState:String = null; private var _firstName:String; private var _id:Number; private var _lastName:String; private var _version:Number; meta function isInitialized(name:String = null):Boolean { if (!name) return __initialized; var property:* = this[name]; return ( (!(property is Person) || (property as Person).meta::isInitialized()) && (!(property is IPersistentCollection) || (property as IPersistentCollection).isInitialized()) ); } public function set firstName(value:String):void { _firstName = value; } public function get firstName():String { return _firstName; } public function get id():Number { return _id; } public function set lastName(value:String):void { _lastName = value; } public function get lastName():String { return _lastName; } public function readExternal(input:IDataInput):void { __initialized = input.readObject() as Boolean; __detachedState = input.readObject() as String; if (meta::isInitialized()) { _firstName = input.readObject() as String; _id = function(o:*):Number { return (o is Number ? o as Number : Number.NaN) } (input.readObject()); _lastName = input.readObject() as String; _version = function(o:*):Number { return (o is Number ? o as Number : Number.NaN) } (input.readObject()); } else { _id = function(o:*):Number { return (o is Number ? o as Number : Number.NaN) } (input.readObject()); } } public function writeExternal(output:IDataOutput):void { output.writeObject(__initialized); output.writeObject(__detachedState); if (meta::isInitialized()) { output.writeObject(_firstName); output.writeObject(_id); output.writeObject(_lastName); output.writeObject(_version); } else { output.writeObject(_id); } } } }
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
<class-getter type="org.granite.hibernate.HibernateClassGetter"/>
<externalizers>
<externalizer type="org.granite.hibernate.HibernateExternalizer">
<include type="com.myapp.entity.Person"/>
</externalizer>
</externalizers>
</granite-config>
This instructs GraniteDS to externalize all classes named com.myapp.entity.Person
by using the org.granite.hibernate.HibernateExternalizer
.
Note that the HibernateClassGetter
configuration is necessary to detect Hibernate proxies (lazy-initialized beans). See more about this feature in the JPA and lazy initialization section.
If you use an abstract entity bean as a parent to all your entity beans you could use this declaration, but note that type
in the example above
is replaced by instance-of
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
<class-getter type="org.granite.hibernate.HibernateClassGetter"/>
<externalizers>
<externalizer type="org.granite.hibernate.HibernateExternalizer">
<include instance-of="com.myapp.entity.AbstractEntity"/>
</externalizer>
</externalizers>
</granite-config>
This will avoid the need of writing externalization instructions for all your beans, and all instances of AbstractEntity
will be automatically externalized.
You may also use an annotated-with
attribute as follows:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
<class-getter type="org.granite.hibernate.HibernateClassGetter"/>
<externalizers>
<externalizer type="org.granite.hibernate.HibernateExternalizer">
<include annotated-with="javax.persistence.Entity"/>
<include annotated-with="javax.persistence.MappedSuperclass"/>
<include annotated-with="javax.persistence.Embeddable"/>
</externalizer>
</externalizers>
</granite-config>
Of course, you may mix these different attributes as you want.
Note, however, that there are precedence rules for these three configuration options: type
has precedence over annotated-with
and annotated-with
has precedence over instance-of
.
Playing with rule precedence provides a way to override general rules with more specific rules for particular classes.
Instead of configuring externalizers with the above method, you may use the autoscan feature:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config scan="true"/>
GraniteDS comes with a set of built-in externalizers for the most usual kinds of Java classes:
org.granite.messaging.amf.io.util.externalizer.DefaultExternalizer
: this externalizer may be used with any POJO bean.
org.granite.messaging.amf.io.util.externalizer.EnumExternalizer
: this externalizer may be used with Java enum
types. When autoscan is enabled, it will be automatically used for all enum
types.
org.granite.hibernate.HibernateExternalizer
: This externalizer may be used with all JPA/Hibernate entities (i.e., all classes annotated with @Entity
, @MappedSuperclass
or @Embeddable
annotations). Include granite-hibernate.jar
in your classpath in order to use this feature.
org.granite.toplink.TopLinkExternalizer
: this externalizer may be used with all JPA/TopLink entities (i.e., all classes annotated with @Entity
, @MappedSuperclass
or @Embeddable
annotations). Include granite-toplink.jar
in your classpath in order to use this feature.
org.granite.eclipselink.EclipseLinkExternalizer
: this externalizer will be used with the new version of TopLink (renamed EclipseLink). Include granite-eclipselink.jar
in your classpath in order to use this feature.
org.granite.openjpa.OpenJpaExternalizer
: this externalizer may be used with all JPA/OpenJPA (formerly WebLogic Kodo) entities (mainly in WebLogic environments). Include granite-openjpa.jar
in your classpath in order to use this feature.
org.granite.datanucleus.DataNucleusExternalizer
: this externalizer may be used with all JPA/DataNucleus entities. Include granite-datanucleus.jar
in your classpath in order to use this feature.
org.granite.tide.cdi.TideEventExternalizer
: this externalizer externalizes classes annotated with the TideEvent
annotation.
org.granite.messaging.amf.io.util.externalizer.LongExternalizer
: externalizes Java long
or Long
values.
org.granite.messaging.amf.io.util.externalizer.BigIntegerExternalizer
: externalizes Java BigInteger
values.
org.granite.messaging.amf.io.util.externalizer.BigDecimalExternalizer
: externalizes Java BigDecimal
values.
Two standard annotations are available that give you more control over the externalization process:
@ExternalizedBean(type=path.to.MyExternalizer.class)
public class MyExternalizedBean {
...
}
public class MyBean {
private int value;
...
@ExternalizedProperty
public int getSquare() {
return value * value;
}
}
Two instantiators come with GDS:
org.granite.messaging.amf.io.util.instantiator.EnumInstantiator
: This instantiator is used in order to get an Enum
constant
value from an Enum
class and value (the String
representation of the constant), by means of the
java.lang.Enum.valueOf(Class<? extends Enum> enumType, String name)
method.
org.granite.hibernate.HibernateProxyInstantiator
: It is used when GDS needs to recreate an HibernateProxy
.
See source code for details.
Let's say you have a JavaBean like this one:
package org.test;
import java.util.Map;
import java.util.HashMap;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public class MyBean {
private final static Map<String, MyBean> beans = new HashMap<String, MyBean>();
private final String name;
private final String encodedName;
protected MyBean(String name) {
this.name = name;
try {
this.encodedName = URLEncoder.encode(name, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public static MyBean getInstance(String name) {
MyBean bean = null;
synchronized (beans) {
bean = beans.get(name);
if (bean == null) {
bean = new MyBean(name);
beans.put(name, bean);
}
}
return bean;
}
public String getName() {
return name;
}
public String getEncodedName() {
return encodedName;
}
}
The solution is to write a custom instantiator that will be used at deserialization time:
package org.test;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import org.granite.messaging.amf.io.util.instantiator.AbstractInstanciator;
public class MyBeanInstanciator extends AbstractInstanciator<MyBean> {
private static final long serialVersionUID = -1L;
private static final List<String> orderedFields;
static {
List<String> of = new ArrayList<String>(1);
of.add("name");
orderedFields = Collections.unmodifiableList(of);
}
@Override
public List<String> getOrderedFieldNames() {
return orderedFields;
}
@Override
public MyBean newInstance() {
return MyBean.getInstance((String)get("name"));
}
}
You should finally use a granite-config.xml
file as follows in order to use your instantiator:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
<externalizers>
<externalizer type="org.granite.messaging.amf.io.util.externalizer.DefaultExternalizer">
<include type="org.test.MyBean"/>
</externalizer>
</externalizers>
<instanciators>
<instanciator type="org.test.MyBean">org.test.MyBeanInstanciator</instanciator>
</instanciators>
</granite-config>
In your JPA entity bean, you may have a single-valued association like this:
@Entity
public class MyEntity {
@Id @GeneratedValue
private Integer id;
@OneToOne(fetch=FetchType.LAZY)
private MyOtherEntity other;
// Skipped code...
}
@Entity
public class MyOtherEntity {
@Id @GeneratedValue
private Integer id;
// Skipped code...
}
[Bindable] [RemoteClass(alias="path.to.MyEntity"] public class MyEntity { private var __initialized:Boolean = true; private var __detachedState:String = null; private var _id:Number; private var _other:MyOtherEntity; meta function isInitialized(name:String = null):Boolean { if (!name) return __initialized; var property:* = this[name]; return ( (!(property is Welcome) || (property as Welcome).meta::isInitialized()) && (!(property is IPersistentCollection) || (property as IPersistentCollection).isInitialized()) ); } // Skipped code... public override function readExternal(input:IDataInput):void { __initialized = input.readObject() as Boolean; __detachedState = input.readObject() as String; if (meta::isInitialized()) { _id = function(o:*):Number { return (o is Number ? o as Number : Number.NaN) } (input.readObject()); _other = input.readObject() as MyOtherEntity; // read remaining MyEntity fields... } else _id = function(o:*):Number { return (o is Number ? o as Number : Number.NaN) } (input.readObject()); } } [Bindable] [RemoteClass(alias="path.to.MyOtherEntity"] public class MyOtherEntity { private var __initialized:Boolean = true; private var __detachedState:String = null; private var _id:Number; // Skipped code... public override function readExternal(input:IDataInput):void { __initialized = input.readObject() as Boolean; __detachedState = input.readObject() as String; if (meta::isInitialized()) { _id = input.readObject() as int; // read remaining MyOtherEntity fields... } else _id = input.readObject() as int; } }
For example, in this persistent set:
package com.myapp.entity;
import java.util.HashSet;
import java.util.Set;
...
import javax.persistence.CascadeType;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
@Entity
public class Person extends AbstractEntity {
...
@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY, mappedBy="person")
private Set<Contact> contacts = new HashSet<Contact>();
...
public Set<Contact> getContacts() {
return contacts;
}
public void setContacts(Set<Contact> contacts) {
this.contacts = contacts;
}
}
// code for Contact skipped...
package com.myapp.entity {
...
import mx.collections.ListCollectionView;
[Bindable]
[RemoteClass(alias="test.granite.ejb3.entity.Person")]
public class Person implements IExternalizable {
...
private var _contacts:ListCollectionView;
...
public function set contacts(value:ListCollectionView):void {
_contacts = value;
}
public function get contacts():ListCollectionView{
return _contacts;
}
...
public override function readExternal(input:IDataInput):void {
...
_contacts = input.readObject() as ListCollectionView;
...
}
public override function writeExternal(output:IDataOutput):void {
...
output.writeObject(_contacts);
...
}
// code for Contact skipped...
scan
set to false
), you must use the appropriate class getter together with the persistence externalizer
(eg. org.granite.openjpa.OpenJpaClassGetter
with org.granite.openjpa.OpenJpaExternalizer
).
granite-hibernate.jar
and granite-eclipselink.jar
,
the application should work under both JBoss (which bundles Hibernate) and GlassFish 3 (which bundles EclipseLink).
When you are using Java Enterprise frameworks such as Seam or Spring together with GraniteDS, you may use specific Seam Security or Spring Security implementations instead of the previous container-based services: please refer to Seam Services or Spring Services for more information.
To enable security, you simply put this kind of declaration in your granite-config.xml
file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
...
<security type="org.granite.messaging.service.security.TomcatSecurityService"/>
<!--
Alternatively for Jetty 6.x
<security type="org.granite.messaging.service.security.Jetty6SecurityService"/>
For Jetty 7.x/8.x (available at eclipse.org)
<security type="org.granite.messaging.service.security.Jetty7SecurityService"/>
For GlassFish 2.x
<security type="org.granite.messaging.service.security.GlassFishSecurityService"/>
For GlassFish 3.x
<security type="org.granite.messaging.service.security.GlassFishV3SecurityService"/>
For WebLogic
<security type="org.granite.messaging.service.security.WebLogicSecurityService"/>
-->
</granite-config>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
...
<security type="org.granite.messaging.service.security.TomcatSecurityService">
<param name="service" value="your-tomcat-service-name-here"/>
</security>
</granite-config>
You may now use role-based security on destination in your services-config.xml
file:
<?xml version="1.0" encoding="UTF-8"?>
<services-config>
<services>
<service id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<destination id="person">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<scope>session</scope>
<source>com.myapp.PersonService</source>
</properties>
<security>
<security-constraint>
<auth-method>Custom</auth-method>
<roles>
<role>user</role>
<role>admin</role>
</roles>
</security-constraint>
</security>
</destination>
<destination id="restrictedPerson">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<scope>session</scope>
<source>com.myapp.RestrictedPersonService</source>
</properties>
<security>
<security-constraint>
<auth-method>Custom</auth-method>
<roles>
<role>admin</role>
</roles>
</security-constraint>
</security>
</destination>
</service>
</services>
...
</services-config>
Please refer to Tomcat and JBoss documentation for setting up your users/roles configuration.
public interface RemotingDestinationSecurizer extends DestinationSecurizer {
public void canExecute(ServiceInvocationContext context)
throws SecurityServiceException;
}
You then have to tell GraniteDS where to use your securizer:
<services-config>
<services>
<service ...>
<destination id="restrictedDestination">
...
<properties>
<securizer>path.to.MyDestinationSecurizer</securizer>
</properties>
</destination>
</service>
</services>
...
</services-config>
Starting with GraniteDS 2.2, Gas3 may also replicate validation annotations in order to use the Flex side validation framework (see Bean Validation (JSR-303))
and may also be configured to generate Long
, BigInteger
and BigDecimal
variable for their Java equivalents (see Big Number Implementations).
The generator (named GAS3) is implemented as an Eclipse plugin and as an Ant task. This Ant task is packaged as an Eclipse 3.2+ Ant plugin but may also be used outside of Eclipse for command line Ant calls. It is also included in the Maven plugin.
The next sections introduce both the Eclipse plugin and Ant task configurations and usages. You may also have a look at the Eclipse Plugins Installation section and at the Hello World revisited tutorial for a sample Eclipse Builder usage.
package org.test;
import java.io.Serializable;
import javax.persistence.Basic;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Welcome implements Serializable {
private static final long serialVersionUID = 1L;
@Id @GeneratedValue
private Integer id;
@Basic
private String name;
public Welcome() {
}
public Welcome(String name) {
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
/** * Generated by Gas3 v2.2.0 (Granite Data Services). * * NOTE: this file is only generated if it does not exist. You may safely put * your custom code here. */ package org.test { [Bindable] [RemoteClass(alias="org.test.Welcome")] public class Welcome extends WelcomeBase { } }
/** * Generated by Gas3 v2.2.0 (Granite Data Services). * * WARNING: DO NOT CHANGE THIS FILE. IT MAY BE OVERWRITTEN EACH TIME YOU USE * THE GENERATOR. INSTEAD, EDIT THE INHERITED CLASS (Welcome.as). */ package org.test { import flash.utils.IDataInput; import flash.utils.IDataOutput; import flash.utils.IExternalizable; import org.granite.collections.IPersistentCollection; import org.granite.meta; use namespace meta; [Bindable] public class WelcomeBase implements IExternalizable { private var __initialized:Boolean = true; private var __detachedState:String = null; private var _id:Number; private var _name:String; meta function isInitialized(name:String = null):Boolean { if (!name) return __initialized; var property:* = this[name]; return ( (!(property is Welcome) || (property as Welcome).meta::isInitialized()) && (!(property is IPersistentCollection) || (property as IPersistentCollection).isInitialized()) ); } public function get id():Number { return _id; } public function set name(value:String):void { _name = value; } public function get name():String { return _name; } public function readExternal(input:IDataInput):void { __initialized = input.readObject() as Boolean; __detachedState = input.readObject() as String; if (meta::isInitialized()) { _id = function(o:*):Number { return (o is Number ? o as Number : Number.NaN) } (input.readObject()); _name = input.readObject() as String; } else { _id = function(o:*):Number { return (o is Number ? o as Number : Number.NaN) } (input.readObject()); } } public function writeExternal(output:IDataOutput):void { output.writeObject(__initialized); output.writeObject(__detachedState); if (meta::isInitialized()) { output.writeObject(_id); output.writeObject(_name); } else { output.writeObject(_id); } } } }
Here are the details for (re)generation conditions:
Templates | Conditions for (re)generation |
---|---|
Dual templates (base + inherited) | The inherited AS3 class is generated only once if it does not exist. The AS3 base one is generated if it does not exist or if its timestamp (last modified time) is less than the Java class one |
Single template (enums or interfaces) | Like the base condition above, the AS3 class is (re)generated if it does not exist or if its timestamp is less than the Java class one |
Type of Java Class | Template | Base Template |
---|---|---|
Standard Java beans | bean.gsp | beanBase.gsp |
JPA entities: all classes annotated with @Entity and @MappedSuperclass | entity.gsp | entityBase.gsp |
Java enums | enum.gsp | (none) |
Java interfaces | interface.gsp | (none) |
Java services: all classes annotated with @RemoteDestination | remote.gsp | remoteBase.gsp |
Java events (CDI): all classes annotated with @TideEvent | bean.gsp | beanBase.gsp |
For each selected Java source folder you may also configure specific output directories:
Output Directory
: A directory relative to your project directory where generated ActionsScript3 classes are put.
Base Output Directory
: An optional directory relative to your project directory where so-called "Base"
generated ActionsScript 3 classes are put. If left empty, the output directory above is used for both "Base" and inherited
ActionScript 3 classes. See here for this distinction.
The next step in the wizard allows you to configure Java project dependencies. This is required when your Java classes make references to
other classes declared in other Java projects. Clicking on the Add project button will open a dialog that lists all other
open Java projects which have the GraniteDS nature:
The next step is classpath configuration. If you do not use any custom classes in the Options panel
you do not need to change anything here since the classpath is automatically configured with your current selected source folders.
In the following picture, the helloworld/bin
directory, where Eclipse compiles your Java source, is preselected,
as well as all libraries in the build path (eg. Java runtime jars, ejb3-persistence.jar
and
jboss-ejb3x.jar
):
The next panel lets you configure custom generation templates.
Those templates are a mix of the JSP syntax and the .
If you need specific code generation, you may write your own template, select one template in the displayed tree, and click on the Edit button:
In the above example, a class:
protocol is used because all standard templates are available in the classpath.
Alternatively, you may use the file:
protocol to load your template from the filesystem. These templates can be
specified either by using absolute paths (eg. file:/absolute/path/to/mytemplate.gsp
) or paths relative to your
current Eclipse project root directory (eg. path/to/mytemplate.gsp
).
Clicking the Use Tide button will configure Tide specific templates for entity beans. Use it if you are configuring the builder for a Tide project.
Clicking the Use LCDS button will configure LCDS specific templates for basic beans and entity beans, and
remove all templates for enum and remote destination services. It will also configure a LCDS specific AS3TypeFactory in the "Options"
panel (see picture below). The LCDS templates generate AS3 beans that can be either [Bindable] (default) or [Managed]. If you need [Managed]
beans, use the following parameter with your include pattern: [managed=true]
. For example, your include patterns could be:
If you want to use a single file generation policy (ie. no "Base" and inherited files), you can reconfigure the templates by using
class:org/granite/generator/template/lcdsStandaloneBean.gsp
as the only template, removing the base template.
WARNING: when switching from dual templates to a single one, be sure to remove and backup any previously generated files.
The last panel lets you configure various options:
Some explanations:
UID Property Name
: If you want your AS3 to implement mx.core.IUID
, you must tell the generator
the name of the Java field that contains this UID; use the same name in all your beans. Default is to search for field named uid
.
AS3TypeFactory Class
: You may use this option to configure a custom factory for special type support.
See Handling Custom Data Types for a detailed example. If you configure this,
you must add your class in the Classpath panel.
EntityFactory Class
: You may use this option to configure a custom factory for special entity support.
Setting this field to org.granite.generator.as3.BVEntityFactory
is useful if you want to use the GraniteDS validation framework.
See Bean Validation (JSR-303) for details.
RemoteDestinationFactory Class
: You may use this option to configure a custom factory for special service support.
You could for example implement a specific factory to analyze services for a particular framework.
"Show debug informations in console"
: If enabled, Gas3 will display more information during the generation process.
"Generate a Flex Builder configuration file"
: If enabled, Gas3 will generate a granite-flex-config.xml
that may be used in your compiler options. Useful to make sure that all generated AS3 beans will be included in your SWF. Note that
all AS3 files present in a Gas3 output directory (even those which are not generated) will be added to the config file.
"Use org.granite.math.Long"
: If enabled, Gas3 will generate AS3 Long
properties for
Java long
or Long
properties. See Big Number Implementations for details.
"Use org.granite.math.BigInteger"
: If enabled, Gas3 will generate AS3 BigInteger
properties for
Java BigInteger
properties. See Big Number Implementations for details.
"Use org.granite.math.BigDecimal"
: If enabled, Gas3 will generate AS3 BigDecimal
properties for
Java BigDecimal
properties. See Big Number Implementations for details.
When you have finished with the wizard, a first generation process will start and you should see something like this in the Eclipse console:
If you need to change your configuration later, you can right-click on your project, select the Properties item,
and you'll be able to modify all GraniteDS Eclipse Builder configuration options:
The panels are exactly the same as those of the wizard and the above documentation applies.
You can use the GraniteDS builder with Flex Builder provided that the three builders (Java, Granite, and Flex) are configured in the correct order.
It will work without modification if you first create a Java project, add the Flex project nature, and then the GraniteDS nature; the builder will
make sure that builders are setup in the correct order. Otherwise, you may have to change this order. Right-click on your project, select the
Properties item, and select the Builders entry:
The order (Java / Granite / Flex) in the above picture is the correct one.
When you have configured your project to use the GraniteDS Eclipse Builder, you may cancel any further generation processes by removing the nature:
Note that the hidden configuration file
.granite
in your project is not removed by this action and you must delete it manually.
Otherwise, it will be reused whenever you add the nature again.
The main purpose of the builder is to generate AS3 files based on Java sources which are added or modified. When a Java source file is deleted or renamed, the builder will append to the name of all potentially AS3 generated files a suffix composed of a dot, the current system millisecond since epoch (1/1/1970) and an additionnal extension ".hid". The idea behind these renaming operations is to make sure that the Flex compilation will detect errors if these classes are used in the project (easing refactoring) and to ensure that any manual editing you have made in these classes is recoverable.
<taskdef name="gas3" classname="org.granite.generator.ant.AntJavaAs3Task"/>
$ ant -lib /gas3libs -f build.xml {target} ...
<target name="generate.as3">
<gas3 outputdir="as3">
<classpath>
<pathelement location="classes"/>
</classpath>
<fileset dir="classes">
<include name="com/myapp/entity/**/*.class"/>
</fileset>
</gas3>
</target>
For each JPA entity (say com.myapp.entity.MyEntity
), Gas3 will generate two AS3 beans:
org.entity.MyEntityBase.as
: This bean mainly contains fields, getters, setters, and IExternalizable
methods
(readExternal
/writeExternal
). This file is generated if it does not exist or if it is outdated.
org.entity.MyEntity.as
: This bean inherits from the "Base" one and is only generated if it does not exist.
<target name="generate.as3">
<gas3 outputdir="as3">
<classpath>
<pathelement location="lib/myclasses.jar"/>
</classpath>
<zipfileset src="lib/myclasses.jar">
<include name="com/myapp/entity/**/*.class"/>
</zipfileset>
</gas3>
</target>
<gas3 ...>
<classpath .../>
<fileset .../>
<translator
java="path.to.my.java.class"
as3="path.to.my.as3.class" />
<translator
java="path.to.my.java.class.special"
as3="otherpath.to.my.as3.class.special" />
...
</gas3>
Here is the complete list of Gas3 node attributes:
outputdir
and baseoutputdir
: We have already seen the outputdir
attribute
in basic usage. baseoutputdir
lets you define a custom output directory for your "Base" generated files.
The default is to use the same directory as specified by the outputdir
attribute.
uid
: If you want your AS3 to implement mx.core.IUID
, you must tell the generator
the name of the Java field that contains this UID. By default, Gas3 will search for a field named uid
.
You may change this by adding a uid="myUid"
attribute to the gas3
node.
If Gas3 does not find this uid
, it will be silently ignored.
tide
: Should we use a Tide specific template instead of the standard base template used for entity beans
(true
or false
, defaut is false
). Setting this attribute has no effect
if you use a custom entity base template. See below.
entitytemplate
and entitybasetemplate
: Templates used for classes annotated with
@Entity
or @MappedSuperclass
.
interfacetemplate
: Template used for Java interfaces.
beantemplate
and beanbasetemplate
: Templates used for other Java classes including @Embeddable
.
enumtemplate
: Template used for java.lang.Enum
types.
remotetemplate
and remotebasetemplate
: Templates used for server services (EJB3, Spring or Seam services).
as3typefactory
: You can plug your own org.granite.generator.as3.As3TypeFactory
implementation
in order to add support for custom types. For example, if you have configured a custom Joda time converter,
you may extend Gas3 accordingly for this custom type. Just extend the org.granite.generator.as3.DefaultAs3TypeFactory
class
and return org.granite.generator.as3.As3Type.DATE
when you encounter a Joda DateTime
instance.
See Handling Custom Data Types for a detailed example.
entityfactory
: Class used to introspect specific entity properties or metadata (default is
org.granite.generator.as3.DefaultEntityFactory
).
You may also use the built-in org.granite.generator.as3.BVEntityFactory
in order to replicate bean validation annotations
into your AS3 model Bean Validation (JSR-303).
remotedestinationfactory
: Class used to introspect specific service properties or metadata (default
is org.granite.generator.as3.DefaultRemoteDestinationFactory
).
transformer
: Class used to control the generation process (very advanced use).
externalizelong
: should we write AS3 Long
variables
(see Big Number Implementations). Default is false
.
externalizebiginteger
: should we write AS3 BigInteger
variables
(see Big Number Implementations). Default is false
.
externalizebigdecimal
: should we write AS3 BigDecimal
variables
(see Big Number Implementations). Default is false
.
For example:
<target name="generate.as3">
<gas3
outputdir="as3"
baseoutputdir="base_as3"
uid="myUidFieldName"
entitytemplate="/myEntityTemplate.gsp"
entitybasetemplate="/myEntityBaseTemplate.gsp"
interfacetemplate="/myInterfaceTemplate.gsp"
beantemplate="/myBeanTemplate.gsp"
beanbasetemplate="/myBeanBaseTemplate.gsp"
enumtemplate="/myEnumTemplate.gsp"
remotetemplate="/myRemoteTemplate.gsp"
remotebasetemplate="/myRemoteBaseTemplate.gsp"
tide="true"
as3typefactory="path.to.MyAs3TypeFactory"
entityfactory="path.to.MyEntityFactory"
remotedestinationfactory="path.to.MyRDFactory"
transformer="path.to.MyTransformer"
externalizelong="true"
externalizebiginteger="true"
externalizebigdecimal="true">
<classpath>
<pathelement location="classes"/>
</classpath>
<fileset dir="classes">
<include name="test/granite/ejb3/entity/**/*.class"/>
</fileset>
</gas3>
</target>
Note that when using a custom as3typefactory
, entityfactory
, remotedestinationfactory
or transformer
attribute, you must configure the classpath in order to make your custom classes available to the Gas3 engine;
either use the classpath attribute in the taskdef
declaration or in the gas3
call.
Gas3 templates are based on a specific implementation of . As such, all Groovy template documentation should apply.
The reason why the standard Groovy implementation was not used was the lack of comments support (<%-- ... --%>
)
and some formatting issues, especially with platform specific carriage returns. This may now be fixed but it was not at that time.
While the language itself is already documented on Groovy site, there are two specific bindings (ie. global variables used in Gas3 templates) that should be referenced.
Template execution is a two-phase process. First, the template is transformed to a standard Groovy script (mainly with expressions
like print(...));
second, the Groovy script is compiled and executed. Of course, the result of the first transformation
and the compiled script is cached, so further executions with the same template are much faster.
There are two bindings available in Gas3 templates:
Name | Type | Description |
---|---|---|
gVersion | String | Version number of the generator, e.g., "2.0.0" |
jClass | Implementation of the JavaType interface | An object describing the Java class for which the generator is writting an ActionScript 3 class |
fAttributes | Map<String, String> | A map which contains key-value pairs, as specified in include patterns (Eclipse builder only) |
JavaType implementations (passed as the jClass
parameter) can be of the following types:
Type | Description |
---|---|
JavaEntityBean |
A class defining a JPA entity bean (ie. a class annotated with a @Entity or with a
@MappedSuperclass persistence annotation)
|
JavaEnum |
A class defining a Java enum class
|
JavaInterface | A class defining a Java interface |
JavaRemoteDestination |
A class defining a remote service annotated with @RemoteDestination
|
JavaBean | A class defining all other Java classes |
The two bindings gVersion
and jClass
can be used in your templates as any
other Groovy script variables. For example:
// Generated by Gas3 v.${gVersion}. package ${jClass.as3Type.packageName} { public class ${jClass.as3Type.name} { ... } }
If you execute this template with Gas3 version 2.3.2 and a Java class named com.myapp.MyClass
, the output will be:
// Generated by Gas3 v.2.3.2. package com.myapp { public class MyClass { ... } }
If you plan to write custom templates, you should have a look at the standard GraniteDS templates and API documentation of the JavaType
implementations listed above.
Let's have a look to the standard GraniteDS template for Java interfaces. You may also see all templates :
<%--
GRANITE DATA SERVICES
Copyright (C) 2007-2008 ADEQUATE SYSTEMS SARL
This file is part of Granite Data Services.
Granite Data Services is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 3 of the License, or (at your
option) any later version.
Granite Data Services is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
for more details.
You should have received a copy of the GNU Lesser General Public License
along with this library; if not, see <http://www.gnu.org/licenses/>.
--%><%
Set as3Imports = new TreeSet();
for (jImport in jClass.imports) {
if (jImport.as3Type.hasPackage() &&
jImport.as3Type.packageName != jClass.as3Type.packageName)
as3Imports.add(jImport.as3Type.qualifiedName);
}
%>/**
* Generated by Gas3 v${gVersion} (Granite Data Services).
*
* WARNING: DO NOT CHANGE THIS FILE. IT MAY BE OVERWRITTEN EACH TIME YOU USE
* THE GENERATOR. INSTEAD, EDIT THE INHERITED INTERFACE (${jClass.as3Type.name}.as).
*/
package ${jClass.as3Type.packageName} {<%
///////////////////////////////////////////////////////////////////////////
// Write Import Statements.
if (as3Imports.size() > 0) {%>
<%
}
for (as3Import in as3Imports) {%>
import ${as3Import};<%
}
///////////////////////////////////////////////////////////////////////////
// Write Interface Declaration.%>
public interface ${jClass.as3Type.name}Base<%
if (jClass.hasSuperInterfaces()) {
%> extends <%
boolean first = true;
for (jInterface in jClass.superInterfaces) {
if (first) {
first = false;
} else {
%>, <%
}
%>${jInterface.as3Type.name}<%
}
}
%> {<%
///////////////////////////////////////////////////////////////////////////
// Write Public Getter/Setter.
for (jProperty in jClass.properties) {
if (jProperty.readable || jProperty.writable) {%>
<%
if (jProperty.writable) {%>
function set ${jProperty.name}(value:${jProperty.as3Type.name}):void;<%
}
if (jProperty.readable) {%>
function get ${jProperty.name}():${jProperty.as3Type.name};<%
}
}
}%>
}
}
The code for this template is rather simple, but it can be very tricky to distinguish between JSP-like expressions, Groovy code, and outputted ActionScript 3 code.
The first block, enclosed with <%-- --%>
, is a template comment: it will be completely ignored at transformation
(Groovy template to Groovy script) time.
The second block, enclosed with <% %>
, is plain Groovy code and will be outputed as is at transformation time.
Its purpose is to collect and sort all references to other classes so we can later write ActionScript 3 import
statements.
Then, the ActionsScript 3 code template really begins with a comment (Gas3 version and warning) and is followed by a package and interface definition with superinterfaces, if any, and finally, by a loop over interface getters/setters. Note that comments like:
/////////////////////////////////////////////////////////////////////////// // Write Import Statements.
... are Groovy script comments. They will be in the Groovy script but you will not find them in the outputted ActionScript3 file.
After the first transformation (Groovy template to Groovy script), the rendered code will be as follows:
Set as3Imports = new TreeSet();
for (jImport in jClass.imports) {
if (jImport.as3Type.hasPackage() &&
jImport.as3Type.packageName != jClass.as3Type.packageName)
as3Imports.add(jImport.as3Type.qualifiedName);
}
print("/**\n");
print(" * Generated by Gas3 v${gVersion} (Granite Data Services).\n");
print(" *\n");
print(" * WARNING: DO NOT CHANGE THIS FILE. IT MAY BE OVERWRITTEN EACH TIME YOU USE\n");
print(" * THE GENERATOR. INSTEAD, EDIT THE INHERITED INTERFACE
(${jClass.as3Type.name}.as).\n");
print(" */\n");
print("\n");
print("package ${jClass.as3Type.packageName} {");
///////////////////////////////////////////////////////////////////////////
// Write Import Statements.
if (as3Imports.size() > 0) {
print("\n");
}
for (as3Import in as3Imports) {
print("\n");
print(" import ${as3Import};");
}
///////////////////////////////////////////////////////////////////////////
// Write Interface Declaration.
print("\n");
print("\n");
print(" public interface ${jClass.as3Type.name}Base");
if (jClass.hasSuperInterfaces()) {
print(" extends ");
boolean first = true;
for (jInterface in jClass.superInterfaces) {
if (first) {
first = false;
} else {
print(", ");
}
print("${jInterface.as3Type.name}");
}
}
print(" {");
///////////////////////////////////////////////////////////////////////////
// Write Public Getter/Setter.
for (jProperty in jClass.properties) {
if (jProperty.readable || jProperty.writable) {
print("\n");
if (jProperty.writable) {
print("\n");
print(" function set ${jProperty.name}(value:${jProperty.as3Type.name}):void;");
}
if (jProperty.readable) {
print("\n");
print(" function get ${jProperty.name}():${jProperty.as3Type.name};");
}
}
}
print("\n");
print(" }\n");
print("}");
</programlisting>
<para>
As you can notice, <literal>${...}</literal> expressions are resolved by the Groovy engine rather than the JSP-like engine.
It would have been possible to use expressions like <literal><%= ... %></literal>, that will result in a script where:
</para>
<programlisting role="JAVA">
print("package ${jClass.as3Type.packageName} {");
</programlisting>
<para>
.. would have been split into three lines:
</para>
<programlisting role="JAVA">
print("package ");
print(jClass.as3Type.packageName);
print(" {");
</programlisting>
<para>
This is just informative, as it does not change anything in the final result.
</para>
<para>
Then, for this Java source code:
</para>
<programlisting role="JAVA">
package com.myapp.entity.types;
public interface NamedEntity {
public String getFirstName();
public void setFirstName(String firstName);
public String getLastName();
public void setLastName(String lastName);
public String getFullName();
}
... you will get this output:
/** * Generated by Gas3 v2.2.0 (Granite Data Services). * * WARNING: DO NOT CHANGE THIS FILE. IT MAY BE OVERWRITTEN EACH TIME YOU USE * THE GENERATOR. INSTEAD, EDIT THE INHERITED INTERFACE (NamedEntity.as). */ package com.myapp.entity.types { public interface NamedEntityBase { function set firstName(value:String):void; function get firstName():String; function get fullName():String; function set lastName(value:String):void; function get lastName():String; } }
Because of the two transformation steps of the template (Groovy template to Groovy script source, then Groovy script source to pre-compiled Groovy script), there are two possible sources of error:
<%
expression.
now TreeSet()
; instead of new TreeSet();
However, since Groovy is an interpreted language, you may get some other errors at execution time:
jClass.neme
instead of jClass.name
.
Whenever these kinds of errors occur, you'll find comprehensive error log in your Shell or Eclipse console. Note that when the error occurs after the first transformation, the Groovy script is printed with line numbers, as well as the Groovy compiler message. It is easy to find the erroneous line in the printed Groovy script, but you have to figure out the corresponding line in the original template:
[gas3] Generating: /dev/workspace/graniteds_ejb3/as3/com/myapp/entity/types/NamedEntityBase.as (output file is outdated) [gas3] org.granite.generator.exception.TemplateCompilationException:Could not compile template: /interfaceBase.gsp [gas3] 1 | [gas3] 2 | Set as3Imports = now TreeSet(); [gas3] 3 | [gas3] 4 | for (jImport in jClass.imports) { [gas3] 5 | if (jImport.as3Type.hasPackage() && jImport.as3Type.packageName != jClass.as3Type.packageName) [gas3] 6 | as3Imports.add(jImport.as3Type.qualifiedName); [gas3] 7 | } [gas3] 8 | [gas3] 9 | [gas3] 10 | print("/**\n"); [gas3] 11 | print(" * Generated by Gas3 v${gVersion} (Granite Data Services).\n"); [gas3] 12 | print(" *\n"); [gas3] 13 | print(" * WARNING: DO NOT CHANGE THIS FILE. IT MAY BE OVERWRITTEN EACH TIME YOU USE\n"); [gas3] 14 | print(" * THE GENERATOR. INSTEAD, EDIT THE INHERITED INTERFACE (${jClass.as3Type.name}.as).\n"); [gas3] 15 | print(" */\n"); [gas3] 16 | print("\n"); [gas3] 17 | print("package ${jClass.as3Type.packageName} {"); [gas3] 18 | [gas3] 19 | [gas3] 20 | /////////////////////////////////////////////////////////////////////////// [gas3] 21 | // Write Import Statements. [gas3] 22 | [gas3] 23 | if (as3Imports.size() > 0) { [gas3] 24 | print("\n"); [gas3] 25 | [gas3] 26 | } [gas3] 27 | for (as3Import in as3Imports) { [gas3] 28 | print("\n"); [gas3] 29 | print(" import ${as3Import};"); [gas3] 30 | [gas3] 31 | } [gas3] 32 | [gas3] 33 | /////////////////////////////////////////////////////////////////////////// [gas3] 34 | // Write Interface Declaration. [gas3] 35 | print("\n"); [gas3] 36 | print("\n"); [gas3] 37 | print(" public interface ${jClass.as3Type.name}Base"); [gas3] 38 | [gas3] 39 | [gas3] 40 | if (jClass.hasSuperInterfaces()) { [gas3] 41 | [gas3] 42 | print(" extends "); [gas3] 43 | [gas3] 44 | boolean first = true; [gas3] 45 | for (jInterface in jClass.superInterfaces) { [gas3] 46 | if (first) { [gas3] 47 | first = false; [gas3] 48 | } else { [gas3] 49 | [gas3] 50 | print(", "); [gas3] 51 | [gas3] 52 | } [gas3] 53 | [gas3] 54 | print("${jInterface.as3Type.name}"); [gas3] 55 | [gas3] 56 | } [gas3] 57 | } [gas3] 58 | [gas3] 59 | [gas3] 60 | print(" {"); [gas3] 61 | [gas3] 62 | [gas3] 63 | /////////////////////////////////////////////////////////////////////////// [gas3] 64 | // Write Public Getter/Setter. [gas3] 65 | [gas3] 66 | for (jProperty in jClass.properties) { [gas3] 67 | [gas3] 68 | if (jProperty.readable || jProperty.writable) { [gas3] 69 | print("\n"); [gas3] 70 | [gas3] 71 | if (jProperty.writable) { [gas3] 72 | print("\n"); [gas3] 73 | print(" function set ${jProperty.name}(value:${jProperty.as3Type.name}):void;"); [gas3] 74 | [gas3] 75 | } [gas3] 76 | if (jProperty.readable) { [gas3] 77 | print("\n"); [gas3] 78 | print(" function get ${jProperty.name}():${jProperty.as3Type.name};"); [gas3] 79 | [gas3] 80 | } [gas3] 81 | } [gas3] 82 | } [gas3] 83 | print("\n"); [gas3] 84 | print(" }\n"); [gas3] 85 | print("}"); [gas3] [gas3] at org.granite.generator.gsp.GroovyTemplate.compile(GroovyTemplate.java:143) [gas3] at org.granite.generator.gsp.GroovyTemplate.execute(GroovyTemplate.java:157) [gas3] at org.granite.generator.as3.JavaAs3GroovyTransformer.generate(JavaAs3GroovyTransformer.java:119) [gas3] at org.granite.generator.as3.JavaAs3GroovyTransformer.generate(JavaAs3GroovyTransformer.java:1) [gas3] at org.granite.generator.Transformer.generate(Transformer.java:71) [gas3] at org.granite.generator.Generator.generate(Generator.java:83) [gas3] at org.granite.generator.ant.AntJavaAs3Task.execute(AntJavaAs3Task.java:327) [gas3] at org.apache.tools.ant.UnknownElement.execute(UnknownElement.java:288) [gas3] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) [gas3] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) [gas3] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) [gas3] at java.lang.reflect.Method.invoke(Method.java:597) [gas3] at org.apache.tools.ant.dispatch.DispatchUtils.execute(DispatchUtils.java:105) [gas3] at org.apache.tools.ant.Task.perform(Task.java:348) [gas3] at org.apache.tools.ant.Target.execute(Target.java:357) [gas3] at org.apache.tools.ant.Target.performTasks(Target.java:385) [gas3] at org.apache.tools.ant.Project.executeSortedTargets(Project.java:1329) [gas3] at org.apache.tools.ant.Project.executeTarget(Project.java:1298) [gas3] at org.apache.tools.ant.helper.DefaultExecutor.executeTargets(DefaultExecutor.java:41) [gas3] at org.eclipse.ant.internal.ui.antsupport.EclipseDefaultExecutor.executeTargets(EclipseDefaultExecutor.java:32) [gas3] at org.apache.tools.ant.Project.executeTargets(Project.java:1181) [gas3] at org.eclipse.ant.internal.ui.antsupport.InternalAntRunner.run(InternalAntRunner.java:423) [gas3] at org.eclipse.ant.internal.ui.antsupport.InternalAntRunner.main(InternalAntRunner.java:137) [gas3] Caused by: org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed,Script1.groovy: 2: expecting EOF, found 'TreeSet' @ line 2, column 26. [gas3] 1 error
The error at line 2, column 26 is:
[gas3] 2 | Set as3Imports = now TreeSet();
Finding the corresponding line in the original template should be straightforward.
Granite Data Services provides a messaging feature, code name Gravity, implemented as a -like service with AMF3 data polling over HTTP (producer/consumer based architecture). This implementation is freely based on the protocol specification (1.0draft1 at this time) and adapted from the Jetty 6.1.x implementation of a comet server.
For a basic sample of GDS/Gravity, download graniteds-***.zip
and import the examples/graniteds_chat
as a new project in Eclipse.
GraniteDS messaging relies on two main AS3 components on the Flex side: org.granite.gravity.Consumer
and org.granite.gravity.Producer
. These classes reproduce almost exactly the original Adobe Flex
and with the specific
internal implementation of GraniteDS.
The only differences are that you must use topic
instead of subtopic
due to a change introduced in Flex 3.
Here is a quick example of GDS Consumer
/Producer
usage:
... import org.granite.gravity.Consumer; import org.granite.gravity.Producer; ... private var consumer:Consumer = null; private var producer:Producer = null; private function connect():void { consumer = new Consumer(); consumer.destination = "gravity"; consumer.topic = "discussion"; consumer.subscribe(); consumer.addEventListener(MessageEvent.MESSAGE, messageHandler); producer = new Producer(); producer.destination = "gravity"; producer.topic = "discussion"; } private function disconnect():void { consumer.unsubscribe(); consumer.disconnect(); consumer = null; producer.disconnect(); producer = null; } private function messageHandler(event:MessageEvent):void { var msg:AsyncMessage = event.message as AsyncMessage; trace("Received message: " + (msg.body as String)); } private function send(message:String):void { var msg:AsyncMessage = new AsyncMessage(); msg.body = message; producer.send(msg); } ...
In this code, the producer sends String
messages, which could of course be of any type, and the producer receives
String
messages as well. These Strings
are sent in AsyncMessage
envelopes,
which is the only envelope type allowed in GDS.
var producer:Producer = new Producer(); producer.destination = "quotes"; producer.topic = "/germany"; producer.send(message); var consumerGermany:Consumer = new Consumer(); consumerGermany.destination = "quotes"; consumerGermany.topic = "/germany"; consumerGermany.subscribe(); var consumerFrance:Consumer = new Consumer(); consumerFrance.destination = "quotes"; consumerFrance.topic = "/france"; consumerFrance.subscribe();
A consumer can specify its message selector before it subscribes to the destination:
var consumerFrance:Consumer = new Consumer(); consumerFrance.destination = "quotes"; consumerFrance.selector = "COUNTRY = 'France'"; consumerFrance.subscribe();
There are three main steps to configure Gravity in an application:
<web-app version="2.4" ...>
...
<listener>
<listener-class>org.granite.config.GraniteConfigListener</listener-class>
</listener>
<servlet>
<servlet-name>GravityServlet</servlet-name>
<servlet-class>org.granite.gravity.tomcat.GravityTomcatServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>GravityServlet</servlet-name>
<url-pattern>/gravityamf/*</url-pattern>
</servlet-mapping>
...
</web-app>
<services-config> <services> <service id="messaging-service" class="flex.messaging.services.MessagingService" messageTypes="flex.messaging.messages.AsyncMessage"> <adapters> <adapter-definition id="default" class="org.granite.gravity.adapters.SimpleServiceAdapter" default="true"/> </adapters> <destination id="topic"> <channels> <channel ref="my-gravityamf"/> </channels> </destination> </service> </services> <channels> <channel-definition id="my-gravityamf" class="org.granite.gravity.channels.GravityChannel"> <endpoint uri="http://{server.name}:{server.port}/{context.root}/gravityamf/amf" class="flex.messaging.endpoints.AMFEndpoint"/> </channel-definition> </channels> </services-config>
Here, we define a GravityChannel
(my-gravityamf
) and we use it in the destination named topic
.
See above destination usage in Consumer/Producer usage.
The topic we have defined uses the default Gravity adapter SimpleServiceAdapter
that is a simple fast in-memory message bus. If you need
more advanced features such as persistent messages or clustering, you should consider using a dedicated messaging implementation such as
.
The simple adapter exposes two configuration properties:
no-local
: default is true
, if set to false
the client producing messages will receive their
own messages
session-selector
: this is an advanced option and instructs Gravity to store the message selector string in the user session. This allows
the server part of the application to override the selector string defined by the Flex Consumer
.
The selector is stored and read from the session attribute named org.granite.gravity.selector.{destinationId}
.
Here is the table of the supported implementations:
Application server | Servlet class | Specific notes |
---|---|---|
Tomcat 6.0.18+ | org.granite.gravity.tomcat.GravityTomcatServlet | Only with APR/NIO enabled (APR highly recommended) |
JBoss 4.2.x | org.granite.gravity.tomcat.GravityTomcatServlet | APR/NIO, disable CommonHeadersFilter |
Jetty 6.1.x | org.granite.gravity.jetty.GravityJettyServlet | Jetty 7 not supported, Jetty 8 using Servlet 3 API |
JBoss 5+ | org.granite.gravity.jbossweb.GravityJBossWebServlet | Only with APR/NIO enabled (APR highly recommended) |
WebLogic 9.1+ | org.granite.gravity.weblogic.GravityWebLogicServlet | See WebLogic documentation for configuration tuning |
GlassFish 3.x | org.granite.gravity.async.GravityAsyncServlet | Using Servlet 3.0 |
Tomcat 7.x / Jetty 8.x | org.granite.gravity.async.GravityAsyncServlet | Using Servlet 3.0 |
Any other | org.granite.gravity.generic.GravityGenericServlet | Using blocking I/O (no asynchronous support) |
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC "-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
<gravity
factory="org.granite.gravity.DefaultGravityFactory"
channel-idle-timeout-millis="1800000"
long-polling-timeout-millis="20000"
reconnect-interval-millis="30000"
reconnect-max-attempts="60">
<thread-pool
core-pool-size="5"
maximum-pool-size="20"
keep-alive-time-millis="10000"
queue-capacity="2147483647" />
</gravity>
</granite-config>
This <gravity> section is purely optional and you may omit it if you accept default values.
Some explanations about these options:
channel-idle-timeout-millis
: the elapsed time after which an idle channel (pure producer or dead client)
may be silently unsubcribed and removed by Gravity. Default is 30 minutes.
long-polling-timeout-millis
: the elapsed time after which an idle connect request is closed, asking the client to reconnect.
Default is 20 seconds. Note that setting this value isn't supported in Tomcat/APR configurations.
thread-pool
attributes: all options are standard parameters for the Gravity ThreadPoolExecutor
instance.
All other configuration options are for advanced use only and you should keep default values.
At least for now, APR is the easiest to configure and the most reliable.
To configure APR, see documentation .
On Windows®, it's simply a matter of downloading a native and putting it
in your WINDOWS/system32
directory – while other and better configurations are possible.
For more recent versions of Tomcat such as the one embedded in JBoss 5 or 6, you will need the latest APR library,
see here.
For JBoss 4.2.*, you must comment out a specific filter in the default global web.xml
(<JBOSS_HOME>/server/default/deploy/jboss-web.deployer/conf/web.xml
):
...
<!-- Comment this out!
<filter>
<filter-name>CommonHeadersFilter</filter-name>
<filter-class>org.jboss.web.tomcat.filters.ReplyHeaderFilter</filter-class>
<init-param>
<param-name>X-Powered-By</param-name>
<param-value>...</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CommonHeadersFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
-->
...
See above for Tomcat configuration.
For JBoss 5+ servers, you must use a specific servlet. JBoss 5 implements its own version of Tomcat, named JBossWeb:
<web-app version="2.4" ...>
...
<servlet>
<servlet-name>GravityServlet</servlet-name>
<servlet-class>org.granite.gravity.jbossweb.GravityJBossWebServlet</servlet-class>
... (see Tomcat configuration above for options)
</servlet>
...
</web-app>
Note that you do not need to comment out the CommonHeadersFilter
with JBoss 5, but you still need to enable APR.
Here is a sample configuration for a default JBoss installation with a brief description of the different options:
<adapters>
<adapter-definition id="jms" class="org.granite.gravity.adapters.JMSServiceAdapter"/>
</adapters>
<destination id="chat-jms">
<properties>
<jms>
<destination-type>Topic</destination-type>
<!-- Optional: forces usage of simple text messages
<message-type>javax.jms.TextMessage</message-type>
-->
<connection-factory>ConnectionFactory</connection-factory>
<destination-jndi-name>topic/testTopic</destination-jndi-name>
<destination-name>TestTopic</destination-name>
<acknowledge-mode>AUTO_ACKNOWLEDGE</acknowledge-mode>
<transacted-sessions>false</transacted-sessions>
<!-- Optional JNDI environment. Specify the external JNDI configuration to access
a remote JMS provider. Sample for a remote JBoss server.
-->
<initial-context-environment>
<property>
<name>Context.SECURITY_PRINCIPAL</name>
<value>guest</value>
</property>
<property>
<name>Context.SECURITY_CREDENTIALS</name>
<value>guest</value>
</property>
<property>
<name>Context.PROVIDER_URL</name>
<value>http://my.host.com:1099</value>
</property>
<property>
<name>Context.INITIAL_CONTEXT_FACTORY</name>
<value>org.jnp.interfaces.NamingContextFactory</value>
</property>
<property>
<name>Context.URL_PKG_PREFIXES</name>
<value>org.jboss.naming:org.jnp.interfaces</value>
</property>
</initial-context-environment>
</jms>
...
</properties>
...
<adapter ref="jms"/>
</destination>
Comments on configuration options:
destination-type
must be Topic
for the moment. Queues may be supported later.
message-type
may be forced to simple text messages by specifying javax.jms.TextMessage
.
connection-factory
and destination-jndi-name
are the JNDI names respectively of the JMS ConnectionFactory
and of the JMS topic.
destination-name
is just a label but still required.
acknowledge-mode
can have the standard values accepted by any JMS provider: AUTO_ACKNOWLEDGE
,
CLIENT_ACKNOWLEDGE
, and DUPS_OK_ACKNOWLEDGE
.
transacted-sessions
allows the use of transactions in sessions when set to true
.
initial-context-environment
: The initial-context
parameters allow to access a remote JMS server
by setting the JNDI context options.
The JMS headers are always copied between Flex and JMS messages
Durable subscriptions are not yet supported
Here is a sample configuration:
<adapters>
<adapter-definition
id="activemq"
class="org.granite.gravity.adapters.ActiveMQServiceAdapter"/>
</adapters>
<destination id="chat-activemq">
<properties>
<jms>
<destination-type>Topic</destination-type>
<!-- Optional: forces usage of simple text messages
<message-type>javax.jms.TextMessage</message-type>
-->
<connection-factory>ConnectionFactory</connection-factory>
<destination-jndi-name>topic/testTopic</destination-jndi-name>
<destination-name>TestTopic</destination-name>
<acknowledge-mode>AUTO_ACKNOWLEDGE</acknowledge-mode>
<transacted-sessions>false</transacted-sessions>
</jms>
<server>
<durable>true</durable>
<file-store-root>/var/activemq/data</file-store-root>
<create-broker>true</create-broker>
<wait-for-start>false</wait-for-start>
</server>
</properties>
...
<adapter ref="activemq"/>
</destination>
Comments on configuration options:
<jms>...</jms>
) are identical to those used in the default JMS configuration.
See above.
durable
, if set to true
, allows for durable messages, stored in the filesystem.
The data store directory of ActiveMQ can be specified by the file-store-root
parameter.
create-broker
is optional, as well as the dependant wait-for-start
attribute.
When create-broker
is false
, creation of the broker is not automatic and has to be done by the application itself.
In this case, wait-for-start
set to true
tells the ActiveMQConnectionFactory
to wait for the effective creation of the broker. Please refer to the ActiveMQ documentation for more details on these options.
Gravity gravity = GravityManager.getGravity(servletContext);
AsyncMessage message = new AsyncMessage();
message.setDestination("my-gravity-destination");
message.setHeader(AsyncMessage.SUBTOPIC_HEADER, "my-topic");
message.setBody("Message content");
gravity.publishMessage(message);
Here is an example on an EJB3 sending a message:
@Stateless
@Local(Test.class)
public class TestBean implements Test {
@Resource
SessionContext ctx;
@Resource(mappedName="java:/ConnectionFactory")
ConnectionFactory jmsConnectionFactory;
@Resource(mappedName="topic/testTopic")
Topic jmsTopic;
public TestBean() {
super();
}
public void notifyClient(Object object) {
try {
Connection connection = jmsConnectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
javax.jms.Message jmsMessage = session.createObjectMessage(person);
MessageProducer producer = session.createProducer(jmsTopic);
producer.send(jmsMessage);
session.close();
connection.close();
}
catch (Exception e) {
log.error("Could not publish notification", e);
}
}
}
Here is an example on a Seam 2 component sending a message:
@Stateless
@Local(Test.class)
@Name("test")
public class TestBean implements Test {
private static Logger log = Logger.getLogger(TestBean.class.getName());
@In
private TopicPublisher testTopicPublisher;
@In
private TopicSession topicSession;
public void notifyClient(Serializable object) {
try {
testTopicPublisher.publish(topicSession.createObjectMessage(object));
}
catch (Exception e) {
log.error("Could not publish notification", e);
}
}
}
jms/destination-name
configuration parameter.
destination-name
parameter with the subtopic
.
Wildcards are supported in the subtopic
following Flex convention and are converted to the ActiveMQ format
(see ), meaning that toto.**
is converted to
toto.>
.
public class Test throws JMSException {
// adapterId should be the id of the JMS adapter as defined in services-config.xml
ConnectionFactory f = new ActiveMQConnectionFactory("vm://adapterId");
Connection connection = jmsConnectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
ActiveMQTopic activeMQTopic= new ActiveMQTopic("destination");
javax.jms.Message jmsMessage = session.createObjectMessage(person);
MessageProducer producer = session.createProducer(activeMQTopic);
producer.send(jmsMessage);
session.close();
connection.close();
}
Securing messaging destination is very similar to security remoting destinations (see here) and most concepts apply to messaging services as well as remoting services.
You can for example setup role-based security on a Gravity destination with the following definition in services-config.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<services-config>
<services>
<service id="messaging-service"
class="flex.messaging.services.MessagingService"
messageTypes="flex.messaging.messages.AsyncMessage">
<adapters>
<adapter-definition
id="default"
class="org.granite.gravity.adapters.SimpleServiceAdapter"
default="true"/>
</adapters>
<destination id="restrictedTopic">
<channels>
<channel ref="my-gravityamf"/>
</channels>
<security>
<security-constraint>
<auth-method>Custom</auth-method>
<roles>
<role>admin</role>
</roles>
</security-constraint>
</security>
</destination>
</service>
</services>
...
</services-config>
In this case, only users with the role admin
will be able to subscribe to the topic restrictedTopic
.
You may write and configure a specific GravityDestinationSecurizer
in order to add fine grained security checks for specific actions.
In particular you can control who can subscribe or publish messages to a particular topic.
public interface GravityDestinationSecurizer extends DestinationSecurizer {
public void canSubscribe(GravityInvocationContext context)
throws SecurityServiceException;
public void canPublish(GravityInvocationContext context)
throws SecurityServiceException;
}
You then have to tell GraniteDS where to use your securizer:
<services-config>
<services>
<service ...>
<destination id="restrictedDestination">
...
<properties>
<securizer>path.to.MyDestinationSecurizer</securizer>
</properties>
</destination>
</service>
</services>
...
</services-config>
EJB 3 are an important part of the . They provide a powerful framework for managing and securing enterprise services in an application server (session beans) as well as an powerful persistence and query language system (JPA).
GraniteDS provides access to EJB 3 services via either the RemoteObject
API or the Tide API for Session Beans methods calls,
and fully supports serialization of JPA entities from and to your Flex application, taking care of lazily loaded associations; both collections and proxies.
This support for JPA entity beans is covered in the section JPA and lazy initialization, so this section will only
describe how to call remotely stateless and stateful session beans from a Flex application. GraniteDS also integrates with container security for
authentication and role-based authorization.
For a basic example with GraniteDS and EJB 3 (stateless and stateful session beans, and entity beans) working together,
have a look to the graniteds_ejb3
example project in the examples
folder of the GraniteDS distribution
graniteds-***.zip
and import it as a new Eclipse project.
You may also have a look at the "Hello, world" Revisited tutorial for another basic example application using EJB 3 technologies together with Granite Eclipse Builder.
The Flex-side usage of the RemoteObject
API is completely independent of the server technology, so everything described in
the Remoting chapter applies for EJBs. This section will only describe the particular configuration
required in various use cases of EJB services.
Configuring remoting for EJB 3 services simply requires adding the org.granite.messaging.service.EjbServiceFactory
service factory in
services-config.xml
and specifying its JNDI lookup string property.
All remoting examples from the Remoting chapter apply for EJBs, here is a basic example:
public interface HelloService {
public String hello(String name);
}
@Stateless
@Local(HelloService.class)
@RemoteDestination(id="helloService")
public class HelloServiceBean implement HelloService {
public String hello(String name) {
return "Hello " + name;
}
}
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
import mx.rpc.events.ResultEvent;
import mx.rpc.events.FaultEvent;
import mx.controls.Alert;
public function resultHandler(event:ResultEvent):void {
// Display received message
outputMessage.text = event.result as String;
}
public function faultHandler(event:FaultEvent):void {
// Show error alert
Alert.show(event.fault.faultString);
}
</mx:Script>
<!-- Connect to a service destination.-->
<mx:RemoteObject id="helloService"
destination="helloService"
result="handleResult(event);"
fault="handleFault(event);"/>
<!-- Provide input data for calling the service. -->
<mx:TextInput id="inputName"/>
<!-- Call the EJB, use the text in a TextInput control as input data.-->
<mx:Button click="helloService.hello(inputName.text)"/>
<!-- Display results data in the user interface. -->
<mx:Label id="outputMessage"/>
</mx:Application>
The main part of the configuration is the factory
declaration in the file services-config.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<services-config>
<services>
<service
id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<destination id="personService">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>ejbFactory</factory>
</properties>
</destination>
</service>
</services>
<factories>
<factory id="ejbFactory" class="org.granite.messaging.service.EjbServiceFactory">
<properties>
<lookup>myapp.ear/{capitalized.destination.id}Bean/local</lookup>
</properties>
</factory>
</factories>
<channels>
<channel-definition id="my-graniteamf" class="mx.messaging.channels.AMFChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
</services-config>
Two elements are important in this configuration :
<ejb-local-ref>
<ejb-ref-name>myapp.ear/PeopleServiceBean</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<local-home/>
<local>com.myapp.service.PeopleService</local>
</ejb-local-ref>
<factory id="ejbFactory" class="org.granite.messaging.service.EjbServiceFactory">
<properties>
<lookup>java:comp/env/myapp.ear/{capitalized.destination.id}Bean</lookup>
</properties>
</factory>
Of course you can share the same factory with many EJB destinations.
<destination id="person">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>ejbFactory</factory>
</properties>
</destination>
<destination id="product">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>ejbFactory</factory>
</properties>
</destination>
The parameters generally depend on the remote application server. Please refer to the standard and to the documentation of your application server for more details.
...
<factory id="ejbFactory" class="org.granite.messaging.service.EjbServiceFactory">
<properties>
<lookup>myApp/{capitalized.destination.id}Bean/local</lookup>
<!-- InitialContext parameters -->
<initial-context-environment>
<property>
<name>Context.PROVIDER_URL</name>
<value>...</value>
</property>
<property>
<name>Context.INITIAL_CONTEXT_FACTORY</name>
<value>...</value>
</property>
<property>
<name>Context.URL_PKG_PREFIXES</name>
<value>...</value>
</property>
<property>
<name>Context.SECURITY_PRINCIPAL</name>
<value>...</value>
</property>
<property>
<name>Context.SECURITY_CREDENTIALS</name>
<value>...</value>
</property>
</initial-context-environment>
</properties>
</factory>
...
For JBoss Application Server for example this declaration looks like this:
...
<factory id="ejbFactory" class="org.granite.messaging.service.EjbServiceFactory">
<properties>
<lookup>myApp/{capitalized.destination.id}Bean/local</lookup>
<!-- InitialContext parameters -->
<initial-context-environment>
<property>
<name>Context.PROVIDER_URL</name>
<value>jnp://remotehostname:1099</value>
</property>
<property>
<name>Context.INITIAL_CONTEXT_FACTORY</name>
<value>org.jnp.interfaces.NamingContextFactory</value>
</property>
<property>
<name>Context.URL_PKG_PREFIXES</name>
<value>org.jboss.naming:org.jnp.interfaces</value>
</property>
</initial-context-environment>
</properties>
</factory>
...
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config scan="true">
...
</granite-config>
@Stateless
@Local(PersonService.class)
@RemoteDestination(id="person", securityRoles={"user","admin"})
public class PersonServiceBean implements PersonService {
...
}
The @RemoteDestination
annotation additionally supports the following attributes:
id
is mandatory and is the destination name
service
is optional if there is only one service for RemotingMessage
defined in services-config.xml
.
Otherwise this should be the name of the service.
channel
is optional if there is only one channel defined in services-config.xml
.
Otherwise this should be the id of the target channel.
channels
may be used instead of channel to define a failover channel.
factory
is optional if there is only one factory in services-config.xml
. Otherwise this should be the factory id.
securityRoles
is an array of role names for securing the destination.
<?xml version="1.0" encoding="UTF-8"?>
<services-config>
<services>
<service
id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<!-- no need to declare destinations here -->
</service>
</services>
<factories>
<factory id="ejbFactory" class="org.granite.messaging.service.EjbServiceFactory">
<properties>
<lookup>myApp/{capitalized.destination.id}Bean/local</lookup>
</properties>
</factory>
</factories>
<channels>
<channel-definition id="my-graniteamf" class="mx.messaging.channels.AMFChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
</services-config>
As the destinations are not defined in services-config.xml
any more, you will have to setup the RemoteObject
endpoint manually in ActionScript (see here for details).
package com.myapp.services;
import javax.ejb.Local;
import javax.ejb.Remove;
import javax.ejb.Stateful;
@Stateful
@Local(PositionService.class)
public class PositionServiceBean implements PositionService {
int x = 300;
public int getX() {
return x;
}
public void saveX(int x) {
this.x = x;
}
@Remove
public void remove() {
}
}
<destination id="position">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>ejbFactory</factory>
<!-- Specific for stateful beans -->
<ejb-stateful>
<remove-method>
<signature>remove</signature>
<retain-if-exception>false</retain-if-exception>
</remove-method>
</ejb-stateful>
</properties>
</destination>
The inner remove-method
node contains information about the remove()
methods of your stateful bean:
signature
: This is the name of the method, optionally followed by a parameter list.
If your remove()
method has arguments, the signature should follow the conventions used in java.lang.reflect.Method.toString()
.
For example, with the following remove()
method:
@Remove public int remove(boolean arg1, Integer arg2, String[] arg3) {...}... you should write this signature:
<signature>remove(boolean,java.lang.Integer,java.lang.String[])</signature>
retain-if-exception
(optional): This is the equivalent of the @Remove
annotation attribute;
the default is false
.
You may of course add multiple remove-method
nodes in the same ejb-stateful
node if necessary.
You can easily protect access to your EJB destinations with destination-based security.
Please refer to the security chapter.
GraniteDS will then pass the user credentials from the Flex RemoteObject
to the EJB security context, making possible
to use role-based authorization with the EJB destination.
Here is an example configuration in services-config.xml
:
<destination id="personService">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>ejbFactory</factory>
</properties>
<security>
<security-constraint>
<auth-method>Custom</auth-method>
<roles>
<role>user</role>
<role>admin</role>
</roles>
</security-constraint>
</security>
</destination>
@Stateless
@Local(PersonService.class)
public class PersonServiceBean implements PersonService {
@PersistenceContext
protected EntityManager manager;
public List<Person> findAllPersons() {
return manager.createQuery("select distinct p from Person p").getResultList();
}
@RolesAllowed({"admin"})
public Person createPerson(Person person) {
return manager.merge(person);
}
@RolesAllowed({"admin"})
public Person modifyPerson(Person person) {
return manager.merge(person);
}
@RolesAllowed({"admin"})
public void deletePerson(Person person) {
person = manager.find(Person.class, person.getId());
manager.remove(person);
}
}
With this configuration, only authenticated users having either the user
or admin
roles will be able to
call the EJB remotely from Flex. Then the EJB container will enforce the particular access on each method due to the @RolesAllowed
annotation and may throw a EJBAccessException
.
Most of what is described in the Tide Remoting section applies for EJB 3, however GraniteDS also provides an improved integration with EJB 3 services.
There are a few noticeable differences in the configuration in this case.
tide-annotations
section of granite-config.xml
the conditions
used to enable remote access to EJB destinations (for example all EJBs annotated with a particular annotation).
org.granite.tide.ejb.EjbServiceFactory
service factory in services-config.xml
.
ejb
in services-config.xml
Ejb.getInstance().getEjbContext()
instead of Tide.getInstance().getContext()
.
Here is a default configuration suitable for most cases:
<granite-config scan="true">
...
<tide-components>
<tide-component annotated-with="org.granite.messaging.service.annotations.RemoteDestination"/>
</tide-components>
</granite-config>
<services-config>
<services>
<service id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<!--
! Use "tideEjbFactory" and "my-graniteamf" for "ejb" destination (see below).
! The destination must be "ejb" when using Tide with default configuration.
!-->
<destination id="ejb">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>tideEjbFactory</factory>
<entity-manager-factory-jndi-name>java:/DefaultEMF</entity-manager-factory-jndi-name>
</properties>
</destination>
</service>
</services>
<!--
! Declare tideEjbFactory service factory.
!-->
<factories>
<factory id="tideEjbFactory" class="org.granite.tide.ejb.EjbServiceFactory">
<properties>
<lookup>myapp.ear/{capitalized.component.name}Bean/local</lookup>
</properties>
</factory>
</factories>
<!--
! Declare my-graniteamf channel.
!-->
<channels>
<channel-definition id="my-graniteamf" class="mx.messaging.channels.AMFChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
</services-config>
The destination named ejb
will be the one and only destination required for all EJB destinations.
<ejb-local-ref>
<ejb-ref-name>myapp/PeopleServiceBean</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<local-home/>
<local>com.myapp.service.PeopleService</local>
</ejb-local-ref>
<factory id="tideEjbFactory" class="org.granite.tide.ejb.EjbServiceFactory">
<properties>
<lookup>java:comp/env/myapp/{capitalized.component.name}Bean</lookup>
</properties>
</factory>
<persistence-unit name="ejb-pu">
...
<properties>
...
<property name="jboss.entity.manager.factory.jndi.name" value="java:/DefaultEMF"/>
<property name="jboss.entity.manager.jndi.name" value="java:/DefaultEM"/>
</properties>
</persistence-unit>
<persistence-unit-ref>
<persistence-unit-ref-name>ejb-pu</persistence-unit-ref-name>
</persistence-unit-ref>
<destination id="ejb">
<channels>
<channel ref="graniteamf"/>
</channels>
<properties>
<factory>tideEjbFactory</factory>
<entity-manager-factory-jndi-name>java:comp/env/ejb-pu</entity-manager-factory-jndi-name>
</properties>
</destination>
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="Ejb.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.ejb.Ejb;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;
[In]
public var helloService:Component;
private function hello(name:String):void {
helloService.hello(name, resultHandler, faultHandler);
}
private function resultHandler(event:TideResultEvent):void {
outputMessage.text = event.result as String;
}
private function faultHandler(event:TideFaultEvent):void {
// Handle fault
}
</mx:Script>
<!-- Provide input data for calling the service. -->
<mx:TextInput id="inputName"/>
<!-- Call the web service, use the text in a TextInput control as input data.-->
<mx:Button click="hello(inputName.text)"/>
<!-- Result message. -->
<mx:Label id="outputMessage"/>
</mx:Application>
This is almost identical to the standard Tide API described in the Tide remoting section, and all other methods apply for EJB.
You can benefit from the capability of the Gas3 code generator (see here) to generate a strongly typed
ActionScript 3 client proxy from the EJB interface when it is annotated with @RemoteDestination
.
In this case, you can inject a typesafe reference to your service and get better compile time error checking and auto completion in your IDE:
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="Ejb.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.ejb.Ejb;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;
import com.myapp.service.HelloService;
[In]
public var helloService:HelloService;
private function hello(name:String):void {
helloService.hello(name, resultHandler, faultHandler);
}
...
</mx:Script>
...
</mx:Application>
private var tideContext:Context = Ejb.getInstance().getEjbContext(); public function login(username:String, password:String):void { tideContext.identity.login(username, password, loginResult, loginFault); } private function loginResult(event:TideResultEvent):void { Alert.show(event.context.identity.loggedIn); } private function loginFault(event:TideFaultEvent):void { Alert.show(event.fault); } public function logout():void { tideContext.identity.logout(); }
[In] public var identity:Identity; public function login(username:String, password:String):void { identity.login(username, password, loginResult, loginFault); } private function loginResult(event:TideResultEvent):void { Alert.show(event.context.identity.loggedIn); } private function loginFault(event:TideFaultEvent):void { Alert.show(event.fault); } public function logout():void { identity.logout(); }
<mx:ViewStack id="main" selectedIndex="{identity.loggedIn ? 1 : 0}">
<views:LoginView id="loginView"/>
<views:MainView id="mainView"/>
</mx:ViewStack>
<mx:Button id="deleteButton"
label="Delete"
enabled="{identity.hasRole('admin')}"
click="myService.deleteEntity(myEntity)"/>
This can also be used as any remote class with result and fault handlers:
public function checkRole(role:String):void { identity.hasRole(role, checkRoleResult, checkRoleFault); } private function checkRoleResult(event:TideResultEvent, role:String):void { if (role == 'admin') { if (event.result) trace("User has admin role"); else trace("User does not have admin role"); } }
The is one of the most popular Java enterprise frameworks. It integrates on a common platform all the necessary services for building enterprise applications: persistence, transactions, security...
GraniteDS provides out-of-the-box integration with Spring 2.5+ and 3.0+ via either the RemoteObject
API or the Tide API
to remotely call Spring services, and fully supports serialization of JPA entities from and to your Flex application, taking care of lazily loaded associations.
The support for JPA entity beans is covered in the section JPA and lazy initialization, so this section will only
describe how to call Spring beans from a Flex application. GraniteDS also fully supports Acegi Security / Spring Security 2.x / Spring Security 3.x.
The support for Spring is included in the library granite-spring.jar
, so you always have to include this library in either
WEB-INF/lib
or lib
for an ear packaging.
Note that to provide a more native experience for Spring developers, the Spring support in GraniteDS can be configured directly in the Spring configuration
files (applicationContext.xml
). Most features of GraniteDS can be configured this way, and it is still possible to fall back to the
default GraniteDS configuration files services-config.xml
and granite-config.xml
for unsupported features.
For a basic example with GraniteDS and Spring working together,
have a look to the graniteds_spring
example project in the examples
folder of the GraniteDS distribution
graniteds-***.zip
and import it as a new Eclipse project.
<!-- Path to Spring config file -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/conf/application-context.xml
</param-value>
</context-param>
<!-- Spring application listener -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Spring listener for web-scopes (request, session) -->
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
<!-- Spring MVC dispatcher servlet for AMF remoting requests -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/graniteamf/*</url-pattern>
</servlet-mapping>
You also have to add an empty file WEB-INF/dispatcher-servlet.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd>
</beans>
The Flex-side usage of the RemoteObject
API is completely independent of the server technology, so everything described in
the Remoting chapter applies for Spring beans. This section will only describe the particular configuration
required in various use cases of Spring services.
All remoting examples from the Remoting chapter apply for Spring beans, here is a basic example with an annotated Spring service:
public interface HelloService {
public String hello(String name);
}
@Service("helloService")
@RemoteDestination(id="helloService", source="helloService")
public class HelloServiceImpl implement HelloService {
public String hello(String name) {
return "Hello " + name;
}
}
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
import mx.rpc.events.ResultEvent;
import mx.rpc.events.FaultEvent;
import mx.controls.Alert;
public function resultHandler(event:ResultEvent):void {
// Display received message
outputMessage.text = event.result as String;
}
public function faultHandler(event:FaultEvent):void {
// Show error alert
Alert.show(event.fault.faultString);
}
</mx:Script>
<!-- Connect to a service destination.-->
<mx:RemoteObject id="helloService"
destination="helloService"
source="helloService"
result="handleResult(event);"
fault="handleFault(event);"/>
<!-- Provide input data for calling the service. -->
<mx:TextInput id="inputName"/>
<!-- Call the Spring service, use the text in a TextInput control as input data.-->
<mx:Button click="helloService.hello(inputName.text)"/>
<!-- Display results data in the user interface. -->
<mx:Label id="outputMessage"/>
</mx:Application>
The main thing to note is the use of the source
property in both the RemoteObject
definition
and in the @RemoteDestination
annotation that should match the name of the Spring bean (here in @Service
).
Besides configuring the dispatcher servlet (see here), configuring GraniteDS in the Spring context
just requires adding the graniteds
namespace and adding a flex-filter
element:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:graniteds="http://www.graniteds.org/config"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.graniteds.org/config http://www.graniteds.org/public/dtd/2.3.0/granite-config-2.3.xsd">
...
<graniteds:flex-filter url-pattern="/*"/>
</beans>
The actual url that will be listened to by the AMF processor is the combination of the url-pattern
in the Spring context
and the servlet-mapping
of the dispatcher servlet in web.xml
.
The configuration described here maps GraniteDS on /graniteamf/*
and is suitable in almost all cases.
When necessary, this configuration can be overriden or completed by the default configuration in services-config.xml
described
in the next section.
In this case, the implicit configuration created by the MVC setup contains the following elements :
granite-service
spring-factory
graniteamf
The MVC setup automatically enables component scanning, so you can just annotate your Spring services with @RemoteDestination
and put an empty META-INF/services-config.properties
file in your services jar or folder to tell GraniteDS where to look for services.
See last paragraph Automatic Configuration of Destinations.
Alternatively you can also declare the remote destinations manually in the Spring context:
<graniteds:remote-destination id="personService" source="personService"/>
You can also specify a secure destination by adding the list of roles required to access the destination:
<graniteds:remote-destination id="personService" source="personService">
<graniteds:roles>
<graniteds:role>ROLE_ADMIN</graniteds:role>
</graniteds:roles>
</graniteds:remote-destination>
The support for Spring Security is automatically enabled and the version of Spring Security is automatically detected.
However if you have configured many AuthenticationManager
s, it will be necessary to instruct GraniteDS which one should be used
for authentication by adding the following line to your Spring configuration :
<graniteds:security-service authentication-manager="myAuthenticationManager"/>
With this declaration you can also provide various configuration elements for the Spring 3 security service implementation :
<graniteds:security-service
authentication-manager="myAuthenticationManager"
allow-anonymous-access="true"
authentication-trust-resolver="com.myapp.MyAuthenticationTrustResolver"
session-authentication-strategy="com.myapp.MySessionAuthenticationStrategy"
security-context-repository="com.myapp.MySecurityContextRepository"
security-interceptor="com.myapp.MySecurityInterceptor"
password-encoder="com.myapp.MyPasswordEncoder"
/>
Finally remember that as there is no services-config.xml
, you will have to manually initialize the endpoints
for your client RemoteObject
s (also see here) :
srv.destination = "personService"; srv.source = "personService"; srv.channelSet = new ChannelSet(); srv.channelSet.addChannel(new AMFChannel("graniteamf", "http://{server.name}:{server.port}/{context.root}/graniteamf/amf"));
granite-config.xml
(scanning is always enabled with a MVC setup).
<granite-config scan="true"/>
META-INF/services-config.properties
marker file in all jars containing Spring services
org.granite.messaging.service.annotations.RemoteDestination
@RemoteDestination(id="personService", source="personService", securityRoles={"user","admin"})
public interface PersonService {
}
@Service("personService")
public class PersonServiceBean implements PersonService {
...
}
The annotation supports the following attributes:
id
is mandatory and is the name of the destination as used from Flex
source
is mandatory and should be the name of the Spring bean
service
is optional when there is only one service for RemotingMessage
defined in services-config.xml
.
Otherwise this should be the name of the service.
channel
is optional if there is only one channel defined in services-config.xml
.
Otherwise this should be the id of the target channel.
channels
may be used instead of channel
to define a failover channel.
factory
is optional if there is only one factory in services-config.xml
. Otherwise this should be the factory id.
securityRoles
is an array of role names for securing the destination.
<granite-config>
...
<!--
! Use Spring based security service.
!-->
<security type="org.granite.spring.security.SpringSecurity3Service"/>
</granite-config>
<granite-config>
...
<!--
! Use Spring based security service.
!-->
<security type="org.granite.messaging.service.security.SpringSecurityService"/>
</granite-config>
<granite-config>
...
<!--
! Use Spring based security service.
!-->
<security type="org.granite.messaging.service.security.AcegiSecurityService"/>
</granite-config>
You may then secure your Flex destinations as shown earlier. Please refer to or documentation for specific configuration details.
Note however that there are two main ways of securing the GraniteDS AMF endpoint:
Most of what is described in the Tide Remoting section applies for Spring, however GraniteDS also provides an improved integration with Spring services.
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:graniteds="http://www.graniteds.org/config"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.graniteds.org/config http://www.graniteds.org/public/dtd/2.3.0/granite-config-2.3.xsd">
...
<graniteds:flex-filter url-pattern="/*" tide="true"/>
</beans>
Other configurations can be done with flex-filter
:
tide-annotations
is equivalent to tide-component annotated-with=""
in granite-config.xml
.
It allows to define the list of annotation names that enable remote access to Spring beans. @RemoteDestination
is always
declared by default, but you can use any other one if you don't want a compilation dependency on the GraniteDS libraries.
tide-roles
allows to define a list of security roles that are required to access the Tide remote destination.
In general it is not necessary to define this destination-wide security and only rely on Spring security for fine-grained access to individual beans.
security-service
allows to specify the security service implementation.
exception-converters
allows to define a list of server-side exception converters.
It's the equivalent to exception-converters
in granite-config.xml
.
amf3-message-interceptor
allows to define a message interceptor.
You have to define the bean name of an existing bean implementing AMFMessageInterceptor
.
Additional elements can also be configured in the Spring beans file:
tide-identity
allows to declare the identity bean.
When using Spring Security ACL you can define here the necessary attributes acl-service
, sid-retrieval-strategy
and object-identity-retrieval-strategy
.
tide-persistence
allows to declare the persistence implementation for your application.
It is not necessary when you have only one Spring transactionManager
, otherwise just specify the name of
the transaction manager to use. Tide/Spring will automatically determine the kind of transaction management it should use (JTA, JPA or Hibernate API).
tide-annotations
section of granite-config.xml
the conditions
used to enable remote access to Spring destinations (for example all beans annotated with a particular annotation).
org.granite.tide.spring.SpringServiceFactory
service factory
in services-config.xml
.
spring
in services-config.xml
Spring.getInstance().getSpringContext()
instead of Tide.getInstance().getContext()
.
Here is a default configuration suitable for most cases:
<granite-config scan="true">
...
<tide-components>
<tide-component annotated-with="org.granite.messaging.service.annotations.RemoteDestination"/>
</tide-components>
</granite-config>
<services-config>
<services>
<service id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<!--
! Use "tideSpringFactory" and "my-graniteamf" for "ejb" destination (see below).
! The destination must be "spring" when using Tide with default configuration.
!-->
<destination id="spring">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>tideSpringFactory</factory>
</properties>
</destination>
</service>
</services>
<!--
! Declare tideSpringFactory service factory.
!-->
<factories>
<factory id="tideSpringFactory" class="org.granite.tide.spring.SpringServiceFactory"/>
</factories>
<!--
! Declare my-graniteamf channel.
!-->
<channels>
<channel-definition id="my-graniteamf" class="mx.messaging.channels.AMFChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
</services-config>
You should also define the correct Spring security service in granite-config.xml
, see here
for details.
You can use the property entity-manager-factory-bean-name
to specify an EntityManagerFactory
bean
that will be used for transparent remote lazy loading of collections.
Here is an example with a Spring JPA/Hibernate configuration:
<persistence-unit name="spring-pu">
...
</persistence-unit>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName">
<value>org.hsqldb.jdbcDriver</value>
</property>
<property name="url">
<value>jdbc:hsqldb:mem:springds</value>
</property>
<property name="username">
<value>sa</value>
</property>
<property name="password">
<value></value>
</property>
</bean>
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="persistenceUnitName" value="spring-pu" />
<property name="jpaVendorAdapter">
<bean
class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="showSql" value="false" />
<property name="generateDdl" value="true" />
<property name="databasePlatform" value="org.hibernate.dialect.HSQLDialect" />
</bean>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
<property name="dataSource" ref="dataSource" />
</bean>
If you use a plain Hibernate session instead of JPA, you cannot use entity-manager-factory-bean-name
, you have to configure
a specific Tide persistence manager in the Spring context (assuming the bean name of the Hibernate session factory is
sessionFactory
):
<!-- All this AOP stuff is to ensure the Tide persistence manager will be transactional -->
<aop:config>
<aop:pointcut id="tidePersistenceManagerMethods"
expression="execution(* org.granite.tide.ITidePersistenceManager.*(..))"/>
<aop:advisor advice-ref="tidePersistenceManagerMethodsTxAdvice"
pointcut-ref="tidePersistenceManagerMethods"/>
</aop:config>
<tx:advice id="tidePersistenceManagerMethodsTxAdvice"
transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" read-only="true"/>
</tx:attributes>
</tx:advice>
<bean id="tidePersistenceManager"
class="org.granite.tide.hibernate.HibernateSessionManager" scope="request">
<constructor-arg>
<ref bean="sessionFactory"/>
</constructor-arg>
</bean>
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="Spring.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.spring.Spring;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;
[In]
public var helloService:Component;
private function hello(name:String):void {
helloService.hello(name, resultHandler, faultHandler);
}
private function resultHandler(event:TideResultEvent):void {
outputMessage.text = event.result as String;
}
private function faultHandler(event:TideFaultEvent):void {
// Handle fault
}
</mx:Script>
<!-- Provide input data for calling the service. -->
<mx:TextInput id="inputName"/>
<!-- Call the web service, use the text in a TextInput control as input data.-->
<mx:Button click="hello(inputName.text)"/>
<!-- Result message. -->
<mx:Label id="outputMessage"/>
</mx:Application>
This is almost identical to the standard Tide API described in the Tide remoting section, and all other methods apply for Spring.
You can benefit from the capability of the Gas3 code generator (see here) to generate a strongly typed
ActionScript 3 client proxy from the Spring interface when it is annotated with @RemoteDestination
.
In this case, you can inject a typesafe reference to your service and get better compile time error checking and auto completion in your IDE:
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="Spring.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.spring.Spring;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;
import com.myapp.service.HelloService;
[In]
public var helloService:HelloService;
private function hello(name:String):void {
helloService.hello(name, resultHandler, faultHandler);
}
...
</mx:Script>
...
</mx:Application>
It is possible to benefit from even more type safety by using the annotation [Inject]
instead of In
.
When using this annotation, the full class name is used to find the target bean in the Spring context instead of the bean name.
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="Spring.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.spring.Spring;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;
import com.myapp.service.HelloService;
[Inject]
public var myService:HelloService;
private function hello(name:String):void {
myService.hello(name, resultHandler, faultHandler);
}
...
</mx:Script>
...
</mx:Application>
<graniteds:tide-identity/>
<graniteds:tide-identity acl-service="myAclService"
object-identity-retrieval-strategy="myObjectIdentityRetrievalStrategory"
sid-retrieval-strategy="mySIDRetrievalStrategy"/>
The Flex identity
component for Spring (of class org.granite.tide.spring.Identity
) predictably provides two methods
login()
and logout()
that can be used as any Tide remote call:
private var tideContext:Context = Spring.getInstance().getSpringContext(); public function login(username:String, password:String):void { tideContext.identity.login(username, password, loginResult, loginFault); } private function loginResult(event:TideResultEvent):void { Alert.show(event.context.identity.loggedIn); } private function loginFault(event:TideFaultEvent):void { Alert.show(event.fault); } public function logout():void { tideContext.identity.logout(); }
Or with dependency injection:
[Inject] public var identity:Identity; public function login(username:String, password:String):void { identity.login(username, password, loginResult, loginFault); } private function loginResult(event:TideResultEvent):void { Alert.show(event.context.identity.loggedIn); } private function loginFault(event:TideFaultEvent):void { Alert.show(event.fault); } public function logout():void { identity.logout(); }
The identity
component also exposes the bindable property loggedIn
that represents the current authentication state.
As it is bindable, it can be used to choose between different views, for example to switch between a login form and the application view with a Flex
ViewStack
component:
<mx:ViewStack id="main" selectedIndex="{identity.loggedIn ? 1 : 0}">
<views:LoginView id="loginView"/>
<views:MainView id="mainView"/>
</mx:ViewStack>
Finally the identity
component is integrated with server-side role-based security and can be used to get information or show/hide UI
depending on the user access rights. It provides methods similar to the Spring Security jsp tags sec:ifAllGranted
,
sec:ifAnyGranted
, sec:ifNotGranted
and sec:hasPermission
.
<mx:Button id="deleteCategoryButton"
label="Delete Category"
enabled="{identity.ifAllGranted('ROLE_ADMIN')}"
click="productService.deleteCategory(category)"/>
<mx:Button id="deleteProductButton" label="Delete Product"
enabled="{productGrid.selectedItem}"
visible="{identity.hasPermission(productGrid.selectedItem, '8,16')}"
click="productService.deleteProduct(productGrid.selectedItem)"/>
With these declaration, the button labeled Delete Category will be enabled only if the user has the role ROLE_ADMIN
and the button Delete Product only if the user has the ACL permissions DELETE (code 8) or ADMINISTER (code 16) for the selected product.
Another possibility is to completely hide the button with the properties visible
and includeInLayout
, or any other
property relevant for the display of the UI component.
The three methods are:
ifAllGranted
/ifAnyGranted
: the user should have the specified role
ifNotGranted
: the user should not have the specified role
hasPermission
: the user should have the specified permission for the specified entity
This can also be used as any remote class with result and fault handlers:
public function checkRole(role:String):void { identity.ifAllGranted(role, checkRoleResult, checkRoleFault); } private function checkRoleResult(event:TideResultEvent, role:String):void { if (role == 'ROLE_ADMIN') { if (event.result) trace("User has admin role"); else trace("User does not have admin role"); } }
You can notice that the result and fault handlers have a second argument so you can use the same handler for many access check calls.
identity.ifAllGranted()
will issue a remote call when it is called the first time, thus its return value cannot be used reliably
to determine if the use has the required role. It will always return false
until the remote call result is received.
It is important to note that identity
caches the user access rights so only the first call to ifAllGranted()
will be remote. If the user rights are changed on the server, or if you want to enforce security more than once per user session, you can clear
the security cache manually with identity.clearSecurityCache()
, for example periodically with a Timer
.
<graniteds:messaging-destination id="myTopic"/>
This declaration supports the properties no-local
and session-selector
(see the
Messaging Configuration section).
You can also define a secure destination by specifying a list of roles required to access the topic:
<graniteds:messaging-destination id="myTopic">
<graniteds:roles>
<graniteds:role>ROLE_ADMIN</graniteds:role>
</graniteds:roles>
<graniteds:messaging-destination/>
JMS Topic:
<graniteds:jms-messaging-destination id="myTopic"
connection-factory="ConnectionFactory"
destination-jndi-name="topic/MyTopic"
transacted-sessions="true"
acknowledge-mode="AUTO_ACKNOWLEDGE"/>
This declaration supports all properties of the default JMS declaration in services-config.xml
except for non local initial context
environments (see the JMS Integration section).
ActiveMQ Topic:
<graniteds:activemq-messaging-destination id="myTopic"
connection-factory="ConnectionFactory"
destination-jndi-name="topic/MyTopic"
transacted-sessions="true"
acknowledge-mode="AUTO_ACKNOWLEDGE"
broker-url="vm://localhost"
create-broker="true"
wait-for-start="true"
durable="true"
file-store-root="/opt/activemq/data"/>
This declaration supports all properties of the default ActiveMQ declaration in services-config.xml
except for non local initial context
environments (see the ActiveMQ Integration section).
Finally note that the Gravity
singleton that is needed to push messages from the server (see here)
is available as a bean in the Spring context and can be autowired by type with @Inject
or @Autowired
:
@Inject
private Gravity gravity;
The is a powerful Java enterprise framework. It integrates on a common platform all the necessary services for building enterprise applications: persistence, transactions, security...
GraniteDS provides out-of-the-box integration with Seam 2.2 via either the RemoteObject
API or the Tide API
to remotely call Seam components, and fully supports serialization of JPA entities from and to your Flex application, taking care of lazily loaded associations.
The support for JPA entity beans is covered in the section JPA and lazy initialization, so this section will only
describe how to call Seam components from a Flex application. GraniteDS also fully supports Seam Security for authentication and authorization.
The support for JBoss Seam 2.2 is included in the library granite-seam21.jar
, so you always have to include this library in either
WEB-INF/lib
or lib
for an ear packaging. As you have maybe noticed in the name of the jar, it can be used with
any version of Seam since 2.1 but it is recommended to use Seam 2.2 with Flex and GraniteDS.
Note that to provide a more native experience for Seam developers, the Seam support in GraniteDS can be configured directly in the Seam configuration
files (components.xml
). Most features of GraniteDS can be configured this way, and it is still possible to fall back to the
default GraniteDS configuration files services-config.xml
and granite-config.xml
for unsupported features.
The Flex-side usage of the RemoteObject
API is completely independent of the server technology, so everything described in
the Remoting chapter applies for Seam components. This section will only describe the particular configuration
required in various use cases of Seam components.
All remoting examples from the Remoting chapter apply for Seam components, here is a basic example:
@Name("helloService")
@RemoteDestination(id="helloService")
public class HelloService {
public String hello(String name) {
return "Hello " + name;
}
}
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
import mx.rpc.events.ResultEvent;
import mx.rpc.events.FaultEvent;
import mx.controls.Alert;
public function resultHandler(event:ResultEvent):void {
// Display received message
outputMessage.text = event.result as String;
}
public function faultHandler(event:FaultEvent):void {
// Show error alert
Alert.show(event.fault.faultString);
}
</mx:Script>
<!-- Connect to a service destination.-->
<mx:RemoteObject id="helloService"
destination="helloService"
source="helloService"
result="handleResult(event);"
fault="handleFault(event);"/>
<!-- Provide input data for calling the service. -->
<mx:TextInput id="inputName"/>
<!-- Call the Seam component, use the text in a TextInput control as input data.-->
<mx:Button click="helloService.hello(inputName.text)"/>
<!-- Display results data in the user interface. -->
<mx:Label id="outputMessage"/>
</mx:Application>
The main thing to note is the use of the source
property in both the RemoteObject
definition
and in the @RemoteDestination
annotation that should match the name of the Seam component.
public dynamic class SeamRemoteObject extends SecureRemoteObject {
...
public function get conversation():Conversation {...}
public function set conversation(conversation:Conversation):void {...}
public function get task():Task {...}
public function set task(task:Task):void {...}
...
}
Basically, Conversation
and Task
only encapsulate an id.
Since SeamRemoteObject
extends SecureRemoteObject
, you may use all security features as explained
here.
It's also necessary to use the specific SeamOperation
class with SeamRemoteObject
to ensure proper
serialization of the parameters:
... import org.granite.seam.SeamRemoteObject; import org.granite.seam.SeamOperation; ... srv = new SeamRemoteObject("myDestination"); var operation:SeamOperation = new SeamOperation(); operation.name = "myMethod"; operation.addEventListener(ResultEvent.RESULT, onMyMethodResult); srv.operations = {myMethod: operation}; ...
Besides configuring the Seam filter (see here), configuring GraniteDS in the Seam configuration
just requires adding the graniteds
namespace and adding a flex-filter
element:
<components xmlns="http://jboss.com/products/seam/components"
xmlns:core="http://jboss.com/products/seam/core"
xmlns:security="http://jboss.com/products/seam/security"
xmlns:transaction="http://jboss.com/products/seam/transaction"
xmlns:persistence="http://jboss.com/products/seam/persistence"
xmlns:framework="http://jboss.com/products/seam/framework"
xmlns:bpm="http://jboss.com/products/seam/bpm"
xmlns:jms="http://jboss.com/products/seam/jms"
xmlns:web="http://jboss.com/products/seam/web"
xmlns:graniteds="http://www.graniteds.org/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://jboss.com/products/seam/core http://jboss.com/products/seam/core-2.0.xsd
http://jboss.com/products/seam/transaction http://jboss.com/products/seam/transaction-2.0.xsd
http://jboss.com/products/seam/persistence http://jboss.com/products/seam/persistence-2.0.xsd
http://jboss.com/products/seam/web http://jboss.com/products/seam/web-2.0.xsd
http://jboss.com/products/seam/jms http://jboss.com/products/seam/jms-2.0.xsd
http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.0.xsd
http://jboss.com/products/seam/bpm http://jboss.com/products/seam/bpm-2.0.xsd
http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.0.xsd
http://jboss.com/products/seam/framework http://jboss.com/products/seam/framework-2.0.xsd
http://www.graniteds.org/config http://www.graniteds.org/public/dtd/2.3.0/granite-config-2.3.xsd">
...
<graniteds:flex-filter url-pattern="/graniteamf/*"/>
</components>
The url-pattern
should be contained within the url mapping of the Seam filter as defined in web.xml
.
The configuration described here maps GraniteDS on /graniteamf/*
and is suitable in almost all cases.
When necessary, this configuration can be overriden or completed by the default configuration in services-config.xml
described
in the next section.
In this case, the implicit configuration created by the native setup contains the following elements :
granite-service
seam-factory
graniteamf
The native setup automatically enables component scanning, so you can just annotate your Seam components with @RemoteDestination
and put an empty META-INF/services-config.properties
file in your services jar or folder to tell GraniteDS where to look for services.
See last paragraph Automatic Configuration of Destinations.
Alternatively you can also declare the remote destinations manually in the Seam configuration:
<graniteds:remote-destination id="personService" source="personService"/>
You can also specify a secure destination by adding the list of roles required to access the destination:
<graniteds:remote-destination id="personService" source="personService">
<graniteds:roles>
<graniteds:role>admin</graniteds:role>
</graniteds:roles>
</graniteds:remote-destination>
Finally remember that as there is no services-config.xml
, you will have to manually initialize the endpoints
for your client RemoteObject
s (also see here) :
srv.destination = "personService"; srv.source = "personService"; srv.channelSet = new ChannelSet(); srv.channelSet.addChannel(new AMFChannel("graniteamf", "http://{server.name}:{server.port}/{context.root}/graniteamf/amf"));
granite-config.xml
(scanning is always enabled with a native setup).
<granite-config scan="true"/>
META-INF/services-config.properties
marker file in all jars containing Seam destinations
org.granite.messaging.service.annotations.RemoteDestination
@Name("personService")
@RemoteDestination(id="person", source="personService", securityRoles={"user","admin"})
public class PersonAction {
...
}
The annotation supports the following attributes:
id
is mandatory and is the name of the destination as used from Flex
source
is mandatory and should be the name of the Seam component
service
is optional when there is only one service for RemotingMessage
defined in services-config.xml
.
Otherwise this should be the name of the service.
channel
is optional if there is only one channel defined in services-config.xml
.
Otherwise this should be the id of the target channel.
channels
may be used instead of channel
to define a failover channel.
factory
is optional if there is only one factory in services-config.xml
. Otherwise this should be the factory id.
securityRoles
is an array of role names for securing the destination.
Most of what is described in the Tide Remoting section applies for Seam 2.x, however GraniteDS also provides a much improved integration with the Seam framework when using the Tide client API.
<?xml version="1.0" encoding="UTF-8"?>
<components xmlns="http://jboss.com/products/seam/components"
xmlns:core="http://jboss.com/products/seam/core"
xmlns:security="http://jboss.com/products/seam/security"
xmlns:transaction="http://jboss.com/products/seam/transaction"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:graniteds="http://www.graniteds.org/config"
xsi:schemaLocation=
"http://jboss.com/products/seam/core
http://jboss.com/products/seam/core-2.1.xsd
http://jboss.com/products/seam/transaction
http://jboss.com/products/seam/transaction-2.1.xsd
http://jboss.com/products/seam/security
http://jboss.com/products/seam/security-2.1.xsd
http://jboss.com/products/seam/components
http://jboss.com/products/seam/components-2.1.xsd
http://www.graniteds.org/config
http://www.graniteds.org/public/dtd/2.3.0/granite-config-2.3.xsd">
<core:init .../>
...
<graniteds:flex-filter url-pattern="/graniteamf/*" tide="true"/>
</components>
Other configurations can be done with flex-filter
:
tide-annotations
is equivalent to tide-component annotated-with=""
in granite-config.xml
.
It allows to define the list of annotation names that enable remote access to Seam components. @RemoteDestination
and
@TideEnabled
are always declared by default, but you can use any other one if you don't want a compilation dependency
on the GraniteDS libraries.
tide-roles
allows to define a list of security roles that are required to access the Tide remote destination.
In general it is not necessary to define this destination-wide security and you can only rely on Seam security for fine-grained access to individual
components.
exception-converters
allows to define a list of server-side exception converters.
It's the equivalent to exception-converters
in granite-config.xml
.
amf3-message-interceptor
allows to define a message interceptor. You have to define an EL expression referencing an
existing component implementing AMFMessageInterceptor
. It's highly recommended to subclass Seam21Interceptor
and call super.before
and super.after
in your implementation.
tide-annotations
section of granite-config.xml
the conditions
used to enable remote access to Seam destinations (for example all components annotated with a particular annotation).
org.granite.tide.seam.SeamServiceFactory
service factory
in services-config.xml
.
seam
in services-config.xml
Seam.getInstance().getSeamContext()
instead of Tide.getInstance().getContext()
.
Here is a default configuration suitable for most cases:
<granite-config scan="true">
...
<tide-components>
<tide-component annotated-with="org.granite.messaging.service.annotations.RemoteDestination"/>
<tide-component annotated-with="org.granite.tide.annotations.TideEnabled"/>
</tide-components>
</granite-config>
<services-config>
<services>
<service id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<!--
! Use "tideSeamFactory" and "my-graniteamf" for "seam" destination (see below).
! The destination must be "seam" when using Tide with default configuration.
!-->
<destination id="seam">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>tideSeamFactory</factory>
</properties>
</destination>
</service>
</services>
<!--
! Declare tideSeamFactory service factory.
!-->
<factories>
<factory id="tideSeamFactory" class="org.granite.tide.seam.SeamServiceFactory"/>
</factories>
<!--
! Declare my-graniteamf channel.
!-->
<channels>
<channel-definition id="my-graniteamf" class="mx.messaging.channels.AMFChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
</services-config>
The destination named seam
will be the one and only destination required for all Seam destinations.
You should also define the correct Seam security service in granite-config.xml
, see here
for details.
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="Seam.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.seam.Seam;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;
[In]
public var helloService:Component;
private function hello(name:String):void {
helloService.hello(name, resultHandler, faultHandler);
}
private function resultHandler(event:TideResultEvent):void {
outputMessage.text = event.result as String;
}
private function faultHandler(event:TideFaultEvent):void {
// Handle fault
}
</mx:Script>
<!-- Provide input data for calling the service. -->
<mx:TextInput id="inputName"/>
<!-- Call the web service, use the text in a TextInput control as input data.-->
<mx:Button click="hello(inputName.text)"/>
<!-- Result message. -->
<mx:Label id="outputMessage"/>
</mx:Application>
This is almost identical to the standard Tide API described in the Tide remoting section, and all other methods apply for Seam.
<fwk:entity-query name="contacts" results="5">
<fwk:ejbql>from Contact</fwk:ejbql>
<fwk:order>lastName</fwk:order>
<fwk:restrictions>
<value>lower(firstName) like lower(concat(#{exampleContact.firstName}, '%'))</value>
<value>lower(lastName) like lower(concat(#{exampleContact.lastName}, '%'))</value>
</fwk:restrictions>
</fwk:entity-query>
<component name="exampleContact" class="org.jboss.seam.example.contactlist.Contact"/>
(1) tideContext.exampleContact = new Contact();
tideContext.exampleContact.firstName = 'Jimi';
tideContext.exampleContact.lastName = 'Hendrix';
(2) tideContext.contacts.getResultList(getContactsHandler);
...
In some cases, notably when using context variables with the client framework (see corresponding section
Tide Client Framework), it can be useful to disable the synchronization between client
and server for certain variables. This is possible by using Seam.getInstance().setComponentRemoteSync(variableName, false)
expressions.
Note that remote synchronization of all variables received from the server is enabled by default.
Variables that are outjected from client components can be remote-enabled with [Out(remote="true")]
.
That allows, for example, to do things like (extract from the Seam booking sample):
@Stateful
@Name("hotelBooking")
@Restrict("#{identity.loggedIn}")
public class HotelBookingAction implements HotelBooking {
...
@In
private User user;
@In(required=false) @Out
private Hotel hotel;
@In(required=false)
@Out(required=false)
private Booking booking;
...
public void bookHotel() {
booking = new Booking(hotel, user);
Calendar calendar = Calendar.getInstance();
booking.setCheckinDate( calendar.getTime() );
calendar.add(Calendar.DAY_OF_MONTH, 1);
booking.setCheckoutDate( calendar.getTime() );
}
...
}
public function bookHotel(hotel:Hotel):void { (1) tideContext.hotel = hotel; (2) tideContext.hotelBooking.bookHotel(bookResult); } private function bookResult(event:TideResultEvent):void { (3) var booking:Booking = event.context.booking as Booking; }
tideContext.meta_resync(resultHandler, faultHandler);
You can also simplify the client code by using dependency injection on the Flex controller (see here):
[In] public var booking:Booking; [Out(remote="true")] public var hotel:Hotel; [In] public var hotelBooking:Component; public function bookHotel(hotel:Hotel):void { (1) this.hotel = hotel; (2) hotelBooking.bookHotel(bookResult); } private function bookResult(event:TideResultEvent):void { (3) Alert.show("Booking processed: " + booking.toString()); }
It is also interesting to note that even the Seam events triggering other components outjection are intercepted, thus allowing full support for complex server-side interactions.
If, for some reason, some outjected variables should not be sent back to the client, it is possible to define a list of disabled component names
in granite-config.xml
, in the section tide-components
:
<tide-components>
<component annotatedwith="com.myapp.some.annotation.for.disabling.Components" disabled="true"/>
<component type="com\.myapp\..*" disabled="true"/>
</tide-components>
The supported component definitions are the same as for enabling components (name
, type
,
annotated-with
, instance-of
). This is relatively flexible and allows to finely control
what part of the context may be shared between server and client.
Tide+Seam intercepts injection and outjection of standard JSF DataModel
s. This is not particularly useful in a Flex environment,
except for reusing existing Seam components, and this is roughly equivalent to using @In
and @Out
.
There is no support for custom data binding.
@Stateful
@Scope(SESSION)
@Name("bookingList")
@Restrict("#{identity.loggedIn}")
@TransactionAttribute(REQUIRES_NEW)
public class BookingListAction implements BookingList, Serializable {
...
@DataModel
private List<Booking> bookings;
@DataModelSelection
private Booking booking;
...
}
public function cancelBooking(booking:Booking):void { (1) tideContext.bookingList.booking = booking; (2) tideContext.bookingList.cancel(cancelBookingResult); } private function cancelBookingResult(event:TideResultEvent):void { (3) bookings = event.context.bookings as ArrayCollection; }
booking
is the name of the component property annotated with
@DataModelSelection
.
cancel
of the component named bookingList
.
DataModel
bookings
from the context as an ArrayCollection
;
it is not encapsulated in ActionScript.
@Stateful
@Name("hotelBooking")
@Restrict("#{identity.loggedIn}")
public class HotelBookingAction implements HotelBooking {
...
@Begin
public void selectHotel(Hotel selectedHotel) {
hotel = em.merge(selectedHotel);
}
...
}
public function selectHotel(hotel:Hotel):void { (1) tideContext.hotelBooking.selectHotel(hotel, selectHotelResult); } private function selectHotelResult(event:TideResultEvent):void { (2) var localContext:Context = event.context as Context; var hotel:Hotel = localContext.hotel; }
var localContext:Context = Seam.getInstance().getSeamContext(conversationId)
[In] public var conversationList:ConversationList
[In] public var conversation:Conversation
This component has three uses :
conversation.description = "Some description"; someConversationalComponent.beginConversation();
conversation.setDescription("Some description");
conversation.getDescription();
@Stateful
@Name("hotelBooking")
@Restrict("#{identity.loggedIn}")
public class HotelBookingAction implements HotelBooking {
...
@End
public void confirm() {
em.persist(booking);
facesMessages.add(
"Thank you, #{user.name}, your confirmation number " +
"for #{hotel.name} is #{booking.id}"
);
log.info("New booking: #{booking.id} for #{user.username}");
events.raiseTransactionSuccessEvent("bookingConfirmed");
}
}
Seam.getInstance().getSeamContext().addContextEventListener("bookingConfirmed", bookingConfirmedHandler, true); private function bookingConfirmedHandler(event:TideContextEvent):void { // No need for remote call, event has been dispatched on // the server and list is outjected. hotelBookings = ArrayCollection(event.context.bookings); }
You can simplify the client code by using the Tide Client Framework:
[Name("bookingsCtl")] public class BookingsCtl { [In] public var bookings:ArrayCollection; [Observer("bookingConfirmed", remote="true")] public function bookingConfirmedHandler(event:TideContextEvent):void { Alert.show("New booking confirmed: total " + bookings.length); } }
<services-config>
<services>
<service id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<!--
! Use "tideSeamFactory" and "my-graniteamf" for "seam" destination (see below).
!-->
<destination id="seam">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>tideSeamFactory</factory>
</properties>
<security>
<security-constraint>
<auth-method>Custom</auth-method>
<roles>
<role>user</role>
<role>admin</role>
</roles>
</security-constraint>
</security>
</destination>
</service>
<service id="gravity-service"
class="flex.messaging.services.MessagingService"
messageTypes="flex.messaging.messages.AsyncMessage">
<adapters>
<adapter-definition id="seam"
class="org.granite.gravity.adapters.SimpleServiceAdapter"/>
</adapters>
<destination id="seamAsync">
<channels>
<channel ref="my-gravityamf"/>
</channels>
<security>
<security-constraint>
<auth-method>Custom</auth-method>
<roles>
<role>user</role>
<role>admin</role>
</roles>
</security-constraint>
</security>
<adapter ref="seam"/>
</destination>
</service>
</services>
<!--
! Declare Tide+Seam service factory.
!-->
<factories>
<factory id="tideSeamFactory" class="org.granite.tide.seam.SeamServiceFactory"/>
</factories>
<!--
! Declare granite channels.
!-->
<channels>
<channel-definition id="my-graniteamf" class="mx.messaging.channels.AMFChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
<channel-definition id="my-gravityamf"
class="org.granite.gravity.channels.GravityChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/gravity/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
</services-config>
Or in component.xml
(recommended):
<graniteds:messaging-destination name="seamAsync"/>
Then the Tide messaging client has to be started and subscribed:
Seam.getInstance().addPlugin(TideAsync.getInstance("seamAsync"));
Then you can simply register remote observers in any client component:
@Stateless
@Name("test")
public class TestAction implements Test {
public void test(String text) {
Events.instance().raiseAsynchronousEvent("myEvent");
}
}
private function init():void { Seam.getInstance().getSeamContext().addContextEventListener( "myEvent", myEventHandler, true); } private function myEventHandler(event:TideContextEvent):void { trace("The event has been received"); }
public class TestObserver { [Observer("myEvent", remote="true")] private function myEventHandler(event:TideContextEvent):void { trace("The event has been received"); } }
public function login():void { tideContext.identity.username = 'joseph'; tideContext.identity.password = 'conrad'; (1) tideContext.identity.login(loginResult); } ... private function loginResult(event:TideResultEvent):void { (2) var welcomeMessage:TideMessage = event.context.statusMessages.messages.getItemAt(0) as TideMessage; var s:String = welcomeMessage.summary; }
Per-control messages can be retrieved with:
private function registerResult(event:TideResultEvent):void { var messages:ArrayCollection = event.context.statusMessages.keyedMessages['username'] as ArrayCollection; if (messages && messages.length > 0) { var s:String = messages.getItemAt(0).summary; Alert.show(s); } }
<mx:Application
...
xmlns:tsv="org.granite.tide.seam.validators">
<mx:TextInput id="username"/>
<tsv:TideControlValidator source="{username}" property="text"/>
</mx:Application>
With Seam, you can easily use the Query
component of the Seam Application Framework as the remote data provider for a paged collection.
Filtering is also supported by using restrictions. All this is supported by the Seam-specific Flex implementation of PagedQuery
. You
can also see the Data Paging section for more details.
import org.granite.tide.seam.framework.PagedQuery; Seam.getInstance().addComponent("people", PagedQuery);
Then declare your Seam Query
component:
<component name="examplePerson" class="com.myapp.entity.Person"/>
<framework:entity-query name="people"
ejbql="select p from Person p"
max-results="36">
<framework:restrictions>
<value>lower(p.lastName) like lower( #{examplePerson.lastName} || '%' )</value>
</framework:restrictions>
</framework:entity-query>
This is a very standard Seam Query
definition, only the max-results
property is important as it will be
used as the page size for the client component.
Note that defining max-results
is mandatory when using server page size and it is necessary that the max-results
page size is greater than the expected maximum size of the UI component that will be bound to the collection.
You can also specify the max-results
property on the client component instead of the server, you can then omit the property on the
server component definition:
Seam.getInstance().addComponentWithFactory("people", PagedQuery, { maxResults: 40 });
To change filter parameters values on the client-side, you just have to set values on the restriction object (here examplePerson
)
in the context. Tide tracks the changes on the object on the Flex side and will update the server filter instance accordingly.
PagedQuery
implicitly forces the detected restriction variables to be synchronized remotely with the server
so you don't have to do [Out(remote="true")]
or Seam.getInstance().setComponentRemoteSync("examplePerson", true)
manually.
For example, you can use a filter like this :
<mx:Script>
[In(create="true")]
public var examplePerson:Person;
[In]
public var people:PagedQuery;
</mx:Script>
<mx:TextInput id="lastName" text="{examplePerson.lastName}"/>
<mx:Button label="Search" click="people.refresh()"/>
<mx:DataGrid id="peopleGrid" dataProvider="{people}">
...
</mx:DataGrid>
public function login(username:String, password:String):void { tideContext.identity.username = username; tideContext.identity.password = password; tideContext.identity.login(loginResult, loginFault); } private function loginResult(event:TideResultEvent):void { Alert.show(event.context.messages.getItemAt(0).summary); } private function loginFault(event:TideFaultEvent):void { Alert.show(event.context.messages.getItemAt(0).summary); }
<mx:ViewStack id="main" selectedIndex="{identity.loggedIn ? 1 : 0}">
<views:LoginView id="loginView"/>
<views:MainView id="mainView"/>
</mx:ViewStack>
<mx:Button id="deleteCategoryButton"
label="Delete Category"
enabled="{identity.hasRole('admin')}"
click="productService.deleteCategory(category)"/>
<mx:Button id="deleteProductButton" label="Delete Product"
enabled="{productGrid.selectedItem}"
visible="{identity.hasPermission(productGrid.selectedItem, 'delete')}"
click="productService.deleteProduct(productGrid.selectedItem)"/>
This can also be used as any remote class with result and fault handlers:
public function checkRole(role:String):void { identity.hasRole(role, checkRoleResult, checkRoleFault); } private function checkRoleResult(event:TideResultEvent, role:String):void { if (role == 'admin') { if (event.result) trace("User has admin role"); else trace("User does not have admin role"); } }
<graniteds:messaging-destination id="myTopic"/>
This declaration supports the properties no-local
and session-selector
(see the
Messaging Configuration section).
You can also define a secure destination by specifying a list of roles required to access the topic:
<graniteds:messaging-destination id="myTopic">
<graniteds:roles>
<graniteds:role>admin</graniteds:role>
</graniteds:roles>
<graniteds:messaging-destination/>
JMS Topic:
<graniteds:jms-messaging-destination id="myTopic"
connection-factory="ConnectionFactory"
destination-jndi-name="topic/MyTopic"
transacted-sessions="true"
acknowledge-mode="AUTO_ACKNOWLEDGE"/>
This declaration supports all properties of the default JMS declaration in services-config.xml
except for non local initial context
environments (see the JMS Integration section).
ActiveMQ Topic:
<graniteds:activemq-messaging-destination id="myTopic"
connection-factory="ConnectionFactory"
destination-jndi-name="topic/MyTopic"
transacted-sessions="true"
acknowledge-mode="AUTO_ACKNOWLEDGE"
broker-url="vm://localhost"
create-broker="true"
wait-for-start="true"
durable="true"
file-store-root="/opt/activemq/data"/>
This declaration supports all properties of the default ActiveMQ declaration in services-config.xml
except for non-local initial context
environments (see the ActiveMQ Integration section).
Finally note that the Gravity
singleton that is needed to push messages from the server (see here)
is available as a Seam 2 component with the name org.granite.seam.gravity
and can be injected in any component :
@In("org.granite.seam.gravity")
private Gravity gravity;
The specification is a powerful new feature of Java EE 6. It integrates on a common programming model all the services provided by Java EE.
GraniteDS provides out-of-the-box integration with CDI via the Tide API. You can remotely call CDI beans, and it fully supports serialization of JPA entities from and to your Flex application, taking care of lazily loaded associations. The support for JPA entity beans is covered in the section JPA and lazy initialization, so this section will only describe how to call Seam components from a Flex application. GraniteDS also integrates with container security for authentication and role-based authorization.
The support for CDI is included in the library granite-cdi.jar
, so you always have to include this library in either
WEB-INF/lib
or lib
for an ear packaging.
Only the reference implementation is supported for now. This is the one used in JBoss 6 and GlassFish v3.
To provide a more native experience for CDI developers when used in a Servlet 3 compliant container, the CDI support in GraniteDS can be configured
with a simple annotated class. The most important features of GraniteDS can be configured this way, and it is still possible to fall back to the
default GraniteDS configuration files services-config.xml
and granite-config.xml
for unsupported features.
@FlexFilter(configProvider=CDIConfigProvider.class)
public class GraniteConfig {
}
@FlexFilter(
tide=true,
type="cdi",
factoryClass=CDIServiceFactory.class,
tideInterfaces={Identity.class}
)
public class GraniteConfig {
}
Other configurations can be done with @FlexFilter
:
tideAnnotations
is equivalent to tide-component annotated-with=""
in granite-config.xml
.
It allows to define the list of annotation names that enable remote access to CDI beans. @RemoteDestination
and
@TideEnabled
are always declared by default, but you can use any other one if you don't want a compilation dependency
on the GraniteDS libraries.
tideInterfaces
is equivalent to tide-component instance-of=""
in granite-config.xml
.
It allows to define the list of interface/class names that enable remote access to CDI beans.
tideRoles
allows to define a list of security roles that are required to access the Tide remote destination.
In general it is not necessary to define this destination-wide security and you can only rely on Java EE security for fine-grained access to individual
beans.
exceptionConverters
allows to define a list of server-side exception converters.
It's the equivalent to exception-converters
in granite-config.xml
.
amf3MessageInterceptor
allows to define a message interceptor. You have to define a class implementing
AMFMessageInterceptor
. It's highly recommended to subclass org.granite.cdi.CDIInterceptor
and call super.before
and super.after
in your implementation.
tide-annotations
section of granite-config.xml
the conditions
used to enable remote access to Seam destinations (for example all beans annotated with a particular annotation).
org.granite.tide.cdi.CDIServiceFactory
service factory
in services-config.xml
.
cdi
in services-config.xml
Cdi.getInstance().getCdiContext()
instead of Tide.getInstance().getContext()
.
Here is a default configuration suitable for most cases:
<granite-config scan="true">
...
<tide-components>
<tide-component annotated-with="org.granite.messaging.service.annotations.RemoteDestination"/>
<tide-component annotated-with="org.granite.tide.annotations.TideEnabled"/>
</tide-components>
</granite-config>
<services-config>
<services>
<service id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<!--
! Use "tideCdiFactory" and "my-graniteamf" for "cdi" destination (see below).
! The destination must be "cdi" when using Tide with default configuration.
!-->
<destination id="cdi">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>tideCdiFactory</factory>
</properties>
</destination>
</service>
</services>
<!--
! Declare tideCdiFactory service factory.
!-->
<factories>
<factory id="tideCdiFactory" class="org.granite.tide.cdi.CdiServiceFactory"/>
</factories>
<!--
! Declare my-graniteamf channel.
!-->
<channels>
<channel-definition id="my-graniteamf" class="mx.messaging.channels.AMFChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
</services-config>
The destination named cdi
will be the one and only destination required for all CDI destinations.
Most of what is described in the Tide Remoting section applies for CDI, however GraniteDS also provides a much improved integration with CDI when using the Tide client API.
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="Cdi.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.cdi.Cdi;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;
[In]
public var helloService:Component;
private function hello(name:String):void {
helloService.hello(name, resultHandler, faultHandler);
}
private function resultHandler(event:TideResultEvent):void {
outputMessage.text = event.result as String;
}
private function faultHandler(event:TideFaultEvent):void {
// Handle fault
}
</mx:Script>
<!-- Provide input data for calling the service. -->
<mx:TextInput id="inputName"/>
<!-- Call the web service, use the text in a TextInput control as input data.-->
<mx:Button click="hello(inputName.text)"/>
<!-- Result message. -->
<mx:Label id="outputMessage"/>
</mx:Application>
This is almost identical to the standard Tide API described in the Tide remoting section, and all other methods apply for CDI.
You can benefit from the capability of the Gas3 code generator (see here) to generate a strongly typed
ActionScript 3 client proxy from the CDI bean interface when it is annotated with @RemoteDestination
.
In this case, you can inject a typesafe reference to your service and get better compile time error checking and auto completion in your IDE:
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="Cdi.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.cdi.Cdi;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;
import com.myapp.service.HelloService;
[In]
public var helloService:HelloService;
private function hello(name:String):void {
helloService.hello(name, resultHandler, faultHandler);
}
...
</mx:Script>
...
</mx:Application>
It is possible to benefit from even more type safety by using the annotation [Inject]
instead of In
.
When using this annotation, the full class name is used to find the target bean in the CDI context instead of the bean name.
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="Cdi.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.cdi.Cdi;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;
import com.myapp.service.HelloService;
[Inject]
public var myService:HelloService;
private function hello(name:String):void {
myService.hello(name, resultHandler, faultHandler);
}
...
</mx:Script>
...
</mx:Application>
This typesafe mode allows to better detect API inconsistencies between the Flex application and the Java services, because the Flex compiler will immediately warn you when a server method signature has changed (and Gas3 has regenerated the client proxy).
@Stateful
public class HotelBookingAction implements HotelBooking {
...
@Inject
private Conversation conversation;
public void selectHotel(Hotel selectedHotel) {
conversation.begin();
hotel = em.merge(selectedHotel);
}
...
}
public function selectHotel(hotel:Hotel):void { (1) tideContext.hotelBooking.selectHotel(hotel, selectHotelResult); } private function selectHotelResult(event:TideResultEvent):void { (2) var localContext:Context = event.context as Context; var hotel:Hotel = localContext.hotel; }
var localContext:Context = Cdi.getInstance().getCdiContext(conversationId)
@Stateful
public class HotelBookingAction implements HotelBooking {
...
@Inject
@Confirmed
private Event<BookingEvent> bookingConfirmedEventSrc;
...
public void confirm() {
em.persist(booking);
bookingConfirmedEventSrc.fire(new BookingEvent(booking));
conversation.end();
}
}
[Observer(remote="true")] public function bookingConfirmedHandler(event:BookingEvent):void { Alert.show("Booking confirmed: " + event.booking); }
private var tideContext:Context = Cdi.getInstance().getCdiContext(); public function login(username:String, password:String):void { tideContext.identity.login(username, password, loginResult, loginFault); } private function loginResult(event:TideResultEvent):void { Alert.show(event.context.identity.loggedIn); } private function loginFault(event:TideFaultEvent):void { Alert.show(event.fault); } public function logout():void { tideContext.identity.logout(); }
[In] public var identity:Identity; public function login(username:String, password:String):void { identity.login(username, password, loginResult, loginFault); } private function loginResult(event:TideResultEvent):void { Alert.show(event.context.identity.loggedIn); } private function loginFault(event:TideFaultEvent):void { Alert.show(event.fault); } public function logout():void { identity.logout(); }
<mx:ViewStack id="main" selectedIndex="{identity.loggedIn ? 1 : 0}">
<views:LoginView id="loginView"/>
<views:MainView id="mainView"/>
</mx:ViewStack>
<mx:Button id="deleteButton"
label="Delete"
enabled="{identity.hasRole('admin')}"
click="myService.deleteEntity(myEntity)"/>
This can also be used as any remote class with result and fault handlers:
public function checkRole(role:String):void { identity.hasRole(role, checkRoleResult, checkRoleFault); } private function checkRoleResult(event:TideResultEvent, role:String):void { if (role == 'admin') { if (event.result) trace("User has admin role"); else trace("User does not have admin role"); } }
@FlexFilter()
public class MyConfig {
@MessagingDestination(noLocal=true, sessionSelector=true)
AbstractMessagingDestination myTopic;
}
This declaration supports the properties no-local
and session-selector
(see the
Messaging Configuration section).
You can also define a secure destination by specifying a list of roles required to access the topic:
@MessagingDestination(noLocal=true, sessionSelector=true, roles={ "admin", "user" })
AbstractMessagingDestination myTopic;
JMS Topic:
@JMSTopicDestination(noLocal=true,
sessionSelector=true,
connectionFactory="ConnectionFactory",
topicJndiName="topic/myTopic",
transactedSessions=true,
acknowledgeMode="AUTO_ACKNOWLEDGE",
roles={ "admin", "user" })
AbstractMessagingDestination myTopic;
This declaration supports all properties of the default JMS declaration in services-config.xml
except for non local initial context
environments (see the JMS Integration section).
ActiveMQ Topic:
@ActiveMQTopicDestination(noLocal=true,
sessionSelector=true,
connectionFactory="ConnectionFactory",
topicJndiName="topic/myTopic",
transactedSessions=true,
acknowledgeMode="AUTO_ACKNOWLEDGE",
brokerUrl="vm://localhost",
createBroker=true,
waitForStart=true,
durable=true,
fileStoreRoot="/opt/activemq/data",
roles={ "admin", "user" })
AbstractMessagingDestination myTopic;
This declaration supports all properties of the default ActiveMQ declaration in services-config.xml
except for non-local initial context
environments (see the ActiveMQ Integration section).
Finally note that the Gravity
singleton that is needed to push messages from the server (see here)
is available as a CDI bean and can be injected in any component :
@Inject
private Gravity gravity;
The "Bean Validation" (aka JSR-303) standardizes an annotation-based validation framework for Java. It provides an easy and powerful way of processing bean validations, with a pre-defined set of constraint annotations, allowing to arbitrarily extend the framework with user specific constraints.
Flex doesn't provide by itself such a framework. The standard way of processing validation is to use subclasses and to bind a validator to each user input (see ). This method is at least time consuming for the developer, source of inconsistencies between the client-side and the server-side validation processes, and source of redundancies in your MXML code.
Starting with the release 2.2, GraniteDS introduces an ActionsScript3 implementation of the Bean Validation specification and provides code generation tools integration so that your Java constraint annotations are reproduced in your AS3 beans.
Constraint | Description |
---|---|
AssertFalse | The annotated element must be false |
AssertTrue | The annotated element must be true |
DecimalMax | The annotated element must be a number whose value must be lower or equal to the specified maximum |
DecimalMin | The annotated element must be a number whose value must be greater or equal to the specified minimum |
Digits | The annotated element must be a number within accepted range |
Future | The annotated element must be a date in the future |
Max | The annotated element must be a number whose value must be lower or equal to the specified maximum |
Min | The annotated element must be a number whose value must be greater or equal to the specified minimum |
NotNull | The annotated element must not be null |
Null | The annotated element must be null |
Past | The annotated element must be a date in the past |
Pattern | The annotated String must match the specified regular expression |
Size | The annotated element size must be between the specified boundaries (included) |
Each of these contraint annotation may be applied on a bean property, depending on its type and its expected value:
Annotated AS3 Bean Properties:
public class MyAnnotatedBean { [NotNull] [Size(min="2", max="8")] public var name:String; private var _description:String; [Size(max="255")] public function get description():String { return _description; } public function set description(value:String) { _description = value; } }
In the above code sample, the name
value must not be null
and its length must be between 2 and 8 characters,
and the description
value may be null
or may have a length of maximum 255 characters. Constraint annotations
must be placed on public properties, either public variables or public accessors (and they may also be placed on the class itself).
In order to validate an instance of the above class, you may use the ValidatorFactory
class.
import org.granite.validation.ValidatorFactory; import org.granite.validation.ConstraintViolation; var bean:MyAnnotatedBean = new MyAnnotatedBean(); var violations:Array = ValidatorFactory.getInstance().validate(bean); trace((violations[0] as ConstraintViolation).message); // "may not be null" bean.name = "123456789"; violations = ValidatorFactory.getInstance().validate(bean); trace((violations[0] as ConstraintViolation).message); // "size must be between 2 and 8" bean.name = "1234"; violations = ValidatorFactory.getInstance().validate(bean); trace(violations.length); // none...
Validation may be much more complex than the above basic sample. GraniteDS validation framework supports all advanced concepts of the specification, such as groups, group sequences, default group redefinition, traversable resolver, message interpolator, etc. Please refer to the specification and the various tutorials you may find on the Net.
Compilation Tip: You must use the compiler option
-keep-as3-metadata+=AssertFalse,AssertTrue,DecimalMax,DecimalMin, Digits,Future,Max,Min,NotNull,Null,Past,Pattern,Size
or the corresponding configuration for your build system (see Project Setup for Ant and Maven)
in order to tell the Flex compiler to keep the constraint annotations in your compiled code (Flash Builder 4 appears to keep all metadata by default,
but the mxmlc
command line compiler doesn't)! If you write your own constraints, you will also have to tell the compiler about
them in the same way.
public class MyBean { [NotNull(message="Name is mandatory"] [Size(min="2", message="Name must have a length of at least {min} characters")] public var name; ... }
In order to add support for different locales, you will have to define variables (eg. name.notnull
and name.minsize
)
and use the built-in support offered by Flex:
public class MyBean { [NotNull(message="{name.notnull}"] [Size(min="2", message="{name.minsize}")] public var name; ... }
locale/en_US/ValidationMessages.properties
name.notnull=Name is mandatory name.minsize=Name must have a length of at least {min} characters
locale/fr_FR/ValidationMessages.properties
name.notnull=Le nom est obligatoire name.minsize=Le nom doit avoir une taille d'au moins {min} caractères
Register your Bundles:
[ResourceBundle("ValidationMessages")]
If you compile your Flex application with support for these two locales (see Flex ), the error messages will be localized in english or french, depending on the current selected locale, with the values set in your property files. You may also redefine standard messages for a given locale in the same way:
locale/en_US/ValidationMessages.properties
name.notnull=Name is mandatory name.minsize=Name must have a length of at least {min} characters javax.validation.constraints.NotNull.message=This value is mandatory
With the above bundle, the default error message for the NotNull
constraint and the locale en_US
will be
redefined to "This value is mandatory" (instead of "may not be null").
Adding support for one or more locales other than the default ones will follow the same principle: create a ValidationMessages.properties
for the new locale, translate all default error messages and add new ones for your customized message keys. Note that the bundle name must always
be set to "ValidationMessages"
.
package path.to { public interface MyGroup {} } ... public class MyBean { [NotNull] [Size(min="2", max="10", goups="path.to.MyGroup")] public var name; ... } ... var bean:MyBean = new MyBean(); // Default group: NotNull fails. ValidatorFactory.getInstance().validate(bean); // MyGroup group: no failure. ValidatorFactory.getInstance().validate(bean, [MyGroup]); // Default & MyGroup groups: NotNull fails. ValidatorFactory.getInstance().validate(bean, [Default, MyGroup]); bean.name = "a"; // Default group: no failure. ValidatorFactory.validate(bean); // MyGroup group: Size fails. ValidatorFactory.getInstance().validate(bean, [MyGroup]); // Default & MyGroup groups: Size fails. ValidatorFactory.getInstance().validate(bean, [Default, MyGroup]);
ValidatorFactory.getInstance().namespaceResolver.registerNamespace("g", "path.to.*"); ... [Size(min="2", max="10", goups="g:MyGroup, g:MyOtherGroup")] public var name;
Note that the
group interface is always registered in the default namespace and may be use without any prefix specification: groups="Default"
is legal and strictly equivalent to groups="org.granite.validation.groups.Default"
(or even groups="javax.validation.groups.Default"
- as the javax
package is handled as an alias
of the granite
's one).
With the Ant task, use the entityfactory
attribute as follow in your build.xml
:
<gas3 entityfactory="org.granite.generator.as3.BVEntityFactory" ...>
...
</gas3>
Then, provided that you have a Java entity bean like this one:
@Entity
public class Person {
@Id @GeneratedValue
private Integer id;
@Basic
@Size(min=1, max=50)
private String firstname;
@Basic
@NotNull(message="You must provide a lastname")
@Size(min=1, max=255)
private String lastname;
// getters and setters...
}
... you will get this generated ActionScript3 code:
[Bindable] public class PersonBase implements IExternalizable { ... public function set firstname(value:String):void { _firstname = value; } [Size(min="1", max="50", message="{javax.validation.constraints.Size.message}")] public function get firstname():String { return _firstname; } public function set lastname(value:String):void { _lastname = value; } [NotNull(message="You must provide a lastname")] [Size(min="1", max="255", message="{javax.validation.constraints.Size.message}")] public function get lastname():String { return _lastname; } .... }
public class PersonChecker extends BaseConstraint { override public function initialize(annotation:Annotation, factory:ValidatorFactory):void { // initialize the BaseContraint with the default message (a bundle key). internalInitialize(factory, annotation, "{personChecker.message}"); } override public function validate(value:*):String { // don't validate null Person beans. if (Null.isNull(value)) return null; // check value type (use helper class). ConstraintHelper.checkValueType(this, value, [Person]); // validate the Person bean: at least one of the firstname or lastname property // must be not null. if (Person(value).firstname == null && Person(value).lastname == null) return message; // return null if validation is successful. return null; } }
The PersonChecker
class actually extends the BaseContraint
class that simplifies IConstraint
implementations. It defines a default message ("{personChecker.message}"
) with a message key that could be used in your validation
messages bundles (see above Working with Error Messages and Localization).
You should then register this new constraint in the validation framework:
ValidatorFactory.getInstance().registerConstraintClass(PersonChecker);
Because Flex annotations have no specific implementation, you may then directly use the constraint annotation in the Person
class:
[Bindable] [PersonChecker] public class Person { [Size(min="1", max="50")] public var firstname; [Size(min="1", max="255")] public var lastname; }
Note that the annotation isn't qualified with any package name: registering two constraint class with the same name but in different packages
will result in using the last registered one only. This behavior may additionaly be used in order to override default constraint implementations:
if you write your own Size
constraint implementation and register it with the ValidatorFactory
class, it will
be used instead of the built-in one.
If the constraint exists in Java and if you use the code generation tools, the unqualified class name of the Java constraint will be generated on top
of the Person
class, just as above.
Don't forget the -keep-as3-metadata+=AssertFalse,...,Size,PersonChecker
compiler option!
See standard constraint implementations in the GraniteDS distribution to know more about specific attributes support and other customization options.
private function startValidationHandler(event:ValidationEvent):void { // reset all error messages... } private function constraintViolationHandler(event:ConstraintViolationEvent):void { // display the error message on the corresponding input... } private function endValidationHandler(event:ValidationEvent):void { // done... } ... bean.addEventListener( ValidationEvent.START_VALIDATION, startValidationHandler, false, 0, true ); bean.addEventListener( ConstraintValidatedEvent.CONSTRAINT_VALIDATED, constraintValidatedHandler, false, 0, true ); bean.addEventListener( ValidationEvent.END_VALIDATION, andValidationHandler, false, 0, true ); ... ValidatorFactory.getInstance().validate(bean);
A sample usage with Flex 4 (using the Person bean introduced above and bidirectional bindings):
<fx:Declarations>
<v:FormValidator id="fValidator" form="{personForm}" entity="{person}"/>
</fx:Declarations>
<fx:Script>
[Bindable]
protected var person:Person = new Person();
protected function savePerson():void {
if (fValidator.validateEntity()) {
// actually save the validated person entity...
}
}
protected function resetPerson():void {
person = new Person();
}
</fx:Script>
<mx:Form id="personForm">
<mx:FormItem label="Firstname">
<s:TextInput id="iFirstname" text="@{person.firstname}"/>
</mx:FormItem>
<mx:FormItem label="Lastname" required="true">
<s:TextInput id="iLastname" text="@{person.lastname}"/>
</mx:FormItem>
</mx:Form>
<s:Button label="Save" click="savePerson()"/>
<s:Button label="Cancel" click="resetPerson()"/>
<fx:Declarations>
<v:FormValidator id="fValidator" form="{personForm}" entity="{model.person}" entityPath="model"/>
</fx:Declarations>
To solve this problem, three options are available:
(1) Unhandled Violations with the "properties" Argument:
[Bindable] [PersonChecker(properties="firstname,lastname"] public class Person { ... }
(2) Unhandled Violations with the unhandledViolationsMessage Property:
<mx:Form id="personForm">
<mx:FormItem label="Firstname">
<s:TextInput id="iFirstname" text="@{person.firstname}"/>
</mx:FormItem>
<mx:FormItem label="Lastname" required="true">
<s:TextInput id="iLastname" text="@{person.lastname}"/>
</mx:FormItem>
<s:Label text="{fValidator.unhandledViolationsMessage}"/>
</mx:Form>
(3) Unhandled Violations with the unhandledViolations Event:
<fx:Declarations>
<v:FormValidator id="fValidator" form="{personForm}" entity="{person}"
unhandledViolations="showUnhandledViolations(event)"/>
</fx:Declarations>
<fx:Script>
protected function showUnhandledViolations(event:ValidationResultEvent ):void {
// display unhandled messages...
}
</fx:Script>
<mx:TextInput id="iFirstname" text="{person.firstname}"/>
...
<mx:TextInput id="iLastname" text="{person.lastname}"/>
...
<mx:Binding destination="person.firstname" source="iFirstname.text"/>
<mx:Binding destination="person.lastname" source="iLastname.text"/>
Note also that with Tide, to simplify the cancel operations, you may reset the entity state with Managed.resetEntity(entity)
(see Data Management. This may be particularly useful if you are not creating a new person but modifying
an existing one.
If you don't want or if you can't use bidirectional bindings, you may still use the FormValidator
component but will need to
specify the property validationSubField
for each input:
<fx:Declarations>
<v:FormValidator id="fValidator" form="{personForm}" entity="{person}"/>
</fx:Declarations>
<fx:Script>
[Bindable]
protected var person:Person = new Person();
protected function savePerson():void {
person.firstname = iFirstname.text == "" ? null : iFirstname.text;
person.lastname = iLastname.text == "" ? null : iLastname.text;
if (fValidator.validateEntity()) {
// actually save the validated person entity...
}
}
protected function resetPerson():void {
person = new Person();
}
</fx:Script>
<mx:Form id="personForm">
<mx:FormItem label="Firstname">
<s:TextInput id="iFirstname" text="{person.firstname}"
validationSubField="firstname"/>
</mx:FormItem>
<mx:FormItem label="Lastname" required="true">
<s:TextInput id="iLastname" text="{person.lastname}"
validationSubField="lastname"/>
</mx:FormItem>
</mx:Form>
This time, you have to set manually input values into your bean, but this will work with Flex 3 as well and these subfields may contain a path
to a subproperty: for example, if you have an Address
bean in your Person
bean, you could write
validationSubField="address.address1"
.
A last option to help the FormValidator
detect the data bindings is to define a global list of properties which will be
considered as UI component targets for bindings. By default, text
, selected
, selectedDate
,
selectedItem
and selectedIndex
are prioritarily considered for binding detection so most standard controls
work correctly (for example TextInput
, TextArea
, CheckBox
or DatePicker
).
All standard constraints should behave exactly in the same way as they behave in Java, except for some advanced Pattern usages: because the regular expression support in ActionScript 3 may differ from the Java one (especially with supported ), you should be aware of few possible inconstancies between Pattern constraints written in Java and in ActionScript3.
The built-in ActionScript 3 reflection API is basically limited to a single method: . This method returns XML data describing its parameter object and is therefore not type-safe and its use is subject to many syntax errors.
GraniteDS provides a Java-like reflection API that encapsulates describeType
calls and offers a type-safe, object-oriented, set of
reflection classes and methods. This API caches its results for better performances and supports advanced features such as ApplicationDomain
and namespaces.
import org.granite.reflect.Type; var type:Type = Type.forName("path.to.MyClass"); // or: var type:Type = Type.forName("path.to::MyClass"); From an Instance import org.granite.reflect.Type; import path.to.MyClass; var type:Type = Type.forInstance(new MyClass());
import org.granite.reflect.Type; import path.to.MyClass; var type:Type = Type.forClass(MyClass);
Whatever method you use, you will get a unique Type
instance for each ActionScript 3 class (see below, however, the
ApplicationDomain Support section for very rare exceptions). This Type
instance will give
you access to all informations about the underlying class, such as superclasses and implemented interfaces, fields, methods, constructor and annotations
(see API documentation ).
You may get all public members of a given Type
via its members property. It will return an array of
subclasses such as
,
and
:
import org.granite.reflect.Type; import org.granite.reflect.Member; import org.granite.reflect.Field; import org.granite.reflect.Method; import org.granite.reflect.Constructor; var type:Type = Type.forName("path.to.MyClass"); var members:Array = type.members; trace("Members of type: " + type.name); for each (var member:Member in members) { if (member is Field) trace("Field " + Field(member).name + ":" + Field(member).type.name); else if (member is Method) trace("Method " + Method(member).name + ":" + Method(member).returnType.name); else if (member is Constructor) trace("Constructor " + Constructor(member).name); }
Instead of using the general members property, you may use specialized properties such as fields
, methods
,
constructor
or even properties
:
properties are all not-static, public, read-write properties of a bean, either variables or accessors (get/set methods).
You may also retrieve a method (or field) by its name:
var type:Type = Type.forName("path.to.MyClass"); var field:Field = type.getInstanceField("myPropertyName"); if (field == null) trace("Could not find 'myPropertyName' field in: " + type.name); var method:Method = type.getInstanceMethod("myMethodName"); if (method == null) trace("Could not find 'myMethodName' method in: " + type.name);
Unlike Java, the API distinguishes getInstanceField
and getStaticField
, as well as getInstanceMethod
and getStaticMethod
: the reason is that the ActionScript 3 language allows a class to declare a static and a instance variable
(or method) with the same name in the same class.
Furthermore, the API allows to filter returned members. For example, if you are interested in instance methods that have at least two parameters, you might write:
var type:Type = Type.forName("path.to.MyClass"); var methods:Array = type.getMethods(function (m:Method):Boolean { return !m.isStatic() && m.parameters.length >= 2; });
You may of course use the same kind of code for filtering fields or properties.
Four main methods are available to play with annotations (see the interface) for classes, fields and methods.
var type:Type = Type.forName("path.to.MyClass"); var annotations:Array = type.annotations; for each (var annotation:Annotation in annotations) { var args:Array = annotation.args; trace(annotation.name + " with " + args.length + "args {"); for each (var arg:Arg in args) trace(arg.key + "=" + arg.value); trace("}"); }
Looking for a specific annotation:
var type:Type = Type.forName("path.to.MyClass"); if (type.isAnnotationPresent("MyAnnotationName")) { trace("Found annotation" + type.getAnnotation("MyAnnotationName").name); }
Filtering annotations based on a name pattern:
var type:Type = Type.forName("path.to.MyClass"); var annotations:Array = type.getAnnotations(false, "MyPrefix.*");
In the later case, the annotation name pattern is a regular expression that matches all annotations that have a name starting with "MyPrefix".
Note also that all these methods allow to look recursively for annotations:
public class MyClass implements MyInterface { public function doSomething():void {} } ... [MyAnnotation1] public interface MyInterface { [MyAnnotation2] function doSomething():void; } ... var type:Type = Type.forName("path.to.MyClass"); var method:Method = type.getInstanceMethod("doSomething"); if (type.isAnnotationPresent("MyAnnotation1", true)) trace("Found annotation" + type.getAnnotation("MyAnnotation1", true).name); if (method.isAnnotationPresent("MyAnnotation2", true)) trace("Found annotation" + method.getAnnotation("MyAnnotation2", true).name);
The boolean parameter set to true
in isAnnotationPresent
and getAnnotation
calls tells the API
to look recursively for the annotation, and this code will actually print that the two annotations were found.
Beside these IAnnotatedElement
methods, the Type class allows to quickly retieve methods or field annotated specific annotations:
var type:Type = Type.forName("path.to.MyClass"); var annotations:Array = type.getAnnotatedFields(false, "Bindable", "MyAnnotation");
This code will return all fields annotated by at least one of the [Bindable]
or [MyAnnotation]
annotations.
The reflection API let you create new instances of a given class the following manner:
Creating new instances of a class:
var type:Type = type.forName("path.to.MyClass"); var instance:Object = type.constructor.newInstance(param1, param2); // or type.constructor.newInstanceWithArray([param1, param2]);
You may also call methods in a similar manner:
var type:Type = type.forName("path.to.MyClass"); var myInstanceMethod:Method= type.getInstanceMethod("myInstanceMethod"); myInstanceMethod.invoke(myClassInstance, param1, param2); // or myInstanceMethod.invokeWithArray(myClassInstance, [param1, param2]); var myStaticMethod:Method= type.getStaticMethod("myStaticMethod"); myStaticMethod.invoke(null, param1, param2); // or myStaticMethod.invokeWithArray(null, [param1, param2]);
var type:Type = type.forName("path.to.MyClass"); var myInstanceField:Field= type.getInstanceField("myInstanceField"); var value:* = myInstanceField.getValue(myClassInstance); myInstanceField.setValue(myClassInstance, "newValue"); var myStaticField:Field= type.getStaticField("myStaticField"); var value:* = myStaticField.getValue(null); myStaticField.setValue(null, "newValue");
Like the class in Java, the ActionScript 3 language has support for class loading in different contexts called . This is an advanced feature that is only useful if you work with multiple Flex modules: SWF modules are loaded at runtime with their own set of classes and these classes may be owned and declared by a specific application domain.
Loading a module in a child ApplicationDomain
:
var childDomain:ApplicationDomain = new ApplicationDomain(ApplicationDomain.currentDomain); var context:LoaderContext = new LoaderContext(false, childDomain); var loader:Loader = new Loader(); loader.load(new URLRequest("module.swf"), context);
If a class is declared only in the above module (but not in the main application), it will be only available in the new child application domain.
As such, the following code will fail with a ClassNotFoundError
exception:
try { var type:Type = Type.forName("path.to.MyModuleClass"); } catch (e:ClassNotFoundError) { // Cannot be found in the main ApplicationDomain. }
The first solution is to pass the child domain as a parameter:
var type:Type = Type.forName("path.to.MyModuleClass", childDomain);
This will work, but a better solution would be to register the child domain when loading the new module, so that the reflection API will look for classes in this child domain if it can't find it in the main domain:
var childDomain:ApplicationDomain = new ApplicationDomain(ApplicationDomain.currentDomain); // register the child domain. Type.registerDomain(childDomain); var context:LoaderContext = new LoaderContext(false, childDomain); var loader:Loader = new Loader(); loader.load(new URLRequest("module.swf"), context); // the type is found in the child domain without explicit reference. var type:Type = Type.forName("path.to.MyModuleClass");
If you use an unknown domain parameter in a Type.forName
call, it is automatically registered. Thus, the sample call to
Type.forName("path.to.MyModuleClass", childDomain)
above will register the childDomain
domain because this domain
isn't already known by the API.
When you unload a module, you should always unregister any specific application domain by calling:
Type.unregisterDomain(childDomain);
This will cleanup the API cache with all classes previously loaded in this domain.
The ApplicationDomain
concept in the Flash VM allows you to load multiple versions of a class (same qualified name) into different domains.
If you have loaded two modules with two versions of the same class and if you have registered their respective two domains with the
registerDomain
method, you must nonetheless explicitly refer to each domain when loading the class by its name.
Otherwise, the Type.forName("path.to.MyClassIn2Domains")
call will throw a AmbiguousClassNameError
exception.
The ActionScript 3 language lets you declare that may be used instead of the usual public namespace. The reflection API may be used in order to find a pethod or a field in a specific namespace:
package my.namespaces { public namespace my_namespace = "http://www.my.org/ns/my_namespace"; } ... public class MyClass { import my.namespaces.my_namespace; my_namespace var myField:String; } ... import my.namespaces.my_namespace; var type:Type = Type.forName("path.to.MyClass"); var field:Field = type.getInstanceField("myField", my_namespace);
Because the myField
variable is declared in a specific namespace, a call to getInstanceField
without the
my_namespace
parameter will return null
. Adding this optional parameter will fix the problem.
When you use the type.fields
property, all accessible fields are returned, including those declared in specific namespaces.
The reflection API comes with a visitor pattern implementation that let you introspect class instances without looping recursively on all their properties. The entry point of this visitor API is the class: it implements an advanced two-phases visitor mechanism (see the interface) that let you first review which property you're interested in and then actually visit the selected ones.
This is a feature for advanced uses only, please refer to the API documentation and .
Number serialization with the standard AMF3 protocol suffers from a lack of precision and support: Java long
(64 bits integers),
and
types are converted to
ActionScript 3 or String
(see ).
These conversions lead to either approximation (significant bits may be lost) or uselessness (you can't do any arithmetic operation with strings and you
can't control the way their string representations are produced).
Because GraniteDS doesn't allow string to number or number to string conversions (see Mapping Java and AS3 Objects),
BigInteger
and BigDecimal
, like long
types, are both converted to Number
by default, with even more potential approximations.
Starting with the release 2.2, GraniteDS offers ActionScript 3 implementations for Long
, BigInteger
and
BigDecimal
, and features a serialization mechanism that preserves the exact value of each type (see API documentation
).
import org.granite.math.Long; var a:Long = new Long("9223372036854775807"); // or 0x7fffffffffffffff (max long value) trace(a); // "9223372036854775807" trace(a.toHexString()); // "7fffffffffffffff" a = a.subtract(7); trace(a); // "9223372036854775800" trace(a.toHexString()); // "7ffffffffffffff8" a = a.rightShift(4); // or a.divide(16) trace(a); // "576460752303423487" trace(a.toHexString()); // "7ffffffffffffff" // etc. // Wrong values with Numbers: var b:Number = new Number("9223372036854775807"); // max long value trace(b); // "9223372036854776000" (truncated value)...
The class, as its Java equivalent, represent an immutable arbitrary-precision integer. It provides analogues to all of ActionScript 3's primitive integer operators (+, -, *, /), as well as comparison operators.
The BigInteger Type:
import org.granite.math.BigInteger; var a:BigInteger = new BigInteger("9223372036854775807"); // max long value a = a.add(1); trace(a); // "9223372036854775808" a = a.multiply(1000000); trace(a); // "9223372036854775808000000" // etc.
With the BigInteger
class, you cannot face the risk of an overflow due to the limited storage of a standard numeric type:
a BigInteger
value can be arbitrary big and its value is only limited by the Flash VM memory.
The class, as its Java equivalent, represent an immutable, arbitrary-precision signed decimal number. It provides operations for arithmetic, scale manipulation, rounding, comparison and format conversion.
The BigDecimal Type:
import org.granite.math.BigDecimal; import org.granite.math.RoundingMode; var a:BigDecimal = new BigDecimal("1"); // or BigDecimal.ONE a = a.divide(3, 2, RoundingMode.DOWN); trace(a); // "0.33" // etc.
With the BigDecimal
class, you can control precisely the scale and the rounding behavior of a division. The above code means:
divide 1 by 3, with 2 digits to the right of the decimal point left in the result and apply a down rounding mode (truncate all extra digits).
Like BigInteger
instances, BigDecimal
instances have no precision limitation other than the Flash VM memory.
Arithmetic binary methods are more versatile than their Java equivalents. You may pass not only BigDecimal
instances as parameters to
add, subtract, multiply and divide, but also int
, Number
or String
literals. They will be
automatically converted to BigDecimal
instances and that's why a.add(3)
is legal, as well as
a.add("3")
and a.add(new BigDecimal("3")
. This is also true for the Long
and
BigInteger
types.
See the API documentation for more informations.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC "-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
<externalizers>
<externalizer
type="org.granite.messaging.amf.io.util.externalizer.LongExternalizer">
<include instance-of="java.lang.Long"/>
</externalizer>
<externalizer
type="org.granite.messaging.amf.io.util.externalizer.BigIntegerExternalizer">
<include instance-of="java.math.BigInteger"/>
</externalizer>
<externalizer
type="org.granite.messaging.amf.io.util.externalizer.BigDecimalExternalizer">
<include instance-of="java.math.BigDecimal"/>
</externalizer>
</externalizers>
<granite-config>
You may of course enable only the externalizers you need, instead of configuring all of them.
import java.math.BigDecimal;
public class TestBigDecimal {
public BigDecimal returnBigValue() {
return new BigDecimal("10000000000000000000000000000.001");
}
public void receiveBigValue(BigDecimal value) {
// do something with the value.
}
}
import org.granite.math.BigDecimal; private var testBigDecimalService:RemoteObject = null; private var value:BigDecimal = null; ... protected function onReturnBigValueResult(event:ResultEvent):void { value = event.result as BigDecimal; } ... protected function sendBigValue():void { testBigDecimalService.receiveBigValue(new BigDecimal("0.3333")); }
The same kind of code will work with long
, Long
and BigInteger
types as well.
Beside calling methods that return or receive big numbers, you may have Java bean or entity properties that use long
,
Long
, BigInteger
or BigDecimal
types. The standard GraniteDS code generation tools
(see Gas3 Code Generator) follow the standard serialization mechanism (ie: converting long
and big number types to AS3 numbers) and generates Number
typed variables for Java long and big number types.
In order to tell the code generation tools to generate AS3 Long
, BigInteger
and BigDecimal
typed variables, you must enable three related options.
With the GraniteDS Eclipse builder, you will have to go to the "Options" panel and enable these three options:
With the Gas3 Ant task, you will use the following configuration in build.xml
:
<gas3
externalizelong="true"
externalizebiginteger="true"
externalizebigdecimal="true"
...>
...
</gas3>
Again, you may enable only one or more of these options, but you must follow the corresponding granite-config.xml
configuration.
Suppose you have this kind of Java bean:
import java.math.BigDecimal;
import java.math.BigInteger;
public class MyBean {
private BigDecimal bd;
private BigInteger bi;
private Long l1;
private long l2;
public BigDecimal getBd() {
return bd;
}
public void setBd(BigDecimal bd) {
this.bd = bd;
}
// other get/set...
}
With all options enabled, the result of generation will be has follow:
import org.granite.math.BigDecimal; import org.granite.math.BigInteger; import org.granite.math.Long; [RemoteClass(alias="path.to.MyBean")] public class MyBean { private var _bd:BigDecimal; private var _bi:BigInteger; private var _l1:Long; private var _l2:Long; public function get bd():BigDecimal { return _bd; } public function set bd(value:BigDecimal):void { _bd = value; } // other get/set... }
With standard Gas3 configuration, the ActionScript 3 type generated for each property would have been Number
.
GraniteDS comes with a built-in client framework named Tide that is based on the concept of contextual components and serves as the basis for many advanced features (such as data management or simplified remoting).
This client framework implements some classic features of usual Java frameworks such as Spring or Seam and provides a programming model that should look familiar to Java developers. However it tries to be as transparent and unobtrusive as possible and yet stay close from the Flex framework concepts and traditional usage.
The framework features notably:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
preinitialize="Tide.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.Tide;
import org.granite.tide.Component;
import org.granite.tide.events.TideResultEvent;
[In]
public var helloService:Component;
private function hello():void {
helloService.hello(iname.text, helloResult);
}
private function helloResult(event:TideResultEvent):void {
lmessage.text = event.result as String;
}
</mx:Script>
<mx:TextInput id="iname"/>
<mx:Button label="Hello" click="hello()"/>
<mx:Label id="lmessage"/>
</mx:Application>
This is almost exactly the same example that we have seen in the Tide Remoting chapter. In this first step
we see only the injection feature of Tide with the annotation [In]
that can be placed on any writeable property of the component
(public or with a setter).
Tide will by default consider any injection point with a type Component
(or any type extending Component
)
as a remoting injection and will create and inject a client proxy for a service named helloService
(by default the name of the property
is used as the name of the injected component).
This is more than enough for a Hello World application, but if we continue the application this way, everything will be in the same MXML. So in a first
iteration we want at least to separate the application initialization part and the real UI part. We can for example create a new MXML file named
Hello.mxml
:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
preinitialize="Tide.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.Tide;
</mx:Script>
<Hello id="hello"/>
</mx:Application>
Hello.mxml:
<?xml version="1.0" encoding="utf-8"?>
<mx:Panel
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*">
<mx:Metadata>[Name]</mx:Metadata>
<mx:Script>
import org.granite.tide.Component;
import org.granite.tide.events.TideResultEvent;
[In]
public var helloService:Component;
private function hello():void {
helloService.hello(iname.text, helloResult);
}
private function helloResult(event:TideResultEvent):void {
lmessage.text = event.result as String;
}
</mx:Script>
<mx:TextInput id="iname"/>
<mx:Button label="Hello" click="hello()"/>
<mx:Label id="lmessage"/>
</mx:Panel>
This is a bit better, the main UI is now defined in its own MXML. Tide has not much to do here, but note that we have added the [Name]
metadata annotation on the MXML to instruct Tide that it has to manage the component and inject the dependencies on properties marked with
[In]
. This was not necessary in the initial case because the main application itself is always registered as a component managed by Tide
(under the name application
).
The next step is to decouple our UI component from the server interaction, so we can for example reuse the same UI component in another context or simplify the testing of the server interaction with a mock controller.
The first thing we can do is introduce a controller component (the C in MVC) that will handle this interaction and an interface so that we can easily switch the controller implementation. Then we can just bind it to the MXML component:
package com.myapp.controller { [Bindable] public interface IHelloController { function hello(name:String):void; function get message():String; } }
package com.myapp.controller { import org.granite.tide.Component; import org.granite.tide.events.TideResultEvent; [Name("helloController")] public class HelloController implements IHelloController { [In] public var helloService:Component; [Bindable] public var message:String; public function hello(name:String):void { helloService.hello(name, helloResult); } private function helloResult(event:TideResultEvent):void { message = event.result as String; } } }
We have to configure the controller in the main MXML and use it in the view:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
preinitialize="Tide.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.Tide;
Tide.getInstance().addComponents([HelloController]);
</mx:Script>
<Hello id="hello"/>
</mx:Application>
<?xml version="1.0" encoding="utf-8"?>
<mx:Panel
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*">
<mx:Metadata>[Name]</mx:Metadata>
<mx:Script>
import com.myapp.controller.IHelloController;
[Bindable] [Inject]
public var helloController:IHelloController;
</mx:Script>
<mx:TextInput id="iname"/>
<mx:Button label="Hello" click="helloController.hello(iname.text)"/>
<mx:Label id="lmessage" text="{helloController.message}"/>
</mx:Panel>
This is already quite clean, and completely typesafe. The annotation [Inject]
indicates that Tide should inject any managed
component which class extends or implements the specified type, contrary to the annotation [In]
that is used to inject a
component by name. Here the instance of HelloController
will be injected, in a test case you could easily configure an
alternative TestHelloController
implementing the same interface.
This kind of architecture is inspired by JSF (Java Server Faces) and works fine. However there is still a bit of coupling between the views and the controllers, and it does not really follow the usual event-based style of the Flex framework. To obtain a more pure MVC model, we have to add a model component that will hold the state of the application, and an event class dispatched through the Tide event bus to decouple the view and the controller:
package com.myapp.events { import org.granite.tide.events.AbstractTideEvent; public class HelloEvent extends AbstractTideEvent { public var name:String; public function HelloEvent(name:String):void { super(); this.name = name; } } }
package com.myapp.model { [Bindable] public interface IHelloModel { function get message():String; function set message(message:String):void; } }
package com.myapp.model { [Name("helloModel")] public class HelloModel implements IHelloModel { [Bindable] public var message:String; } }
The controller will now observe our custom event, and set the value of the message property in the model:
package com.myapp.controller { import org.granite.tide.Component; import org.granite.tide.events.TideResultEvent; import com.myapp.events.HelloEvent; [Name("helloController")] public class HelloController implements IHelloController { [In] public var helloService:Component; [Inject] public var helloModel:IHelloModel; [Observer] public function hello(event:HelloEvent):void { helloService.hello(event.name, helloResult); } private function helloResult(event:TideResultEvent):void { helloModel.message = event.result as String; } } }
Lastly we configure the new model component and dispatch the custom event from the UI:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
preinitialize="Tide.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.Tide;
Tide.getInstance().addComponents([HelloController, HelloModel]);
</mx:Script>
<Hello id="hello"/>
</mx:Application>
<?xml version="1.0" encoding="utf-8"?>
<mx:Panel
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*">
<mx:Metadata>[Name]</mx:Metadata>
<mx:Script>
import com.myapp.events.HelloEvent;
import com.myapp.model.IHelloModel;
[Bindable] [Inject]
public var helloModel:IHelloModel;
</mx:Script>
<mx:TextInput id="iname"/>
<mx:Button label="Hello" click="dispatchEvent(new HelloEvent(iname.text))"/>
<mx:Label id="lmessage" text="{helloModel.message}"/>
</mx:Panel>
The main difference here is that we use an event to communicate between the view and the controller. This would allow for example many controllers
to react to the same user action. The view does not know which component will handle the event, and the controllers simply specify that they are interested
in the event HelloEvent
with the annotation [Observer]
on a public handler method. Tide automatically wires
the dispatcher and the observers through its event bus by matching the event type.
Note that the HelloEvent
class extends a (pseudo) abstract class of the Tide framework. If you don't want any such dependency,
you can use any Flex event but then you have to add an annotation [ManagedEvent]
on the dispatcher to instruct Tide which events it
has to manage. See more below in the section Event Bus.
Now we have a completely decoupled and testable architecture, however everything is wired typesafely, meaning that any error will be detected at compile time and not at runtime.
With some Java server frameworks (Spring and CDI) we can even achieve complete client/server type safety by generating a typed client proxy. The controller would then look like:
package com.myapp.controller { import org.granite.tide.Component; import org.granite.tide.events.TideResultEvent; import com.myapp.events.HelloEvent; import com.myapp.service.HelloService; [Name("helloController")] public class HelloController implements IHelloController { [Inject] public var helloService:HelloService; [Inject] public var helloModel:IHelloModel; [Observer] public function hello(event:HelloEvent):void { helloService.hello(event.name, helloResult); } private function helloResult(event:TideResultEvent):void { helloModel.message = event.result as String; } } }
Hopefully you have now a relatively clear idea on what it's all about. The following sections will describe all this in more details.
<mx:Application ...
preinitialize="Tide.getInstance().initApplication()">
...
</mx:Application>
<mx:Application ...
preinitialize="Spring.getInstance().initApplication()">
...
</mx:Application>
The Tide framework makes heavy use of Flex annotations, so you will need to configure your build system (Flash Builder, Ant or Maven) correctly so the Flex compiler keeps the necessary annotations at runtime (see the Project Setup chapter for Ant, Maven and Flash Builder).
The core concepts of the Tide framework are the context and the component.
There are two main kinds of contexts:
The global context object can easily be retrieved from the Tide singleton:
var tideContext:Context = Tide.getInstance().getContext();
var tideContext:Context = Tide.getInstance().getContext("someConversationId");
Note however that this is not the recommended way of working with conversation contexts. See the Conversations section.
Components can be registered programmatically by any of the following methods:
Tide.getInstance().addComponent()
:
Tide.getInstance().addComponent("myComponent", MyComponent):This method takes two main arguments: the component name and the component class.
Tide.getInstance().addComponent(componentName, componentClass, inConversation, autoCreate, restrict);
inConversation
is a boolean
value indicating whether the component in conversation-scoped
(it is false
by default), autoCreate
is true
by default and indicates that the component will
be automatically instantiated by the container. Finally restrict
is related to security and indicates that the component instance
has to be destroyed when the user logs out from the application (so that its state cannot be accessed by unauthenticated users).
Tide.getInstance().addComponentWithFactory("myComponent", MyComponent, { property1: value1, property2: value2 });Of course, this assumes that the component class has accessible setters for the properties specified in the initialization map. Values may be string expressions of the form
#{component.property}
, and are then evaluated at run time as a chain of properties starting
from the specified contextuel component. All other values are assigned as is.
It is alternatively possible (and indeed recommended) to describe the metadata of the component with annotations in the component class. This simplifies the component registration and is often more readable.
Tide.getInstance().addComponents([MyComponent]); [Name("myComponent")] public class MyComponent { public MyComponent():void { } }
A component class must have a default constructor.
Once a component is registered, you can get an instance of the component from the Context
object by its name, for example
tideContext.myComponent
will return the unique instance of the component MyComponent
that we have defined before.
You can also retrieve the instance of a component that extend a particular type with tideContext.byType(MyComponent)
. Of course
it is more useful when specifying an interface so you can get its configured implementation: tideContext.byType(IMyComponent)
.
When many implementations of an interface are expected to exist in the context, you can use tideContext.allByType(IMyComponent)
to retrieve all of them.
If no component has been registered with a particular name, tideContext.someName
will by default return a client proxy for a remote
service named someName
. In particular tideContext.someName
will return null
only if a component
named someName
has been configured with the metadata autoCreate
set to false
.
When using dependency injection annotations ([In]
, [Out]
and [Inject]
) on component properties,
Tide implicitly registers a component of the target type when it is a concrete class (not an interface):
[Name("myInjectedComponent")] public class MyInjectedComponent { [In] public var myComponent:MyComponent; }
Will implicity register a component of class MyComponent
, even if you have never called Tide.addComponent()
for
this type.
Besides all these options for registering components, it is also possible to dynamically assign a component instance at any time in a Tide context with
tideContext.myComponent = new MyComponent()
. This allows you to precisely control the instantiation of the component and will
implicitly register the corresponding component from the object class. For example you can use this feature to switch at runtime between different
implementations of a component interface.
The last case is the one of UI components that are added and removed from the Flex stage. One of the things that Tide does at initialization time
in the method initApplication()
is registering listeners for the Flex events add
and remove
.
On the add
event, it automatically registers any component annotated with [Name]
and puts its instance in the context.
It also removes the instance from the context when getting the remove
event.
Note that this behaviour can incur a significant performance penalty due to the ridiculously slow implementation of reflection in ActionScript
so it can be disabled by Tide.getInstance().initApplication(false)
. You will then have to wire the UI components manually.
[Name("myInjectedComponent")] public class MyInjectedComponent { [In("myComponent")] public var myComponent:IMyComponent; }
[In] public var myComponent:IMyComponent;
You can also use property chain expressions of the form #{mySourceComponent.myProperty}
:
[In("#{mySourceComponent.myProperty}")] public var myComponent:IMyComponent;
[In(create="true")] public var myComponent:IMyComponent;
This ensures that myComponent
will never be null
.
[Name("myOutjectingComponent")] public class MyOutjectingComponent { [Bindable] [Out] public var myComponent:IMyComponent; public function doSomething():void { myComponent = new MyComponent(); } }
[Name("myOutjectingComponent")] public class MyOutjectingComponent { [Bindable] [Out(remote="true")] public var myEntity:MyEntity; public function doSomething():void { myEntity = new MyEntity(); } }
<?xml version="1.0" encoding="utf-8"?>
<mx:Panel
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*">
<mx:Metadata>[Name]</mx:Metadata>
<mx:Script>
import com.myapp.events.HelloEvent;
[Bindable] [In]
public var message:String;
</mx:Script>
<mx:TextInput id="iname"/>
<mx:Button label="Hello" click="dispatchEvent(new HelloEvent(iname.text))"/>
<mx:Label id="lmessage" text="{message}"/>
</mx:Panel>
package com.myapp.controller { import org.granite.tide.events.TideResultEvent; import com.myapp.events.HelloEvent; import com.myapp.service.HelloService; [Name("helloController")] public class HelloController implements IHelloController { [Inject] public var helloService:HelloService; [Bindable] [Out] public var message:String; [Observer] public function hello(event:HelloEvent):void { helloService.hello(event.name, helloResult); } private function helloResult(event:TideResultEvent):void { this.message = event.result as String; } } }
[Name("myInjectedComponent")] public class MyInjectedComponent { [Inject] public var bla:IMyComponent; }
[Inject] public var myContext:Context;
[Name("myComponent")] public class MyComponent { [PostConstruct] public function init():void { // ... } [Destroy] public function cleanup():void { /// ... } }
Let's see in a first step what kind of events can be managed:
org.granite.tide.events.TideUIEvent
are considered as untyped events and only their name is used to match
against observers.
org.granite.tide.events.TideUIEvent
(or TideUIEvent.TIDE_EVENT
), in particular all events
extending the AbstractTideEvent
class are considered as typed events and only their class is used to match against observers.
[ManagedEvent]
annotation on the dispatcher component are also matched by their type.
There are two ways of dispatching untyped events:
public function doSomething():void { dispatchEvent(new TideUIEvent("myEvent", arg1, { arg2: "value" })); }
TideUIEvent
takes a variable list of arguments that will be propagated to all observers.
public function doSomething():void { tideContext.raiseEvent"myEvent", arg1, { arg2: "value" }); }
public class MyEvent extends Event { public var data:Object; public function MyEvent(data:Object):void { super(TideUIEvent.TIDE_EVENT, true, true); this.data = data ; } }
You can also simply extend the existing AbstractTideEvent
class:
public class MyEvent extends AbstractTideEvent { public var data:Object; public function MyEvent(data:Object):void { super(); this.data = data ; } }
public class MyEvent extends Event { public var data:Object; public function MyEvent(data:Object):void { super("myEvent", true, true); this.data = data ; } }
[Name("myComponent")] [ManagedEvent(name="myEvent")] public class MyComponent extends EventDispatcher { public function doSomething():void { dispatchEvent(new MyEvent({ property: "value" })); } }
[Observer("myEvent")] public function eventHandler(event:TideContextEvent):void { // You can get the arguments from the events.params array var arg1:Object = event.params[0]; var arg2:Object = event.params[1]["arg2"]; ... // arg2 should be equal to "value" }
[Observer("myEvent")] public function eventHandler(arg1:Object, arg2:Object):void { // arg2["arg2"] should be equals to "value" }
One method can listen to more than one event type by specifying multiple [Observer]
annotations:
[Observer("myEvent")] [Observer("myOtherEvent")] public function eventHandler(arg1:Object, arg2:Object):void { // arg2["arg2"] should be equals to "value" }
Or by separating the event types with commas:
[Observer("myEvent, myOtherEvent")] public function eventHandler(arg1:Object, arg2:Object):void { // arg2["arg2"] should be equals to "value" }
Observers for typed events can have only one form:
[Observer] public function eventHandler(event:MyEvent):void { // Do something }
There are other possibilities than the annotation [Observer]
to register event observers:
Tide.getInstance().addEventObserver("myEvent", "myComponent", "myMethod")
can be used to register the method myMethod
of the component myComponent
as observer for the event myEvent
. This is exactly equivalent as putting the
annotation [Observer("myEvent")]
on the method.
Tide.getInstance().addContextEventListener("myEvent", listener)
can be used to directly register an event listener method for a
particular event. It can also be called from the context object with tideContext.addContextEventListener("myEvent", listener)
.
Now you should be able to easily connect all parts of your application through events.
event.context
.
myComponent
is the name of a global component,
tideContext.myComponent
will always return the instance of the global component for any existing context.
[In]
:
[Name("myConversationComponent", scope="conversation")] public class MyConversationComponent { [In] public var myComponent:MyComponent; // This will always inject the instance of the global component }
<mx:List id="list" dataProvider="{customerRecords}"
change="dispatchEvent(new TideUIConversationEvent(list.selectedItem.id, "viewRecord", list.selectedItem))")/>
[Name("customerRecordController")] public class CustomerRecordController { [Observer("viewRecord")] public function selectRecord(record:Record):void { // Start the conversation // For example create a view and display it somewhere } }
Here is a more complete example of end-to-end conversation handling by a controller:
[Name("customerRecordController", scope="conversation")] public class CustomerRecordController { [In] public var mainTabNavigator:TabNavigator; [In(create="true")] public var recordView:RecordView; [Observer("viewRecord")] public function viewRecord(record:Record):void { recordView.record = record; mainTabNavigator.addChild(recordView); } [Observer("closeRecord")] public function closeRecord(event:TideContextEvent):void { mainTabNavigator.removeChild(recordView); event.context.meta_end(); } }
<mx:Panel label="Record #{record.id}"> <mx:Metadata>[Name("recordView", scope="conversation")]</mx:Metadata> <mx:Script> [Bindable] public var record:Record; </mx:Script> <mx:Label text="{record.description}"/> <mx:Button label="Close" click="dispatchEvent(new TideUIEvent('closeRecord'))"/> </mx:Panel>
viewRecord
.
[In(create="true")]
), sets it
record
property and adds it to the main tab navigator. Note that we could have outjected the record from the controller and
injected it in the view but
If the user clicks on many elements in the list, one tab will be created for each element.
Each context having its own entity cache has some implications:
uid
) can exist once in each context.
IConversationEvent
)
or the other way (global observers of conversation events), Tide has to translate the event payload from one cache to the other. In the previous example,
the Record
received by the controller is NOT the same instance as the one dispatched from the list, it is the copy of this object
in the conversation context entity cache. That means that you can do whatever you want on this object, it will not be reflected on the source list.
tideContext.meta_mergeInGlobalContext()
or tideContext.meta_mergeInParentContext()
(for nested conversations).
Also when ending a conversation context, tideContext.meta_end(true)
will merge the changes in the parent context before ending the
conversation. tideContext.meta_end(false)
will drop any change made in the conversation context and is suitable for
Cancel buttons for example.
Tide provides a few extension points that can be used to extend its functionality.
First there are four events that are dispatched on some internal events:
org.granite.tide.startup
: Dispatched at application startup, can be used to do some global initialization.
org.granite.tide.contextCreate
: Dispatched at creation of a new conversation context, can be used to do initialization of the
context.
org.granite.tide.contextDestroy
: Dispatched at destruction of a conversation context, can be used to cleanup resources.
org.granite.tide.contextResult
: Dispatched at each remoting result.
org.granite.tide.contextFault
: Dispatched at each remoting fault, can be used to run global handling of faults.
org.granite.tide.login
: Dispatched at user login (or relogin when the user refreshes the browser page).
org.granite.tide.logout
: Dispatched before user logout.
org.granite.tide.loggedOut
: Dispatched after user logout.
All these events can be observed from any component as standard Tide events:
[Name("myComponent")] public class MyComponent { [Observe("org.granite.tide.startup")] public function startup():void { // Do some initialization stuff here... } }
org.granite.tide.plugin.addComponent
: Dispatched when a new component is registered, can be used to participate in the scan of
the annotations. event.params.descriptor
contains the internal component descriptor (of type ComponentDescriptor
),
see the API documentation for details on this class, and event.params.type
contains the Type
for the
component class that can be used to retrieve annotations or metadata.
org.granite.tide.plugin.setCredentials
: Dispatched when the user credentials are defined, it can be used to set the user credentials
on some object of the plugin. event.params
has two parameters username
and password
.
org.granite.tide.plugin.loginSuccess
: Dispatched when the user has been logged in successfully. event.params.sessionId
contains the user session id received from the server.
org.granite.tide.plugin.loginFault
: Dispatched when the user login has failed.
org.granite.tide.plugin.logout
: Dispatched when the user logs out.
public class TideTrace implements ITidePlugin { private static var _tideTrace:TideTrace; public static function getInstance():TideTrace { if (!_tideTrace) _tideTrace = new TideTrace(); return _tideTrace; } public function set tide(tide:Tide):void { tide.addEventListener(Tide.PLUGIN_ADD_COMPONENT, addComponent); } private function addComponent(event:TidePluginEvent):void { var descriptor:ComponentDescriptor = event.params.descriptor as ComponentDescriptor; var type:Type = event.params.type as Type; var anno:Annotation = type.getAnnotationNoCache('Trace'); if (anno != null) trace("Component added: " + descriptor.name); } }
Tide.getInstance().addModule(MyModule); public class MyModule implements ITideModule { public function init(tide:Tide):void { tide.addExceptionHandler(ValidationExceptionHandler); ... tide.addComponents([Component1, Component2]); tide.addComponent("comp3", Component3); ... } }
You can think of it as a XML configuration file, such as Seam components.xml
or Spring context.xml
.
Here is an example on how to handle dynamic loading of Flex modules :
private var _moduleAppDomain:ApplicationDomain; public function loadModule(path:String):void { var info:IModuleInfo = ModuleManager.getModule(path); info.addEventListener(ModuleEvent.READY, moduleReadyHandler, false, 0, true); _moduleAppDomain = new ApplicationDomain(ApplicationDomain.currentDomain); info.load(appDomain); } private function moduleReadyHandler(event:ModuleEvent):void { var loadedModule:Object = event.module.factory.create(); Tide.getInstance().addModule(loadedModule, _moduleAppDomain); }
<mx:ModuleLoader id="moduleLoader"
applicationDomain="{new ApplicationDomain(ApplicationDomain.currentDomain)}"
ready="Tide.getInstance().addModule(moduleLoader.child, moduleLoader.applicationDomain)"/>
You can then change the loaded module with this code :
private function changeModule(modulePath:String):void { if (moduleLoader.url != modulePath) { moduleLoader.applicationDomain = new ApplicationDomain(ApplicationDomain.currentDomain); moduleLoader.unloadModule(); moduleLoader.loadModule(modulePath); } }
Tide provides a way to integrate deep linking with the MVC framework. It uses a technique inspired by JAX-RS
so that changes in the
browser url will trigger a method on a component. It first requires to enable the corresponding Tide plugin just after the Tide initialization with :
Tide.getInstance().initApplication(); Tide.getInstance().addPlugin(TideUrlMapping.getInstance());
You will also need to keep the annotation [Path]
in your compilation options in Flash Builder / Ant / Maven.
Once enabled, the plugin will listen to browser url changes, and split the url after # in two parts. The part before the first slash will identify the
target controller, and the part after the first slash will determine the target method.
In the previous example, the controller has to be annotated with [Path("product")]
and the method with [Path("display/tv")]
:
[Name("productController")] [Path("product")] public class ProductController { [Path("display/tv")] public function displayTv():void { // Do something to display the TV... } }
Of course you won't want to have a different method for each kind of resource so you can use placeholders that will match method arguments :
[Name("productController")] [Path("product")] public class ProductController { [Path("display/{0}")] public function display(productType:String):void { // Do something to display the product... } }
GraniteDS provides various features that simplify the handling of data between Flex and Java EE, in particular when using JPA or Hibernate as a persistence mechanism.
From the Flex documentation of IManaged
:
private var _property:String; public function get property():String { return Manager.getProperty(this, "property", _property); } public function set property(value:String):void { Manager.setProperty(this, "property", _property, _property = value); }
@MappedSuperclass
@EntityListeners({AbstractEntity.AbstractEntityListener.class})
public abstract class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id @GeneratedValue
private Long id;
/* "UUID" and "UID" are Oracle reserved keywords -> "ENTITY_UID" */
@Column(name="ENTITY_UID", unique=true, nullable=false, updatable=false, length=36)
private String uid;
@Version
private Integer version;
public Long getId() {
return id;
}
public Integer getVersion() {
return version;
}
@Override
public boolean equals(Object o) {
return (o == this || (o instanceof AbstractEntity && uid().equals(((AbstractEntity)o).uid())));
}
@Override
public int hashCode() {
return uid().hashCode();
}
public static class AbstractEntityListener {
@PrePersist
public void onPrePersist(AbstractEntity abstractEntity) {
abstractEntity.uid();
}
}
private String uid() {
if (uid == null)
uid = UUID.randomUUID().toString();
return uid;
}
}
<gas3 outputdir="as3" tide="true">
<classpath>
<pathelement location="classes"/>
</classpath>
<fileset dir="classes">
<include name="com/myapp/entity/**/*.class"/>
</fileset>
</gas3>
In a typical Flex/app server/database application, an entity lives in three layers:
Object(myEntity.myCollection).withInitialized(function(collection:IList):void { // Do something with the content of the list var obj:Object = collection.getItemAt(0); });
public function savePerson():void {
person.lastName = "Test";
personService.save(person); // This will send all loaded collections associated to the Person object
}
var uperson:Person = new EntityGraphUnintializer(tideContext).uninitializeEntityGraph(person) as Person; personService.save(uperson); // This will send only the Person object without any loaded collection
person.contacts.getItemAt(0).email = '[email protected]'; var uperson:Person = new EntityGraphUnintializer(tideContext).uninitializeEntityGraph(person) as Person; personService.save(uperson); // This will send the Person object with only the contacts collection loaded
Tide uses the client data tracking (the same used for dirty checking, see below) to determine which parts of the graph need to be sent.
If you need to uninitialize more than one argument for a remote call, you have to use the same EntityGraphUninitializer
. It is important
because the uninitializer copies the entities in a temporary context before uninitializing their associations so the normal context keeps unchanged, and
all processed entities have to share this same temporary context.
var egu:EntityGraphUnintializer = new EntityGraphUninitialize(tideContext); uperson1 = egu.uninitializeEntityGraph(person1); uperson2 = egu.uninitializeEntityGraph(person2); personService.save(uperson1, uperson2);
Calling the EntityGraphUninitializer
manually is a bit tedious and ugly, so there is a cleaner possibility when you are using
generated typesafe service proxies. You can annotate your service method arguments with @org.granite.tide.data.Lazy
:
public void save(@Lazy Person person) {
}
Gas3 will then generate a [Lazy]
annotation on your service methods (so take care that you have added the [Lazy] annotation to your Flex
metadata compilation configuration). Next in the Flex application, register the UninitializeArgumentPreprocessor
component in Tide as follows :
Tide.getInstance().addComponents([UninitializeArgumentPreprocessor]);
Once you have done this, all calls to PersonService.save()
will automatically use a properly uninitialized version of the person
argument.
This new feature cannot be yet applied to local context variables (mostly used with Seam or CDI stateful components). Only method arguments can be processed, but this should cover be the vast majority of use cases. Support for context variables will come with GDS 3.0.
A particular entity instance can be in two states :
The current state of an entity can be accessed with :
entity.meta_dirty
<mx:Button label="Save" click="entityService.save()"
enabled="{tideContext.meta_dirty}"/>
In a typical client/server interaction, here is what happens :
private function restore():void { Managed.resetEntity(entity); }
You can also reset all entities in the context to their last stable state with :
private function restoreAll():void { tideContext.meta_resetAllEntities(); }
There are two ways of managing this: either you just rely on optimistic locking and intercept the corresponding server exceptions to display a message to the user, or you use data push (see section Data Push) so all Flex clients are updated in near real-time. Note however that even with data push, there can still be conflicts between changes made by a user and updates received from the server.
With normal optimistic locking, the remote service call at step 3 will trigger a OptimisticLockException
. Tide provides a built-in
exception handler to handle this case: it will extract the entity
argument of the exception, compare its state with the client state
and dispatch a conflict event TideDataConflictEvent
on the Tide context when it's not identical. The exception handler can be enabled with :
Tide.getInstance().addExceptionHandler(OptimisticLockExceptionHandler);
When data push is used, an entity instance can be updated with data received from the server at any time. If the current user was working on this instance,
it is obviously not desirable that his work is overwritten without notice. Similarly to the previous case, Tide will determine that an incoming data from
another user session is in conflict with the local data and dispatch a TideDataConflictEvent
on the Tide context.
What can you do with this event ? Basically there are two possibilities : accept the server-side state or keep the client state. Here is an example of a conflict handler defined in a Flex application, generally in the main mxml :
<mx:Application ...
preinitialize="Tide.getInstance().initApplication()"
creationComplete="init();">
<mx:Script>
private function init():void {
Tide.getInstance().getEjbContext().addEventListener(
TideDataConflictsEvent.DATA_CONFLICTS, conflictsHandler);
}
private var _conflicts:Conflicts;
private function conflictsHandler(event:TideDataConflictsEvent):void {
_conflicts = event.conflicts;
Alert.show("Keep local state ?", "Data conflict",
Alert.YES|Alert.NO, null, conflictsCloseHandler);
}
private function conflictsCloseHandler(event:CloseEvent):void {
if (event.detail == Alert.YES)
_conflicts.acceptAllClient();
else
_conflicts.acceptAllServer();
}
</mx:Script>
...
</mx:Application>
If you look at the ASDoc for Conflicts
, there are a few properties that give more details about the conflicts and make possible
to present a better alert message to the user.
When using the Hibernate native API (Session
), the optimistick lock exception StaleObjectStateException
is unfortunately missing a critical information to allow for correct conflict handling (that is present in the JPA OptimistickLockException
).
In this case, you should use the provided Hibernate event wrappers that add the missing data to the Hibernate exception. Here is what is will look like when
configuring the SessionFactory
with Spring :
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
<prop key="hibernate.show_sql">false</prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
<property name="eventListeners">
<map>
<entry key="merge"><bean class="org.granite.tide.hibernate.HibernateMergeListener"/></entry>
<entry key="create"><bean class="org.granite.tide.hibernate.HibernatePersistListener"/></entry>
<entry key="create-onflush"><bean class="org.granite.tide.hibernate.HibernatePersistOnFlushListener"/></entry>
<entry key="delete"><bean class="org.granite.tide.hibernate.HibernateDeleteListener"/></entry>
<entry key="update"><bean class="org.granite.tide.hibernate.HibernateSaveOrUpdateListener"/></entry>
<entry key="save-update"><bean class="org.granite.tide.hibernate.HibernateSaveOrUpdateListener"/></entry>
<entry key="save"><bean class="org.granite.tide.hibernate.HibernateSaveOrUpdateListener"/></entry>
<entry key="lock"><bean class="org.granite.tide.hibernate.HibernateLockListener"/></entry>
<entry key="flush"><bean class="org.granite.tide.hibernate.HibernateFlushListener"/></entry>
<entry key="auto-flush"><bean class="org.granite.tide.hibernate.HibernateAutoFlushListener"/></entry>
</map>
</property>
...
</bean>
When using client conversations, the situation becomes a bit more complex as each conversation has its own entity cache. That means that when a user modifies some data in a conversation context, the change is not immediately visible in others even if they contain the same entity instance.
The Tide context object provides a few methods to deal with such situations :
tideContext.meta_mergeInParentContext()
This will merge all stable state of the current conversation context in its parent context and all its children. In the common case where you don't use nested conversations, that just means that the changes are merged in the global context as well as all other conversations.
If you use nested conversations, the merge will be done only in the direct parent context and all its children, but not in the global context.
tideContext.meta_mergeInGlobalContext()
This will merge all stable state of the current conversation context in its parent context, in the global context and in all child contexts recursively.
Starting with GraniteDS 2.2, however, the preferred way to execute validation is a Flex-side, JSR-303 like, validation framework (see Bean Validation (JSR-303) for details).
The server support for Hibernate Validator 3 is available in granite-hibernate.jar
, and the support for Bean Validation is available
in granite-beanvalidation.jar
. You will have to add one of these jars in your application lib
folder.
The validator integration is based on the GraniteDS exception handling framework. A server exception converter is registered to handle the
InvalidStateException
, and a client exception handler can be registered with:
Seam.getInstance().addExceptionHandler(ValidatorExceptionHandler);
This exception handler intercepts all server validation faults and dispatches a validation event on the context. To complete the integration,
a TideEntityValidator
Flex component can be attached to any UI component:
<mx:Application
...
xmlns:tv="org.granite.tide.validators.*">
<tv:TideEntityValidator id="teval" entity="{tideContext.booking}"
property="creditCardName" listener="{creditCardNameInput}"/>
<mx:TextInput id="creditCardNameInput" text="{tideContext.booking.creditCardName}"/>
...
</mx:Application>
You can have more control on the validation behaviour by directly listening to validation events on the context:
public function setup():void { tideContext.addEventListener(TideValidatorEvent.INVALID, validationHandler); } private function validationHandler(event:TideValidatorEvent):void { var invalidValues:Array = event.invalidValues; for each (var iv:Object in invalidValues) { var invalidValue:InvalidValue = iv as InvalidValue; Alert.show( "Invalid property: " + invalidValue.path + " on object " + invalidValue.bean ); } }
Another possibility is to use an input validator that calls the server when the validation is triggered.
With Seam on the server, it then uses the Validators
component to get a proper ClassValidator
, and thus just works
with Hibernate Validator 3.x for now. With other server technologies, it uses a built-in validator handler which supports both HV3 and Bean Validation
implementations.
The use of this component is quite simple and very similar to any other Flex validator, with additional parameters to define the entity to validate.
<tv:TideInputValidator id="tival"
source="{creditCardNameInput}" property="text"
entity="{tideContext.booking}" entityProperty="creditCardName"
entityManager="{tideContext}"/>
<mx:TextInput id="creditCardNameInput" text="{tideContext.booking.creditCardName}"/>
The property entityManager
of the validator may be optional if the entity is maintained in the context (i.e., ctx.myEntity
)
and has a proper entityManager
defined (with meta_getEntityManager
). This is the case for all entities retrieved from
the server.
On the client-side, you first need to register the client component with:
<mx:Script>
import org.granite.tide.collections.PagedQuery;
Tide.getInstance().addComponent("people", PagedQuery);
</mx:Script>
<mx:Script>
import org.granite.tide.collections.PagedQuery;
Tide.getInstance().addComponentWithFactory("people", PagedQuery, { maxResults: 36 });
</mx:Script>
<mx:DataGrid id="people" dataProvider="{ctx.people}" width="100%" height="100%"
liveScrolling="false">
<mx:columns>
<mx:DataGridColumn dataField="firstName" headerText="First name"/>
<mx:DataGridColumn dataField="lastName" headerText="Last name"/>
</mx:columns>
</mx:DataGrid>
<s:List>
<mx:AsyncListView list="{people}"/>
</s:List>
See for more details in the Flex 4 documentation.
Just like the paged collections of LCDS, everything works only if there is a correct uid
property on the entities.
It is thus recommended to use the Tide templates for the Gas3 generator, which provide a default implementation of uid
if there is none.
The default AsyncListView
does not support automatic propagation of the UI control sorting to its data provider.
Tide provides an alternative SortableAsyncListView
that works in all cases.
<s:VGroup ...
xmlns:c="org.granite.tide.collections.*">
<s:DataGrid ...>
<c:SortableAsyncListView list="{people}"/>
</s:DataGrid>
The PagedQuery
components expects that the corresponding server component implements a specific method to fetch elements.
There are two ways of handling filtering, either with an untyped map or with a typesafe filter object:
For untyped filters, the server component shoud implement the following method:
public Map find(Map filter, int first, int max, String order, boolean desc);
first
, max
, order
and desc
are straightforward.
filter
is a map containing the parameter values of the query. These values can be set on the client by:
tideContext.pagedQuery.filter.<parameter1> = <value1>; tideContext.pagedQuery.filter.<parameter2> = <value2>; ...
The return object must be a map containing four properties:
firstResult
: Should be exactly the same as the argument passed in (int first).
maxResults
: Should be exactly the same as the argument passed in (int max), except when its value is 0, meaning that the client
component is initializing and needs a max value. In this case, you have to set the page value, which must absolutely be greater than the maximum
expected number of elements displayed simultaneously in a table.
resultCount
: Count of results.
resultList
: List of results.
The following code snippet is a quick and dirty implementation and can be used as a base for other implementations (here this is a Spring service but the equivalent implementations for EJB3 or CDI would be extremely similar):
@Service("people")
@Transactional(readOnly=true)
public class PeopleServiceImpl implements PeopleService {
@PersistenceContext
protected EntityManager manager;
public Map find(Map filter, int first, int max, String order, boolean desc) {
Map result = new HashMap(4);
String from = "from Person e ";
String where = "where lower(e.lastName) like '%' || lower(:lastName) || '%' ";
String orderBy = (
order != null ? "order by e." + order + (desc ? " desc" : "") : ""
);
String lastName = (
filter.containsKey("lastName") ? (String)filter.get("lastName") : ""
);
Query qc = manager.createQuery("select count(e) " + from + where);
qc.setParameter("lastName", lastName);
long resultCount = (Long)qc.getSingleResult();
if (max == 0)
max = 36;
Query ql = manager.createQuery("select e " + from + where + orderBy);
ql.setFirstResult(first);
ql.setMaxResults(max);
ql.setParameter("lastName", lastName);
List resultList = ql.getResultList();
result.put("firstResult", first);
result.put("maxResults", max);
result.put("resultCount", resultCount);
result.put("resultList", resultList);
return result;
}
}
It is also possible to define on the Flex size an alternative remote component name and method name that will implement the querying :
<mx:Script>
Spring.getInstance().addComponentWithFactory("people", PagedQuery,
{ maxResults: 36, remoteComponentName: "peopleService", methodName: "list" });
</mx:Script>
In this case, the previous component would be :
@Service("peopleService")
@Transactional(readOnly=true)
public class PeopleServiceImpl implements PeopleService {
@PersistenceContext
protected EntityManager manager;
public Map list(Map filter, int first, int max, String order, boolean desc) {
Map result = new HashMap();
String from = "from Person e ";
String where = "where lower(e.lastName) like '%' || lower(:lastName) || '%' ";
String orderBy = (
order != null ? "order by e." + order + (desc ? " desc" : "") : ""
);
String lastName = (
filter.containsKey("lastName") ? (String)filter.get("lastName") : ""
);
Query qc = manager.createQuery("select count(e) " + from + where);
qc.setParameter("lastName", lastName);
long resultCount = (Long)qc.getSingleResult();
if (max == 0)
max = 36;
Query ql = manager.createQuery("select e " + from + where + orderBy);
ql.setFirstResult(first);
ql.setMaxResults(max);
ql.setParameter("lastName", lastName);
List resultList = ql.getResultList();
result.put("resultCount", resultCount);
result.put("resultList", resultList);
return result;
}
}
Note that this time we did not have to return firstResult
and maxResults
because the page size is defined on the client.
It is finally possible to use a typesafe filter object instead of the Map
. The server implementation will then be something like :
@Service("peopleService")
@Transactional(readOnly=true)
public class PeopleServiceImpl implements PeopleService {
@PersistenceContext
protected EntityManager manager;
public Map list(Person examplePerson, int first, int max, String order, boolean desc) {
Map result = new HashMap();
String from = "from Person e ";
String where = "where lower(e.lastName) like '%' || lower(:lastName) || '%' ";
String orderBy = (
order != null ? "order by e." + order + (desc ? " desc" : "") : ""
);
String lastName = (
examplePerson.getLastName() != null ? examplePerson.getLastName() : ""
);
Query qc = manager.createQuery("select count(e) " + from + where);
qc.setParameter("lastName", lastName);
long resultCount = (Long)qc.getSingleResult();
if (max == 0)
max = 36;
Query ql = manager.createQuery("select e " + from + where + orderBy);
ql.setFirstResult(first);
ql.setMaxResults(max);
ql.setParameter("lastName", lastName);
List resultList = ql.getResultList();
result.put("resultCount", resultCount);
result.put("resultList", resultList);
return result;
}
}
To use this, you also have to define the filter class on the client component :
<mx:Script>
import org.granite.tide.collections.PagedQuery;
Tide.getInstance().addComponentWithFactory("people", PagedQuery, { maxResults: 36, filterClass: Person });
</mx:Script>
If the filter class is bindable, then people.filter
will be an instance of the provided filter class.
If not, the PagedQuery
will create an ObjectProxy
that wraps the filter instance to track changes on it.
You can also directly provide your own instance of the filter instead of letting the component instantiate the class itself:
<mx:Script>
import org.granite.tide.collections.PagedQuery;
Tide.getInstance().addComponentWithFactory("people", PagedQuery, { maxResults: 36, filter: new Person() });
</mx:Script>
This requires a bit of configuration :
Let's see all this in details :
Define a Gravity topic: in the standard case, it can be done in services-config.xml
:
<service id="gravity-service"
class="flex.messaging.services.MessagingService"
messageTypes="flex.messaging.messages.AsyncMessage">
<adapters>
<adapter-definition id="simple"
class="org.granite.gravity.adapters.SimpleServiceAdapter"/>
</adapters>
<destination id="dataTopic">
<properties>
<no-local>true</no-local>
<session-selector>true</session-selector>
</properties>
<channels>
<channel ref="gravityamf"/>
</channels>
<adapter ref="simple"/>
</destination>
</service>
...
<channel-definition id="gravityamf" class="org.granite.gravity.channels.GravityChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/gravityamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
<graniteds:messaging-destination id="dataTopic" no-local="true" session-selector="true"/>
<graniteds:messaging-destination name="dataTopic" no-local="true" session-selector="true"/>
The two important parameters for the topic definition are :
no-local
should be set to true
. That means that the client that triggers the modification will not receive
the result of the update twice : first by the remoting call, then by the messaging update.
session-selector
must be set to true
. Tide uses JMS-style selectors to determine which data will be sent
to which clients and thus needs to store the current messaging selector state in the user session.
Add the Tide JPA publishing listener on the entities that should be tracked:
@Entity
@EntityListeners({DataPublishListener.class})
public abstract class MyEntity {
...
}
Add the Tide data annotation on all services, example here with a Spring service:
@DataEnabled(topic="dataTopic", publish=PublishMode.ON_SUCCESS)
public interface MyService {
...
}
The attributes of this annotations are :
topic
: the name of the messaging topic that will be used to dispatch updates. Obviously this is the one we declared just before in
services-config.xml
.
publish
: the publishing mode. PublishMode.MANUAL
means that you will have to trigger the dispatch manually,
for example in an interceptor. PublishMode.ON_SUCCESS
means that Tide will automatically dispatch the updates on any successful call.
PublishMode.ON_COMMIT
is not yet implemented.
params
: a filter class that will define to which updates are sent to which clients. By default there is no filtering,
otherwise see below for a more detailed explanation.
It is possible to tell the Tide engine how it should dispatch each update (i.e. to which clients).
public class AddressBookParams implements DataTopicParams {
public void observes(DataObserveParams params) {
params.addValue("user", Identity.instance().getCredentials().getUsername());
params.addValue("user", "__public__");
}
public void publishes(DataPublishParams params, Object entity) {
if (((AbstractEntity)entity).isRestricted())
params.setValue("user", ((AbstractEntity)entity).getCreatedBy());
else
params.setValue("user", "__public__");
}
}
public void observes(DataObserveParams params) {
params.addValue("user", Identity.instance().getCredentials().getUsername());
params.addValue("user", "__public__");
params.addValue("group", "admin");
params.addValue("group", "superadmin");
}
Will generate the following selector :
(user = 'username' OR user = '__public__') AND (group = 'admin' OR group = 'superadmin')
There are three publishing modes :
PublishMode.ON_SUCCESS
is the easiest to use, and dispatch updates after each successful remote call, regardless of the actual
result of the transaction (if there is one).
PublishMode.ON_COMMIT
allows for a transactional behaviour, and does the dispatch only on transaction commit.
PublishMode.MANUAL
lets you do the dispatch manually in your services when you want, giving the most control.
@DataEnabled(topic="dataTopic", publishMode=PublishMode.ON_COMMIT, useInterceptor=true)
public class MyService {
}
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:graniteds="http://www.graniteds.org/config"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.graniteds.org/config http://www.graniteds.org/public/dtd/2.3.0/granite-config-2.3.xsd">
...
<graniteds:tide-data-publishing-advice/>
For CDI, enable the interceptor in beans.xml
:
<beans
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
<interceptors>
<class>org.granite.tide.cdi.TideDataPublishingInterceptor</class>
</interceptors>
</beans>
For EJB 3, you can define a global interceptor in ejb-jar.xml
:
<assembly-descriptor>
<interceptor-binding>
<ejb-name>*</ejb-name>
<interceptor-class>org.granite.tide.ejb.TideDataPublishingInterceptor</interceptor-class>
</interceptor-binding>
...
</assembly-descriptor>
Or alternatively configure the interceptor on each EJB 3 :
@Stateless
@Local(MyService.class)
@Interceptors(TideDataPublishingInterceptor.class)
@DataEnabled(topic="myTopic", publish=PublishMode.ON_COMMIT, useInterceptor=true)
public class MyServiceBean {
...
}
@DataEnabled(topic="dataTopic", params=DefaultDataTopicParams.class, publishMode=PublishMode.MANUAL, useInterceptor=true)
public class MyService {
@Inject
private Gravity gravity;
public void doSomething() {
DataContext.init(gravity, "dataTopic", DefaultDataTopicParams.class, PublishMode.MANUAL);
try {
Object result = invocation.proceed();
DataContext.publish(PublishMode.MANUAL);
return result;
}
finally {
DataContext.remove();
}
}
}
@Interceptor
public class CustomPublishInterceptor {
@Inject
private Gravity gravity;
@AroundInvoke
public Object aroundInvoke(InvocationContext invocation) throws Exception {
DataContext.init(gravity, "dataTopic", DefaultDataTopicParams.class, PublishMode.MANUAL);
try {
Object result = invocation.proceed();
DataContext.publish(PublishMode.MANUAL);
return result;
}
finally {
DataContext.remove();
}
}
}
<destination id="dataTopic">
<properties>
<jms>
<destination-type>Topic</destination-type>
<connection-factory>ConnectionFactory</connection-factory>
<destination-jndi-name>topic/dataTopic</destination-jndi-name>
<destination-name>dataTopic</destination-name>
<acknowledge-mode>AUTO_ACKNOWLEDGE</acknowledge-mode>
<transacted-sessions>true</transacted-sessions>
<no-local>true</no-local>
</jms>
<no-local>true</no-local>
<session-selector>true</session-selector>
</properties>
<channels>
<channel ref="gravityamf"/>
</channels>
<adapter ref="jms"/>
</destination>
JodaDateTime2Date
converter/reverter:
Here is a complete implementation of a Joda DateTime
converter/reverter:
package com.myapp.converters;
import java.lang.reflect.Type;
import java.util.Date;
import org.granite.messaging.amf.io.convert.Converter;
import org.granite.messaging.amf.io.convert.Converters;
import org.granite.messaging.amf.io.convert.Reverter;
import org.granite.util.ClassUtil;
import org.joda.time.DateTime;
public class JodaDateTime2Date extends Converter implements Reverter {
public JodaDateTime2Date(Converters converters) {
super(converters);
}
// AMF3Deserialization (Converter)...
@Override
protected boolean internalCanConvert(Object value, Type targetType) {
Class<?> targetClass = ClassUtil.classOfType(targetType);
return (
targetClass.isAssignableFrom(DateTime.class) &&
(value == null || value instanceof Date)
);
}
@Override
protected Object internalConvert(Object value, Type targetType) {
return (value == null ? null : new DateTime(((Date)value).getTime()));
}
// AMF3Serialization (Reverter)...
public boolean canRevert(Object value) {
return value instanceof DateTime;
}
public Object revert(Object value) {
return ((DateTime)value).toDate();
}
}
The converter should be setup in granite-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
<converters>
<converter type="com.myapp.converters.JodaDateTime2Date" />
</converters>
</granite-config>
import org.joda.time.DateTime; private var myDate:DateTime = null;
package com.myapp.converters;
import org.granite.generator.as3.As3Type;
import org.granite.generator.as3.DefaultAs3TypeFactory;
import org.joda.time.DateTime;
public class CustomAs3TypeFactory extends DefaultAs3TypeFactory {
@Override
protected As3Type createAs3Type(Class<?> jType) {
if (DataTime.class.isAssignableFrom(jType))
return As3Type.DATE;
return super.createAs3Type(jType);
}
}
Then, declare this new factory in the Gas3 task (here for example in an Ant build file):
<gas3 as3typefactory="com.myapp.converters.CustomAs3TypeFactory" ...>
...
<classpath>
...
<pathelement location="path/to/my/factory"/>
</classpath>
...
</gas3>
package org.granite.messaging.service.security;
import java.util.Map;
public interface SecurityService {
public void configure(Map<String, String> params);
public void login(Object credentials) throws SecurityServiceException;
public void login(Object credentials, String charset) throws SecurityServiceException;
public Object authorize(AbstractSecurityContext context) throws Exception;
public void logout() throws SecurityServiceException;
public void handleSecurityException(SecurityServiceException e);
}
configure
: This method is called at startup time and gives a chance to pass parameters to the security service.
login
: This method is called when you call one of the setCredentials
or setRemoteCredentials
RemoteObject
's method. Note that these method calls do not fire any request by themselves but only pass credentials on
the next destination service method call. The login
method is responsible for creating and exposing a java.security.Principal
or throwing an appropriate org.granite.messaging.service.security.SecurityServiceException
if credentials are invalid.
Note that credentials are a Base64 string with the common "username:password"
format. An additional login
method with an extra charset
parameter is available, so you can use the RemoteObject.setCredentials(user, pass, charset)
method and specify the charset used in the username/password string (default is ISO-8859-1).
authorize
: This method is called upon each and every service method call invocations (RemoteObject
)
or subscribe/publish actions (Consumer
/Producer
). When used with RemoteObject
s,
the authorize
method is responsible for checking security, calling the service method, and returning the corresponding result.
When used with Consumer
s/Producer
s, it is simply responsible for checking security;
no service method invocation, no result.
If authorization fails, either because the user is not logged in or because it doesn't have required rights, it must throw an appropriate
org.granite.messaging.service.security.SecurityServiceException
.
logout
: This method is called when you call the RemoteObject
's logout
method.
Note that the RemoteObject.logout
method fires a remote request by itself.
handleSecurityException
: This method is called whenever a SecurityServiceException
is thrown by
a login or logout operation. The default implementation of this method in AbstractSecurityService
is to do nothing,
but you may add extra care for these security exceptions if you need so.
<?xml version="1.0" encoding="UTF-8"?>
<services-config>
...
<factories>
<factory id="..." class="...">
<properties>
<service-exception-handler>
path.to.my.CustomServiceExceptionHandler
</service-exception-handler>
...
</properties>
</factory>
</factories>
...
</services-config>
public ServiceException handleNoSuchMethodException(
Message request,
Destination destination,
Object invokee,
String method,
Object[] args,
NoSuchMethodException e
);
public ServiceException handleInvocationException(
ServiceInvocationContext context,
Throwable t
);
<?xml version="1.0" encoding="UTF-8"?>
<granite-config>
...
<amf3-message-interceptor type="com.myapp.MyMessageInterceptor"/>
</granite-config>
amf3MessageInterceptor=com.myapp.MyMessageInterceptor
You may write and plugin your own Java or ActionScript3 descriptors, for example:
public class MyJavaClassDescriptor
extends org.granite.messaging.amf.io.util.JavaClassDescriptor {
public MyJavaClassDescriptor(Class type) {
super(type);
}
@Override
protected List<Property> introspectProperties() {
// put your custom code here...
}
}
public class MyAS3ClassDescriptor
extends org.granite.messaging.amf.io.util.ActionScriptClassDescriptor {
public MyAS3ClassDescriptor(String type, byte encoding) {
super(type, encoding);
}
@Override
public void defineProperty(String name) {
// put your custom code here...
}
@Override
public Object newJavaInstance() {
// put your custom code here...
}
}
Then, you have to declare these descriptors in your granite-config.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
<descriptors>
<descriptor
type="path.to.MyClass"
java="path.to.MyJavaClassDescriptor"
as3="path.to.MyAS3ClassDescriptor" />
<descriptor
instance-of="path.to.MyBaseClass"
java="path.to.MyJavaClassDescriptor"
as3="path.to.MyAS3ClassDescriptor" />
<!-- other descriptor configuration... -->
</descriptors>
</granite-config>
public class MyAMF3Serializer implements java.io.ObjectOutput {
public MyAMF3Serializer(java.io.OutputStream out) {
// ...
}
// ObjectOutput implemention...
}
Then, you must register this serializer in granite-config.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
<amf3-serializer type="path.to.MyAMF3Serializer"/>
</granite-config>
public class MyAMF3Deserializer implements java.io.ObjectInput {
public MyAMF3Deserializer(java.io.InputStream in) {
// ...
}
// ObjectInput implemention...
}
Then, you have to register this deserializer in granite-config.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
<amf3-deserializer type="path.to.MyAMF3Deserializer"/>
</granite-config>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
<invocation-listener type="path.to.MyServiceInvocationListener"/>
</granite-config>
public Object[] beforeMethodSearch(Object invokee, String methodName, Object[] args);
public void beforeInvocation(ServiceInvocationContext context);
public void afterInvocationError(ServiceInvocationContext context, Throwable t);
public Object afterInvocation(ServiceInvocationContext context, Object result);
The two main files used to configure GraniteDS are granite-config.xml
and services-config.xml
. By default these files
should be present in the web archive in WEB-INF/granite/granite-config.xml
and WEB-INF/flex/services-config.xml
.
If absolutely needed, they can be placed in another location, but then you will have to specify two servlet parameters in web.xml
to
indicate GraniteDS where to look for them:
<context-param>
<param-name>servicesConfigPath</param-name>
<param-value>/WEB-INF/flex/services-config.xml</param-value>
</context-param>
<context-param>
<param-name>graniteConfigPath</param-name>
<param-value>/WEB-INF/granite/granite-config.xml</param-value>
</context-param>
<granite scan="true">
: instructs GraniteDS to scan application archives and to automatically register the configuration elements it
discovers.
<amf3-deserializer type="com.myapp.custom.CustomAMF3Deserializer">
: registers a custom deserializer that should implement
the interface java.io.ObjectInput
. The default is org.granite.messaging.amf.io.AMF3Deserializer
.
<amf3-serializer type="com.myapp.custom.CustomAMF3Serializer">
: registers a custom serializer that should implement
the interface java.io.ObjectOutput
. The default is org.granite.messaging.amf.io.AMF3Serializer
.
<amf3-message-interceptor type="">
: registers an optional message interceptor that should implement
org.granite.messaging.amf.process.AMF3MessageInterceptor
.
<class-getter type="">
: registers a class getter that should implement
org.granite.messaging.amf.io.util.ClassGetter
.
<converters>
: registers a list of data converters that should implement
org.granite.messaging.amf.io.convert.Converter
.
<descriptors>
: registers a list of type descriptors that should extend either
org.granite.messaging.amf.io.util.ActionScriptClassDescriptor
or
org.granite.messaging.amf.io.util.JavaClassDescriptor
.
<exception-converters>
: registers a list of exception converters that should implement
org.granite.messaging.service.ExceptionConverter
.
<externalizers>
: registers custom externalizers that should implement
org.granite.messaging.amf.io.util.externalizer.Externalizer
. See also here.
<!-- XML : generated by JHighlight v1.0 (http://jhighlight.dev.java.net) --> <span class="xml_plain"></span><br /> <span class="xml_tag_symbols"><</span><span class="xml_tag_name">externalizers</span><span class="xml_tag_symbols">></span><span class="xml_plain"></span><br /> <span class="xml_plain"> </span><span class="xml_tag_symbols"><</span><span class="xml_tag_name">configuration</span><span class="xml_tag_symbols">></span><span class="xml_plain"></span><br /> <span class="xml_plain"> </span><span class="xml_tag_symbols"></</span><span class="xml_tag_name">configuration</span><span class="xml_tag_symbols">></span><span class="xml_plain"></span><br /> <span class="xml_plain"> </span><span class="xml_tag_symbols"><</span><span class="xml_tag_name">externalizer</span><span class="xml_plain"> </span><span class="xml_attribute_name">type</span><span class="xml_tag_symbols">=</span><span class="xml_attribute_value">""</span><span class="xml_tag_symbols">/></span><span class="xml_plain"></span><br /> <span class="xml_tag_symbols"></</span><span class="xml_tag_name">externalizers</span><span class="xml_tag_symbols">></span><span class="xml_plain"></span><br /> <span class="xml_plain"> </span><br />
<gravity>
: configures the Gravity internal parameters. See here.
<instantiators>
: registers custom instantiators that should implement
org.granite.messaging.amf.io.util.instantiator.AbstractInstantiator
.
<invocation-listener type="">
: registers an invocation listener that will be called at each invocation and should
implement org.granite.messaging.service.ServiceInvocationListener
.
<message-selector>
: registers a custom message selector implementation that should implement
org.granite.gravity.selector.MessageSelector
. 3 implementations are available, the default is GravityMessageSelector
.
<method-matcher type="">
: registers a custom method matcher that should implement
org.granite.messaging.service.MethodMatcher
.
<security>
: registers a custom security service that should implement
org.granite.messaging.service.security.SecurityService
.
<tide-components>
: registers a list of component matchers to enable remote access for Tide service factories. There are 4
ways or enabling or diabling access to Tide components:
<!-- XML : generated by JHighlight v1.0 (http://jhighlight.dev.java.net) --> <span class="xml_plain"></span><br /> <span class="xml_tag_symbols"><</span><span class="xml_tag_name">tide-components</span><span class="xml_tag_symbols">></span><span class="xml_plain"></span><br /> <span class="xml_plain"> </span><span class="xml_tag_symbols"><</span><span class="xml_tag_name">tide-component</span><span class="xml_plain"> </span><span class="xml_attribute_name">annotated-with</span><span class="xml_tag_symbols">=</span><span class="xml_attribute_value">""</span><span class="xml_tag_symbols">/></span><span class="xml_plain"></span><br /> <span class="xml_plain"> </span><span class="xml_tag_symbols"><</span><span class="xml_tag_name">tide-component</span><span class="xml_plain"> </span><span class="xml_attribute_name">instance-of</span><span class="xml_tag_symbols">=</span><span class="xml_attribute_value">""</span><span class="xml_tag_symbols">/></span><span class="xml_plain"></span><br /> <span class="xml_plain"> </span><span class="xml_tag_symbols"><</span><span class="xml_tag_name">tide-component</span><span class="xml_plain"> </span><span class="xml_attribute_name">name</span><span class="xml_tag_symbols">=</span><span class="xml_attribute_value">""</span><span class="xml_tag_symbols">/></span><span class="xml_plain"></span><br /> <span class="xml_plain"> </span><span class="xml_tag_symbols"><</span><span class="xml_tag_name">tide-component</span><span class="xml_plain"> </span><span class="xml_attribute_name">type</span><span class="xml_tag_symbols">=</span><span class="xml_attribute_value">""</span><span class="xml_plain"> </span><span class="xml_attribute_name">disabled</span><span class="xml_tag_symbols">=</span><span class="xml_attribute_value">"true"</span><span class="xml_tag_symbols">/></span><span class="xml_plain"></span><br /> <span class="xml_tag_symbols"></</span><span class="xml_tag_name">tide-components</span><span class="xml_tag_symbols">></span><span class="xml_plain"></span><br /> <span class="xml_plain"> </span><br />
annotated-with
: component class is annotated with the specified annotation class.
instance-of
: component class extends or implements the specified interface or class.
name
: component name matches the specified name regular expression.
type
: component class matches the specified class name regular expression.
A channel definition mostly contains the endpoint url and the client channel implementation:
<channels>
<channel-definition id="my-graniteamf" class="mx.messaging.channels.AMFChannel">
<endpoint
uri="http://{server.name}:{server.port}/{context.root}/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
GraniteDS provides 4 implementations of the Flex Channel
:
mx.messaging.channels.AMFChannel
: standard HTTP remoting channel.
mx.messaging.channels.SecureAMFChannel
: standard HTTPS remoting channel.
org.granite.gravity.channels.GravityChannel
: standard HTTP messaging channel.
org.granite.gravity.channels.SecureGravityChannel
: standard HTTPS messaging channel.
<services>
<service
id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<destination id="cars">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>guiceFactory</factory>
<source>test.granite.guice.services.Cars</source>
</properties>
</destination>
</service>
</services>
A destination can also define a list of security roles that are allowed to access the remote component. See Remoting Security.
<service id="gravity-service"
class="flex.messaging.services.MessagingService"
messageTypes="flex.messaging.messages.AsyncMessage">
<adapters>
<adapter-definition id="simple" class="org.granite.gravity.adapters.SimpleServiceAdapter"/>
<!--adapter-definition id="jms" class="org.granite.gravity.adapters.JMSServiceAdapter"/-->
</adapters>
<destination id="addressBookTopic">
<properties>
<!--jms>
<destination-type>Topic</destination-type>
<connection-factory>ConnectionFactory</connection-factory>
<destination-jndi-name>topic/testTopic</destination-jndi-name>
<destination-name>dataTopic</destination-name>
<acknowledge-mode>AUTO_ACKNOWLEDGE</acknowledge-mode>
<transacted-sessions>true</transacted-sessions>
<no-local>true</no-local>
</jms-->
<no-local>true</no-local>
<session-selector>true</session-selector>
</properties>
<channels>
<channel ref="gravityamf"/>
</channels>
<adapter ref="simple"/>
<!--adapter ref="jms"/-->
</destination>
</service>
A destination can also define a list of security roles that are allowed to access the remote component. See Messaging Security.
You can reference the GraniteDS Configuration DTD with:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE granite-config PUBLIC
"-//Granite Data Services//DTD granite-config internal//EN"
"http://www.graniteds.org/public/dtd/2.3.0/granite-config.dtd">
<granite-config>
...
</granite-config>
You can reference the GraniteDS Configuration XSD in your Spring or Seam configuration with:
<?xml version="1.0" encoding="UTF-8"?>
<beans
...
xmlns:graniteds="http://www.graniteds.org/config"
xsi:schemaLocation="
...
http://www.graniteds.org/config http://www.graniteds.org/public/dtd/2.3.0/granite-config-2.3.xsd">
...
</beans>