Login required
Download

GraniteDS Flex Plugin

(11)
Author(s): wdrai
Current Release: 0.7.2
Grails Version: ?
Tags flex ria rich client spring security integration

Installation

Use this from the project folder:

grails install-plugin gdsflex

Warning: in some cases, there can be a compilation problem after having installed the plugin with 'Gas3 class not found. If you encounter this, just type this once:

grails gas3

To compile mxml files,use:

grails mxmlc file
For now,only support mx:Application or mx:Module.

Note: mxmlc must run after run-app

A simple example project can be downloaded here(only work for 0.2). It demonstrates most features of the plugin, and is described in details on this blog entry.

Plugin history

0.7.2 (27/09/2009)

  • Fixed plugin compilation on JDK 5
  • Updated GDS libs

0.7.1 (20/08/2009)

  • Fixed support for controllers with package names in Flex application generator

0.7 (11/08/2009)

  • Improved as3 template for domain classes including Grails constraints and association information
  • Added support for byte[] and Blob properties
  • Added scaffolding templates for gdsflex-enabled controllers (installed with grails install-flex-templates), including support for file upload/download from Flex
  • Added html-wrapper script to generate an html wrapper (web-app/app.html or grails-app/views/flexindex.gsp)
  • Support for Flex deep linking with [Path] annotation in Tide components
  • Generation of a Flex application from the domain classes with grails generate-flex-app using client-side dynamic UI builder

0.6.2 (16/07/2009)

  • Fixed as3 template for domain classes
  • Fixed support of gas3 extraClasses

0.6.1 (13/07/2009)

  • Updated GDS libraries to simplify Grails externalization
  • Fixed broken support for GAE
  • Fixed serialization of inherited domain classes
  • Improved fix for GORM events
  • Generation of Grails constraints in the AS3 classes

0.6 (09/07/2009)

  • Upgrade to GraniteDS 2.0 SP1 libraries
  • Restored Flex autoCompile
  • Initial support for app-engine plugin and deployment to Google App Engine
  • Completely refactored handling of Grails domain classes

0.5 (22/06/2009)

  • Upgrade to GraniteDS 2.0 GA libraries(see more details)
  • add mxmlc command:for example,grails mxmlc main.mxml or grails mxmlc main or grails mxmlc(will find all application and modules)
  • remove the auto-compilation for mxml/as etc.

0.4 (19/05/2009)

  • Upgrade to GraniteDS 2.0 RC1 libraries
  • Changed Flex compilation to compile in background in the grails-app/views/flex folder instead of using the web compiler
  • Support for pure GORM domain classes without JPA annotations

0.3.2 (13/04/2009)

  • fix the runtime error on windows

0.3.1 (12/04/2009)

  • Add domain support for JPA jar(configured in GraniteDSConfig.groovy:as3Config.domainJar)
  • Compile(incrementally) the mxml file from HTTP request to grails pacakage and monitor the modifying
  • mxml files are now put into grails-app/views/mxml,the swf files are compiled into views/swf

0.3 (06/04/2009)

  • Add support for Spring security authorizations (similar to ifAllGranted / ifAnyGranted tags)
  • Add support for Gravity server push and multiuser data updates
  • More info here

0.2 (26/03/2009)

  • Bug fixes in GDS core in handling of Grails controllers
  • Support for Spring validation errors
  • Web compiler now detects changes on ActionScript files

0.1 (16/03/2009)

  • Initial release

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:

  • resultCount
  • resultList
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.

grails html-wrapper

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

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

How to localize the app with the embedded compiler :

Modify web-app/flex/flex-config.xml and add :

<!-- Specifies the locales for internationalization. -->
<locale>
     <locale-element>{locale}</locale-element>
</locale>

<!-- List of path elements that form the roots of ActionScript class hierarchies. --> <source-path> <path-element>grails-app/views/flex/locale/{locale}</path-element> </source-path>

Where {locale} is the wanted locale (default is en_US).

The config is different from the one with Flex Builder because the base path for the embedded compiler is set to the Grails project folder and not to the grails-app/views/flex folder.

A Basic CRUD with validator