2009
12.02

Building a Google Wave Robot using Maven

A Google Wave Robot, in the simplest sense, is a servlet that answers to a set of RPC calls and which is hosted on Google App Engine (appspot.com). At the time of this writing, those appears to be the only restrictions. Mind you, hosting a servlet on Google App Engine brings about other restrictions, such as saving all data via JDO. However, those restrictions might not apply to Wave Robots if Google allows to have them hosted somewhere else in the future.

The RPC calls that must be supported by a robot are mostly implemented in a set of libraries available from Google. However, those libraries have not been pushed to the Maven repositories, so they must be first installed in the local Maven repository so that a robot can be built using Maven.

This article describes this process.

There are three libraries required. The first library, the Wave Robot API, is the open source code which is needed to implement the RPC calls. The other two libraries are dependencies needed by the Wave Robot API. The following steps show how to download those libraries and install them in a local Maven repository.

> wget http://wave-robot-java-client.googlecode.com/files/wave-robot-api-20090916.jar
> mvn install:install-file -Dfile=wave-robot-api-20090916.jar -DgroupId=com.google -DartifactId=wave-robot-api -Dversion=1.0.20090916 -Dpackaging=jar -DgeneratePom=true
> wget http://wave-robot-java-client.googlecode.com/files/jsonrpc.jar
> mvn install:install-file -Dfile=jsonrpc.jar -DgroupId=com.google -DartifactId=jsonrpc -Dversion=1.0.20090528 -Dpackaging=jar -DgeneratePom=true
> wget http://wave-robot-java-client.googlecode.com/files/json.jar
> mvn install:install-file -Dfile=json.jar -DgroupId=com.google -DartifactId=json -Dversion=1.0.20090528 -Dpackaging=jar -DgeneratePom=true

At this point, you can create a regular Maven project that yields a WAR application, using your favourite environment. If you must do it from the command line:

> mvn archetype:create -DgroupId=abc.com -DartifactId=robot -DarchetypeArtifactId=maven-archetype-webapp

Next, you must install the libraries previously downloaded as dependencies of your new project. Edit pom.xml and add:

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.google</groupId>
            <artifactId>wave-robot-api</artifactId>
            <version>1.0.20090916</version>
        </dependency>
        <dependency>
            <groupId>com.google</groupId>
            <artifactId>jsonrpc</artifactId>
            <version>1.0.20090528</version>
        </dependency>
        <dependency>
            <groupId>com.google</groupId>
            <artifactId>json</artifactId>
            <version>1.0.20090528</version>
        </dependency>
    </dependencies>

Also to be added in pom.xml should be directives to use Java 6:

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

That pretty much sets up the project. Now, the robot needs to answers to a number of important requests, which are covered below.

Capabilities
A robot must answer to ‘/_wave/capabilities.xml’ with an XML document describing its capabilities. The easiest way to do this is to create a document called ‘capabilities.xml’ in the directory …/src/main/webapp/_wave

> mkdir -p src/main/webapp/_wave
> gedit src/main/webapp/_wave/capabilities.xml

with the following content:

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

The above content must be modified to reflect the events the robot is interested in receiving.

App Engine Configuration
Because the robot is deployed on AppEngine, a couple of files are necessary/useful. The file app-engine.xml contains information that deals with the application, in this case the robot, that is deployed on the service. The logging.properties file is useful to be able to track logs on the AppEngine service.
Create a app-engine.xml file at the appropriate location (…/src/main/webapp/WEB-INF)

> mkdir -p src/main/webapp/WEB-INF
> gedit src/main/webapp/WEB-INF/app-engine.xml

and provide the following content:

<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
    <application>app_name</application>
    <version>0</version>
   
    <!-- Configure java.util.logging -->
    <system-properties>
        <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
    </system-properties>
   
</appengine-web-app>

The application name in the file above must be adjusted to the name given on AppEngine. Also, the version must change every time the ‘capabilities’ document is modified.

The logging file should be created as well

> gedit src/main/webapp/WEB-INF/logging.properties

and given some content:

# A default java.util.logging configuration.
# (All App Engine logging is through java.util.logging by default).
#
# To use this configuration, copy it into your application's WEB-INF
# folder and add the following to your appengine-web.xml:
#
# <system-properties>
#   <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
# </system-properties>
#

# Set the default logging level for all loggers to WARNING
.level = WARNING

# Set the default logging level for ORM, specifically, to WARNING
DataNucleus.JDO.level=WARNING
DataNucleus.Persistence.level=WARNING
DataNucleus.Cache.level=WARNING
DataNucleus.MetaData.level=WARNING
DataNucleus.General.level=WARNING
DataNucleus.Utility.level=WARNING
DataNucleus.Transaction.level=WARNING
DataNucleus.Datastore.level=WARNING
DataNucleus.ClassLoading.level=WARNING
DataNucleus.Plugin.level=WARNING
DataNucleus.ValueGeneration.level=WARNING
DataNucleus.Enhancer.level=WARNING
DataNucleus.SchemaTool.level=WARNING

Other Wave Requests

Now, this is where one needs to write some Java code. A wave robot requires to handle the following requests:

  • /_wave/robot/jsonrpc
  • /_wave/robot/profile

First, create a web.xml file to dispatch those calls at the right place:

> mkdir -p src/main/webapp/WEB-INF
> gedit src/main/webapp/WEB-INF/web.xml

Then, adjust content of web.xml:

<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"

    version="2.5">

    <display-name>App Name</display-name>

    <servlet>
        <servlet-name>RobotServlet</servlet-name>
        <servlet-class>com.abc.RobotServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>RobotServlet</servlet-name>
        <url-pattern>/_wave/robot/jsonrpc</url-pattern>
    </servlet-mapping>
   
    <servlet>
        <servlet-name>RobotProfile</servlet-name>
        <servlet-class>com.abc.RobotProfile</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>RobotProfile</servlet-name>
        <url-pattern>/_wave/robot/profile</url-pattern>
    </servlet-mapping>


    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
</web-app>

Obviously, the above XML file must be adjusted to reflect what classes are used in your project.

The RobotProfile class should look something like this:

package com.abc;

import java.io.IOException;
import java.util.logging.Logger;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.wave.api.ProfileServlet;

@SuppressWarnings("serial")
public class RobotProfile extends ProfileServlet {

    static private final Logger logger = Logger.getLogger(RobotProfile.class.getName());

    static private String BASE_NAME = "Robot Name";
   
    private String rootUrl = null;
    private String name = BASE_NAME;

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
       
        logger.info(""+this.getClass().getName()+" is up");
    }

    @Override
    public String getRobotName() {
        return name;
    }
   
    @Override
    public String getRobotAvatarUrl() {
        if( null == rootUrl ) {
            return super.getRobotAvatarUrl();
        }
       
        return rootUrl+"/images/icon.png";
    }
   
    @Override
    public String getRobotProfilePageUrl() {
        if( null == rootUrl ) {
            return super.getRobotProfilePageUrl();
        }
       
        return rootUrl+"/";
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        if( null == rootUrl ) {
            String requestUrl = request.getRequestURL().toString();
            String servletPath = request.getServletPath();
            if( null != requestUrl && null != servletPath ) {
                int index = requestUrl.indexOf(servletPath);
                if( index >= 0 ) {
                    rootUrl = requestUrl.substring(0, index);
                    logger.info("rootUrl : "+rootUrl);
                }
            }
        }
       
        super.service(request, response);
    }
   
}

The code above figures out what the real URL for the robot is and then uses it to serve out the robot’s home page and avatar. With the code above, create a HTML home page and save it into …/src/main/webapp/index.html Also, saving an avatar at …/src/main/webapp/images/icon.png will provide that image for displaying in the robot’s profile.

Finally, an example of the RobotServlet is given below. This article is not aimed at explaining the details of this code, but an example is provided to help readers along.

package com.abc;

import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;

import com.google.wave.api.AbstractRobotServlet;
import com.google.wave.api.Blip;
import com.google.wave.api.Element;
import com.google.wave.api.Event;
import com.google.wave.api.Gadget;
import com.google.wave.api.GadgetView;
import com.google.wave.api.Range;
import com.google.wave.api.RobotMessageBundle;
import com.google.wave.api.TextView;
import com.google.wave.api.Wavelet;

@SuppressWarnings("serial")
public class RobotServlet extends AbstractRobotServlet {

    static private final Logger logger = Logger.getLogger(RobotServlet.class.getName());
   
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
       
        // Report to log
        logger.info(""+this.getClass().getName()+" is running");
    }

    @Override
    public void processEvents(RobotMessageBundle bundle) {
        List<Event> events = bundle.getEvents();

        for (Event event : events) {
            switch ( event.getType() ){
            case WAVELET_SELF_ADDED:
                handleSelfAdded( bundle, event );
                break;
            // Triggered when "Done" is hit on a blip
            case BLIP_SUBMITTED:
                handleBlipSubmitted( bundle, event );
                break;
            case FORM_BUTTON_CLICKED:
                handleFormButtonClicked( bundle, event );
                break;
            }
        }
    }

    private void handleFormButtonClicked(RobotMessageBundle bundle, Event event) {
        logger.info("handleFormButtonClicked called");
        // TODO add some stuff here
    }

    private void handleBlipSubmitted( RobotMessageBundle bundle, Event event ) {
        logger.info("handleBlipSubmitted called");
        // TODO add some stuff here
    }


    private void handleSelfAdded(RobotMessageBundle bundle, Event event) {
        logger.info("handleSelfAdded called");

        Wavelet wavelet = bundle.getWavelet();
       
        Blip blip = wavelet.appendBlip();
        TextView textView = blip.getDocument();
        textView.delete();

        String myId = ">unknown<";
        for (String participant : event.getAddedParticipants()) {
            myId = participant;
        }

        textView.append("I'm alive! \n " + getRobotAddress() + " \n " + myId);
    }

}

Finishing Touches

As discussed above, add a home page and an avatar for your robot. If you wish, the profile code above handles the case where the home page and avatar are included right in the WAR file. The home page should be located at …/src/main/webapp/index.html The avatar should be a 100×100 pixels image saved at …/src/main/webapp/images/icon.png

Generate WAR file

Just like any other WAR generated via maven:

> mvn install

This should produce a WAR file under …/target This WAR file is just like any other and can be deployed anywhere. However, to be able to use it as a robot, one must deploy it on AppEngine. Create a project on AppEngine for this robot and deploy using AppCfg (http://code.google.com/appengine/docs/java/tools/uploadinganapp.html

If using the command line tool for AppCfg:

> ./appcfg.sh update ~/.../target/robot-0.0.1-SNAPSHOT

Note in the above exmaple that AppCfg does not accept a WAR file, but a directory with the WAR content. This is produced naturally by Maven, so you can use the directory directly. More information on an automated script can be found here: Uploading a Maven generated application on Google AppEngine

One can test the robot in any servlet container. If the robot is working, the following should be observed:

  1. Using a browser to access a page at ‘…/_wave/capabilities.xml’ should return the capabilities document
  2. Using a browser to access a page at ‘…/_wave/robot/profile’ should return a JSON document with the profile information

Using Robot

Using the robot in a Wave is akin to adding a participant. The address for the robot is <robot-name>@appspot.com

With luck, you will get there without a hitch. Remember, luck beats brains anytime.

1 comment so far

Add Your Comment
  1. If you port this article to use the new Google Wave robots API version 2, please let me know. I would love to add it to the documentation for the Wave APIs. Thanks!