mardi 8 avril 2008

Quick presentation of the GraniteDS Tide project

We are in the process of releasing the GraniteDS Tide subproject as part of GraniteDS 1.1.

This new subproject is aimed at improving the data services part of GraniteDS and simplify the Flex client programming model when integrating with server side frameworks.

In a first step Tide will be focused on a strong integration with Seam and Hibernate, built on the existing GraniteDS/Seam integration, to extend the Seam programming model to the Flex client and make the use of remote Seam components very easy.

The main goal of Tide is to remove all the useless and redundant declarations and integration code both on Flex side and on server side, for example the dozens of destinations for RemoteObjects in service-config.xml and mxmls, and the necessary service adapters on the server.

Tide is heavily based on the concept of a client context, which proxies all communications between the client and the server. This client context provides an entity cache and a component container, similar to the server PersistenceContext and Seam context.

HelloWorld with Tide

Seam component :

@Stateless
@Name("helloAction")
public class HelloAction implements HelloLocal {
public String sayHello(String name) {
return "Hello, " + name;
}
}


Flex code :

public function init():void {
// The main piece : getting the client context
var tideContext:Context = Seam.getInstance().getSeamContext();
// Triggers the remote call of the 'helloAction' component
tideContext.helloAction.sayHello("Jimi", resultHandler);
}

private function resultHandler(event:TideResultEvent):void {
// Should output 'Hello, Jimi'...
trace(event.result);
}


This is very similar to the JavaScript Seam remoting way of calling server components. The Context object proxies all calls on its managed components and transmits them to the server through a single hidden RemoteObject. That means that just one destination has to be declared in services-config.xml.
A faultHandler can optionally be specified for handling errors.


Using existing Seam components

Let's go one step further with an extract of the Seam contactlist example :

Seam components :

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

Flex code :

public function init():void {
var tideContext:Context = Seam.getInstance().getSeamContext();
tideContext.exampleContact.firstName = '';
tideContext.exampleContact.lastName = 'hendrix';
// Triggers the remote call of the 'contacts' component.
tideContext.contacts.getResultList(resultHandler);
}

private function resultHandler(event:TideResultEvent):void {
var list:ArrayCollection = ArrayCollection(event.result);
}


Here the main point is that the exampleContact properties have been sent to the server along with the remote call of getResultList. As the Context is an ActionScript proxy, it is able to detect all assignments on objects attached to it and to propagate them when needed.

Another kind of use is also possible and makes binding of server objects in mxml very easy :

MXML code :

<mx:textinput id="inputFirstName"/>
<mx:textinput id="inputLastName"/>
<mx:button label="Go"
click="tideContext.exampleContact.firstName = inputFirstName.text;
tideContext.examplePerson.lastName = inputLastName.text;
tideContext.contacts.refresh();"/>

<mx:datagrid id="contacts" dataprovider="{tideContext.contacts.resultList}">
...
</mx:datagrid>


The resultList collection will be updated each time the refresh() method is called on the remote component. Interestingly, the collection instance itself is not replaced, instead data received from the server are merged with the existing data, and even the instances of Contact are kept and merged when possible, allowing the DataGrid to correctly keep its selectedItem (this would not be the case if the object instances were overwritten).


Integration with Seam bijection

Still better, Tide integrates with the Seam component bijection interceptor and all outjected objects and datamodels are propagated from the server to the client context.
Here is an example from Seam booking :

Seam component :

@Stateful
@Name("hotelSearch")
@Scope(ScopeType.SESSION)
@Restrict("#{identity.loggedIn}")
public class HotelSearchingAction implements HotelSearching {

@PersistenceContext private EntityManager em;

private String searchString;
...

@DataModel private List hotels;

public void find() {
page = 0;
queryHotels();
}

public void nextPage() {
page++;
queryHotels();
}
...

public String getSearchString() {
return searchString;
}

public void setSearchString(String searchString) {
this.searchString = searchString;
}
...
}

MXML code :

<mx:textinput id="inputSearchString"/>
<mx:button label="Search"
click="tideContext.hotelSearch.searchString = inputSearchString.text;
tideContext.hotelSearch.find();"/>

<mx:datagrid id="hotels" dataprovider="{tideContext.hotels}">
...
</mx:datagrid>



This 3 relatively simple examples are significantly less verbose and easier to read than the classic way with RemoteObjects.


Advanced features

But Tide does not stop here and comes with some more advanced features which allow a more complete integration with the server side :

Propagation of context messages from the server

All Seam context messages are serialized back to the client and available in the client context after each remote call.

Flex code :

public function login(username:String, password:String):void {
tideContext.identity.username = username;
tideContext.identity.password = password;
tideContext.identity.login(loginHandler);
}

private function loginHandler(event:TideResultEvent):void {
var message:FacesMessage = tideContext.messages.getItemAt(0);
}


Integration with Seam security, in particular the identity component

The Tide client identity component takes care of both the RemoteObject credentials and the server Seam security, allowing a clean login/logout functionality.


Support for client conversations with possible transparent synchronization with a server conversation.
A new separate context object is created for each new server conversation. All remote calls made from this new context are then considered to be in the same server conversation.

Seam component :

@Stateful
@Name("hotelBooking")
@Restrict("#{identity.loggedIn}")
public class HotelBookingAction implements HotelBooking {
...
@Begin
public void selectHotel(Hotel selectedHotel) {
hotel = em.merge(selectedHotel);
}
...
}

Flex code :

private function selectHotel(hotel:Hotel):void {
tideContext.hotelBooking.selectHotel(hotel, selectHotelHandler);
}

private function selectHotelHandler(event:TideResultEvent):void {
// Get a new client context allowing interaction with the server conversation
var conversationContext:Context = event.context as Context;
}


Integration with server side validation (Hibernate Validator)

A TideValidator component listening for server validation errors can be attached to any Flex control and trigger the display of the error message.

MXML code :

<mx:textinput id="inputEmail" text="{tideContext.contactHome.instance.email}"/>

<tide:tidevalidator entity="{tideContext.contactHome.instance}"
property="email" listener="{inputEmail}"/>


Conclusion

The project is still alpha and can only be found in the nightly builds or built from the subversion trunk for the moment, but we expect to get a preview version in the forthcoming GraniteDS 1.1 RC and a stable version as part of the final release of GraniteDS 1.1.

Two sample projects are available :
- The graniteds_seam_booking project, using most features of Tide.
- The graniteds_seam simple address book.

We hope that it will greatly simplify the development for people currently working with both Flex and Seam.

Don't hesitate to try it and give some feedback.

4 commentaires:

Chris a dit…

Sounds very promising so I gave it a try. Now I'm facing the following issue (and didn't find any solution either in the seam-Forums not anywhere else):

09:01:32,016 DEBUG [AMF3Serializer] writeAMF3Object(o=flex.messaging.messages.ErrorMessage {
faultCode = Could not instantiate factory: org.granite.tide.seam.SeamServiceFactory@b434dc {
serviceExceptionHandler: org.granite.tide.seam.SeamServiceExceptionHandler@fa751c
}
faultDetail = nullorg.granite.messaging.service.ServiceException
at org.granite.messaging.service.ServiceFactory.getServiceFactory(ServiceFactory.java:81)
at org.granite.messaging.service.ServiceFactory.getFactoryInstance(ServiceFactory.java:62)
at org.granite.messaging.amf.process.AMF3MessageProcessor.processRemotingMessage(AMF3MessageProcessor.java:109)

...

Caused by: java.lang.IllegalStateException: No application context active
at org.jboss.seam.Component.forName(Component.java:1807)
at org.granite.tide.seam.SeamServiceFactory.configure(SeamServiceFactory.java:64)
at org.granite.messaging.service.ServiceFactory.getServiceFactory(ServiceFactory.java:79)
... 32 more

faultString = null
rootCause = java.lang.IllegalStateException: No application context active
extendedData = {}
correlationId = 5B348966-1F76-742B-75BD-47E04AEFA528
destination = null
headers = {}
messageId = C5722834-45C3-4336-9C1C-C1EF07F6CA8C
timestamp = 1208091691897
clientId = 9F745076-5295-4C8E-8FAC-1F2A158A8B75
timeToLive = 0
body = null
}) - Done

Chris a dit…

It's working using the latest stuff from SVN!

William Draï a dit…

Yes, the link in the post is broken. I will change this.
Thanks for trying anyway, the svn trunk is almost frozen for the release and should be working fine, as the latest nightly builds.

Adam Wells a dit…

The project works for me under JBoss 4.2.0 / windows, but under Debian, I get the following error (any ideas?):

22:28:15,139 INFO [Server] JBoss (MX MicroKernel) [4.2.0.GA_CP01 (build: SVNTag=JBPAPP_4_2_0_GA_CP01 date=200709131706)] Started in 1m:17s:590ms
22:28:55,782 ERROR [AMFMessageServlet] AMF message error
java.lang.ClassCastException: flex.messaging.messages.CommandMessage
at org.granite.messaging.amf.process.AMF0MessageProcessor.process(AMF0MessageProcessor.java:59)
at org.granite.messaging.webapp.AMFMessageServlet.doPost(AMFMessageServlet.java:59)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:710)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.granite.messaging.webapp.AMFMessageFilter.doFilter(AMFMessageFilter.java:87)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:83)
at org.jboss.seam.debug.hot.HotDeployFilter.doFilter(HotDeployFilter.java:68)
at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
at org.jboss.seam.web.MultipartFilter.doFilter(MultipartFilter.java:85)
at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
at org.jboss.seam.web.ExceptionFilter.doFilter(ExceptionFilter.java:64)
at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
at org.jboss.seam.web.RedirectFilter.doFilter(RedirectFilter.java:45)
at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
at org.ajax4jsf.webapp.BaseXMLFilter.doXmlFilter(BaseXMLFilter.java:141)
at org.ajax4jsf.webapp.BaseFilter.doFilter(BaseFilter.java:281)
at org.jboss.seam.web.Ajax4jsfFilter.doFilter(Ajax4jsfFilter.java:60)
at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
at org.jboss.seam.web.LoggingFilter.doFilter(LoggingFilter.java:58)
at org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69)
at org.jboss.seam.servlet.SeamFilter.doFilter(SeamFilter.java:158)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:230)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
at org.jboss.web.tomcat.security.SecurityAssociationValve.invoke(SecurityAssociationValve.java:179)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:433)
at org.jboss.web.tomcat.security.JaccContextValve.invoke(JaccContextValve.java:84)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:128)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:104)
at org.jboss.web.tomcat.service.jca.CachedConnectionValve.invoke(CachedConnectionValve.java:157)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:241)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:844)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:580)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:447)
at java.lang.Thread.run(Thread.java:595)