lundi 20 avril 2009

GraniteDS 2.0 on Google App Engine

Google App Engine is the buzzword of the week for Java developers, so of course we have tried to make GraniteDS 2.0 work on it.

After fixing a few issues we managed to have an application working correctly, that you can see at http://gdsgae.appspot.com (log in with admin/admin or user/user). The application is relatively simple, but it demonstrates a complete integration between GraniteDS, Spring MVC and JDO/DataNucleus. The Eclipse project can be downloaded here.

Having a working environment in the local development server has been quite easy, but two bugs in the GAE production environment prevented GDS to work correctly. First there is a bug in XML/XPath support (GAE 1255) that we use for configuration parsing, and we had to add support for Xalan as an alternative XPath provider. Second there is a bug in request.getInputStream (GAE 1339) that throws EOFExceptions and that we could work around by buffering the stream in the AMF servlet.

So the first good news is that the latest GraniteDS 2.0 snapshot perfectly works in GAE if you add xalan.jar and serializer.jar (from the Apache Xalan distribution) in your WEB-INF/lib folder.

Then by pure coincidence we had been working lately on JDO and DataNucleus support in GDS (thanks a lot to Stephen More for his work on it) so it was natural to try it with the GAE datastore. The second good news is that GraniteDS fully supports JDO detached objects from GAE, and that all Tide features can work with JDO annotated entities (including lazy loading).
There seem to be a lot of bugs in the GAE persistence support though when not using Key objects as entity ids. So we have added the GAEKeyConverter that can serialize GAE keys to strings in ActionScript, and support can be configured in granite-config.xml with :
<converters>
<converter type="org.granite.messaging.amf.io.convert.impl.GAEKeyConverter"/>
</converters>

JDO annotated entities has also been implemented in the GDS Eclipse builder to generate AS3 classes from a JDO model and it works with the Google Eclipse plugin. If you also use Flex Builder, it's recommended to setup the order or the builders in the project properties as follows: Java, GraniteDS, Enhancer, Flex, GAE Project validator, GAE Webapp validator.

So we're happy to be able to add Google App Engine to the list of the Java platforms supported by GraniteDS, after Tomcat, Jetty, JBoss 4/5, GlassFish V2/V3 and WebLogic.

15 commentaires:

Kartik Shah a dit…

I am getting 403: Forbidden error when I click on the http://gdsgae.appspot.com

Unknown a dit…

Question: does this GraniteDS configuration allow server push feature in GAE-- i.e., is it possible in GAE+graniteDS to fire a message from server to the client??

Thanks. Merci.


P.S. I wanted to look at this demo also but I too saw the 403 error.

William Draï a dit…

The link is fixed.

GraniteDS does not support push in GAE for now as it relies on Jetty continuations that are not available. We are trying to find a workaround but the 30s request limit is also an issue.

fineline a dit…

Typical - read this just as I had finished working through those two bugs myself! Much nicer to have an "official" fix than my hacked version though.

Unknown a dit…

I'm getting a Type not found, or not a compile time constant error in the Task.as source.

public function get id():Key {
return _id;
}
Any suggestions? Thanks

Puran a dit…

i tried to run it locally and i am getting this exception
SEVERE: AMF message error
org.granite.messaging.amf.io.AMF3SerializationException

Caused by: java.lang.RuntimeException: Could not read externalized object: org.graniteds.example.gae.entities.Task@41a2e40f
at org.granite.messaging.amf.io.AMF3Deserializer.readAMF3Object(AMF3Deserializer.java:373)
at org.granite.messaging.amf.io.AMF3Deserializer.readObject(AMF3Deserializer.java:126)
at org.granite.messaging.amf.io.AMF3Deserializer.readAMF3Object(AMF3Deserializer.java:417)
at org.granite.messaging.amf.io.AMF3Deserializer.readObject(AMF3Deserializer.java:126)
at org.granite.messaging.amf.io.AMF3Deserializer.readObject(AMF3Deserializer.java:88)
... 35 more
Caused by: java.lang.RuntimeException: org.granite.messaging.amf.io.convert.NoConverterFoundException: Cannot convert: true to: class java.lang.String
at org.granite.messaging.amf.io.util.FieldProperty.setProperty(FieldProperty.java:62)
at org.granite.messaging.amf.io.util.Property.setProperty(Property.java:50)
at org.granite.messaging.amf.io.util.externalizer.DefaultExternalizer.readExternal(DefaultExternalizer.java:119)
at org.granite.datanucleus.DataNucleusExternalizer.readExternal(DataNucleusExternalizer.java:124)
at org.granite.messaging.amf.io.AMF3Deserializer.readAMF3Object(AMF3Deserializer.java:369)
... 39 more
Caused by: org.granite.messaging.amf.io.convert.NoConverterFoundException: Cannot convert: true to: class java.lang.String
at org.granite.messaging.amf.io.convert.Converters.getConverter(Converters.java:119)
at org.granite.messaging.amf.io.convert.Converters.convert(Converters.java:132)
at org.granite.messaging.amf.io.util.Property.convert(Property.java:60)
at org.granite.messaging.amf.io.util.FieldProperty.setProperty(FieldProperty.java:60)
... 43 more
Apr 28, 2009 6:53:33 PM com.google.apphosting.utils.jetty.JettyLogger warn
WARNING: /graniteamf/amf: org.granite.messaging.amf.io.AMF3SerializationException
Apr 28, 2009 6:53:33 PM org.granite.logging.JdkLogger error
SEVERE: AMF message error
org.granite.messaging.amf.io.AMF3SerializationException
at org.granite.messaging.amf.io.AMF3Deserializer.readObject(AMF3Deserializer.java:94)
at org.granite.messaging.amf.io.AMF3Deserializer.readAMF3Array(AMF3Deserializer.java:261)
at org.granite.messaging.amf.io.AMF3Deserializer.readObject(AMF3Deserializer.java:124)
at org.granite.messaging.amf.io.AMF3Deserializer.readObject(AMF3Deserializer.java:88)
at org.granite.messaging.amf.io.AMF3Deserializer.readAMF3Array(AMF3Deserializer.java:261)
at org.granite.messaging.amf.io.AMF3Deserializer.readObject(AMF3Deserializer.java:124)
at org.granite.messaging.amf.io.AMF3Deserializer.readAMF3Object(AMF3Deserializer.java:403)
at org.granite.messaging.amf.io.AMF3Deserializer.readObject(AMF3Deserializer.java:126)
at org.granite.messaging.amf.io.AMF3Deserializer.readObject(AMF3Deserializer.java:88)
at org.granite.messaging.amf.io.AMF0Deserializer.readAMF3Data(AMF0Deserializer.java:324)
at org.granite.messaging.amf.io.AMF0Deserializer.readData(AMF0Deserializer.java:376)
at org.granite.messaging.amf.io.AMF0Deserializer.readArray(AMF0Deserializer.java:239)
at org.granite.messaging.amf.io.AMF0Deserializer.readData(AMF0Deserializer.java:362)
at org.granite.messaging.amf.io.AMF0Deserializer.readBodies(AMF0Deserializer.java:155)
at org.granite.messaging.amf.io.AMF0Deserializer.(AMF0Deserializer.java:94)
at org.granite.messaging.webapp.AMFMessageFilter.doFilter(AMFMessageFilter.java:81)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
at com.google.apphosting.utils.servlet.TransactionCleanupFilter.doFilter(TransactionCleanupFilter.java:43)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:360)
at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:712)
at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
at com.google.apphosting.utils.jetty.DevAppEngineWebAppContext.handle(DevAppEngineWebAppContext.java:54)
at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
at com.google.appengine.tools.development.JettyContainerService$ApiProxyHandler.handle(JettyContainerService.java:306)
at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
at org.mortbay.jetty.Server.handle(Server.java:313)
at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:506)
at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:844)
at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:644)
at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:211)
at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:381)
at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:396)
at org.mortbay.thread.BoundedThreadPool$PoolThread.run(BoundedThreadPool.java:442)
Caused by: java.lang.RuntimeException: Could not read externalized object: org.graniteds.example.gae.entities.Task@589d0bde
at org.granite.messaging.amf.io.AMF3Deserializer.readAMF3Object(AMF3Deserializer.java:373)
at org.granite.messaging.amf.io.AMF3Deserializer.readObject(AMF3Deserializer.java:126)
at org.granite.messaging.amf.io.AMF3Deserializer.readAMF3Object(AMF3Deserializer.java:417)
at org.granite.messaging.amf.io.AMF3Deserializer.readObject(AMF3Deserializer.java:126)
at org.granite.messaging.amf.io.AMF3Deserializer.readObject(AMF3Deserializer.java:88)
... 35 more
Caused by: java.lang.RuntimeException: org.granite.messaging.amf.io.convert.NoConverterFoundException: Cannot convert: true to: class java.lang.String
at org.granite.messaging.amf.io.util.FieldProperty.setProperty(FieldProperty.java:62)
at org.granite.messaging.amf.io.util.Property.setProperty(Property.java:50)
at org.granite.messaging.amf.io.util.externalizer.DefaultExternalizer.readExternal(DefaultExternalizer.java:119)
at org.granite.datanucleus.DataNucleusExternalizer.readExternal(DataNucleusExternalizer.java:124)
at org.granite.messaging.amf.io.AMF3Deserializer.readAMF3Object(AMF3Deserializer.java:369)
... 39 more
Caused by: org.granite.messaging.amf.io.convert.NoConverterFoundException: Cannot convert: true to: class java.lang.String
at org.granite.messaging.amf.io.convert.Converters.getConverter(Converters.java:119)
at org.granite.messaging.amf.io.convert.Converters.convert(Converters.java:132)
at org.granite.messaging.amf.io.util.Property.convert(Property.java:60)
at org.granite.messaging.amf.io.util.FieldProperty.setProperty(FieldProperty.java:60)
... 43 more

William Draï a dit…

For the 2 last comments, you probably have regenerated the entities with a GraniteDS 1.2 Eclipse builder that is neither compatible with the GraniteDS 2.0 runtime nor knows anything about the GAE/J Key class (and thus does not generate String ids).
You can try to use the latest snapshot of the GDS 2.0 builder.

Anonyme a dit…

Hello William,

Thank you for this post. It is a very nice example and much better than the code snippets in the granite documentation.

So far I was only using LCDS and the move is not exactly easy.

What I was wondering is how you would structure a more complex project. Let us say you have several MXML files for different views. In that case it would be nice to have some sort of central backing bean that interfaces to the server side. I read the doc and there you speak about "client components" which seems to be such a thing. So I tried to use what is described there in the Task project but I can not get it to work.
Could you give an advice how to do that?

Thank you,
Tobias

Anonyme a dit…

Hello William,

I have another thing I wanted to ask. Is it possible to use as authentication provider the Google API. I mean so that users can sign in using their google account?
That would be a great feature....

Cheers,

Tobias

William Draï a dit…

Using a client component is relatively easy. Just create a controller class, say BookManagerCtl.as annotated with a [Name].

[Name('taskManagerCtl')]
[Bindable]
public class TaskManagerCtl {

[In]
public var taskController:Object;

[In(create="true")]
public var taskList:ArrayCollection;

[Observer("createTask")]
public function create():void {
var task:Task = new Task();
task.title = title.text;
title.text = '';
taskController.create({task: task}, createResult);
}

private function createResult(event:TideResultEvent):void {
taskList.addItem(event.context.task);
}

Then register the controller in a static block in the Script part of the main mxml :
Spring.getInstance().addComponents(
[TaskManagerCtl]
);

Then you can call send Tide events from the main mxml :

<mx:Button label="Save" click="dispatchEvent(new TideUIEvent('createTask'))"/>

I'll post later a more complete and polished example with Spring, but you can also have a look to the graniteds_tide_spring example in the GDS distribution.


Concerning Google Accounts API, the example just uses Spring Security, and I would be very surprised that noone has built a Spring authentication provider for Google Accounts.

Anonyme a dit…

Hello William,

I think I got everything running now. It is really cool how GraniteDS can connect components and events without hardcoding them.

The thing is that I found a strange thing. In fact I spend hours on that but I can not find a solution.
Finally I could track down the problem to the fact that my class was containing a java.util.Date property.
To verify this I added a date field to your task example:

public class Task {
....

@Persistent
private Date lastChaned;

this field breaks the entire thing. You can create tasks but when you modify them (e.g. assign) they are not any more persisted (the status stays on CREATED).
public ModelMap assignTaskToCurrentUser(@RequestParam("taskId") Key taskId) {
PersistenceManager pm = pmf.getPersistenceManager();
try {
Task task = (Task)pm.getObjectById(Task.class, taskId);
task.setLastChaned(new Date());

In the logs I found this:
FINE: Object "org.graniteds.example.gae.entities.Task@7cb66a" (id="org.graniteds.example.gae.entities.Task:Task(36)") is having the value in field "lastChaned" replaced by a SCO wrapper

I really did not do anything else than to add this field and to set a new date every time something is persisted.

I dont know if this is a GAE or a Granite problem. Do you have any idea what I am doing wrong?

Thank you,

Tobias

William Draï a dit…

It's probably due to this: http://code.google.com/p/datanucleus-appengine/issues/detail?id=30

Romain a dit…

Hi William,

Great job but I am still having a problem using JPA. When I transfer objects from datastore to Flex, no problem I get it on Flex side, can see every properties. But when I transfer my object back to the server (to update it in datastore), I receive this exception because my object is not in detached state:

org.datanucleus.exceptions.NucleusUserException: Attempt
was made to manually set the id component of a Key primary key. If
you want to control the value of the primary key, set the name
component instead.

When the Key of my DTO is deserialized, datanucleus is not able anymore to persist it.

I am now trying to use Gilead framework which seems able to keep all information of an enhanced object from datastore to transfer it on the wire.

You do not have this problem in your example because you do not update object directly coming from Flex but only some fields. But my persistence object has other persistent object in it, so it's much complicated. Do you have any idea how to do that?

William Draï a dit…

I can't reproduce this error. Maybe you can post your entities and server code on the GDS forums.

Oz a dit…

The granite-config.xml provided in the blog is not well formed. See the line:

<gae polling-interval="500">

I've coreccted it and it's still giving errors on GAE (1.2.6). I've also downloaded the gdsgaepush example and the files look different than the blog.