vendredi 18 juillet 2008

New features of GDS/Tide in 1.1 RC3

The GDS 1.1 RC3 is an important release for the Tide project because it brings a host of new features, most of which have been requested by the Tide users, and which are very powerful and easy to use :

1. Configuration of Tide-enabled components
2. Transparent lazy loading of collections
3. Paged collections, based on the Seam Query component
4. Propagation of Seam events on the Flex client, both synchronous and asynchronous

This makes this release of Tide feature complete. The upcoming final 1.1 release will contain only bugfixes.


Configuration of Tide-enabled components :

The early Tide versions were quite permissive concerning component access.
It was possible to call any component, even system components or entityManager or anything else. This was obviously a security hole.

The RC3 mandates a new (small) configuration to enable only the needed components.
In granite-config.xml, there is a new section which looks like this :

<tide-components>
<component type="org\.myapp\..*"/>
<component name="org\.myapp\..*"/>
<component instanceof="org.jboss.seam.framework.Query"/>
<component annotatedwith="javax.ejb.Stateless"/>
</tide-components>

The 4 methods of enabling Seam components for Tide are :
- by class name match (type=".."), which enables all Seam components which qualified class name matches the regex
- by component name match (name=".."), which enables all Seam components which qualified name matches the regex
- by component class type (instanceof=".."), which enables all Seam components which class extends or implements the specified class/interface
- by component class annotation (annotatedwith=".."), which enables all Seam components which class is annotated with the specified class/interface

The last method can be used in conjuction with either the WebRemote annotation of Seam or with the Tide-specific org.granite.tide.annotations.TideEnabled annotation.

This is important to carefully expose your Seam components, otherwise your application can have a big security hole.
This new configuration is flexible enough and if correctly used, should not be too hard to maintain.


Transparent lazy loading :

This is a most requested feature, as lazy exceptions are already a pain in a pure web environment, and even more in a RIA.
Tide tries to provide a convenient approach to automate this task, particularly in the context of a Seam conversation.

All uninitialized collections retrieved from the server are wrapped by a particular collection implementation, which can be used as data provider for Flex UI component.
When requested by the UI, these collections will trigger a server call to retrieve initialized data. This process follows the standard Flex ItemPendingError mechanism and works with any component handling correctly this mechanism (i.e. DataGrid and List).

For example, if you have an Order object with a lazy collection of Items, you can do :

<mx:DataGrid id="orders" dataProvider="{tideContext.orders}" change="if (selectedItem) { orderItems.dataProvider=selectedItem.items }">
...
</mx:DataGrid>

<mx:DataGrid id="orderItems">
...
</mx:DataGrid>

The collection is fetched only if it is bound to a component, so if the bindings are correctly designed, this transparent loading should not degrade performance by issuing too many server calls.


Paged collection :

The Flex UI components are designed to be able to fetch data on demand, and wait for remote operation when needed, by the use of the ItemPendingError exception.
Unfortunately, there is no standard collection implementation in the Flex SDK which is able to benefit from this feature and provide paginated access to remote data.

The paginated data access is part of the enterprise features of Adobe LCDS, and even this comes with some limitations :
- remote data are retrieved by page, but never cleaned, so when iterating on a LCDS paged collection, you finish by having the whole data on the client
- remote sorting is not automatic, it necessitates to trigger manually a server call on DataGrid events
- there is no way of managing remote filtering

On the other side, Seam provides a very handy Query component, which can simplify many operations on queries, in particular sorting and filtering data.

The goal of the Tide Query integration is to provide a paged collection implementation which completely manages the interaction between the Flex client and the Seam Query component.
It provides the following features :
- it supports any Flex component correctly managing ItemPendingError exceptions (DataGrid and List do)
- the client caches at most 2 pages of data, that allows to manage any amount of remote data without filling the Flash player memory
- it manages automatically the remote sorting of data. There is absolutely nothing to do manually, the selected Sort on the client is automatically propagated to the order property of the Seam component.
- it manages automatically the remote filtering of data. When used in conjunction with the Tide client context, an example object can be tracked for changes and be sent to the Seam query to be used as restrictions for the query.

A simple example is probably more clear :

Seam components.xml :

<component name="examplePerson" class="test.granite.ejb3.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 gives us a very classic Seam Query component, the only important thing being the max-results property, which will serve as a the page size for the client collection. It is absolutely necessary that the page size is greater than the maximum number of elements expected to be displayed on the UI component (in general 30-50 is a good fit).

On the Flex side,

import org.granite.tide.seam.framework.PagedQuery;

var seamContext:Context = Seam.getInstance().getSeamContext();

function initColl():void {
Seam.getInstance().addComponent("people", PagedQuery);

people.dataProvider = seamContext.people;
}

<mx:TextInput id="search" text="{seamContext.examplePerson.lastName}"
enter="seamContext.examplePerson.lastName = search.text; seamContext.people.refresh();"/>
<mx:button label="Search" click="seamContext.examplePerson.lastName = search.text; seamContext.people.refresh();">
<mx:datagrid id="people">
<mx:column datafield="firstName" headertext="First Name"/>
<mx:column datafield="lastName" headertext="Last Name"/>
</mx:DataGrid>

This is basically all which is needed to bridge our Seam query component with the Flex UI.
Note that the PagedQuery class actually extends ListCollectionView, and thus can be used whereever a collection can be used in Flex.


Propagation of Seam events :


In this part, we will only show a simple propagation of synchronous events. The asynchronous processing necessitates a little more configuration as Gravity is involved and this will be the subject for the second part of this post.

A new method in the Context object allows the client to register listeners for server events it is interested in.
For example, you can call :
seamContext.addContextEventListener(type:String, listener:Function, remote:Boolean = false);

- type is the type of the Seam event,
- listener is a handler function, which a signature function listener(event:TideContextEvent):void
- remote indicates that the event is expected to come from the remote server

Once registered, the listener function will be called after any remote call during which the interesting event type is raised by a component.

Let's take the Seam booking example :

public class HotelBookingAction implements HotelBooking {

@In private Events events;
...

@End
public void confirm() {
...
events.raiseTransactionSuccessEvent("bookingConfirmed");
}
}

On the Flex side, we could do this :

private function init():void {
...
seamContext.addContextEventListener("bookingConfirmed", bookingConfirmedHandler, true);
...
}

private function confirmBooking():void {
seamContext.hotelBooking.confirm();
}

private function bookingConfirmedHandler(event:TideContextEvent):void {
Alert.show("Booking confirmed");
seamContext.bookingList.refresh();
}

The bookingConfirmedHandler callback should be called when the remote call to hotelBooking.confirm() returns, because we have registered interest in the event, and it has been raised by the Seam component.

Handling of asynchronous events is very similar but needs a little more configuration to integrate with Gravity push.
The integration is made at two levels :
- events raised asynchronously by Seam components of the user context
- events raised by asynchronous method calls from Seam components of the user context

The first thing is to configure a messaging destination named 'seamAsync' in services-config.xml which will handle communication with the server components :

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


Second, we need to startup the messaging client at the beginning of the application, for example in a static initializer block of our main mxml :

{
Seam.getInstance().addPlugin(TideAsync.getInstance("seamAsync"));
}

That's all.


Now we are able to listen to events raised asynchronously with Events.instance().raiseAsynchronousEvent() or raiseTimedEvent(), or to events raised from an asynchronous method call.

The client part is exactly the same as for synchronous events :

tideContext.addContextEventListener("notification", notificationHandler, true);

For security reasons, only events initiated by a component in the current user session context can be received. This cannot be used for now to communicate between different users.

Possible uses of this feature could be :
- notify the user of incoming events (new mail, new chat message, ...)
- notify the user periodically of the progress of a long process running on the server

This is really powerful and makes the use of the Seam asynchronicity framework almost transparent for the Flex client.
The client code is exactly the same, so you can make any method call asynchronous or not depending on the needs of the application.


You can try and see most of these things working on the example projects (graniteds_seam and graniteds_seam_booking). You can also go to the GDS forum for any help or comments.

A similar integration for Spring is in progress and may be included in a future release (maybe 1.2).