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.
Post a Comment