Thursday, April 09, 2009

1st look at App Engine using JDO persistence capable classes over GWT RPC

After the exciting launch of App Engine for Java at Campfire One, I wanted to create a simple GWT + App Engine app using JDO persistence capable POJOs over RPC.

Getting started using the new Google Plugin for Eclipse was easy. The plugin even downloads both the GWT and App Engine SDKs for you. Things couldn't be easier.

  • In Eclipse, select 'File -> New Web Application Project' or just click on the new 'g' icon
  • Enter a project name (MyApp) and a default package (fredsa.myapp); click OK

This give you a simple GWT/App Engine stub application to modify further. Included is a GreetingService which allows the user to post messages to the server via RPC and receive a simple reply.

I want to modify the application to save the messages to the datastore. I need to introduce a data model class. Using standard JDO annotations is an easy way to turn a POJO into a class that can be easily persisted to the App Engine data store. Here's my new Message class:

package fredsa.myapp.client;

import com.google.appengine.api.datastore.Key;
import java.io.Serializable;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

@PersistenceCapable
public class Message implements Serializable {

  @PrimaryKey
  @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  Key id;

  @Persistent
  private String author;

  @Persistent
  private String message;

  public String getMessage() {
    return message;
  }

  public void setMessage(String message) {
    this.message = message;
  }

  public String getAuthor() {
    return author;
  }

  public void setAuthor(String author) {
    this.author = author;
  }
}

I rewrite the default GreetingService interface (and also GreetingServiceAsync and GreetingServiceImpl) to take my new Message class as an argument rather than a plain old String. Everything compiles nicely in the IDE so I launch hosted mode.

  • Right-click 'MyApp' project and select 'Run As' and then the new 'Web Application' option provided by the plugin

Unfortunately hosted mode reports an error:

[ERROR] Errors in 'file:/C:/fred/MyApp/src/fredsa/myapp/client/Message.java'
[ERROR] Line 3: The import com.google.appengine cannot be resolved
[ERROR] Line 16: Key cannot be resolved to a type

What's up with that? Everything compiled just fine in the IDE?

Missing source files is a common stumbling block. What we've run into here is that the GWT compiler cannot find the source code for the com.google.appengine.Key class, something Sriram Narayan alluded to in his recent post.

My IDE was happy because all it needs is a *.class file. GWT, however, needs access to a suitable *.java file.

Usually the original source is all the GWT compiler needs. In some cases, such as the emulated JRE classes, you can provide the GWT compiler with alternative implementations so that you can do things in a browser / JavaScript friendly way. In other cases, you may not need a full implementation in client code, and a stub will suffice.

Since I'm not doing anything with the Key class on the client I'm going to stub it out. This actually requires a few steps and involves the super-src feature of GWT XML module files. Here goes:

  • In my src directory create a new Java package fredsa.appengine.workaround
  • Right-click this package and create a new file called Datastore.gwt.xml with the following contents:
<?xml version="1.0" encoding="UTF-8"?>
<module>
  <super-source path="translatable" />
</module>

  • Right-click on 'MyApp' to create a new directory called super; note this is a regular directory and specifically not a source directory so the icons for src and super will look different in the Eclipse Package Explorer
  • In the super directory create a file fredsa/appengine/workaround/translatable/com/google/appengine/api/datastore/Key.java with the following contents:

package com.google.appengine.api.datastore;

import java.io.Serializable;

public class Key implements Serializable, Comparable {

  private String appId;

  private long id;

  private Key() {
  }

  public int compareTo(Object o) {
    throw new UnsupportedOperationException();
  }

}



  • Modify MyApp.gwt.xml to include the following line:

<inherits name="fredsa.appengine.workaround.Datastore" />

  • Right-click on 'MyApp' project and select 'Google ->Web Toolkit'; delete the Datastore entry point module; Note, this will also remove the module from the launch configuration, which is what will avoid the following error when launching hosted mode:

java.lang.NullPointerException
 at com.google.gwt.core.ext.linker.impl.StandardLinkerContext.(StandardLinkerContext.java:164)
 at com.google.gwt.dev.HostedMode.link(HostedMode.java:452)
 at com.google.gwt.dev.HostedMode.doStartup(HostedMode.java:353)
 at com.google.gwt.dev.HostedModeBase.startUp(HostedModeBase.java:585)
 at com.google.gwt.dev.HostedModeBase.run(HostedModeBase.java:397)
 at com.google.gwt.dev.HostedMode.main(HostedMode.java:232)




We're finally ready to use the Key class in client code.

  • Again right-click 'MyApp' project and select 'Run As -> Web Application'

The application should lunch and you should be able to type a message, hit 'Send', and then see the reply come back over RPC.

Now we need to actually persist the messages to the datastore. That's easy. Using the PMF class from the JDO documentation we simply add a couple of lines to GreetingServerImpl:

public String greetServer(Message message) {
  PersistenceManager pm = PMF.get().getPersistenceManager();
  pm.makePersistent(message);

  String serverInfo = getServletContext().getServerInfo();
  String userAgent = getThreadLocalRequest().getHeader("User-Agent");
  return "Hello, " + message.getAuthor() + "! I am running " + serverInfo
      + ". It looks like you are using:" + userAgent;
}




We can now even deploy this application to the cloud with just a few clicks:

  • As a workaround for the plugin upload functionality, we're going to temporarily treat the super folder as a source folder; Right-click 'MyApp' project and select 'Build Path -> Use as source folder'; this will cause the project to have an error, which we will ignore:
The declared package "com.google.appengine.api.datastore" does not match the expected package "fredsa.appengine.workaround.translatable.com.google.appengine.api.datastore"


  • Right-click 'MyApp' project and select 'Google -> Deploy to App Engine'
  • Click on the 'App Engine project settings...' to enter (the missing) unique application id, which you registered at http://appengine.google.com/
  • Enter your username/password
  • Click deploy; answer 'Yes' when asked about the error in the project

Your app has now been deployed to the cloud. You can go to http://your-app-id.appspot.com, or to your own domain name if you've set that app via a Google Apps account. Go ahead and create a couple of messages and then browse the messages you created in the datastore:

  • Login to http://appengine.google.com/
  • Click on your app-id
  • Select 'Data Viewer' on the left, choose 'Message' (i.e. the class name we used above in our code) in the Entity drop down

I'll leave it as an exercise for you to query the datastore and display the most recent messages to the user.

Enjoy.

16 comments:

Alejandro D. Garin said...

Thank you for your post, very usefull.

balopat said...
This comment has been removed by the author.
balopat said...

hey!

Good article!
Though I tried it, gwtc still did not see Key.java.
I found that in my build.xml adding the line

<pathelement location="super"/>

to the gwtc target and also the hosted target into the classpath solved my problem, if I wanted to use ant to launch the program.

I had to also add the super folder to the classpath in the run configuration, using Advanced..->Add directory (MyApp/super)

cheers,
balopat

Unknown said...

I believe the intent here might be to not use domain/transactional model objects as DTOs. In which case, you wouldn't have run into this problem.

Ray Ryan said...

Nice write up, Fred. But I challenge you to add a collection of any kind to your Message class.

Brandon Donnelson said...

I finally figured out a work around to get the JDO to work.

I would rather do the object that gets Serialized but this will do for now. I embedded the object.

http://mathflashcard.appspot.com/
http://code.google.com/p/gwt-examples/source/browse/trunk/FlashCard/src/com/gawkat/flashcard/server/jdo/MathDataJdo.java

Sam E. said...

Fred, you are awesome.

This totally solved my problem and now I can use my class on both the client and server. I didn't have a problem with "Key" but "Text" instead, but the same idea.

I also use your GWT-LOG library and that has been soo helpful along the way.

Thanks!

Sam E. said...

Ok, I lied about getting Text to work. It did compile, but when returning the data from the RPC call, it goes nuts and says:

"This application is out of date, please click the refresh button on your browser."

Anonymous said...

> Nice write up, Fred. But I challenge you to add a collection of any kind to your Message class.

+1

George Armhold said...

Is it true that Collections will not work?

Unknown said...

Anybody know how to do this for User type? I tried following and now my hosted mode crashes with an exit code of 2. Any ideas?

Prashant said...

while i compiling this code then it will throw this error. So, Pls give me right solution...
and also give the full details of "super" directory in which what i should that i don't know...

[Error:]
No source code is available for type com.google.appengine.api.datastore.Key; did you forget to inherit a required module?

Finding entry point classes
[ERROR] Unable to find type 'fredsa.myapp.client.MyApp'
[ERROR] Hint: Previous compiler errors may have made this type unavailable
[ERROR] Hint: Check the inheritance chain from your module; it may not be inheriting a required module or a module may not be adding its source path entries properly


================
Pls give right solution as soon as possible becoz i want to implement this code in my application.

DeliveryNinja said...

If only this worked for Text class as well. I've created a blog which has posts but the posts may exceed the 500 char limit of String so I needed Text but ran into the problem of appengine import can't be resolved. Its all a bit of a nightmare. I might try to do a cast on the server side of the application and see if that helps at all. Thanks for the info!

Anonymous said...

I'm actually attempting to use the properties of Key on the gwt side with a bean-copy of the rest of the Entity in order to access parent-child key relationships.

this is the Key proxy i've come up with, wish me luck...


package com.google.appengine.api.datastore;

import java.io.Serializable;

public final class Key implements Serializable, Comparable {
private static final long serialVersionUID = -448150158203091507L;
static final long NOT_ASSIGNED = 0L;
private Key parentKey;
private String kind;
private String appId;
private long id;
private String name;
private transient Serializable appIdNamespace;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Key)) return false;

Key key = (Key) o;

if (id != key.id) return false;
if (appId != null ? !appId.equals(key.appId) : key.appId != null) return false;
if (appIdNamespace != null ? !appIdNamespace.equals(key.appIdNamespace) : key.appIdNamespace != null)
return false;
if (kind != null ? !kind.equals(key.kind) : key.kind != null) return false;
if (name != null ? !name.equals(key.name) : key.name != null) return false;
if (parentKey != null ? !parentKey.equals(key.parentKey) : key.parentKey != null) return false;

return true;
}

@Override
public int hashCode() {
int result = parentKey != null ? parentKey.hashCode() : 0;
result = 31 * result + (kind != null ? kind.hashCode() : 0);
result = 31 * result + (appId != null ? appId.hashCode() : 0);
result = 31 * result + (int) (id ^ (id >>> 32));
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (appIdNamespace != null ? appIdNamespace.hashCode() : 0);
return result;
}

@Override
public int compareTo(Key o) {
int x = 0;
return 0 == (x = o.parentKey.compareTo(parentKey)) ?
0 == (x = o.kind.compareTo(kind)) ?
0 == (x = o.appId.compareTo(appId)) ?
0 == (x = ((Long) o.id).compareTo((Long) id)) ?
0 == (x = o.name.compareTo(name)) ? x : x : x : x : x : x;


}
}

pushkar.00 said...

Question - Did you try to get the source code for key.java?
Wouldnt that be better. I am giving to give it shot ..

http://code.google.com/p/googleappengine/source/checkout

thanks

Unknown said...

This is still helping me 2 years later - thanks!

(Keys.java source code doesn't work -it has other libraries which are now supported in GWT)