Developing Google Gadget with Eclipse GWT plugin
These day I'm interested in this technlogy, because I think that many of our site can be "refactored" and implemented with Google Gadgets.
These, being only simple HTML and JavaScript, can be then "used" or "viewed" from the users on their preferred site, may be Google itself, their home page or whatever.
After reading the documentation, I suddenly focused on developing gadget with GWT, my technology of choice. And here I lost a lot of time due to many bugs that affected the GWT version of the library and GWT itself.
Create a new "Web Application Project" in Eclipse+Google Plugins:
This create a sample application, with an EntryPoint and a test service that we'll use to experiment with our gadget.
Our first Gadget
Now let's implement a really simple Google Gadget that will send a call to a server and print the respondo on the gadget itself. This is the implementing class:
@com.google.gwt.gadgets.client.Gadget.ModulePrefs(
title = "SimpleGadget",
author = "Luca Masini",
author_email = "luca.masini@gmail.com"
)
public class GaeGadget extends Gadget<UserPreferences> implements
AsyncCallback<String>, NeedsIntrinsics {
private final GreetingServiceAsync service = GWT
.create(GreetingService.class);
final Panel panel = new VerticalPanel();
final TextBox input = new TextBox();
final Button simpleButton = new Button("Click me to call the server !!!");
final HTML label = new HTML();
@Override
protected void init(UserPreferences preferences) {
simpleButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
service.greetServer(input.getText(), GaeGadget.this);
}
});
panel.add(input);
panel.add(simpleButton);
panel.add(label);
RootPanel.get().add(panel);
}
@Override
public void onFailure(Throwable caught) {
Window.alert(caught.getLocalizedMessage());
}
@Override
public void onSuccess(String result) {
label.setHTML(result);
}
@Override
public void initializeFeature(IntrinsicFeature feature) {
ServiceDefTarget serviceDef = (ServiceDefTarget) service;
String rpcUrl = serviceDef.getServiceEntryPoint();
rpcUrl = feature.getCachedUrl(rpcUrl);
serviceDef.setServiceEntryPoint(rpcUrl);
}
}
A CTRL-O (Fix Imports) and we'll see that we are missing the gwt-gadget library, we can download it from here, and put it on the Web Project Classpath. All the build errors will disappear, but we still have to include it on the module configuration. Open it from src/net/lucamasini/simplegadget/GaeGadget.gwt.xml and add it this line of code:
<inherits name="com.google.gwt.gadgets.Gadgets" />
After this we can select the project, Google-->Deploy on App Engine (here use your App Engine account of course :) ):
and see this output from the Console:
Compiling module net.lucamasini.gae.gadget.Gae_gadget
Compiling 5 permutations
Permutation compile succeeded
Linking into C:\progetti\gameplanet\gae-gadget\war
Link succeeded
Compilation succeeded -- 4,141s
Creating staging directory
Scanning for jsp files.
Scanning files on local disk.
Initiating update.
Cloning 30 static files.
Cloning 53 application files.
Uploading 0 files.
Deploying new version.
Will check again in 1 seconds
Will check again in 2 seconds
Closing update: new version is ready to start serving.
Uploading index definitions.
Deployment completed successfully
Installing the Gadget
Cool !!! Everything now is ok, the code is on AppEngine and we can try the Gadget !!!! First of all go to iGoogle page to add this new Gadget:
(this is the Developer Tab of iGoogle, to add it simply click "Add a Tab" and write "Development" in the appearing DialogBox). In the red highlighted bar enter the gadget url, in our case is http://gae-gadget.appspot.com/gaegadget/net.lucamasini.simplegadget.client.GaeGadget.gadget.xml, but it depends on your Google App Engine account of course.
The Gadget will appear:
Please remember to uncheck the "Cached" checkbox in this development Tab, so that you'll always see the last version of the Gadget.
Running in Hosted mode during development
Now get back to our GWT Shell, we want to start it to debug our Gadget. Following what is written in the tutorial you need to set the -noserver because iGoogle will be the container and the point directly at it instead of the default hosting HTML page. With the plugin this mean changing two parameters, the first for the -noserver, uncheck the highlighted checkbox:
then the URL:
I think it's a good idea also to disable warning about the fact we are going on an untrusted site and also some more Java Heap:
Now we can click Debug and start our debug session, we have followed all the istructions on the Google website, but......
Don't want to link to you
[DEBUG] Bootstrap link for command-line module 'net.lucamasini.simplegadget.GaeGadget'
[DEBUG] Linking module 'gaegadget'
[TRACE] Invoking Linker Google Gadget
[ERROR] No gadget manifest found in ArtifactSet.
[ERROR] Failed to link
Whaaaaat ??? But it's there, in my war\gaegadget folder, generated when I deployed on the App Engine. So what ??
I think that the problem is due to the fact that the original "GadgetLinker" was made for GWT 1.5 that has a different layout and it looks into the public folder. But know we have a real war layout, everything is inside the war folder, generated or not.
So how to fix this ?? Well, we can switch to the old layout, but is something the Eclipse Plugin for GWT doesn't manage very well, and really, I like to look forward and not ahead.
I used another approach, I fixed the GadgetLinker looking at that resource in the right place. Writing a new linker is very easy, I overidden the XSLinker and implemented the link method:
public final class GadgetLinkerGWT16 extends XSLinker {
private final GadgetLinker gadgetLinker = new GadgetLinker();
@Override
public ArtifactSet link(TreeLogger logger, LinkerContext context,
ArtifactSet artifacts) throws UnableToCompleteException {
// We are in the war directory, here we can refer
// to the module folder only using the module name.
File moduleFolder = new File(context.getModuleName());
// Look for the gadget manifest file into the module folder
// generated during the compile phase.
// This need a compile done before the first time the Shell
// is launched
ArtifactSet as = new ArtifactSet(artifacts);
for (String file : moduleFolder.list()) {
// If a manifest is found is added to the list of artifact
if (file.endsWith(".gadget.xml")) {
as.add(new StandardGeneratedResource(GadgetGenerator.class,
file, new File(moduleFolder, file)));
}
}
// Return to the original Google Gadget Linker
return gadgetLinker.link(logger, context, as);
}
}
This is not enough. We miss the relink method (and this misses also in the original GadgetLinker to be honest). Here i decided to simply redo the link, with the patched linker, I mean that I simply call the link method just written:
@Override
public ArtifactSet relink(TreeLogger logger, LinkerContext context,
ArtifactSet newArtifacts) throws UnableToCompleteException {
return link(logger, context, newArtifacts);
}
Then we must register the new linker inside our module XML, rightly after the Gadgets inherits (the position is important !!!):
<define-linker name="gadget1"
class="net.lucamasini.simplegadget.linker.GadgetLinkerGWT16" />
<add-linker name="gadget1" />
Remember to do a Compile before lanching the Shell for the first time,now it will be able to startup and........
You think it'll work ??? Nope, a new Exception for us !!!!!!
Incorrect Versioning
GWT check the version that compiled the bootstrap JavaScript versus the runtime in the GWT Shell, and we got this:
[ERROR] Invalid version number "1.5" passed to external.gwtOnLoad(), expected "1.6"; your hosted mode bootstrap file may be out of date;
if you are using -noserver try recompiling and redeploying your app
This is error is also filed as a bug on GWT Google API's site, but I think that the solution there are not so good. Download the entire gadget-api and Google time is a show stopper for many (I got it to build GWT 2.0, but is often updated and I think they should switch to Maven), so I seeked for another solution. I found that the class that does the check is the BrowserWidget, into the validHostedHtmlVersion. But looking in there i found that it checks the version from the bootstrap JavaScript (the one generated by the modified linker) against this constant:
/**
* The version number that should be passed into gwtOnLoad. Must match the
* version in hosted.html.
*/
private static final String EXPECTED_GWT_ONLOAD_VERSION = "1.6";
But......I'm using GWT 1.7, so what ?? Also without the filed bug, I would receive the ERROR, this time saying that I passed 1.7 but it expected 1.6.......and here I decided to DISABLE this check, I took this class from gwt-dev-windows, I copied it into my project and then I commented out the check so that the method validHostedHtmlVersion now returns always true.
I know, it's not so good, but I hope that soon they will fix this in GWT version 1.7.1 or whatever and in a new release of Google Gadget.
Stats not available
Ok, now the page is loaded, we can try to call the service and see if everything works as exptected but, as soon as we click on the gadget's button, we receive this exception:
[ERROR] Uncaught exception escaped
com.google.gwt.core.client.JavaScriptException: (TypeError): '$stats' is undefined
number: -2146823279
description: '$stats' is undefined
at com.google.gwt.user.client.rpc.impl.RemoteServiceProxy.isStatsAvailable(Native Method)
at net.lucamasini.simplegadget.client.GreetingService_Proxy.greetServer(transient source for net.lucamasini.simplegadget.client.GreetingService_Proxy:23)
at net.lucamasini.simplegadget.client.GaeGadget$1.onClick(GaeGadget.java:49)
at com.google.gwt.event.dom.client.ClickEvent.dispatch(ClickEvent.java:54)
at com.google.gwt.event.dom.client.ClickEvent.dispatch(ClickEvent.java:1)
at com.google.gwt.event.shared.HandlerManager$HandlerRegistry.fireEvent(HandlerManager.java:65)
at com.google.gwt.event.shared.HandlerManager$HandlerRegistry.access$1(HandlerManager.java:53)
at com.google.gwt.event.shared.HandlerManager.fireEvent(HandlerManager.java:178)
at com.google.gwt.user.client.ui.Widget.fireEvent(Widget.java:52)
at com.google.gwt.event.dom.client.DomEvent.fireNativeEvent(DomEvent.java:116)
at com.google.gwt.user.client.ui.Widget.onBrowserEvent(Widget.java:90)
at com.google.gwt.user.client.DOM.dispatchEventImpl(DOM.java:1320)
at com.google.gwt.user.client.DOM.dispatchEventAndCatch(DOM.java:1299)
at com.google.gwt.user.client.DOM.dispatchEvent(DOM.java:1262)
I found that this is a known problem, Issue 220 of google-gadgets, they closed it as solved but it's still there for me.
Anyway, I understood that the usual GadgetLinker was not that good at generating the bootstrap HTML for the hosted mode. Now they wrote a better template but still is not working for me, so I did my way, disabling stats with some JSNI:
static {
disableStats();
}
private static native void disableStats() /*-{
$wnd.$stats = null;
}-*/;
You can put this code in any class that is called before the service. For instance I put it into the Gadget entry point itself, so that is ready before anything is executed.
Same-Origin Policy
The next problem that arise is the SOP violation that is done by the iGoogle container.
Infact, we use the proxy to query data from our server, and the proxy URL is injected by the container in the initializeFeature method:
String rpcUrl = serviceDef.getServiceEntryPoint();
rpcUrl = feature.getCachedUrl(rpcUrl);
but then I analyzed the rpcUrl and I saw that is not that good. Infact the origin of our gadget is ig.gmodules.com, but the proxy url is calculated as www.ig.gmodules.com. This way, any browser will complain about security violation and will prevent us from actually making the call.
By the way, quering the DNS, I saw that both are aliases of another Google's server:
C:\>nslookup ig.gmodules.com
Non-authoritative answer:
Name: googlehosted.l.google.com
Address: 74.125.43.132
Aliases: ig.gmodules.com
C:\>nslookup www.ig.gmodules.com
Server: mylocalserver
Address: xx.xx.xx.xx
Non-authoritative answer:
Name: googlehosted.l.google.com
Address: 74.125.43.132
Aliases: www.ig.gmodules.com
So I decided to recalculate the rpcUrl this way:
String protocol = rpcUrl.substring(0, rpcUrl.indexOf(':'));
String file =
rpcUrl.
substring(rpcUrl.indexOf('/', rpcUrl.indexOf(':')+3));
serviceDef.setServiceEntryPoint(protocol+"://"+getDomain()+file);
where the getDomain method use JSNI to calculate the domain address from which the Gadget was downloaded:
private static native String getDomain() /*-{
return $wnd.document.domain;
}-*/;
Don't POST to me
Now the browser sandbox will allow us to make the call, but, you can trust this or not, we got another error:
HTTP/1.1 405 HTTP method POST is not supported by this URL
A bit discouraged I investigated and noticed that someone else opened an issue about this, but with not real response from Google Gadgets guys. The problem is that the RemoteServiceProxy does a POST to send data (from RemoteServiceProxy class):
private <T> RequestBuilder doPrepareRequestBuilderImpl(
ResponseReader responseReader, String methodName, int invocationCount,
String requestData, AsyncCallback<T> callback) {
…
…
…
RequestBuilder rb = new RequestBuilder(RequestBuilder.POST,
getServiceEntryPoint());
but the iGoogle container seems not to support POST anymore. Infact we are using a legacy layout for our gadgets, as stated in the library overview, and I found in the new documentation that the POST is proxied with a GET, passing POST data into a parameter of the GET itself.
I did some experiment with this new API and I saw that POST works great, so I decided to develop a POC.
Make POST request with OpenSocial makeRequest
I looked at the generated RemoteServiceProxy (adding the -gen parameter now the generated source are under the war folder), and I saw that, for a POC it's not that difficult to call a remote service using GWT-RPC serialization data. What we need is a method that use the services of iGoogle sandbox to proxy request to our backstream server, and I wrote this native method:
private native void makePostRequest(String url, String postdata, AsyncCallback<String> callback) /*-{
var params = {};
params[$wnd.gadgets.io.RequestParameters.METHOD] = $wnd.gadgets.io.MethodType.POST; //(1)
params[$wnd.gadgets.io.RequestParameters.POST_DATA]= postdata; //(2)
$wnd.gadgets.io.makeRequest(url, response, params); //(3)
function response(obj) { //(4)
@net.lucamasini.simplegadget.client.GaeGadget::onSuccessInternal
(Lnet/lucamasini/simplegadget/client/GadgetResponse;Lcom/google/gwt/user/client/rpc/AsyncCallback;)
(obj, callback);
};
}-*/;
Here we tell the container to use the POST Method (1), we put our post data into the request parameter (2) and then we call the Google Gadget makeRequest function (3). The response function (4) then is only a pass-throuh a Java method that know what to do with the returned response (Exception management code is omitted).
GadgetResponse is a simple overlay type over the Open Social response object:
public class GadgetResponse extends JavaScriptObject {
protected GadgetResponse() {
}
public final native String getText() /*-{ return this.text; }-*/;
public final native String[] getErrors() /*-{ return this.errors; }-*/;
public final native String getData() /*-{ return this.data; }-*/;
}
Take control of the RPC with RequestBuilder
Now we must instruct the RPC Proxy generator not to make itself the call, but to only generate a RequestBuilder object that we can use to make requests with our makePostRequestMethod. This is simply done chaning the return type of the service method into the Async interface:
public interface GreetingServiceAsync {
RequestBuilder greetServer(String input, AsyncCallback<String> callback);
}
Now the call to the greetServer is no more a real call but only an initialization that create the serialized data and also the deserializer for the response. And we can use it really simply, changing the old onClick method this way:
@Override
public void onClick(ClickEvent event) {
RequestBuilder requestBuilder = service.greetServer(input.getText(), this);
makePostRequest(requestBuilder.getUrl(), requestBuilder.getRequestData(), requestBuilder.getCallback());
}
the response handler method is even simpler:
static void onSuccessInternal(final GadgetResponse response, RequestCallback callback) {
try {
callback.onResponseReceived(null, new FakeResponse(response));
} catch (Exception e) {
callback.onError(null, e);
}
}
We must only notice two things:
I pass null as the first parameter because the Request object is never used inside the callback method (and because I don't know how to create one :) )
I create a FakeResponse wrapper class of response that return:
@Override
public String getText() {
return response.getText();
}
@Override
public String getStatusText() {
return "OK";
}
and nothing else in the other methods
Of course we must comment out the code inside the initializeFeature method, that proxy URL is no more valid !!!!
Happy end ?????
Believe it or not, this time the Simple Gadget is able to fetch the expected data from the mainstream server:
I must admit that google gadgets API are a bit in early stage, a lot of bugs could be show stopper but everytime I felt the sentence "use the force Luke", because the alternative way was to use pure JavaScript and as I hated Lisp at Univ, now I have the same feeling about his son JavaScript.
Now I want to dig in two directions:
It's a good idea to use GWT-RPC or can be better, for this Gadgets, to use JSON ??
Find a productive way to use GWT with this API. I can't deploy everytime on the GAE remote engine, I have a GAE on my PC !!!!
Enjoy your gadgets.