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:

               @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:

Enjoy your gadgets.