2010
01.13

This note relates to Mozilla Firefox 3.5.7 running on Ubuntu Karmic (9.10).

The problem is that Firefox appears to be freezing for a quick moment, every 4 to 5 seconds. This is annoying when editing text and watching a video on YouTube.

I came across this helpful post: http://support.mozilla.com/en-US/forum/1/524464

My approach to the fix is to give up on session restore, altogether. I do not like it, anyway. The post above might help you with tweaking better values if you wish to retain this feature.

1. Open firefox to this page: about:config
2. In the filter box, type: browser.sessionstore
3. Double click on “browser.sessionstore.interval” and enter a larger value of your choice (I used 100000, but the next step might make this irrelevant)
4. Double click on “browser.sessionstore.resume_from_crash”, resetting the value (i.e. make it “false”)

This fixed the annoying behaviour. I hope it works for you.

2009
12.18

If you have built a Google Wave Robot that interacts with a gadget, you have probably come to a number of frustrating dead ends. As of this writing, the ability for a robot to observe changes in the state of a gadget is easily determined using the Wave Robot API. However, the difficulty comes in attempting to have the robot change the state of the gadget.

References:

Reading the library code for the Java Wave-Robot-API (http://wave-robot-java-client.googlecode.com/svn/trunk/), it becomes apparent that facilities available to a robot to perform changes on gadget states have a very coarse resolution. In that, I mean that there appears to be only the ability of changing a whole gadget state, not a single field.

When a robot receives a set of events from the Wave server, it has an opportunity to return a number of operations to be executed. This is the opportunity to modify content of the Wave. Techniques to observe the discussion between a Wave server and a robot are explained in another post.

When it comes to changing the content of the text, a robot can perform operation with fine resolution. This means that the operations deal with small changes. These operations are not likely to collide with operations of other users concurrently editing the same content, and if they do, the collision does not yield a large error. To understand better this phenomena, look at discussions on Operational Transforms, which are used internally by Wave to keep track of changes.

However, when it comes to managing elements (gadget are elements), there appears to be capabilities designed in the protocol to transform properties/fields of the element, however the library code is not making use of them. Therefore, at this time, it is unclear if these functions are available.

The problem is compounded by the fact that “whole-element” operations do not always work as expected. One such operation is “DOCUMENT_REPLACE_ELEMENT” which, according to the library code, should enable a robot to replace a gadget (or any element) with another copy. Using the facilities to replace an element fails and the Wave server returns an error.

This is an interesting side observation. Since the operations performed by a robot are returned as a reply to the incoming events, the Wave server contacts the robot again with incoming events containing the error. Currently, the Java Wave-Robot-API is not able to decode these events, so they show up in the logs as a critical error, where the following is observed:

java.lang.IllegalArgumentException: No enum const class com.google.wave.api.EventType.OPERATION_ERROR

If you see the above, it means that the Wave server was not able act upon a returned operation.

To come back to the whole replacement of an element, a robot must first perform a DOCUMENT_ELEMENT_DELETE operation, followed by a DOCUMENT_ELEMENT_INSERT operation. In Java, using the current Wave-Robot-API (published 2009-09-16), the following code would be needed:

        TextView textView = blip.getDocument();
        if( null != textView ) {
            GadgetView gv = textView.getGadgetView();
            Gadget gadget = gv.getGadget( gadgetUrl );
            if ( gadget != null ){
                // --> make changes to gadget state here <--

                int gadgetPos = textView.getPosition(gadget);
                gv.delete(gadget);
                textView.insertElement(gadgetPos, gadget);
            }
        }

What does this mean? Basically, if a robot and a user are attempting to use a gadget simultaneously, the changes that a robot makes will wipe out the changes from the user. Also, as the gadget is removed and added, there is a visual cue that this happened if the user is looking at the wave. This is far from an acceptable work-around.

Even when the bug above is fixed, the resolution of the changes will still be too coarse. The only long-term acceptable solution will be based on the DOCUMENT_ELEMENT_MODIFY_ATTRS operation, but it is not clear when this will happen.

Therefore, the really exciting Wave applications, where robots and gadgets interact to provide a truly immersed environment, will have to wait or suffer the visual glitches.

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

Over the past couple of days, I have seen a situation where I boot my laptop (Ubuntu Karmic) and the Network Manager applet insists that the wireless is disabled. Rebooting does not help. Examining the syslog showed that the OS found 2 kill switches and one of them was disabling the wireless. Examining the state showed that each kill switch state was affected by the single physical kill switch, but their states were the opposite of each other. So regardless of which position the kill switch is in, one of the kill switch devices would disable the wireless.

I turned the kill switch off and reboot the computer. When it came up, the kill switch drivers were now synchronized and turning the kill switch on caused the Network Manager to start looking for Wireless connections. Listing the kill switch states shows they are again synchronized.

$ rfkill list
0: dell-wifi: Wireless LAN
    Soft blocked: no
    Hard blocked: no
1: phy0: Wireless LAN
    Soft blocked: no
    Hard blocked: no

This post at LinuxTrap shows that others are experiencing a similar condition.

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

Wave Extension Installer

Once you have built your wave extension, you will need a way to install it into the user interface of the Google Wave client. A Wave Extension Installer is just that beast. You will need to create an Extension Manifest, which is an XML file describing where to find your extension, where you want it installed in the client UI and other information you want displayed.

We started out with the Wave Extension Installer Guide, but this document has some problems with the robot extension installer sample. Maybe its out of date, I don’t know.

The part that is in error is that there is no action element defined within the menuHook element. This is easily fixed by wrapping the participant element by the createNewWave element.

<extension
   name="Tweety"
   description="Creates a new Twitter wave.">
  <menuHook location="newwavemenu" text="Creates a new Twitter wave."
     iconUrl="http://wave-skynet.appspot.com/public/extensions/tweety/icon.png">
    <createNewWave>
       <participant id="tweety-wave@appspot.com"></participant>
    </createNewWave>
  </menuHook>
</extension>

Once this was straightened out, we put together an extension manifest for our existing sample robot, Frakky.

<extension
   name="Frakky"
   description="Creates a new Frakky wave."
   thumbnailUrl="http://robot-1.appspot.com/images/icon.png"
   >
  <author name="BitsByThePound"/>
  <menuHook location="newwavemenu" text="Creates a new Frakky wave."
     iconUrl="http://robot-1.appspot.com/images/icon.png">
    <createNewWave>
       <participant id="robot-1@appspot.com"></participant>
    </createNewWave>
  </menuHook>
</extension>

Once you have your extension manifest set up for your extension, you need to make it Internet accessible. A reasonable place to put this is next to the index.html file for your robot, in the war directory. We named the file install.xml. Then whenever you upload your robot to Google App Engine, the extension manifest will be uploaded to http:/yourappname.appspot.com/install.xml. To be able to install this manifest in Google Wave Preview, you will need to install the Extension Installer. Yeah, its name is a little confusing but I am not sure that calling it Extension Installer Extension (which better describes its function) is any less confusing.

Once this is done, you can select the dropdown next to New Wave in the center panel and choose New Extension Installer. When you are asked for the Extension Manifest URL, be sure to specify the URL including the http:// portion or else you will end up receiving an error message that implies there is something wrong with the manifest instead of saying it just cannot find the manifest. This creates a wave with a puzzle piece from which you can install the manifest into the Google Wave client. Push the Install Extension button to hook it up.

Extension Almost Installed

Extension Almost Installed

You will notice that the attributes you specified in the extension element such as name, description and thumbnailURL are used to display the puzzle piece.

Now, if you check the muted wave, Extension Settings, your extension is listed at the bottom.

Installed Extension

Installed Extension

Since our manifest added a new item to the New Wave button, clicking the dropdown next to this gives us an entry that says “Creates a new Frakky wave.” Clicking this creates a new wave with our robot set as a participant.