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.

2009
11.23

Note these instructions are for Ubuntu 9.10 (Karmic).

In prior versions of Ubuntu, I have had some problems getting the VGA and S-Video outputs on my Inspiron 1720 working, so today I tackled this problem. In Karmic, this process is quite straightforward. Once you connect your video cable to the laptop, whether it is S-Video to a television or VGA to a monitor, you can cycle through various video distribution modes using the Fn-F8 keys. This is marked as “CRT/LCD” on the F8 key. Here is the cycle that I found using S-Video output.

1. LCD on at full resolution ; TV off
2. LCD off ; TV on at 848×480
3. LCD on at full resolution ; TV on and to right of LCD, 848×480 ; desktop shared across both screens
4. LCD on, 1024×768 ; TV on, 1024×768 ; mirrored desktop

You can change the parameters of each of these cycles by running the Display Preferences applet (System->Preferences->Display). This applet has a spacial depiction of  both displays and you may drag a display to be in a specific location with respect to the other display. For example, if in 3 above, you wanted the TV to appear logically above the CRT, you would drag the TV rectangle (labeled “unknown” for me) above the rectangle labeled “Laptop 17″.

Mode 3 shares the desktop over the 2 screens so that you can move the mouse cursor from one screen to the other screen, effectively extending your desktop. If you use an image as your desktop background, you may get the background color showing around the edges of the image as the image is centered in the extended display resolution.

Mode 4 shows the same content on the laptop LCD panel as on the connected screen. Note that in this mode for a connected television, the video quality is less crisp than in mode 3 due to the scaling of the 1024×768 resolution to something the television can handle.

If you connect a VGA cable to a monitor, a similar cycle is used, but the VGA resolutions used will be different.

Out of curiosity, I connected both a VGA monitor and S-Video to the TV, but preference seems to be given to the VGA monitor such that the TV never gets a signal.

2009
11.23

Free Remote PC Control software

Extracted from a Slashdot article.

http://www.teamviewer.com/ [teamviewer.com]

https://secure.logmein.com/products/free/ [logmein.com]

http://www.copilot.com/ [copilot.com]

http://skype.com/ [skype.com]

http://www.uvnc.com/addons/singleclick.html [uvnc.com] (reverse VNC)

http://www.crossloop.com/ [crossloop.com]

http://www.mikogo.com/en/ [mikogo.com]

http://showmypc.com/ [showmypc.com]

https://www.ntrconnect.com/ [ntrconnect.com]

http://www.zolved.com/remote_control [zolved.com]

http://www.wippien.com/ [wippien.com] (VPN)

http://code.google.com/p/gitso/ [google.com] (reverse VNC)

2009
11.15

Foxit, a better PDF reader for Linux

Today I got fed up with the overall poor search performance and poor rendering of Evince on 64 bit Ubuntu and looked around for an alternative. I use Foxit on my Windows machine and am generally very happy with it, having only had problems with it with government security clearance documents. A quick search shows that there is indeed a Linux version of Foxit.

Note that Foxit is a 32 bit executable so if you are running a 64 bit version of Linux, you will need to install ia32-libs from your repository.

Since a new version is supposed to be available within a couple of months, we may as make the upgrade path easier. I chose to download it to a local directory and then after uncompressing, moving it to /usr/local/foxit1.1

> cd /tmp
> wget http://mirrors.foxitsoftware.com/pub/foxit/reader/desktop/linux/1.x/1.1/enu/FoxitReader-1.1.0.tar.bz2
> bunzip2 FoxitReader-1.1.0.tar.bz2
> tar xvf FoxitReader-1.1.0.tar
> sudo mv 1.1-release /usr/local/foxit1.1
> sudo ln -s /usr/local/foxit1.1 /usr/local/foxit

We now have a link to the directory at /usr/local/foxit which we can change to the 1.2 version when it becomes available.

Now lets add it into the Gnome menu. Click on System->Preferences->Main Menu. Select Accessories then click new Item. Fill in the form, pointing to FoxitReader.

Create Launcher for Foxit

Click OK and now Foxit shows up in your Accessories list.

Now, lets change the file associations for PDF files so that if you click on a PDF from the File Browser that it opens Foxit. From the File Browser, find a PDF file and then right click and select Properties and click the Open With tab.

Adding Foxit as a PDF Handler

Click on Add, then dropdown Use a custom command and browse to where FoxitReader lives. Click Close.

Add Foxit

This adds FoxitReader into the Open With list. Select FoxitReader and click Close.

Associate

Now you should be able to click on any PDF in the File Browser and Foxit will be launched with the contents of the document.

2009
11.11

Installing Wink on 64 bit Ubuntu

Wink is great tutorial building software for Linux and Windows. It used to be available in the Ubuntu repositories but is no longer included. Currently, the Linux variant of Wink is version 1.5 and the Windows variant is version 2.0. Even though they share the same build number, they are not the same.

Unfortunately, running the newer, Windows version under Wine is not an option that I can see because there isn’t a way to capture the input from native Linux windows.

Go to the download page and download the wink15.tar.gz tarball to a temporary directory. This makes cleanup easier, since it is actually a tarbomb, extracting to the current directory.

Unzip the tarball which creates an installer.sh script and an installdata tarball.

> tar xzvf wink15.tar.gz

Now, run the installer script.

> ./installer.sh
This installer only has x86 binaries. Sorry.

Oops. I am running a 64 bit version of Ubuntu and this is a 32 bit binary only. Lets check what is in the installdata.tar.gz tarbomb.

> rm installer.sh
> tar xzvf installdata.tar.gz

This creates an entire directory structure in place including the wink executable. Lets install some basic 32 bit libraries.

> sudo apt-get install ia32-libs

Now lets check whats missing.

> ldd wink
linux-gate.so.1 =>  (0xf779e000)
libgtk-x11-2.0.so.0 => /usr/lib32/libgtk-x11-2.0.so.0 (0xf73bc000)
libgdk-x11-2.0.so.0 => /usr/lib32/libgdk-x11-2.0.so.0 (0xf7327000)
libatk-1.0.so.0 => /usr/lib32/libatk-1.0.so.0 (0xf7309000)
libgdk_pixbuf-2.0.so.0 => /usr/lib32/libgdk_pixbuf-2.0.so.0 (0xf72ef000)
libpangoxft-1.0.so.0 => /usr/lib32/libpangoxft-1.0.so.0 (0xf72e6000)
libpangox-1.0.so.0 => /usr/lib32/libpangox-1.0.so.0 (0xf72d9000)
libpango-1.0.so.0 => /usr/lib32/libpango-1.0.so.0 (0xf7291000)
libgobject-2.0.so.0 => /usr/lib32/libgobject-2.0.so.0 (0xf7252000)
libgmodule-2.0.so.0 => /usr/lib32/libgmodule-2.0.so.0 (0xf724d000)
libdl.so.2 => /lib32/libdl.so.2 (0xf7249000)
libglib-2.0.so.0 => /lib32/libglib-2.0.so.0 (0xf7193000)
libgthread-2.0.so.0 => /usr/lib32/libgthread-2.0.so.0 (0xf718d000)
libpthread.so.0 => /lib32/libpthread.so.0 (0xf7174000)
libXi.so.6 => /usr/lib32/libXi.so.6 (0xf7168000)
libXext.so.6 => /usr/lib32/libXext.so.6 (0xf7158000)
libX11.so.6 => /usr/lib32/libX11.so.6 (0xf7029000)
libm.so.6 => /lib32/libm.so.6 (0xf7003000)
libstdc++.so.5 => /usr/lib/libstdc++.so.5 (0xf6f49000)
libgcc_s.so.1 => /usr/lib32/libgcc_s.so.1 (0xf6f2b000)
libc.so.6 => /lib32/libc.so.6 (0xf6de6000)
libfreetype.so.6 => /usr/lib32/libfreetype.so.6 (0xf6d67000)
libpangoft2-1.0.so.0 => /usr/lib32/libpangoft2-1.0.so.0 (0xf6d3e000)
libexpat.so.0 => not found
libpangocairo-1.0.so.0 => /usr/lib32/libpangocairo-1.0.so.0 (0xf6d31000)
libXcomposite.so.1 => /usr/lib32/libXcomposite.so.1 (0xf6d2c000)
libXdamage.so.1 => /usr/lib32/libXdamage.so.1 (0xf6d29000)
libXfixes.so.3 => /usr/lib32/libXfixes.so.3 (0xf6d23000)
libcairo.so.2 => /usr/lib32/libcairo.so.2 (0xf6c9c000)
libgio-2.0.so.0 => /usr/lib32/libgio-2.0.so.0 (0xf6c06000)
libfontconfig.so.1 => /usr/lib32/libfontconfig.so.1 (0xf6bd8000)
libXrender.so.1 => /usr/lib32/libXrender.so.1 (0xf6bce000)
libXinerama.so.1 => /usr/lib32/libXinerama.so.1 (0xf6bcb000)
libXrandr.so.2 => /usr/lib32/libXrandr.so.2 (0xf6bc2000)
libXcursor.so.1 => /usr/lib32/libXcursor.so.1 (0xf6bb7000)
libXft.so.2 => /usr/lib32/libXft.so.2 (0xf6ba2000)
libz.so.1 => /usr/lib32/libz.so.1 (0xf6b8c000)
libpcre.so.3 => /lib32/libpcre.so.3 (0xf6b5b000)
/lib/ld-linux.so.2 (0xf779f000)
librt.so.1 => /lib32/librt.so.1 (0xf6b52000)
libXau.so.6 => /usr/lib32/libXau.so.6 (0xf6b4e000)
libxcb.so.1 => /usr/lib32/libxcb.so.1 (0xf6b2f000)
libpixman-1.so.0 => /usr/lib32/libpixman-1.so.0 (0xf6ae8000)
libdirectfb-1.2.so.0 => /usr/lib32/libdirectfb-1.2.so.0 (0xf6a6e000)
libfusion-1.2.so.0 => /usr/lib32/libfusion-1.2.so.0 (0xf6a64000)
libdirect-1.2.so.0 => /usr/lib32/libdirect-1.2.so.0 (0xf6a4c000)
libpng12.so.0 => /usr/lib32/libpng12.so.0 (0xf6a23000)
libxcb-render-util.so.0 => /usr/lib32/libxcb-render-util.so.0 (0xf6a1e000)
libxcb-render.so.0 => /usr/lib32/libxcb-render.so.0 (0xf6a15000)
libresolv.so.2 => /lib32/libresolv.so.2 (0xf6a01000)
libselinux.so.1 => /lib32/libselinux.so.1 (0xf69e6000)
libexpat.so.1 => /lib32/libexpat.so.1 (0xf69be000)
libXdmcp.so.6 => /usr/lib32/libXdmcp.so.6 (0xf69b9000)

The “libexpat.so.0 => not found” line tells us that libexpat.so.0 is missing. What is odd is that the executable also links against libexpat.so.1.

I could try and find a 32 bit version of libexpat.so.0, but it might be worth a try to fake it.

> sudo ln -s /lib32/libexpat.so.1 /lib32/libexpat.so.0
> ./wink

Ha, it runs! Initial tests shows that it captures fine and can save to HTML, PDF and Flash.

Copy the entire directory structure to a its final resting place.

> rm installdata.tar.gz
> mkdir ~/wink
> cp -r * ~/wink
> rm -rf *

Now you can either run it from the wink directory or set up some desktop links to it.

2009
11.07

Wave Robot API shortcomings

While building a Robot that inserts a Gadget into the wave I discovered something that made me shake my head. A lot of the Wave Robot API classes have methods that violate their documented contracts. The worst offender I think has to be the Event interface. Whenever an event is delivered to your Robot, you interrogate the event to gather the information the robot requires. But there are many different events that can occur and each is trying to tell the robot a different thing.

For example, one of the methods on the Event interface is getAddedParticipants(). This method makes sense only on an event where the participants have changed such as BLIP_CONTRIBUTORS_CHANGED. However the method is available for all events. The javadoc for this method is

getAddedParticipants

java.util.Collection<java.lang.String> getAddedParticipants()
Returns a list of participants added to the Wavelet (if applicable).
Returns:
a list of participants.

You will of course note its contract specifies that it returns a list of participants. So you write your code so that it wil handle a Collection of participants. And if there are no added participants, it will return an empty collection. Except that it does not return an empty Collection when there are no added participants. It returns null. Um, that behaviour is not in the contract.

Another case that is really nasty is the case of Wavelet.getRootBlip(). There are many reasons why you may want to access the root blip. Maybe the robot created a widget there and wants access to it. Sometimes you might want to compare the id of the root blip to see if it is the same as the id of the modified blip in the event you just received. The javadoc says that it returns “the root blip”. Just don’t do the following while handling the BLIP_SUBMITTED event or you will be sorely surprised.

  Blip rootBlip = wavelet.getRootBlip();
  if ( rootBlip != null ){
    String rblipid = rootBlip.getBlipId();
    // compare root blip id against changed blip id
    ...
  }

Apparently when you receive an event, you do not receive the entire wavelet, but rather only information about the current blip for which the event occurred and that blip’s parent. If neither of those is the root blip then wavelet.getRootBlip() returns a non-null value, but this blip is not the actual root blip and contains no blip data in violation of its contract. This blip has been described as empty or hollow. So when you call getBlipId() on what you thought was the root blip, a null pointer exception occurs in getBlipId() because it attempts to dereference a null blip data object within the hollow root blip.

However, using Wavelet.getRootBlipId() gives you the ID of the root blip without the API experiencing a null pointer exception. Which is fine if you only want to compare blip IDs, but does not help you if you want to actually examine the contents of the root blip, since you don’t have access to it. The recommendation from Google is to save all needed blips in your AppEngine datastore and update them as they change and retrieve them as necessary.

Hopefully these annoying API contract violations (and bugs) will be fixed soon.