A Wave Gadget + Robot using GWT 2.0 and Google App Engine, Part 1
[tweetmeme source=”jonashuckestein” only_single=false]I finally got around to playing with Google Wave (wave me: captaincurk@googlewave.com) and decided to build my own extension. Things got somewhat complex, so I made this into a multi-part tutorial. We are going to develop a gadget for Google Wave (in case you don’t have an invite yet, drop me a line) using Google Web Toolkit and wire it up to a robot that lives on the App Engine cloud.
In this part, we are going to develop a simple collaborative gadget that lives on a wave. You should be familiar with Google Wave and have some experience in developing applications using Google Web Toolkit.
- Click here to download the entire Eclipse project including all source code and required libraries
- Add the following URL as a gadget to a Google wave to see the gadget in action: http://pretty.latest.wavelistgadgetgwt.appspot.com/wavelistgadgetgwt/com.thezukunft.tutorials.client.WaveListGadgetGWT.gadget.xml
- If you’re on the wave sandbox, click here to join a public test wave
Pretext: The Anatomy of a Wave Gadget
Let’s quickly run through how a gadget works with Google Wave.
The original idea of a Google Gadget is to run third-party (javascript) code in a safe environment on a host site. The gadget is provided a confined area in which it can render its content or UI. A gadget is defined by an xml file–the gadget specification–that must be publicly available on the internet. The most popular use for gadgets seems to be iGoogle, Google’s personalized landing page. IIRC original gadgets should run just fine in Wave.
Wave gadgets can additionally make use of the Wave API, which allows the gadget to maintain an internal state and other contextual information that is automatically shared with other participants.
- A gadget can register callback functions with the API that are called when
- the wave’s participants change
- the gadget’s state changes
- the mode of the wave changes (e.g. from read to edit or playback mode)
- The state object is a String to String map
- Running instances of the gadget may submit a change–a “delta”–to the state object that is also a String to String map
- State updates always include the entire state. It is not known what key in the state map was changed.
Using the same mechanisms that allowed developers to build gadgets using GWT–namely the Google API Libraries for GWT–we can now develop wave gadgets that are aware of their multi-user environment using GWT. The API Libraries thankfully take care of generating the gadget xml specification for us. We can easily access the added API features wave offers (e.g. the callback registrations, the state and the list of participants) using Hilbrand’s Cobogwave Java wrapper.
What are we building?
In this tutorial, we are going to build a gadget that manages a list of messages as follows
- The UI contains an input field and an “Add” button.
- Clicking the add button saves the text in the input field and its author to the gadget’s state object
- Below the button and input we have a table containing all messages and the name and picture of the authors.
So how do we organize our state object? This is one of the most difficult questions for new wave developers. It is important to understand that the only atomic state operations wave supports are the change of a single key. Inspired by Avital Oliver’s great blog post, I came up with the following scheme for saving gadget state:
- The key is for each value has the following structure: {type}_{uniqueId}_{property}
- Nested properties can be saved by appending two keys. For example “family_{id}_child_{cId}_name” might be a good key to save the name of family’s child.
In our example every message would be saved as under two keys: “msg_{id}_message” and “msg_{id}_author”.
Implementation
Before you start, make sure to have set up a GWT + AppEngine project (I use Eclipse and the Google Plugin and called the project WaveListGadgetGWT).
- You have to download the Cobogwave GWT wrapper for the Google Wave Gadget API and add the .jar to your classpath. If you don’t want to follow along the turoial, click here to download the sourcecode directly.
- Download The Official Google API Libraries for Google Web Toolkit and add gwt-gadget.1.0.3.jar to your classpath.
The GWT Module (WaveListGadgetGWT.gwt.xml)
Modifiy your GWT module XML file to inherit Gadgets.gwt.xml and WaveGadget.gwt.xml. Wihtout comments, my WaveListGadgetGWT.gwt.xml looks like this:
<?xml version="1.0" encoding="UTF-8"?> <module rename-to='wavelistgadgetgwt'> <inherits name='com.google.gwt.user.User'/> <inherits name='com.google.gwt.user.theme.standard.Standard'/> <inherits name="com.google.gwt.gadgets.Gadgets" /> <inherits name='org.cobogw.gwt.waveapi.gadget.WaveGadget' /> <entry-point class='com.onetwopoll.tutorials.client.WaveListGadgetGWT'/> <source path='client'/> </module>
The UI Template (ListWidget.ui.xml)
We are going to use GWT 2.0’s brand new UiBinder feature. If you haven’t done this before, enjoy, it’s easy!
Right-click on your src/ folder in eclipse and choose New -> UiBinder from the context menu. Call the widget “ListWidget” and put it into a package of your liking (in my case com.onetwopoll.tutorials.view). Eclipse will set up a .ui.xml and a corrseponding .java file (possibly with sample content for you). Modify your .ui.xml to create the aforementioned minimal user interface like this:
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent"> <ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder" xmlns:g="urn:import:com.google.gwt.user.client.ui"> <g:HTMLPanel> <g:FlowPanel> <g:TextBox ui:field="messageBox" /> <g:Button ui:field="addButton" text="Add" /> </g:FlowPanel> <g:FlexTable ui:field="messagesTable" /> </g:HTMLPanel> </ui:UiBinder>
The UI Class (ListWidget.java)
First we’ll set up the class members:
private static ListWidgetUiBinder uiBinder = GWT .create(ListWidgetUiBinder.class); interface ListWidgetUiBinder extends UiBinder { } @UiField protected Button addButton; @UiField protected FlexTable messagesTable; @UiField protected TextBox messageBox; protected WaveFeature wave;
The UiFields will be bound to the UI template to create the user interface and the WaveFeature is required because ListWidget.java is going to contain all our logic. This manual dependency injection is not necessarily best-practice, but it reduces the amount of code for this tutorial.
Next we need to write the constructor, which sets up the UI and most importantly registers a StateUpdateEventHandler (from cobogw’s wrappers) with the wave:
public ListWidget(WaveFeature wave) { this.wave = wave; initWidget(uiBinder.createAndBindUi(this)); // refill the table when the state changes wave.addStateUpdateEventHandler(new org.cobogw.gwt.waveapi.gadget.client.StateUpdateEventHandler() { @Override public void onUpdate(StateUpdateEvent event) { refillTable(event.getState()); } }); }
The actual logic is implemented in the refillTable function and addButton’s clickHandler. Let’s start with the latter; all we need to do is build a HashMap with the keys we want to update and submit it to the gadget’s state. Wave will take care of creating stateUpdateEvent’s in all running gadget instances:
@UiHandler("addButton") void onClick(ClickEvent e) { if(!messageBox.getText().isEmpty()) { HashMap<String, String> delta = new HashMap<String,String>(); // generate a unique id String id = Long.toString(new Date().getTime()); // add the two state keys delta.put("msg_"+id+"_message", messageBox.getText()); delta.put("msg_"+id+"_author", wave.getViewer().getId()); // push the change to wave // note how we don't update the UI just yet // because this will immediately trigger a state update event wave.getState().submitDelta(delta); } } }
Note that we do not need to update the UI now. An update event will be fired even in the gadget instance that has submitted the change.
Let’s look at the refillTable function. It takes a state as argument and then render’s the state’s contents in the messagesTable. Obviously deleting the entire table and refilling it is not very efficient, but it illustrates how each state update event contains the entire state and not only changed keys.
protected void refillTable(State state) { messagesTable.removeAllRows(); Map<String, Integer> rowMap = new HashMap<String, Integer>(); // iterate over all keys in the state for(int i=0; i<state.getKeys().length(); i++) { String[] split = state.getKeys().get(i).split("_"); String id = split[1]; String field = split[2]; String value = state.get(state.getKeys().get(i)); int row = -1; // since every message is saved in two state keys, // we need to find out if we have already seen this one if(rowMap.containsKey(id)) row = rowMap.get(id); else { row = messagesTable.getRowCount(); rowMap.put(id, row); } if(field.equals("message")) { messagesTable.setWidget(row, 0, new Label(value)); } else { FlowPanel authorPanel = new FlowPanel(); Participant author = wave.getParticipantById(value); if(author != null) { authorPanel.add(new Image(author.getThumbnailUrl())); authorPanel.add(new Label(author.getDisplayName())); } messagesTable.setWidget(row, 1, authorPanel); } } }
This really is the most minimal logic I could think of that recognizes multiple users and has a orderly state structure. Feel free to play around with the logic; for instance, try adding a ParticipantUpdateEventHandler to the wave object which deletes all a user’s messages when he leaves the wave. You could also add a delete button behind all messages as soon as the user switches to edit mode.
The Entry Point Class (WaveListGadgetGWT.java)
Now we’ll fix up the entry point class. Most importantly (and despite the entry-point declaration in the module file), our class does no longer implement EntryPoint. Instead, it extends WaveGadget<UserPreferences> and is annotated with @ModulePrefs. the ModulePrefs are used when generating the gadget’s XML spec.
WaveGadget provides the WaveFeature object that we used above and subclasses must implement init(UserPreferences), which is called when the gadget is ready. All we need to do now is attach a new ListWidget to our RootPanel in init and we’re done! Ignore the UserPreferences for now. My entire class looks like this:
package com.thezukunft.tutorials.client; import org.cobogw.gwt.waveapi.gadget.client.WaveGadget; import com.google.gwt.gadgets.client.UserPreferences; import com.google.gwt.gadgets.client.Gadget.ModulePrefs; import com.google.gwt.user.client.ui.RootPanel; @ModulePrefs(title = "WaveListGadgetGWT", author="Jonas Huckestein", author_email="jonas.huckestein@me.com", height=400) public class WaveListGadgetGWT extends WaveGadget<UserPreferences> { @Override protected void init(UserPreferences preferences) { RootPanel.get().add(new ListWidget(getWave())); } }
Note that at the time init() is called, the state and participants of the wave are not initialized. They are only initialized when their corresponding events are fired.
Run!
Let’s get this thing running in a wave. Here’s what you need to do:
- Compile your gadget using the Google plugin for eclipse. In case you are uploading it to the App Engine, this will be done automatically in the next step.
- Upload the contents of your build directory (in my case, by default, war/wavelistgadgetgwt/) to some public place in the internet. I used App Engine for this. Just click the App Engine icon in Eclipse and make sure your project is set up to use an appengine application you own. I deployed the gadget to the application identifierwavelistgadgetgwt.
- Locate your gadget XML specification on the internet. In my case this was in http://wavelistgadgetgwt.appspot.com/wavelistgadgetgwt/com.thezukunft.tutorials.client.WaveListGadgetGWT.gadget.xml.
- Add the Gadget to a wave! Press the “Add Gadget by URL” icon in Wave and paste your gadget XML specification URL.
Voila, you should now see the UI. If everything went well, it should look like the following image. If it didn’t, check out the links on debugging at the bottom of this post or drop me a line in the comments.
Try adding some messages and using the playback mechanism. This is great.
Done! (for now)
You can download the source code and all required libraries of this tutorial as Eclipse project from here. I went ahead and added some styles and something called a dynamic height feature so that the height automatically adjusts with the content of the gadget. The end result looks like this:
You can add the widget to your own wave by using the following gadget spec URL: http://pretty.latest.wavelistgadgetgwt.appspot.com/wavelistgadgetgwt/com.thezukunft.tutorials.client.WaveListGadgetGWT.gadget.xml
Okay, let’s recap:
- We wrote our Gadget using GWT and the cobogwave Wave API for GWT.
- We compiled the Gadget to a .gadget.xml file
- We deployed the .gadget.xml file to the AppEngine (or anywhere else)
- We added the Gadget to a Wave and it worked
Some things we did not discuss today, most notably how to locally test your gadget, so stay tuned for what’s coming next
- How to test and debug your Gadgets locally (spoiler: by implementing mocks for all Wave API classes) and how to remove clutter from the development process.
- How to build a Wave robot that integrates with our gadget
- How to deploy a finished extension
In the meantime, enjoy these other reads:
[…] This post was mentioned on Twitter by Erik Reisig, Jonas Huckestein. Jonas Huckestein said: A Wave Gadget Robot using GWT 2.0 and Google App Engine, Part 1 http://wp.me/pKZ6O-m #google #wave #gwt […]
Tweets that mention A Wave Gadget + Robot using GWT 2.0 and Google App Engine, Part 1 « The Zukunft -- Topsy.com
2010/02/09 at 5:43 am
Social comments and analytics for this post…
This post was mentioned on Twitter by jonashuckestein: A Wave Gadget Robot using GWT 2.0 and Google App Engine, Part 1 http://wp.me/pKZ6O-m #google #wave #gwt…
uberVU - social comments
2010/02/09 at 10:18 am
Very good until here I’m undestading all!!!!!
What about the next part?
hiperion
2010/02/10 at 3:45 am
Have you got an error like this ?
Invoking Linker Google Gadget
[ERROR] No gadget manifest found in ArtifactSet.
Deployed the project is ok this error is i host mode.
hiperion
2010/02/10 at 7:47 am
Hi,
thanks for following along!
It is currently not possible to test Google Gadgets in hosted mode at all. This is because of the way the Google Gadgets API works.
I have built my own solution-the WaveConnector-that decouples the actual gadget implementation from the Gadget API and provides mock Wave API methods thus allowing you to test your gadget in hosted mode (with some restrictions).
I am expecting to publish that today :) Stay tuned!
Part 2 of the tutorial will be on adding a robot and it will be available either by the end of this week or the beginning of next week.
Cheers,
Jonas
Jonas Huckestein
2010/02/10 at 11:50 am
[…] site has everything you need to get started! It already contains the little project I made for the tutorial last week, so you can just downloaded the waveconnector-gwt-turnkey archive and start from there. There is no […]
WaveConnector for GWT: Local Testing and Turnkey Gadget Development « The Zukunft
2010/02/10 at 3:32 pm
Hello,
Nice work! I just want to know how you can browse the app directory after uploading it to Google app Engine. I mean how can I locate my gadget xml file after uploading to Google app Engine.
Thanks.
Kayode Odeyemi
2010/02/12 at 10:44 am
Just locate it in your local project structure and then build the URL. The root of your AppEngine deployment (yourproject.appspot.com) corresponds to the /war folder in your project. There will be a subfolder called /yourproject that contains the file (and more importantly the filename). Your URL will look something like this:
http://yourproject.appspot.com/yourproject/com.yourpackage.YourProject.gadget.xml
Hope this helps. Cheers, Jonas
Jonas Huckestein
2010/02/12 at 2:31 pm