GraniteDS Flex Plugin
This plugin provides integration between Grails and Adobe Flex using Granite Data Services
GraniteDS
GraniteDS is an open source project that provides integration between Adobe Flex and Java frameworks. It has had full support Hibernate and Spring for a long time, so implementing the integration with Grails is quite natural.
The plugin is currently a beta version and has been tested with Grails 1.1.
Features
- Automatic configuration: the various parts required in web.xml and other Flex/GraniteDS configuration files are automatically setup by the plugin. GraniteDS 2.0.1 Java and Flex libraries are included.
- Automatic generation of the Flex domain classes: the embedded Gas3 generator automatically generates as3 classes from the Groovy model. Gas3 is registed as a compilation event listener, so there is no manual operation needed in most cases. Even if it is recommended to use JPA annotations for the Groovy entities, pure GORM entities are now supported since 0.4.
- On-the-fly Flex compilation: the Flex OEM compiler is automatically triggered in background when changing any mxml/as/css in the grails-app/views/flex folder. The swf will be built in grails-app/views/swf. The main mxml file must have the same name than the Grails application.
- Includes the GraniteDS Tide client framework: it gives full support for transparent lazy loading of collections, paged data and other data related features.
- Grails services and controllers can be exposed as Flex remoting destinations with a simple annotation: no particular configuration is needed to expose a Grails component to Flex, just use the org.granite.tide.annotations.TideEnabled. Newly deployed or redeployed services are automatically exposed without any manual operation.
- Grails controllers results can be used with Flex data binding: the model variables returned by controller components are available as Tide context variables and can easily be bound to the Flex UI.
- Includes the html-wrapper Flex task to generate an html page embedding the swf application.
- Flex application generator to get started quickly with a simple CRUD application.
- Integration with Spring security: remote calls can be secured and credentials are propagated to Grails. The Identity Flex component can be used to show/hide parts of the UI depending on the user access rights.
- Support for Gravity server push: server push with Jetty continuations or Tomcat Comet support can be enabled and data can be exchanged between Flex clients or from Grails to Flex.
Installation
Use this from the project folder:
grails install-plugin gdsflex
Flex compilation
The GDS plugin embeds the Flex OEM compiler to automatically compile mxml and as files present in the grails-app/views/flex folder. The swf will be built in grails-app/views/swf and gdsflex contains a simple servlet that maps all incoming requests for *.swf to this folder.
For example, requesting http://localhost:8080/myApp/myApp.swf will look for an existing swf in grails-app/views/swf/myApp.swf.
Notice that you may sometimes get the compilation warning message "Warning: Line number support for XML..." that can be ignored.
ActionScript classes for the domain model are generated in grails-app/views/flex from the Groovy domain classes.
When using Flex Builder, you can setup grails-app/views/flex as a Flex source folder and build the swf in grails-app/views/swf.
Exposing Grails services
Exposing a Grails service as a Flex destination just requires adding the org.granite.tide.annotations.TideEnabled annotation to the service:
class Person { String uid String firstName String lastName
}import org.granite.tide.annotations.TideEnabled@TideEnabled
class PeopleService { static transactional = true def list() {
return Person.list()
}
}Note that the entity can be a GORM entity or a JPA entity. In both cases, it is highly recommended (but not mandatory) to have a persistent uid property that will be used as a common identifier between Flex, Hibernate and the database layers.
Using the service from Flex can be done using the Tide client API:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
preinitialize="Spring.getInstance().initApplication()">
<mx:Script>
import mx.collections.ArrayCollection;
import org.granite.tide.spring.Spring;
import org.granite.tide.spring.Context;
import org.granite.tide.events.TideResultEvent; private var tideContext:Context
= Spring.getInstance().getSpringContext(); private function listPeople():void {
tideContext.peopleService.list(listResult);
} private function listResult(event:TideResultEvent):void {
people = event.result as ArrayCollection;
} private var dummyPerson:Person; [Bindable]
public var people:ArrayCollection = new ArrayCollection();
</mx:Script> <mx:Button label="List people" click="listPeople()"/>
<mx:DataGrid dataProvider="{people}">
<mx:columns>
<mx:DataGridColumn dataField="firstName"/>
<mx:DataGridColumn dataField="lastName"/>
</mx:columns>
</mx:DataGrid></mx:Application>There are a few important things here :
- the preinitialize handler is used to register the application with the Tide framework
- the Tide context allows to get client proxies to server components
- tideContext.peopleService gets a client proxy to the PeopleService Grails service
- the dummyPerson is necessary here to force the Flex compiler to include the Person class in the swf. If not present, you will get a serialization error. That variable will not be needed as soon as you use the Person class somewhere else.
- the 'people' property needs to be public (or at least provide a setter), because Tide needs to merge the collection instance and ensure it remains the same instance throughout remote calls. If the property is private, there will be a degraded mode where the collection instance is not kept identical between calls.
The Tide framework can also inject references to client proxies in Flex components:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
preinitialize="Spring.getInstance().initApplication()">
<mx:Script>
import mx.collections.ArrayCollection;
import org.granite.tide.spring.Spring;
import org.granite.tide.events.TideResultEvent; [In]
public var peopleService:Object; private function listPeople():void {
peopleService.list(listResult);
} private function listResult(event:TideResultEvent):void {
hello = event.result as ArrayCollection;
} private var dummyPerson:Person; [Bindable]
private var people:ArrayCollection = new ArrayCollection();
</mx:Script> <mx:Button label="List people" click="listPeople()"/>
<mx:DataGrid dataProvider="{people}">
<mx:columns>
<mx:DataGridColumn dataField="firstName"/>
<mx:DataGridColumn dataField="lastName"/>
</mx:columns>
</mx:DataGrid></mx:Application>One important thing when using the Tide framework is that the entities keep managed on the Flex client and can be considered as real JPA/Hibernate detached entities.
Entities received as arguments for a service can be directly saved or updated with the GORM extra persistence methods save, update, merge and delete. It is recommended though to use 'merge' instead of 'update' or 'save' when updating an existing object.
Exposing Grails controllers
Grails controllers can also be exposed with the TideEnabled annotation. The model variables received from controller method will be bound to the Tide context as context variables.
import org.granite.tide.annotations.TideEnabled@TideEnabled
class PeopleController { List people def list = {
people = Person.list()
}
}<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
preinitialize="Spring.getInstance().initApplication()">
<mx:Script>
import mx.collections.ArrayCollection;
import org.granite.tide.spring.Spring; [In]
public var peopleController:Object; private var dummyPerson:Person; [Bindable] [In]
public var people:ArrayCollection;
</mx:Script> <mx:Button label="List people" click="peopleController.list()"/>
<mx:DataGrid dataProvider="{people}">
<mx:columns>
<mx:DataGridColumn dataField="firstName"/>
<mx:DataGridColumn dataField="lastName"/>
</mx:columns>
</mx:DataGrid></mx:Application>You can also transmit request parameters from the Flex client, they will be available in the controller params map.
import org.granite.tide.annotations.TideEnabled@TideEnabled
class PeopleController { List people def list = {
people = Person.findByName(params.searchString)
}
}<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
preinitialize="Spring.getInstance().initApplication()">
<mx:Script>
import mx.collections.ArrayCollection;
import org.granite.tide.spring.Spring; [In]
public var peopleController:Object; private var dummyPerson:Person; [Bindable] [In]
public var people:ArrayCollection = new ArrayCollection();
</mx:Script> <mx:TextInput id="tiText"/>
<mx:Button label="List people"
click="peopleController.list({searchString: tiText.text})"/>
<mx:DataGrid dataProvider="{people}">
<mx:columns>
<mx:DataGridColumn dataField="firstName"/>
<mx:DataGridColumn dataField="lastName"/>
</mx:columns>
</mx:DataGrid></mx:Application>Choosing between using services and controllers is a matter of preference and application design. Using services requires a little more work on the client side, using controllers simplifies the integration but implies stronger coupling between the Flex view and the Grails controller.
Using transparent lazy loading
Tide maintains a client image of the server persistence context. It is able to trigger initialization of lazy loaded collections when they are bound to a UI component. While this feature should not be overused because of the extra network traffic it can cause, it is very handy when dealing with master-detail elements or tree structures.
class Person { String uid String name Set<Contact> contacts
static hasMany = [contacts:Contact]
static mapping = {
contacts cascade:"all,delete-orphan"
}
}class Contact { String uid Person person String email
}<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
xmlns:gv="org.granite.tide.validators.*"
layout="vertical"
backgroundGradientColors="[#0e2e7d, #6479ab]"
preinitialize="Spring.getInstance().initApplication()"> <mx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import org.granite.tide.spring.Spring;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;
import Person;
import Contact; [Bindable] [In]
public var peopleController:Object; [Bindable] [In]
public var people:ArrayCollection; public var dummyPerson:Person;
public var dummyContact:Contact; private function getList():void {
peopleController.list();
}
]]>
</mx:Script> <mx:VBox width="100%">
<mx:HBox>
<mx:Button id="bList" label="Get list" click="getList()"/>
</mx:HBox> <mx:HBox width="100%">
<mx:DataGrid id="dgPeople"
dataProvider="{people}"
change="dgContacts.dataProvider
= dgPeople.selectedItem.contacts">
<mx:columns>
<mx:DataGridColumn dataField="name"/>
</mx:columns>
</mx:DataGrid> <mx:DataGrid id="dgContacts">
<mx:columns>
<mx:DataGridColumn dataField="email"/>
</mx:columns>
</mx:DataGrid>
</mx:HBox>
</mx:VBox></mx:Application>Using paged data
Tide provides the PagedQuery client component that handles client data paging and interaction
with a Grails service/controller that executes queries.
The service must implement a find method with the following signature:
Map<String, Object> find(
Map<String, Object> filter,
// map containing criteria
int first,
// Index of first requested element
int max,
// Max number of elements
String order,
// order field
boolean desc
// true when descendent sorting
)The returned map should contain at least 2 elements:
And optionally when paged size needs to be defined server-side
- firstResult (identical to first)
- maxResults
import org.granite.tide.annotations.TideEnabled@TideEnabled
class PeopleService { def find(filter, first, max, order, desc) {
if (max == 0)
max = 36 List resultList = Person.list(max: max,
offset: first,
sort: order,
order: (desc ? "desc" : "asc")
)
int resultCount = Person.count() return [
resultList: resultList,
resultCount: resultCount
]
}
}<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
xmlns:gv="org.granite.tide.validators.*"
layout="vertical"
backgroundGradientColors="[#0e2e7d, #6479ab]"
preinitialize="Spring.getInstance().initApplication()"> <mx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import org.granite.tide.spring.Spring;
import org.granite.tide.spring.PagedQuery;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;
import Person;
import Contact; Spring.getInstance().addComponentWithFactory(
"peopleService",
PagedQuery, { maxResults: 36 }); [Bindable] [In]
public var peopleService:PagedQuery; private var dummyPerson:Person;
]]>
</mx:Script> <mx:VBox width="100%">
<mx:HBox>
<mx:Button id="bList"
label="Get list"
click="getList()" />
</mx:HBox> <mx:HBox width="100%">
<mx:DataGrid id="dgPeople"
dataProvider="{peopleService}">
<mx:columns>
<mx:DataGridColumn dataField="name"/>
</mx:columns>
</mx:DataGrid>
</mx:HBox>
</mx:VBox></mx:Application>
- The client PagedQuery component can be used as a data provider for Flex UI components.
Since 0.4, controllers can also be used as server providers for the PagedQuery component,
even with scaffolding. In this case, the closure 'list' will be called :
import org.granite.tide.annotations.TideEnabled@TideEnabled
class PersonController { def list = {
if (params.max == 0)
params.max = 36 List resultList = Person.list(params)
int resultCount = Person.count() return [
resultList: resultList,
resultCount: resultCount
]
}
}Or
import org.granite.tide.annotations.TideEnabled@TideEnabled
class PersonController { def scaffold = Person
}<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
xmlns:gv="org.granite.tide.validators.*"
layout="vertical"
backgroundGradientColors="[#0e2e7d, #6479ab]"
preinitialize="Spring.getInstance().initApplication()"> <mx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import org.granite.tide.spring.Spring;
import org.granite.tide.spring.PagedQuery;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;
import Person;
import Contact; Spring.getInstance().addComponentWithFactory(
"people",
PagedQuery, {
useGrailsController: true,
remoteComponentName: "personController",
maxResults: 36
}
); [Bindable] [In]
public var people:PagedQuery; private var dummyPerson:Person;
]]>
</mx:Script> <mx:VBox width="100%">
<mx:HBox>
<mx:Button id="bList"
label="Get list"
click="getList()" />
</mx:HBox> <mx:HBox width="100%">
<mx:DataGrid id="dgPeople"
dataProvider="{people}">
<mx:columns>
<mx:DataGridColumn dataField="name"/>
</mx:columns>
</mx:DataGrid>
</mx:HBox>
</mx:VBox></mx:Application>Scaffolding templates for gdsflex enabled controllers
The plugin comes with templates for Grails scaffolding that include the necessary actions for use from Flex :
find, persist, merge, remove, upload and download.
The templates can be installed with
grails install-flex-templates
Integration of html-wrapper Flex task
The plugin includes the html-wrapper task to generate a html file embedding the swf file.
This is necessary for example to use the Flex browser integration and deep linking functionality.
The html file is generated both in web-app/{appname}.html with all necessary files for the Flex wrapper (js, css...),
and in grails-app/views/flexindex.gsp. The generated gsp can for example be defined as the target for Grails url mappings.
Support for deep linking
Tide now includes a simple plugin to easily handle deep linking.
Tide components annotated with [Path] are marked as handlers for browser url changes and can take appropriate actions.
The plugin can be setup in the main application by :
import org.granite.tide.deeplinking.TideUrlMapping;
…
Spring.getInstance().addPlugin(TideUrlMapping.getInstance());
[Path("book")]
public class BookUI { [Path("list")]
public function listHandler():void {
// Do something interesting, for example show the relevant list view
}
}The listHandler method will be called when the browser url will be changed to '/book/list'.
Path mapping also supports variable mapping (similar to JAX-RS) :
[Path("book")]
public class BookUI { [Path("show/{id}")]
public function showHandler(id:Number):void {
// Do something interesting, for example show the requested book
}
}The variables are mapped as arguments to the handler method in the same order than the path mapping.
Flex application generator
Gdsflex includes a simple Flex application generator driven by the domain classes.
The UI is dynamically built using the included Tide UI builder library.
The process to generate a Flex app is the following :
// Create and edit the domain classes
grails create-domain-class com.myapp.Domain1
grails create-domain-class com.myapp.Domain2
...// Create and edit the corresponding controllers with scaffolding
// (don't forget to add the @TideEnabled annotation)
grails create-controller com.myapp.Domain1
grails create-controller com.myapp.Domain2
...// Generate the as3 domain classes
grails gas3// Generate the main Flex application
grails generate-flex-app// Compile the Flex application and optionally generate an html wrapper
grails mxmlc
grails html-wrapper// Run the application
grails run-app
The application can be accessed at http://localhost:8080/{myapp}/flexindex.gsp.
The UI builder supports most Grails constraints on properties and the generated application is comparable in functionality to the html Grails scaffolding.
Currently supported relationships are :
- oneToOne/manyToOne: Displayed as a ComboBox, requires a controller for the associated entity (to get the list of possible values).
- bidirectional oneToMany: Displayed as an editable DataGrid, requires that the associated entity and owner implement particular constructors. For example Book hasMany => Chapter requires that Book has a constructor Book() that initializes the chapters collections with chapters = new ArrayCollection() and Chapter has a constructor Chapter(book:Book = null).
- unidirectional oneToMany / manyToMany: Displayed as a DataGrid with a list of possible values that can be dragged in the grid, requires a controller for the associated entity
Lazy associations are supported for oneToMany and manyToMany relationships, but not for manyToOne or oneToOne that must be marked lazy:false.
The generated as3 constraints map contains everything that is present in the Grails ConstrainedProperty, but not all are currently used by the UI generator.
- display:false => field not displayed
- editable:false => field not editable (for now works only with oneToMany and manyToMany)
- inCreate:false => field not present in create view
- inEdit:false => field not present in edit view
- inList:"[val1,val2,val3]" => use ComboBox with specified values for string fields
- widget:"textArea" => use TextArea instead of TextInput for strings
- widget:"image" => use Image instead of download button for ByteArray / Blob
- format:"DD/MM/YYYY" => formatString in dateFormatter for Date fields
- format: "2" => precision in numberFormatter for Number fields
The generated application includes support for deep linking and file upload/download with Flex FileReference and can be seen as an example of a generic CRUD application with Flex and GraniteDS.
Most parts of the generated application can be overriden by user-defined elements :
To override the global UI builder :
[Name("tideUIBuilder")]
public class MyGlobalUIBuilder extends DefaultUIBuilder { protected override function buildEditFormItem(property:Object, create:Boolean):EntityProperty {
if (property.name == "someProperty") {
var entityProperty:EntityProperty = new EntityProperty();
entityProperty.property = property.name;
entityProperty.bound = false;
…
return entityProperty;
}
else
return super.buildEditFormItem(property, create);
}
}Or only for a particular entity (here for com.myapp.Author) :
[Name("com.myapp.author.tideUIBuilder")]
public class AuthorUIBuilder extends DefaultUIBuilder { protected override function buildEditFormItem(property:Object, create:Boolean):EntityProperty {
if (property.name == "someProperty") {
var entityProperty:EntityProperty = new EntityProperty();
entityProperty.property = property.name;
entityProperty.bound = false;
…
return entityProperty;
}
else
return super.buildEditFormItem(property, create);
}
}More information and documentation on the UI builder will be available later.
Integrating with Spring security
Integration can be done by uncommenting the Spring security service in web-app/WEB-INF/granite/granite-config.xml.
GraniteDS will propagate the security credentials from Flex RemoteObject to Spring security. Securing the Grails controller/service methods is not in the scope of this plugin but can probably be done with the Spring security plugin.
More details and documentation on the use of the GraniteDS Tide framework for Spring can be found on the GDS site at
http://www.graniteds.org/confluence/display/DOC20/6.+Tide+Data+Framework.
Deploying on Google App Engine
The gdsflex plugin is able to detect that the app-engine plugin (0.8.2 minimum) has been enabled for a project and defines an appropriate GraniteDS setup (support of DataNucleus JDO/JPA entities instead of Hibernate).
To correctly run the application, it is though necessary to update manually the web-app/WEB-INF/flex/services-config.xml and remove the {context.root} from the channel endpoint : uri="http://{server.name}:{server.port}/graniteamf/amf" because GAE deploys in the root context.
Using Grails and Google App Engine is generally very far from a pleasant experience (often due to GAE), but the following recommendations can limit problems :
- Always define the domain classes in packages (the default package will not work)
- Use the Key class for primary keys
- gdsflex requires that the primary key is named 'id' (like GORM domain classes)
- Use preferably JDO than JPA (except if you like extreme S/M experiments).
Here is an example of a JDO entity suitable for gdsflex :
package org.gdsgaegrails.entityimport javax.jdo.annotations.*
import com.google.appengine.api.datastore.Key@PersistenceCapable(identityType=IdentityType.APPLICATION,
detachable="true")
class Task { static constraints = {
} @PrimaryKey
@Persistent(primaryKey="true",
valueStrategy=IdGeneratorStrategy.IDENTITY)
Key id @Persistent
String uid @Persistent
String description
}A basic example is deployed on
http://gdsgaegrails.appspot.com/gdsgaegrails.swf.