Gregor's Ramblings
Home Patterns Ramblings Articles Talks Download Links Books Contact

My First Google Wave Robot

August 1, 2009

Recent Ramblings
Explaining Stuff
DDD - Diagram Driven Design
What Does It Mean to Use Messaging?
A Chapter a Day...
EIP Visions
Clouds and Integration Patterns at JavaOne
My First Google Wave Robot
Google I/O
Into the Clouds on New Acid
Design Patterns: More than meets the eye
Reflecting on Enterprise Integration Patterns
Google Gears Live From Japan
Double-Dipping: OOPSLA and Colorado Software Summit
Bubble 2.0
Enterprise Mashup Summit
Facebook Developer Garage
Mashups Tools Market
Mashups == EAI 2.0?
Mashup Camp
ALL RAMBLINGS

By now everyone must have heard about Google Wave, the communication and collaboration platform announced at Google I/O. Google just announced that they are ramping up towards 20,000 developers on the current sandbox and will extend to 100,000 users by end of September. This will give many more people to opportunity to experiment with Wave.

One of the key features of Wave is its extensibility. For example, you can create robots, which can participate in waves just like humans do (that is once they are invited in). As I hinted last time, I intended to build a "make the Sun lawyers happy" robot, which would replace any mention of "Java" with the preferred version of "Java(TM)-based technology". Alas, I was not able to get it working in time for JavaOne. So I took advantage of the Tokyo Wave Hackathon organized by the Tokyo Google Technology User group to receive some debugging aid and get my robot working.

Wave Robots

Wave has two primary extension mechanisms, gadgets and robots. Gadgets are embedded inside a wave and run inside the browser client. Robots are automated wave participants, which can edit text, add new blips, etc. Time for a super-quick Wave vocabulary lesson: interaction between participants happens through waves (think "discussion thread++"), which contain wavelets made up of blips. In a traditional e-mail thread, a blip would correspond to a single e-mail post. Unlike gadgets, robots are hosted externally and can be invited to a wave just like a human user. Currently, all robots are hosted via Google App Engine at appspot.com , but I hear that restriction is going to be lifted in the future. Since I am a back-end kind of guy (and my JavaScript skills are very crude), I decided to write a robot.

High-level Architecture

Writing robots is relatively simple thanks to the Wave Robot API, which was just open sourced. App Engine (for Java) works with servlets, so it's natural that the API provides a servlet base class, AbstractRobotServlet. Your own servlet inherits from this base class and gains access to methods that check for different event types and read or modify the wave's blips. Of course, the App Engine servlet as access to all the other App Engine API's, such as persistence through JPA or JDO.

A robot cannot simply call Wave or read a wave. It has to be explicitly invited by a user first. The robot name is currently derived from the App Engine name, meaning your servlet at xyz.appspot.com will be known to the wave sandbox as xyz@appspot.com. Once invited into a wave conversation, the robot reacts to events issued by wave, such as the addition of a new participant or the submission of a new blip. Each robot exposes a capabilities.xml file, which describes the events that the robot would like to listen to.

A Text Replacement Robot

My robot's task is to replace text, particularly occurrences of "Java" with "Java(TM)-based technology. Playing an existing wave back through the cool Playback feature shows what the robot did to the sentence containing Java (it also played with the number of "o"s in "Google"):

The Wave Replay Feature Shows What the Robot Did

Since I like building software in small iterations, I started with a simple, hard-coded replacement. Whenever a blip with the word "Java" is submitted to a wave, which the robot listens to, the robot will replace this particular text. Any robot needs a capabilities.xml file, so I start there. The Wave Robot Overview shows an example file, but forgets to list all allowable capabilities. I assume they match the names of the constants in EventType.java. I did not see a statement as to whether casing matters, so I'll stick with the lowercase from the sample. Details, details – what do I expect from living on the bleeding edge? (note: it seems that in fact casing does not matter) Our robot only needs to subscribe to a single event, blip_submitted, which is triggered whenever a blip is submitted (duh!). Many robots are more eager and subscribe to document_changed, which is invoked as a user types. For now, I am happy to wait for the robot to kick in once the blip is "done". The capabilities.xml file, deployed under the relative path /_wave looks as follows:

<?xml version="1.0" encoding="utf-8"?>
<w:robot xmlns:w="http://wave.google.com/extensions/robots/1.0">
    <w:capabilities>
        <w:capability name="blip_submitted" content="true" />    
    </w:capabilities>
    <w:version>0.33</w:version>
</w:robot>

The servlet itself is relatively simple. It inherits from the AbstractRobotServlet base class and overrides the processEvents method:

public class WaveServlet extends AbstractRobotServlet  {

@Override
public void processEvents(RobotMessageBundle events) {
  if (events.wasSelfAdded()) {
    Wavelet wavelet = events.getWavelet();
    Blip blip = wavelet.appendBlip();
    TextView textView = blip.getDocument();
    textView.append("Trademark Bot listening...");
    return;
  }

  List<Event> submittedEvents = events.getBlipSubmittedEvents();
  for (Event e : submittedEvents ) {
    Blip blip = e.getBlip();
    if (!blip.getCreator().equals("your robot")) {
      TextView textView = blip.getDocument();
      replaceWords(dictionary, textView);
    }
  }
}

The method responds to two types of events: WAVELET_SELF_ADDED and BLIP_SUBMITTED. We did not actually subscribe to the former, but it arrives by default whenever the robot is added to a wave. In response, the robot appends a new blip and populates the blip text with a simple greeting. The "meat" of the robot is in the remainder of the method, which loops over each submitted blip. If the blip is not submitted by this robot (trying to stay out of infinite loops), it replaces occurrences of words based on a dictionary. I hacked this together really quickly, so the algorithm replaces only the first occurrence of each word, so don't repeat yourself too much. I guess should have written a test first.

final static Map<String, String> dictionary = new HashMap<String, String>();

static {
  dictionary.put("Java", "Java(TM) based technology");
  dictionary.put("Microsoft", "Micro$oft");
}
    
boolean isSeparator(char c) {
  return Character.isWhitespace(c) || ",.;!\"".indexOf(c) >= 0; 	
}
                
void replaceWords(Map<String, String> dictionary, TextView textView) {
  String text = textView.getText();
  for (String word : dictionary.keySet()) {
    int pos = text.indexOf(word);
    if (pos >= 0) {
      // make sure this is a word
      if (pos > 0 && !isSeparator(text.charAt(pos-1))) {
        break;
      }
      if (pos < text.length()-1 && !isSeparator(text.charAt(pos + word.length()))) {
        break;
      }
      textView.replace(new Range(pos, pos + word.length()), dictionary.get(word));
      text = textView.getText();
    }
  }
}

A couple of items to note: the BLIP_SUBMITTED event appears to be triggered when a new blip is submitted or when an existing one is edited. Unfortunately the JavaDoc for the event constants is still empty, so it's better to run a quick check to make sure your robot gets the events you were expecting. Also, my robot does not work on the Wave title. I would have to subscribe to WAVELET_TITLE_CHANGED to catch this event.

The rest of the robot is standard servlet fare. You need a web.xml file, which maps relative URL's to servlets. The robot servlet has to live under the relative path /_wave/robot/jsonrpc. The name of the robot as a wave participant is derived from your App Engine application name, meaning a robot at xyz.appspot.com will be known to the wave sandbox as xyz@appspot.com. Somehow, it became a quick fashion to name all robots ending in a ""y", giving it a cute-y name like "spelly" or "groupy". Following this rule I should have named mine "replacey", but I felt it sounds too dorky.

DLL Hell has Many Faces

This robot is very simple, but it still took me some time to get it working. In the end it turned out I had some nasty version mismatches between the Wave Robot SDK, the App Engine run-time and the Eclipse plug-in. When your robot does not seem to receive the desired event or does not respond at all, things can be pretty frustrating. Once you have a basic robot working, debug and test cycles are relatively swift thanks to the easy deployment of App Engine applications. So I'd always recommend to start with a "Hello World" robot and go from there. The bottom of the Wave Robot Api Overview has some useful troubleshooting tips. If your robot is not well, it's best is to check out the App Engine application logs, which are available from the App Engine console. For example, my logs alerted me to missing object ID's on my JDO classes.

The Shortcuts Robot

To make our robot a bit more useful (and interesting), I decided to give it a dictionary data store, meaning you can define your own terms and chose their replacements. Replacing Java with Java(TM) will be just one of many useful replacements. For example, you could replace the phrase "Moron!" with "Thank you very much for your suggestion. I will be sure to bring up your feedback at our next internal meeting." This way you get all the satisfaction without losing political capital.

App Engine for Java supports JPA and JDO for persistence. Having successfully escaped from both throughout my career, I decided to see what it would take to get JDO-based persistence working for someone who has no clue. The good news is that this part was pretty pain free. The App Engine Eclipse plug-in deploys the right jar files (assuming you have matching versions of the plug-in and SDK) and sets up a jdoconfig.xml file for you.

I setup a simple immutable value object for each dictionary entry, equipped it with a primary key, and made it persistable:

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class DictEntry {

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

  @Persistent
  private String key;

  @Persistent
  private String value;

  public DictEntry(String key, String value) {
    super();
    this.key = key;
    this.value = value;
  }

  public String getKey() { return key; }
  public String getValue() { return value; }
}

Now I can load my dictionary from the persistent data store:

Map<String,String> loadDictionary() {
  Map<String,String> dict = new HashMap<String,String>();
  PersistenceManager pm = PMF.get().getPersistenceManager();
  Query query = pm.newQuery(DictEntry.class);
  try {
   List<DictEntry> results = (List<DictEntry>) query.execute();
   if (results.iterator().hasNext()) {
     for (DictEntry e : results) {
       dict.put(e.getKey(), e.getValue());    
     }
   } 
  } finally {
    query.closeAll();
    pm.close();
  }
  return dict;
}

PMF is a singleton PersistenceManagerFactory . The rest of the code is pretty self-explanatory: it inserts one entry into the map for each row in the data store.

Loading the dictionary from the database is only useful if you can also insert data:

boolean addToDict(String key, String value) {
  deleteFromDict(key);
  DictEntry entry = new DictEntry(key, value);
  PersistenceManager pm = PMF.get().getPersistenceManager();	  
  try {
    pm.makePersistent(entry);
    return true;
  } catch (Exception e) {
    return false;
  } finally {
    pm.close();
  }
}

deleteDict iterates over all entries matching the key and deletes them one by one. In order to enable management of the persistent dictionary, I made the robot understand and respond to a simple set of commands, #help, #replace, #showdict. If a blip starts with such a command, the robot responds with a child blip by calling createChild on the blip. I had some struggles getting the output formatted in any reasonable way. The docs state that some HTML is allowed, but did not indulge us in which tags are part of that elite club. I got tired of trying pretty quickly, so the output looks pretty raw. What really tripped me up was that TextView.append does simply nothing if the string contains a "greater than" sign. You'll have to use > instead, so it does not think you are trying to do some invalid HTML. It would have been nice to mention that in the docs. Ironically, the supplied JavaDoc for appendMarkup makes a similar mistake by also forgetting to escape special characters, causing the generated JavaDoc HTML page to interpret the tags instead of displaying them as text. When I was trying to figure out why my test is now appended to the TextView, I figured I'd simply check out the source code, but most of the methods of the robot API simply translate the Java call into an instance of Operation, which is then serialized and returned to the Wave server. This means looking at the robot API source does not really help you answer detailed questions like these.

Robot Gotchas

Because you can invite more than one robot into a wave, you can inadvertently end up building a new version of Robot Wars. For example, one robot could reply to another one's changes and vice versa. The current solution is to give robots limited quota, so infinite loops will ultimaltely run out of steam.

Robot Profile

Now that everything is working, you can put the icing on the cake and give our robot a profile servlet. This servlet equips the robot with a proper name and an avatar by overriding the respective methods of the ProfileServlet base class. Map this servlet to /_wave/robot/profile in web.xml and you are good to go. I chose a picture I took of the live size Gundam, which is currently on display in Odaiba, Tokyo. Robots don't get much cooler than that.

MORE RAMBLINGS    Subscribe  SUBSCRIBE TO GREGOR'S RAMBLINGS


Gregor is a software architect with Google. He is a frequent speaker on asynchronous messaging and service-oriented architectures and co-authored Enterprise Integration Patterns (Addison-Wesley). His mission is to make integration and distributed system development easier by harvesting common patterns and best practices from many different technologies.
www.eaipatterns.com