BarCampDC3

 

One of the cool things about BarCamp is that you tend to find out about startups and people before they are big. Though I usually prefer 2-day camps, I made an exception for this one as it gave me a reason to go to the Air & Space Museum for the first time since middle school. 
 
The day started with introductions and session proposals which was pretty cool so that no one felt like a total stranger. I proposed a postmortem on developing for Google Wave. Unfortunately some folks misunderstood my use of postmortem as a declaration that Google Wave was dead. I even had to correct a couple of folks who came up to me later and said "I agree that Wave is dead..." I guess it's hard to gauge that sort of thing when you live in GOOG's backyard. It never entered my mind that it could've been read that way. 
 
There were lots of smart people there, I just  wish I weren't so jet-lagged or I would have conversed more. The night before I jumped on a flight at 10:10P MV time and via Philly, I was in DC 8 hrs later. The transcon was only enough for a quick nap (4hrs) and I needed much coffee to make it through my session. It was a mix of general Google Wave/gadget vs. robot stuff and a bit about Ribbit. I was really glad to see a session on Groovy/Grails... in DC of all places, the epitome of button-down conservatism (well when it comes to software at least ;-) ) At about 3P, I hit my wall and had to crash. Unfortunately I wasn't back up in time to make it to the afterparty, maybe next time. 
 
My VirginMobile Broadband2Go card saved the day once again as the venue had flaky wifi. It was the basement of the MLK Library which had less coverage than the above ground floors. It was also the one spot in the city where all T-Mo phones had problems but AT&T worked like a dream.

 

RuPy 2009

Having caught back up on lost sleep, I'd say that RuPy is one of my favorite smaller conferences this year. It took place in Poznan, Poland on November 7-8th. As the name indicates, the first couple of incarnations of RuPy were focused on Ruby and Python. This year they expanded the field to dynamic languages in general. However, I was the ONLY non-Ruby/Python presenter there. I guess some folks got scared by the distance and or language. All sessions at RuPy were done in English. It did take a while to go to Poznan from California, but the warmth of the organizers made it worthwhile.
 
I presented Griffon: Swing just got fun again to a nearly full room and it went really well despite some hiccups in demos due to my having been sleep deprived. You see I had a dev event for work that I had attend so I flew from SFO on a British Airways red-eye to London at 7:50PM Thursday, then hopped on a LOT Polish Airlines flight to Warsaw and a prop plane to Poznan on Friday evening. I arrived in Poznan about 8 hrs before I was due to present. 
 
During the Q&A portion of my preso, I had to address the "Strachan incident." Sigh.
 
Charlie Nutter attended my session, we were in the same time slot at StrangeLoop, and was a good sport about the minor ribbing I gave him about Groovy's win at the ScriptBowl. His tweet about "implementing Griffon in 75 lines of code" caused a bit of a tempest in a teapot but it was a joke. So much for my saying that Groovy and JRuby's relationship has evolved and the two are comfortable with each other.
 
The organizers had to roll with the punches a little as some folks cancelled at the last minute, as in no-showed or got to Poznan but became incredibly ill. If you are ill or cancel with adequate notice, I understand, but we developers live and die by our word, integrity, and body of work. It doesn't matter that it is RuPy and not RubyConf. If you give your word, you should keep it. And we all should learn from Andy Azula that flying the morning of a speaking engagement is incredibly risky.
 
The Rubyists and Pythonistas(yes, they call themselves that) I met were really cool but I would have liked more Groovy people to have come out. There is a lot our communities can learn from each other and cooperate on. 
 
Video of the session will available in about 3 weeks or so I've been told.

MongoDB made more Groovy

The session after mine here at RuPy was MongoDB presented by Mike Dirlof(@mdirlof). I looked at the Java examples yet they left a bit to be desired as they had the usual Java verbosity problems. One of the cooler lesser known things about Groovy is that Groovy can coerce objects to a specific interface. We use this alot for one-off WindowListerners interfaces and such. That same concept can be applied to the classes as well. We can take code from the Java MongoDB tutorial to make it more Groovy.

import com.mongodb.*

def m = new Mongo()

def db = m.getDB("mydb")
def coll = db.getCollection("testCollection")

coll.drop()
 
def doc = [name:"MongoDB", type:"database", count:1,
            info: [x:203, y:102]
          ] as BasicDBObject
def doc2 = [name:"MongoDB2", type:"database", count:2,
    info: [x:203, y:102] ] as BasicDBObject

    
coll.insert(doc)
coll.insert(doc2)

println coll.getCount()

def obj = coll.findOne([count:1] as BasicDBObject)
println obj

println "showing a custom query"
def cur = coll.find([count:['$lt':3]] as BasicDBObject)
while(cur.hasNext()) {
    println cur.next()
}

Because MongoDB's BasicDBObject is a subclass of HashMap, we can produce concise code that looks closer to the Ruby, Python, and Javascript examples Matt has presented. We can also nest documents within documents, turtles all the way down. Only the othermost document needs to be cast, the rest get cast to documents automatically. It seems that MongoDB's special params begin with a dollar sign ($gt, $lt, etc) need to be passed as a String literal with apostrophes.

Griffon Guice Plugin

Having come back from StrangeLoop last week and talking to some our users, I've been thinking a lot potential blockers that might be preventing people from using Griffon in their work applications. One is those is probably dependency injection. Why I chose Guice - I detest XML. I like Guice's modules as a non-XML tag soup way to specify classes to be bound. - Guice's footprint is several magnitudes smaller than Spring. The smallest Guice application requires only 600KB of jar dependencies whereas a comparable Spring application would require several megabytes. Our users are already taking a hit for having to get the groovy-all.jar over the wire. I don't want to add to that pain.

Getting Started

Given an app with the Guice plugin installed(griffon install-plugin guice), let's start by creating a couple classes in our src/ directory. Below is a Notifier interface and an implementation:

Notifier.groovy

public interface Notifier {
	public void sendMessage(String message);
}

Mail.groovy

public class Mail implements Notifier {
	public void sendMessage(String message) {
		println "Sending ${message} by Mail"
	}
}

As mentioned before, Guice uses Modules instead of XML to specify binding. We can bind our Notifier to the implementation Mail very easily:

GuiceAppModule.groovy

import com.google.inject.*

public class GuiceAppModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(Notifier.class).to(Mail.class);
    }
}

Wiring in the Griffon bits

Our plugin does most of the heavy lifting resolving the names of our modules and injecting a Guice injector into the required classes. We just need to give it a few configuration details in griffon-app/conf/Application.groovy. We need to tell Guice where to inject and what modules to inject:

guice {
	injectInto = ["controller"]
	modules = ["GuiceAppModule"]
}

The last piece is injecting and using our member fields from our controller. In the controller or wherever you deem appropriate, the following will inject the members:

GuiceDemoController.groovy

import com.google.inject.*

class GuiceDemoController {
    // these will be injected by Griffon
    def model
    def view
	@Inject Notifier notifier

    void mvcGroupInit(Map args) {
        // this method is called after model and view are injected
		injector.injectMembers(this)
		notifier.sendMessage("test");
    }
}

When run, the app will print "Sending test by Mail" to the console. The maintainers of Guice caution somewhat against member injection as being less testable. A more traditional constructor-based injection would force Guice to reach below the plugin layer. It's still early days, it might find its way down there anyways.

Application Window Fadeout with Griffon and Trident

Ubuntu Karmic Koala and Windows 7 were released recently and though both have wizzier effects on the operating system level, you might want to add it to your applications. With Java 6 or higher and a decent graphics card you can do just that. The original inspiration for this post was my friend Josh's Griffon Tip on Intercepting Window Closing events.

Instead of sticking to his more practical application of the concept, I decided to add a little more bling and use Trident to implement a window slowly fading out on application shutdown instead of an immediate destruction of the window. In our controller we set up a timeline to update the opacity on timeline pulse. It will execute app.shutdown() when the timeline has completed:

import com.sun.awt.AWTUtilities
import org.pushingpixels.trident.Timeline
import org.pushingpixels.trident.callback.TimelineCallback

class TestfadeController {
    // these will be injected by Griffon
    def model
    def view

    void mvcGroupInit(Map args) {
        // this method is called after model and view are injected
		model.timeline = new Timeline(this)
		model.timeline.setDuration(3000)
		def cb = [
			onTimelineStateChanged:{oldState,newState,durationFraction,timelinePosition ->					
					if (timelinePosition == 1.0f)
						app.shutdown()
				},
			onTimelinePulse: {durationFraction,timelinePosition -> 
					AWTUtilities.setWindowOpacity(app.appFrames[0], (float)(1-durationFraction))
				}
		] as TimelineCallback
		model.timeline.addCallback(cb)
    }

    /*
    def action = { evt = null ->
    }
    */
}

To handle the machines that might not be high-powered enough to handle translucency, my System76 Starling netbook being one of them, we need to adjust our windowClosing closure to check for translucency support and initiate a regular shutdown if it is not.

import javax.swing.WindowConstants
import org.pushingpixels.trident.Timeline
import com.sun.awt.AWTUtilities

application(title:'testfade',
  size:[320,480],
  defaultCloseOperation:WindowConstants.DO_NOTHING_ON_CLOSE,
	windowClosing: {evt ->
		if (AWTUtilities.isTranslucencySupported(AWTUtilities.Translucency.TRANSLUCENT)) {
      		model.timeline.play()
		} else app.shutdown()
		
	}
) {
    // add content here
    label('Content Goes Here') // deleteme
}

Like Josh said in his post, we have to make sure the autoShutdown property in griffon-app/conf/Application.groovy is set to false. Though I didn't use TridentBuilder specifically, I installed the plugin to get the basic Trident assets.

Preview of Ubuntu Quickly

I wanted to write a review of Canonical's desktop development tool, Quickly, but due to several issues I can only do somewhat of a preview.
 
To start developing with Quickly, you need a pretty healthy Internet connection. On a clean install of Karmic 9.10 Beta running in VirtualBox, sudo apt-get install quickly pulled down approximately 214MB of packages.
 
Quickly uses commands such as "quickly new <name of project>" or quickly dialog <name> to execute the fundamentals of application building. As a Griffonite, it feels very similar to paradigms I'm used to. 
 
Though it failed partway through, the package command seemed to create a Debian package for the created app. Running apt-get upgrade didn't totally solve the problem, it only made the process fail later.
 
Some other highlights:
  • Python under the covers
  • Has built-in support for Launchpad
  • Glade for user interface layout
  • Couchdb for persistence
 
Conclusion:
I like the concept but it didn't feel fully baked. Integration with CouchDB for object persistence was a nice touch. All too often, persistence is a second thought that has to strapped on adhoc after the fact. Glade, a design tool for GNOME UIs, was a good way to dogfood what was already on the Ubuntu/GNOME platform and reduce the barrier to entry. Though the stated goal is to make Ubuntu apps quickly and easily, Python is not a Ubuntu only language. I would have liked some option to send an app to a friend on OS X or Windows and that doesn't seem possible yet. Griffon, on the other hand, is mostly platform agnostic.
 
Launchpad is great if you are an Ubuntu dev but a lot of folks have sweat equity invested in Github or a personal SVN repo. I feel that forcing them on Launchpad/bzr might not be the best option. FYI I love bzr as a VCS. Python isn't my cup of tea personally but it makes sense in their ecosystem. 
 
I applaud Canonical for this first effort which I must state again is probably not the final version. I know from my experience with Griffon how hard it is to put out a well-designed rock solid project.

StrangeLoop Recap

No conference is without faults the first time it is run but I believe StrangeLoop was a net success.

 

The Venue

The Tivoli theatre is a gem of the old theatre era, reminding me a bit of the old Art Deco style. It was warm and inviting with comfy seats unlike the usual hotel ballroom with rows and rows of astere office chairs. It was a nice touch to have the concession stand serving free soft drinks to attendees with candy and treats for sale. As it's not designed for wifi, it was a bit of a challenge to find a open slot to connect to the routers. The problem was further exacerbated by those who got in holding on to a connection far longer than they needed for fear that they wouldn't be able to get it back. Wifi is expensive and hard to provide in decent quantitites for "greedy" consumers like we techies. I'd rather have wifi not be as good if the alternative is having to cut back on other perks of the conference.

 

Strange Passions/Blueberry Hill

At the end of the first night Alex(@puredanger) rented out a room in Blueberry Hill (a local bar) for 4 hours. FOUR HOURS where there was an OPEN BAR. It was a welcome change from the bigger confs that frankly half-ass it by having a "party" where you can only get 2 drinks. The inflated cost of that third drink usually drives developers elsewhere to not feel gouged. There was lots of time to mingle, listen to a couple StrangePassion talks, the Marshmallow Tests and the Greeks of Options Trading were my favorites, and generally be merry.

 

Sessions

Though slightly traumatized by seeing @crazybob (Bob Lee) in a speedo (picture in his slides), I really enjoyed both his Future of Java keynote on Thursday and his Ghost References talk on Friday. I think Matt Dirlolf made a good case for why you should use MongoDB, the problems it solves and how it differs from CouchDB and traditional RDBMS. Alex Buckley's Java Modularity left a bit to be desired. It seems spurious to claim "we are getting rid of the classpath" when you introduce modulepaths as a new concept. They seem to essentially be a classpath with versioning information. I also think that if you are the spec lead responsible for the Java language and JVM, you can't consider "it wasn't my decision" as an appropriate answer for why the reference implementation of Project Jigsaw(which isn't ruled by a JSR and generally w/o a spec) and JSR 294 are co-mingling in the same project. The reference implementations need to be split or Jigsaw needs to come under the JSR process. 

I gave a tweaked version of "Griffon: Swing just got fun again" to an almost full audience of about 50-60 people in one of the side theatres. 

 

Tips

Register early.

You'll get the best prices (early early early registration was $75 this year). StrangeLoop 2009 sold out. As the positive reviews start trickling out the next week or so, I can say with good certainty that it will sell out next year as well, probably earlier.

Bring a raincoat or umbrella.

StrangeLoop was book-ended by sunny clear weather but there was a serious downpour during the conf. I'm talking a "puddle lane, soaked to the bone, like a Florida tropical depression" downpour.

Consider buying a USB modem card. 

Before the conf, I bought a Virgin Mobile Broadband 2 Go USB modem(available at Best Buy). You take a bit of a hit for the initial cost ($150 for the modem) and data plans(see chart below) but it's great for filling the gaps in conference wifi and doesn't require a contract. If you go to a couple conferences a year, the savings in wireless costs will quickly pay for the card...provided you don't try to watch a bunch of Hulu or Netflix. You'll save what you would have been paying for hotel or airport wifi. STL airport doesn't have free wifi :(

Cost Data Limit Expiration of Data
$10 100MB 10 days
$20 250MB 30 days
$40 600MB 30 days
$60 1GB 30 days

Griffon Tip Maximizing application windows

Often you'll want to have an application run true full-screen and hide the toolbars. In this Griffon tip, we'll wire an application to toggle between full screen and a preferred window size with the click of a button.

Under normal circumstances, you might want to put your actions in a separate file but for simplicity's sake, I've included it in the view.

TestFullScreenView.groovy

 
application(title:'TestFullScreen',
  //size:[320,480],
  pack:true,
  //location:[50,50],
  locationByPlatform:true,
  iconImage: imageIcon('/griffon-icon-48x48.png').image,
  iconImages: [imageIcon('/griffon-icon-48x48.png').image,
               imageIcon('/griffon-icon-32x32.png').image,
               imageIcon('/griffon-icon-16x16.png').image]
) {
   actions { 
       action(id: "maximize", 
          name: "Maximize/Unmaximize", 
          keyStroke: shortcut("M"), 
          shortDescription: "Maximize/Unmaximize", 
          closure: controller.maxUnmax) 
     
   } 
    button(maximize)
}

In our controller, we wire up the maxUnmax closure. Not that we have to hide and dispose the frame before toggling between fullscreen and not. The following code was inspired by snippets I saw on http://gpsnippets.blogspot.com/2007/08/toggle-fullscreen-mode.html. Because Dimensions don't seem to implement Comparable or Comparator, we have to compare its sub-elements. I was lazy and just did the comparison based on width. If your application has more than one appFrame, you would probably want to reference it by traversing the MVC Groups.

TestFullScreenController.groovy

 
import java.awt.*

class TestFullScreenController {
    // these will be injected by Griffon
    def model
    def view

    void mvcGroupInit(Map args) {
        // this method is called after model and view are injected
    }

    def maxUnmax = {
        def ge = GraphicsEnvironment.getLocalGraphicsEnvironment()
        def gs = ge.getScreenDevices()
        
        def prefSize = [160,54] as Dimension
        
        def maxSize = Toolkit.getDefaultToolkit().getScreenSize()
        def currentSize = app.appFrames[0].getSize()
        
        app.appFrames[0].hide()
        app.appFrames[0].dispose()
        if (currentSize.width < maxSize.width) {
            gs[0].setFullScreenWindow(app.appFrames[0]);
            app.appFrames[0].undecorated = true
            app.appFrames[0].size = maxSize
        } else {
            gs[0].setFullScreenWindow(null);
            app.appFrames[0].undecorated = false
            app.appFrames[0].size = prefSize
        }
        app.appFrames[0].show()
    }
}

Griffon and OAuth

Though giving your username/password deets to a locally running Java app can be infinitely more secure than giving it to an arbitrary website, it does involve a level of trust on the user's part. OAuth gives the user the peace of mind that you aren't doing anything hinky with their account details. In this post, we'll code a simple Griffon app with OAuth to use Twitter. First you'll need to login to Twitter. Go to http://twitter.com/apps and click Register a new application. Enter some details for your app and make sure to select Client as the Application Type. Don't worry about the Callback URL or the Default Access Type. Hit save and take note of the consumer and secret keys. Copy them into your GriffonOAuthModel.groovy file.

GriffonOAuthModel.groovy

 
import groovy.beans.Bindable

class GriffonOAuthModel {
   @Bindable twitter
   def consumerKey 
   def secretKey 
   @Bindable requestToken
   @Bindable accessToken
   
}

Twitter's client OAuth process gives you a pin code to enter so we'll need a space to enter than in our user interface which will consist of two buttons and a text field.

GriffonOAuthView.groovy

 
application(title:'GriffonOAuth',
  //size:[320,480],
  pack:true,
  //location:[50,50],
  locationByPlatform:true,
  iconImage: imageIcon('/griffon-icon-48x48.png').image,
  iconImages: [imageIcon('/griffon-icon-48x48.png').image,
               imageIcon('/griffon-icon-32x32.png').image,
               imageIcon('/griffon-icon-16x16.png').image]
) {
	vbox {
		button(id:"login", "Login with Twitter", 
actionPerformed: {controller.login() })
		textField(id:"pinCode", columns:10)
		button(id:"done", enabled:false, "Press here when done", 
			actionPerformed: {controller.showLastTweet()})
	}
}

Our controller handles the heavy lifting of getting a request token, launching a browser and checking the validity of the pin code. If a valid access token exists, the app retrieves the last received direct message from the authenticated account.

GriffonOAuthController.groovy

 
import twitter4j.*
import twitter4j.http.*
import javax.swing.JOptionPane
class GriffonOAuthController {
    // these will be injected by Griffon
    def model
    def view

    void mvcGroupInit(Map args) {
        // this method is called after model and view are injected
    }

    def login = {
		model.twitter = new Twitter()
		model.twitter.setOAuthConsumer(model.consumerKey, model.secretKey)
		model.requestToken = model.twitter.getOAuthRequestToken()
		def url = model.requestToken.getAuthorizationURL()
		BareBonesBrowserLaunch.openURL(url)
		view.done.enabled = true
	}
	
	def showLastTweet = {
	    try {
	        if (!model.accessToken) {
		    model.accessToken = model.twitter.getOAuthAccessToken(model.requestToken, view.pinCode.text)
				
		}
		def msg = model.twitter.getDirectMessages(new Paging(1,1))[0]
		def sender = msg.getSender().getName()
		def text = msg.getText()
		def recipient = msg.getRecipient().getName()
		def output = "From ${sender} to ${recipient} \n : ${text}"
		edt {
					            JOptionPane.showMessageDialog(null, output)
		}
	} catch (TwitterException te) {
		if (te.getStatusCode() == 401) {
		    edt {JOptionPane.showMessageDialog(null, "Unable to get access token.")}
		} else {
		    te.printStackTrace()
		}				
	} 
    }
}

The last little piece is a helper class from http://www.centerkey.com/java/browser/ to launch a browser from Java. I tweaked it a bit to add listings for chrome and google-chrome (the executable filenames of Chrome on Windows and Linux respectively) so that it didn't default to a Firefox browser.

Learn more about OAuth here.

Download the source code here.

Griffon on Google Wave

You can log this post under something that while fun to do just to say you can, it's not very practical. For starters, startup time for most applets isn't exactly what you'd call speedy. Also, because of Griffon's use of Groovy, it adds another 5MB of jars for the initial download. Every extra second of waiting gives a potential user a reason to leave before the download is complete. So remember that you've been warned. The smallest test you can do to demonstrate Wave is a simple count incrementer that updates all clients when the count is incremented. Our view and model are relatively simple:

TestWaveView.groovy

 
application(title:'TestWave',
  //size:[320,480],
  pack:true,
  //location:[50,50],
  locationByPlatform:true,
  iconImage: imageIcon('/griffon-icon-48x48.png').image,
  iconImages: [imageIcon('/griffon-icon-48x48.png').image,
               imageIcon('/griffon-icon-32x32.png').image,
               imageIcon('/griffon-icon-16x16.png').image]
) {
    vbox {
    	label(text:bind{model.number})
		button(text:"Increment", actionPerformed:{controller.increment()})
	}
}

TestWaveModel.groovy

 
import groovy.beans.Bindable

class TestWaveModel {
   @Bindable number
   def timer
}

Our controller is where the interesting stuff happens. We are using Javascript proxies to evaluate a javascript function in out gadget xml to increment the count. That function increments the count on the gadget state. Separately, our Griffon applet polls a javascript getValue function which retrieves the state of the variable. The optimal solution would have been to register a function with the stateChanged callback to invoke stuff on the Java side. The problem is that to do so, the javascript has to reach into the applet and invoke a function on the controller. It can be done, it's just a little easier to poll from the controller. The value is pulled every second.

TestWaveController.groovy

 
import java.applet.*
import java.util.*
import java.util.timer.*
import netscape.javascript.*

class TestWaveController {
    // these will be injected by Griffon
    def model
    def view

    void mvcGroupInit(Map args) {
        // this method is called after model and view are injected
	getValueFromWave()
		
	model.timer = new Timer()
	def task = new GroovyTimerTask()
	task.closure = {getValueFromWave()}
	model.timer.schedule(task, 2000,1000)
    }

    def increment = {
	try {
		def win = (JSObject)JSObject.getWindow((Applet)app)
			win.eval("javascript:increment()")
	} catch(Exception ex) {
		ex.printStackTrace()
	}
}

def getValueFromWave = {
	try {
		def win = (JSObject)JSObject.getWindow((Applet)app)
		model.number = win.eval("javascript:getValue()")
	} catch (Exception ex) { }
	}
}

Lastly, our gadget/applet franken-code. As referenced before, the increment function grabs a number variable from the gadget state and sets it to 0 if it doesn't exist. It increments it and puts it back of the wave state. getValue simply returns the value of num.

<?xml version="1.0" encoding="UTF-8" ?>
<Module>http://dl.getdropbox.com/u/738191/staging/applet.html
<ModulePrefs title="Hello Wave">
<Require feature="wave" />
</ModulePrefs>
<Content type="html">
<![CDATA[
<script>

function increment() {
var num = wave.getState().get("num", 0);
num++;
wave.getState().submitDelta({"num":num});
}
function getValue() {
return wave.getState().get("num", 0);
}
</script>
<script src="http://java.com/js/deployJava.js"></script>
<script>
var attributes = {id: 'TestWave',
codebase:'<codebase>',
code:'griffon.applet.GriffonApplet',
archive:'griffon-rt-0.2-BETA.jar,TestWave.jar,plugin.jar,groovy-all-1.6.4.jar',
width:'480', height:'320'} ;
var parameters = {fontSize:16,
java_arguments: "-Djnlp.packEnabled=true",
jnlp_href:'<codebase>/applet.jnlp',
draggable:'true',
image:'griffon.png',
boxmessage:'Loading TestWave',
boxbgcolor:'#FFFFFF', boxfgcolor:'#000000',
codebase_lookup: 'false'} ;
var version = '1.5.0' ;
deployJava.runApplet(attributes, parameters, version);
</script>
<!-- <APPLET CODEBASE='<codebase>'
CODE='griffon.applet.GriffonApplet'
ARCHIVE='griffon-rt-0.2-BETA.jar,TestWave.jar,plugin.jar,groovy-all-1.6.4.jar'
WIDTH='240' HEIGHT='320'>
<PARAM NAME="java_arguments" VALUE="-Djnlp.packEnabled=true">
<PARAM NAME='jnlp_href' VALUE='<codebase>/applet.jnlp'>
<PARAM NAME='dragggable' VALUE='true'>
<PARAM NAME='image' VALUE='griffon.png'>
<PARAM NAME='boxmessage' VALUE='Loading TestWave'>
<PARAM NAME='boxbgcolor' VALUE='#FFFFFF'>
<PARAM NAME='boxfgcolor' VALUE='#000000'>
<PARAM NAME='codebase_lookup' VALUE='false'>
</APPLET-->


]]>
</Content>
</Module>

If you're one of the lucky people with Wave accounts, wave to me at j@wavesandbox.com or James.L.Williams@googlewave.com and I'll give you a demo.