2009
12.18

You have written a Google Wave Robot, uploaded it on AppEngine, and it does not seem to behave the way you expected?  You would not be searching the Internet if it was working, would you?

First, you are not alone. The robot API appears to be changing, and I have observed changes in the amount of information my robots receive, over time. The best way to figure out what is going on with the robot is to look at the raw data sent by the Wave server to the robot. These messages are remote procedure calls encoded in JSON (jsonrpc).

This post assumes that the Google Wave Robot is deployed on AppEngine.  At the time of writing this post, this is the only avenue for deploying Wave Robots.

To see the raw data transmitted to a Wave Robot, one must:

  1. Enable the robot to record FINEST log messages
  2. Capture the logs from the AppEngine web site
  3. Decode the JSON message in human readable format

This post deals with 1 and 3.

Enable FINEST logging

In your robot project, there is a properties file used to configure the logger. In general, this file is located in …/WEB-INF/logging.properties  This location can change, since it is declared in a file called …/WEB-INF/appengine-web.xml. If you can not find the logging properties files, inspect the appengine-web.xml file. It should look like this:

<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
    <application>myCoolRobotName</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>

If the system property called “java.util.logging.config.file” is not present, then you should add it and create the “logging.properties” file. If the property already exists, then you have a clue where the file can be found.

The “logging.properties” file should contain something as follows:

# 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 = FINEST

# 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

The trick is to change the property called “.level” to a value of “FINEST”. By default, most example give a value of “WARNING” to this property.

Once this change is done, upload the robot on AppEngine, again, for the new logging level to take effect.

Capturing the logs

Once the robot is uploaded on AppEngine, interacting with a wave that includes the robot should generate messages sent to it. Using the AppEngine administrative page for the robot, navigate to the “logs” page. Changing the filter to “INFO” should refresh the page with a number of “I” icons. Those are the information entries in the logs. Some of those should display the incoming events and outgoing operations.

Copy the large JSON object from the incoming events into the clip board.

Format JSON for a human

A number of online services will readily translate JSON to a more readable format. One of those is: http://jsonformatter.curiousconcept.com/ Pasting the content obtained in the previous step into the web form and formatting should give you the full picture of the information available to the robot.

2009
12.18

This note is written based on:

  • appengine-java-sdk-1.2.6
  • Apache Maven 2.2.1
  • Apache Ant 1.7.1

If you create an application destined to Google AppEngine using Maven, the use of the Google AppEngine SDK may not be optimal. This is because, in some instances, the generated artifact (a WAR file) is not what is expected from the SDK. In general, when generating a WAR file using Maven, an interim directory is created with all the files found inside the WAR archive. This interim directory is the expected input for the AppEngine SDK.

However, there are circumstances where an interim directory is not created. Using the Cargo Plug-in for Maven and generating an Uberwar is one example where the generated artifact does not naturally produce the needed interim directory. I find Cargo useful to build Google Wave Robots that include their own gadgets. Since Google Wave Robots must currently be hosted on Google AppEngine, I needed a script to load them automatically.

This note introduces an ANT script that unpacks the WAR file and upload the application on Google AppEngine. Two files are needed:

  • build.xml This is the script that uploads the application
  • build.properties This file includes user specific information

build.xml
Create a file called “build.xml” in the root directory of the Maven project. The content of the file should be as follows:

<?xml version="1.0" encoding="UTF-8"?>
<project name="appengine-upload" default="default">
   
    <description>
        This project provides helper tasks for appengine
    </description>

    <!-- Allow overriding of properties -->
    <property file="./build.properties"/>
   
    <property name="appcfg.command" value="./appcfg.sh"/>
    <property name="uber.target.dir" location="./target"/>
   
    <!-- ================================= -->
    <target name="default" description="Do nothing">
    </target>
   
    <!-- ================================= -->
    <target name="update" depends="-update-check-variables" description="Updates appengine">
        <property name="temp.dir" location="${uber.target.dir}/appengine"/>
       
        <delete dir="${temp.dir}" failonerror="false"/>
       
        <mkdir dir="${temp.dir}"/>
       
        <unjar dest="${temp.dir}">
            <fileset dir="${uber.target.dir}">
                <include name="*.war"/>
            </fileset>
        </unjar>
   
        <exec
            executable="${appcfg.command}"
            dir="${appcfg.dir}/bin"
            inputstring="${appcfg.password}"
            >
            <arg line="--email=${appcfg.email} --passin update ${temp.dir}"/>
        </exec>
    </target>
    <target name="-update-check-variables" description="Check variables">
        <fail
            unless="appcfg.dir"
            message="Directory to appengine tools must be specified in 'appcfg.dir'"/>
        <fail
            unless="appcfg.email"
            message="Appengine e-mail must be specified in 'appcfg.email'"/>
        <fail
            unless="appcfg.password"
            message="Appengine password must be specified in 'appcfg.password'"/>
    </target>
</project>

build.properties
Create a file called “build.properties” in the same directory as “build.xml”. The content of that file should look like:

# Directory to appengine tools
appcfg.dir=.../appengine-java-sdk-1.2.6

# E-mail address associated with appengine account
appcfg.email=user@gmail.com

# Password associated with account
appcfg.password=password

The content of the file above should be adjusted to reflect the user’s specific SDK installation, appengine user name and password.

Running
After the files are configured correctly, uploading the application to AppEngine should be done with the following command, executed from the root directory of the project:

ant update
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.

2009
11.29

Today, we presented an introduction to Google Wave Gadgets at the Ottawa WaveCamp (Hackathon). This presentation covers the creation, deployment and use of Wave Gadgets.

The presentation slides and example source code is available on a Google project. More information available here: http://owh01-ig.googlecode.com/svn/trunk/web/index.html

2009
11.28

When deploying a Google Wave Gadget that depends on external resources such as images, javascript libraries and CSS stylesheets, resources referred to by relative paths do not load correctly.

The problem is as follows:

  • a gadget is deployed on a third party server
  • a gadget is loaded as a plugin of the Wave framework
  • to the client browser, the gadget appears to be loading from the Wave service URL
  • relative paths attempt to resolve to the Wave service domain, while the resources are sitting on the third party server

A naive solution would be to translate all relative paths to absolute paths that refers to the “real” location of the gadget, mainly URLs based on the third party server domain. However, this is not a workable solution. The problem with absolute URLs has to do with browser security, where certain resources can not be loaded from domains other than the domain of the original page. Since the resources associated with the gadget are not located in the same domain as the Wave service, then some resources will not load, depending on type and browser security enforcement.

The working solution is to proxy all resource requests via the Wave service. In this manner, all resources associated with the gadget appear to be served from the same domain as the original page.

This solution is based on the “Gadget libraries” and is not restricted to Google Wave Gadgets, per se. It might be applicable to other social gadgets as well.

The described solution is to:

  1. Insert “bootstrap” code in the gadget module XML file
  2. Use bootstrap code to load external resources

The bootstrap code is shown below:

<script type="text/javascript">
function reportError(errString) {
  if(!window.__error) window.__error = {};
  if( !window.__error[errString] ) {
    // Suppress multiple
    window.__error[errString] = true;
    alert(errString);
  }
};
function getModuleBase() {
  if( window.__moduleBase ) return window.__moduleBase;
  if( _args ) {
    var moduleBase=_args()['url'];
    moduleBase=moduleBase.substring(0,moduleBase.lastIndexOf('/')+1);
    window.__moduleBase = moduleBase;
    return window.__moduleBase;
  };
  reportError('Can not find module base. Gadget may not work properly.');
  return '';
};
function rebaseRelativeUrl(relativeUrl,cached) {
  var moduleBase = getModuleBase();
  var absUrl = moduleBase+relativeUrl;
  if( cached &amp;&amp; _IG_GetCachedUrl ) {
    absUrl = _IG_GetCachedUrl(absUrl);
  };
  return absUrl;
};
function addStylesheet(cssRelativeUrl) {
  var rebasedUrl = rebaseRelativeUrl(cssRelativeUrl,true);
  document.write('<link rel="stylesheet" href="'+rebasedUrl+'">')
};
function addScript(jsRelativeUrl) {
  var rebasedUrl = rebaseRelativeUrl(jsRelativeUrl,true);
  document.write('<script src="'+rebasedUrl+'"><\/script&gt;')
};
gadgets.util.registerOnLoadHandler(
  function(){
    addScript('counter.js');
    addStylesheet('counter.css');
  }
);
</script>

getModuleBase()

This function retrieves the “real” URL of the gadget and returns the absolute path to the directory containing the gadget. This function computes the absolute path by retrieving the Gadget parameter called ‘url’. This information is provided by the gadget framework, which provide all gadget parameters via an associative array obtained with a call to _args()

rebaseRelativeUrl(relPath, shouldProxy)

This function accepts a path (relPath) relative to the gadget location and returns an equivalent absolute URL. This function uses the getModuleBase() to make this computation. If the flag ‘shouldProxy’ is set, then the returned URL is an equivalent URL, however it is proxied via the gadget framework. This conversion is possible because of the _IG_GetCachedUrl() available with the gadget libraries. Therefore, when the flag is set, a path relative to the location of the gadget is converted to an absolute URL proxied via the gadget framework, avoiding browser security issues.

addStylesheet() and addScript()

These utility functions add style sheets and external javascript by using the rebaseRelativeUrl() and adding the appropriate elements to the document. These functions are available at run-time and should probably not be used before the gadget is loaded.

gadgets.util.registerOnLoadHandler()

This function is often used in gadget development and generally well understood. It will not be covered here. The reason for inclusion in the post is that this is the location where the external resources should be loaded, as shown in the example above.

Example, please?

Sure. An example gadget is available here: http://owh01-ig.googlecode.com/svn/trunk/gadgets/counter2/counter2.xml

2009
11.27

Log in to your Dev Preview Account and then follow this link:

https://www.google.com/a/wavesandbox.com/ChangePassword?service=mail&hl=en

This worked as of the end of November 2009.

2009
11.04

Setting up Apache Tomcat for Upgrades

Plan for Change.

Note: This article is based on Apache Tomcat 6.0.20

If you set up a lot of Tomcat sandboxes, here is a sample structure to make it easy to upgrade to a new version of Tomcat without having to reinstall all applications. The trick is to have a number of Tomcat sandboxes pointing to a symbolic link, which in turn points to the latest version of Tomcat.

Download Tomcat

Get latest version of Tomcat from Apache. Check signature. Decompress. Untar.

> cd ...
> tar -xvf apache-tomcat-6.X.X.tar

Now create a symbolic link to this latest version

> ln -s apache-tomcat-6.X.X apache-tomcat-latest

Create a sandbox

A sandbox refers to a directory where you are testing a web application. Portions of the sandbox are linked back to the latest version of Tomcat. Other portions of the sandbox are original to it.

> mkdir tomcat.sandbox
> cd tomcat.sandbox
> ln -s ../apache-tomcat-latest/bin .
> ln -s ../apache-tomcat-latest/lib .
> ln -s ../apache-tomcat-latest/conf .
> mkdir work
> mkdir logs
> mkdir webapps

To add a WAR file to Tomcat, simply copy it to the tomcat.sandbox/webapps directory. It will be auto detected when Tomcat starts and deployed.

Finally, start Tomcat.

> cd bin
> ./catalina.sh run

Obviously, there is more to configuring and running Tomcat. You can get more information by starting at the Tomcat Homepage.

Upgrading

When a new version of Tomcat becomes available, repeat the first step. Download, verify, decompress and untar the latest version, alongside the first version. Then, change the symbolic link of latest Tomcat to the new one.

> cd ...
> tar -xvf apache-tomcat-6.Y.Y.tar
> rm apache-tomcat-latest
> ln -s apache-tomcat-6.Y.Y apache-tomcat-latest

Now, all sandboxes are pointing at the latest libraries. Restart all sandboxes.

Unless major changes have occurred in configuration files, this trick should let you upgrade painlessly.

2009
11.04

Setting up Jetty for Upgrades

Plan for change.

Note: this article refers to Jetty-7

If you set up a lot of Jetty sandboxes, here is a sample structure to make it easy to upgrade to a new version of Jetty without having to reinstall all applications. The trick is to have a number of Jetty sandboxes pointing to a symbolic link, which in turn points to the latest version of Jetty.

Download Jetty

Get latest version of Jetty from http://www.eclipse.org/jetty/downloads.php Check signature. Decompress. Untar.

> cd ...
> tar -xvf jetty-distribution-7.X.X

Now create a symbolic link to this latest version

> ln -s jetty-distribution-7.X.X jetty-7-latest
> chmod a+x jetty-7-latest/bin/*.sh

Create a sandbox

A sandbox refers to a directory where you are testing a web application. Portions of the sandbox are linked back to the latest version of Jetty. Other portions are copied from the latest Jetty. Finally, some portions of the sandbox are original to it.

> mkdir jetty.sandbox
> cd jetty.sandbox
> ln -s ../jetty-7-latest/bin .
> ln -s ../jetty-7-latest/lib .
> ln -s ../jetty-7-latest/start.ini .
> ln -s ../jetty-7-latest/start.jar .
> cp -rp ../jetty-7-latest/etc .
> cp -rp ../jetty-7-latest/resources .
> mkdir contexts
> mkdir logs
> mkdir webapps

At this point, we must configure the sandbox. There are two steps to adding a WAR file:

  1. Copy WAR file to jetty.sandbox/webapps
  2. Create a context file (.xml) located in jetty.sandbox/contexts to enable application. Look at jetty-7-latest/contexts for inspiration.

Finally, start jetty.

> bin/jetty.sh run

Obviously, there is more to configuring and running Jetty. You can get more information by starting at the Jetty Homepage.

Upgrading

When a new version of Jetty becomes available, repeat the first step. Download, verify, decompress and untar the latest version, alongside the first version. Then, change the symbolic link of latest Jetty distribution to the new one.

> cd ...
> tar -xvf jetty-distribution-7.Y.Y
> rm jetty-7-latest
> ln -s jetty-distribution-7.Y.Y jetty-7-latest
> chmod a+x jetty-7-latest/bin/*.sh

Now, all sandboxes are pointing at the latest libraries. Restart all sandboxes.

Unless major changes have occurred in configuration files, this trick should let you upgrade painlessly.

2009
10.31

GWT Module using Maven

Note, this discussion is based on:

  • GWT libraries 1.6.4
  • Apache Maven 2.2.1

I like Maven because it makes me much more productive. It takes care of a lot a project management details and makes the code saved in my repository smaller. When it comes to creating GWT modules (reusable libraries), I was hoping that I could use Maven to keep track of dependencies between applications and modules, as well as dependencies between modules. This discussion presents what is needed to make a GWT module in a Maven project.

There are two important points to remember when making a GWT module using Maven:

  1. The produced JAR file must contain a module file (*.gwt.xml)
  2. The produced JAR file must contain the source files needed in the module. This is because these source files must be compiled in the context of the application.

Maven Project

The first step is to set up a normal Maven project to produce a JAR file artifact.

> mvn archetype:create -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DgroupId=<group-id> -DartifactId=<artifact-id>

This command will generate two java files called App.java and AppTest.java. You can delete them.

In an example:

> mvn archetype:create -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DgroupId=gwt -DartifactId=gwt-module0
> cd gwt-module0
> rm src/main/java/gwt/App.java
> rm src/test/java/gwt/AppTest.java
> find .
.
./src
./src/main
./src/main/java
./src/main/java/gwt
./src/test
./src/test/java
./src/test/java/gwt
./pom.xml

Modules must be compiled using Java 6, therefore the pom.xml file must be modified accordingly. Edit the pom.xml file

> vi pom.xml

and add the following content at the appropriate place:

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

Module File

A module file is required. In Maven, this file should be in the resource directory. Create a module file in the usual resources directory with an appropriate name and an extension of .gwt.xml

> mkdir -p src/main/resources
> vi src/main/resources/Module0.gwt.xml

Use the following content as a starting point for the module file:

<!DOCTYPE module PUBLIC "//gwt-module/" "http://google-web-toolkit.googlecode.com/svn/tags/1.6.2/distro-source/core/src/gwt-module.dtd">
<module>
</module>

There is nothing else required in the module file. Note that the name of the module is “Module0″, in the global name space. If one wants to use a longer name space such as “com.example.Module0″, then the full modulfe file name would be: “src/main/resources/com/example/Module0.gwt.xml”.

Since the Java source files for the module are to be located in a subdirectory of the module other than the standard “client” subdirectory (in this case, the name of the directory is “gwt”), one must add a “source” directive to the module file, as such:

<!DOCTYPE module PUBLIC "//gwt-module/" "http://google-web-toolkit.googlecode.com/svn/tags/1.6.2/distro-source/core/src/gwt-module.dtd">
<module>
   <source path="command"/>
</module>

Include Source Files in JAR artifact

As stated above, source files must be added to the JAR artifact. Therefore, the pom.xml file must be augmented to include the source files as well as the standard resources directory. Edit pom.xml and add the following:

<build>
   <resources>
      <resource>
         <directory>src/main/java</directory>
         <includes>
            <include>gwt/**</include>
         </includes>
      </resource>
      <resource>
         <directory>src/main/resources</directory>
      </resource>
   </resources>
</build>

Test JAR generation

For example, create a Bean class to test:

> vi src/main/java/gwt/MyBean.java

with the following content:

package gwt;

import java.io.Serializable;

@SuppressWarnings("serial")
public class MyBean implements Serializable {

   private String id;

   public String getId() {
      return id;
   }
   public void setId(String id) {
      this.id = id;
   }
}

Now, generate artifact:

> mvn install

You can verify the content of your JAR artifact:

> jar -tf target/gwt-module0-1.0-SNAPSHOT.jar
META-INF/
META-INF/MANIFEST.MF
gwt/
gwt/MyBean.java
gwt/MyBean.class
Module0.gwt.xml
META-INF/maven/
META-INF/maven/gwt/
META-INF/maven/gwt/gwt-module0/
META-INF/maven/gwt/gwt-module0/pom.xml
META-INF/maven/gwt/gwt-module0/pom.properties

Note that the module file, the source and compiled version of the class are included.

Include Module in an application

To include the new module in an application or another module, two steps are required:

  1. Add a reference to the module file in the upstream application/module.
  2. Add the module JAR as a dependency of the upstream application/module.

Edit the GWT module file (.gwt.xml) in the upstream application/module and add the following directive:

<inherits name='Module0'/>

Do not forget to put the fully qualified name, including the name space, if required.

If the upstream application/module uses Maven, then adding the module as a dependency is simple. Edit the pom.xml file in the upstream project and add the JAR artifact produced by the module:

</dependencies>
   <dependency>
      <groupId>gwt</groupId>
      <artifactId>gwt-module0</artifactId>
      <version>1.0-SNAPSHOT</version>
   </dependency>
</dependencies>

Create a module with GWT Widgets

In the module, if one wishes to create GWT widgets, then one must include the core Web toolkit module from GWT. Two steps are required:

  1. Add dependency in the module file
  2. Add dependency in the Maven pom.xml file.

Edit the module file

> vi src/main/resources/Module0.gwt.xml

and add the following directive:

<inherits name='com.google.gwt.user.User'/>

Then, edit the pom.xml file

> vi pom.xml

and add the dependency

<dependencies>
   <dependency>
      <groupId>com.google.gwt</groupId>
      <artifactId>gwt-user</artifactId>
      <version>1.6.4</version>
      <scope>provided</scope>
   </dependency>
</dependencies>
2009
10.30

Rocky Road Ahead -> Defensive Programming Required!

You have been warned.

Old programmers tend to revert to printf() when it is time to find where the bugs are. It is much nicer to have a debugger handy. However, while trying our first Google Wave Robot, I longed for a better comprehension of the events passed to the robot and started printing the received events directly in the wave. This is when I found out that minor changes, that looked otherwise inconspicuous, would greatly affect the robot’s behaviour. In fact, small changes would stop the robot altogether.

As it turns out, there exist currently calls to the Google Wave Java API that throws exceptions, although the signatures do not report any. If an exception is raised in the printing while trying to debug a piece of code, then the robot appears unresponsive. For more specifics, look at bugs:

Therefore, I propose here a set of functions to print an instance of RobotMessageBundle received in the robot processEvents() call. NOTE: be careful in using these functions only in situations where receiving new submitted blips are not the trigger of submitting newer blips. This would be an unwanted recursive situation.

The following example is safe since the debug print happens only on WAVELET_SELF_ADDED events:

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

      printBundle(bundle,textView,null);
   }

Here are the printing/debugging functions:

   public void printBundle(RobotMessageBundle bundle, TextView printTextView, String prefix) {
      if( null == prefix ) {
         prefix = "";
      }

      // is new wave
      try {
         printTextView.append(prefix+"bundle.isNewWave() "+bundle.isNewWave()+" \n ");
      } catch(Exception e) {
         printTextView.append(prefix+"bundle.isNewWave() "+exceptionToString(e)+" \n ");
      }

      // was self added
      try {
         printTextView.append(prefix+"bundle.wasSelfAdded() "+bundle.wasSelfAdded()+" \n ");
      } catch(Exception e) {
         printTextView.append(prefix+"bundle.wasSelfAdded() "+exceptionToString(e)+" \n ");
      }

      // was self removed
      try {
         printTextView.append(prefix+"bundle.wasSelfRemoved() "+bundle.wasSelfRemoved()+" \n ");
      } catch(Exception e) {
         printTextView.append(prefix+"bundle.wasSelfRemoved() "+exceptionToString(e)+" \n ");
      }

      // robot address
      try {
         printTextView.append(prefix+"bundle.getRobotAddress() "+bundle.getRobotAddress()+" \n ");
      } catch(Exception e) {
         printTextView.append(prefix+"bundle.getRobotAddress() "+exceptionToString(e)+" \n ");
      }

      // Events
      try {
         List events = bundle.getEvents();
         printTextView.append(prefix+"bundle.getEvents() "+((events==null)?"null":"list of "+events.size()+" events")+" \n ");
         if( null != events ) {
            for(Event event : events) {
               printBundleEvent(event, printTextView, prefix+"   ");
            }
         }
      } catch(Exception e) {
         printTextView.append(prefix+"*** Error on printing event: "+e.getMessage()+" \n ");
      }
   }

   public void printBundleEvent(Event event, TextView printTextView, String prefix) {
      if( null == prefix ) {
         prefix = "";
      }

      // Event type
      try {
         EventType eventType = event.getType();
         printTextView.append(prefix+"Event.getType() "+((eventType==null)?"null":eventType.name())+" \n ");
      } catch(Exception e) {
         printTextView.append(prefix+"Event.getType() "+exceptionToString(e)+" \n ");
      }

      // button name
      try {
         printTextView.append(prefix+"Event.getButtonName() "+event.getButtonName()+" \n ");
      } catch(Exception e) {
         printTextView.append(prefix+"Event.getButtonName() "+exceptionToString(e)+" \n ");
      }

      // changed title
      try {
         printTextView.append(prefix+"Event.getChangedTitle() "+event.getChangedTitle()+" \n ");
      } catch(Exception e) {
         printTextView.append(prefix+"Event.getChangedTitle() "+exceptionToString(e)+" \n ");
      }

      // changed title
      try {
         printTextView.append(prefix+"Event.getCreatedBlipId() "+event.getCreatedBlipId()+" \n ");
      } catch(Exception e) {
         printTextView.append(prefix+"Event.getCreatedBlipId() "+exceptionToString(e)+" \n ");
      }

      // removed blip id
      try {
         printTextView.append(prefix+"Event.getRemovedBlipId() "+event.getRemovedBlipId()+" \n ");
      } catch(Exception e) {
         printTextView.append(prefix+"Event.getRemovedBlipId() "+exceptionToString(e)+" \n ");
      }

      // modified by
      try {
         printTextView.append(prefix+"Event.getModifiedBy() "+event.getModifiedBy()+" \n ");
      } catch(Exception e) {
         printTextView.append(prefix+"Event.getModifiedBy() "+exceptionToString(e)+" \n ");
      }

      // changed version
      try {
         printTextView.append(prefix+"Event.getChangedVersion() "+event.getChangedVersion()+" \n ");
      } catch(Exception e) {
         printTextView.append(prefix+"Event.getChangedVersion() "+exceptionToString(e)+" \n ");
      }

      // time stamp
      try {
         printTextView.append(prefix+"Event.getTimestamp() "+event.getTimestamp()+" \n ");
      } catch(Exception e) {
         printTextView.append(prefix+"Event.getTimestamp() "+exceptionToString(e)+" \n ");
      }

      // blip
      try {
         printTextView.append(prefix+"Event.getBlip() "+event.getBlip()+" \n ");
      } catch(Exception e) {
         printTextView.append(prefix+"Event.getBlip() "+exceptionToString(e)+" \n ");
      }

      // Added participants
      try {
         Collection participants = event.getAddedParticipants();
         printTextView.append(prefix+"Event.getAddedParticipants() "+
               ((participants==null)?"null":"collection of "+participants.size()+" strings")+" \n ");
         if( null != participants ) {
            for(String participant : participants) {
               printTextView.append(prefix+"   "+participant+" \n ");
            }
         }
      } catch(Exception e) {
         printTextView.append(prefix+"Event.getAddedParticipants() "+exceptionToString(e)+" \n ");
      }

      // Removed participants
      try {
         Collection participants = event.getRemovedParticipants();
         printTextView.append(prefix+"Event.getRemovedParticipants() "+
            ((participants==null)?"null":"collection of "+participants.size()+" strings")+" \n ");
         if( null != participants ) {
            for(String participant : participants) {
               printTextView.append(prefix+"   "+participant+" \n ");
            }
         }
      } catch(Exception e) {
         printTextView.append(prefix+"Event.getRemovedParticipants() "+exceptionToString(e)+" \n ");
      }
   }

   public String exceptionToString(Exception e) {
      return "ERROR:"+e.getClass().getName()+"("+e.getMessage()+")";
   }