MozSwing with Griffon

MozSwing is kinda has been in the back of my mind for a while now. We (the Griffon Team) even evaluated it for one of our early builds. But we thought, "JWebPane is going to be out soon and probably will be more lightweight than MozSwing(50MB)." Needless to say, JWebPane is no where to be found so I thought it would a good idea to reevaluate MozSwing.

Before you object and say your project can't accommodate an increase in size of 50MB, it won't be that much. Best case scenario, it would be 2MB, worse case is 24MB. The reason for the span in sizes is that MozSwing bundles xulrunner for all major platforms(Win32, Linux, OSX, and Solaris). If XULRunner is already on the machine, you only need 2MB of jars and it'll generally pick up whatever plugins you have installed for Firefox.

The demo app runs at this point but not in a Griffon context. It can't seem to find xulrunner without being told the path. I just set up an environment variable for it under XULRUNNER_HOME and overrode that line in MozillaConfig.

For those that are getting tense, you don't have to build MozSwing. Just open up the mozswing jar in your favorite archive manager, navigate down to the org/mozilla/browser and delete everything that
starts with MozillaConfig. Grab MozillaConfig.java, drop in the src dir of your Griffon app and add/modify this line

private static File xulRunnerHome = new File(System.getenv().get("XULRUNNER_HOME"));


Now you can call new MozillaPanel() to create a browser panel and load("address") to load an address in that panel. And here is the photographic proof that it works:

Because of Linux's modular nature, you have to explicitly copy plugins from your Firefox installation to the XULRunner directory whereas Windows seems to pick up those plugins automatically. Plugin sitch is unknown on OS X or Solaris.

Mozswing http://sourceforge.net/projects/mozswing

Update: It is noteworthy to mention that JDIC has had a JDICplus project for a while integrating IE in Swing but because IE is dying and is mostly non-compliant to standards, I don't see it as a viable option.

 

 

Why JavaFX installs do not matter

Update: While doing research for this post, I found this link dated Jan 29th from Tech Crunch, Adobe AIR Is Flying: 100 Million Installations Accounted For .

 

Griffon Groks JOGL

With the improvements from the last post, adding JOGL support to Griffon is now incredibly easy. Assuming you have a fairly recent revision from SVN, add the following closure to Config.groovy in the griffon closure:

extensions {
		jarUrls = ["http://download.java.net/media/applet-launcher/applet-launcher.jar", "http://download.java.net/media/jogl/builds/archive/jsr-231-webstart-current/jogl.jar"]
		jnlpUrls = ["http://download.java.net/media/jogl/builds/archive/jsr-231-webstart-current/jogl.jnlp"]
}

There is a little setup you'll need to do to initialize the canvas:

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

    void mvcGroupInit(Map args) {
	model.canvas = new GLCanvas(createCaps())
	model.animator = new Animator(model.canvas)
	model.canvas.addGLEventListener(new Lesson03())

	model.animator = new Animator(model.canvas)
        model.animator.start()
    }
    
    def createCaps = {
	def glCaps = new GLCapabilities()
	glCaps.with {
	   setRedBits(8)
	   setBlueBits(8)
	   setGreenBits(8)
	   setAlphaBits(8)
	}
	return glCaps
    }
}

Next, we add our canvas and helper variables to our model and view:

class JoglDemoModel {
   @Bindable canvas
   @Bindable animator
   @Bindable gl
}

JoglDemoView.groovy

application(title:'jogldemo',  size:[512,384]) {
    widget(model.canvas)
}

On our canvas, we use a slightly tweaked Java file from the Nehe examples.

Nehe Lesson 3

Lastly, we need jogl.jar. This will enable run-applet and run-webstart. To get run-app working, you would have to install your specific operating systems native OpenGL libraries(.dll's on Win, .so's on Linux, .jnilib's on OSX). Download jogl.jar and/or OS-specific files.

I used Lesson03 from Nehe mostly unmodified.

Download the project.

Griffon Gets Remote JNLP Extenstions

One of the previously unimplemented features of Griffon was real support for remote JARs and JNLPs. Sure you could add them after the fact but it didn't really matter because calling run-webstart or run-applet would regenerated all the files anyway, destroying any changes you had made.

Griffon now has basic support for remote JARs and JNLPs. These are essential for avoiding the need to package libraries that have OS-specific implementations such as JOGL.

To add a remote JAR or JNLP, add the following closure under the griffon closure in Config.groovy:

extensions {
    jarUrls =  [ (jar urls here)]
    jnlpUrls = [ (jnlp urls here)]
} 

Griffon takes this info and builds the appropriate applet embed code(both new school and old school) and webstart files with jnlp extensions. This has not been pushed into a release yet so if you want to try it out, you'll need to build from SVN.

Problematic Twitter API Cap

Though unfortunate, if tuned and re-architected a bit,  I don't believe the 20,000 calls per hour cap would shutter many Twitter dependent services. That is, if they don't try to continue to operate as they have. I believe the future might be a return to the concept of timeslices from the dark ages of computer science. More apps should run on the desktop. Forget supported by ads, it should additionally be supported by processed work units. For the average user, I think even netbooks have enough headroom to not be affected. I admit that each situation is unique and this isn't a panacea and is more of a braindump.

One of the benefits of the a desktop app and Java applets in particular, though the latter has be vilified by the faux pas of amateur Swing developers, is the fact that that they usually run in a sandboxed VM on the user's machine. This is a feature I took advantage of when creating FriendBackup. Because it ran on the user's machine so I never had access to their data and because they were communicating directly with the Twitter API, I didn't incur a cap. I know that the browser is the lowest common denominator but who says the meat of the application has to run only your servers. I'm not saying you should use Joe Schmo's machine to parse the data from @Scobleizer but you could easily wire up your own pseudo-SETI@HOME with some Hadoop/MapReduce magic to have the users process a couple packets of information and send it back. It's not optimal but it should work.

Though I've not tried them (but have signed up for their beta), Plura Processing allows developers to earmark CPU time to process work units. They currently support Java applications and websites. It would be a pain to engineer but it would provide some headroom to work with the caps. As long as it's unobstructive, clearly stated in your ToS, I don't think users would mind a couple packets here or there.

Why does everything have to run in the browser? We can use AIR, Griffon, Silverlight, a draggable Java6 applet, whatever. It is starting to box us in. The decision of Google devs to make the Native Client is becoming more clear.
 

Picasa Download with Griffon

I wrote this application partially as a port of an existing application and partially to solve a problem. You see, I recently moved to California from Florida and drove my car with my dad. Along the way we stopped at the Alamo (which is much smaller in real life), Fort Davis and a couple other places. Having forgotten his camera, he used mine but didn't have a thumb drive to download the photos. Email was out as I didn't feel like trying to attach individual 1MB photos to emails or sending a 50MB tar of the files. It's a great chance to port another AIR app to Griffon(well technically flump is based on Flickr). So after a couple months of nagging, I finally got to work, here's the product of three hours of messing around.

Edit: Unless otherwise noted, demos assume you are using the most recent Griffon release available at the time the entry is published, in this case 0.1-SNAPSHOT. I apologize for any inconvenience or general head-banging due to this omission.

Webstart version of the application located here.

The layout is pretty simple. I could've done something a little more snazzy but a quick tabular layout with MigLayout served my needs.


import net.miginfocom.swing.MigLayout
application(title:'PicasaDownload', size:[480,300], locationByPlatform:true) {
    panel (layout:new MigLayout()) {
        label(text:"Enter Picasa Account Info:")
        textField(id:'userId', columns:25, constraints:'newline')
        button(id:'retrieve', text:'Retrieve albums', constraints:'wrap', actionPerformed:{controller.getAlbums()})
        label(text:'Select albums:', constraints:'wrap')
        comboBox(id:'albumsCombo')
        checkBox(id:'allAlbums', text:'All')
        label(text:'Location:', constraints:'newline, wrap')
        textField(id:'saveLocation', columns:25, text:bind{model.location}, enabled:false)
        button(text:'Browse', actionPerformed:{controller.fileChooser()})
        button(id:'download', text:'Download', constraints:'cell 1 6', actionPerformed:{controller.download()})
        
    }
}

The getFeed method sends a HTTP GET request, follows any redirects, waits for the XML response and parses the response. Because there can be some latency and they didn't note if it was being down already, I wrapped it in a Thread.start. The getAlbums closure retrieves the user feed and puts any public albums in a combo box. The downloadFile closure is nothing really new, just a generic file download from a URL.


def getAlbums = { evt ->
    Thread.start {
        def feedUrl = new URL("http://picasaweb.google.com/data/feed/api/user/"
+view.userId.text+"?kind=album");
        model.userFeed = model.picasaService.getFeed(feedUrl, UserFeed.class)
        view.albumsCombo.removeAllItems()
        for (album in model.userFeed.getAlbumEntries()) {
            view.albumsCombo.addItem(album.getTitle().getPlainText())
        }
    }
}
 
def downloadFile = {url, title, fileName ->
    def is = new URL(url).openStream()
    def out = new BufferedOutputStream(new FileOutputStream(model.location+File.separator+title+File.separator+fileName))
    byte[] buff = new byte[1024];
    int bytesRead = 0;
 
    while((bytesRead = is.read(buff)) != -1) {
        out.write(buff, 0, bytesRead);
    }
 
    out.close();
    is.close();
}

You can download the source code here.

JSR 223 Scripting API with Griffon

So you would really like to benefit from the features of Griffon but don't exactly know Groovy? One of the previously unexplored areas is JSR 223: Scripting API. In this tutorial, we will use JSR 223 to embed scripts into our application and run non-Groovy code in response to application events.

Getting Started

If you don't have Griffon installed already, go and get it. If you are using Griffon with Java 6, you already have JSR 223 and support for Rhino(which we'll be using for our examples). Alternatively, the concepts apply to any JSR 223 language such as JRuby or Jython.

Scripting API Hello World

For our first foray into Griffon powered by JSR 223, we'll keep it simple and code a basic UI with a label, textfield, and button. Our view looks like this:

application(title:'JSR223Hello', size:[100,200], pack:true, locationByPlatform:true) {
    hbox {
        label('Name')
        textField(columns:25, text:bind(target:model, targetProperty:'name'))
        button(text:'Click', actionPerformed:{controller.hello()})
    }
}
		

We've also bind to a model property name and invoke a actionPerformed closure on button click. Before we get around to defining the actual hello closure, we have to do a little setup work to initialize the ScriptingEngineManager. I executed the following code in Startup.groovy but it could have been executed earlier if desired:

import javax.script.ScriptEngineManager

Thread.start {
    def model = app.models.JSR223Hello
    model.manager = new ScriptEngineManager()
    model.engine = model.manager.getEngineByName("js")
}
		

In the above snippet, we retrieve an instance of the model and instantiate the ScriptEngineManager and Javascript engine. Because we retain a instance of the manager, we could theorhetically use several engines concurrently. Now on to the part that actually does something. Given that I'm a relative novice in Javascript and this is a simple demo, our code just retrieves the name typed in the textfield and constructs a Hello, < name > dialog box. Here's the controller with the uninteresting bits cut out:

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

...

    def hello = {
	    def js = "importPackage(javax.swing);"+
		    "JOptionPane.showMessageDialog(null, 'Hello, ${model.name}');"
		
        model.engine.eval(js)
    }
}
		

If you are planning to use several functions, it would probably be best to put them in the resources directory as either a big file or mini-scripts and then evaluate and invoke them on demand.

Please note that Groovy remains the official language of Griffon until otherwise noted. No expressed efforts are planned at this time to improve integration with other languages other than is already provided by JSR 223.

Griffon Weather App

James Clarke recently updated his JSON Weather Service application to fête the release of JavaFX 1.0. I happened upon it on DZone and decided to port it to Griffon. The original source is 227 lines of code whereas the Griffon version is 151.

Here's the code breakdown:

Name Files LOC
Controllers 1 38
Models 1 13
Views 1 28
Lifecycle 5 68
Integration Tests 1 4
Total 9 151



The first major difference is that instead of an instance class Weather, all the variables live in the GriffonWeatherServiceModel.groovy.

import groovy.beans.Bindable

class GriffonWeatherServiceModel {
   @Bindable stationName = ''
   @Bindable clouds = ''
   @Bindable windDirection = 0
   @Bindable windSpeed = 0
   @Bindable temperature = 0
   @Bindable dewPoint = 0
   @Bindable humidity = 0
   @Bindable seaLevelPressure = 0
   @Bindable observation = ''
   @Bindable code = ''
}


One place where I was able to condense code was in GriffonWeatherServiceController.groovy.

import net.sf.json.JSONArray
import javax.swing.JOptionPane
import net.sf.json.groovy.JsonSlurper

class GriffonWeatherServiceController {
    // 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 parseJSON = { evt = null, text ->
        JsonSlurper slurper = new JsonSlurper()
        def json = slurper.parseText(text)?.get('weatherObservation')
        if (json != null) {
        for (prop in model.getProperties()) {
            if (json.get(prop.key) != null) {
                model.{prop.key} = json.getString(prop.key)
            }
        }
        } else {
            JOptionPane.showMessageDialog(null, "Either the code is invalid or there was a problem.", "error", JOptionPane.ERROR_MESSAGE)
        }
    }
    
    def getJSON = { url ->
        doLater {
            app.appFrames[0].getGlassPane().setSize(app.appFrames[0].getSize())
            app.appFrames[0].getGlassPane().setVisible(true)
            app.appFrames[0].getGlassPane().setBusy(true)
        }
        doOutside{
            def result = new java.net.URL(url).getText()
            parseJSON(null, result)
            app.appFrames[0].pack()
            app.appFrames[0].getGlassPane().setBusy(false)
            app.appFrames[0].getGlassPane().setVisible(false)
        }
    }
}


Instead of walking the tree for each attribute, I inspect the properties of the model and see if they exist in the JSONObject. If so, they are assigned. If an incorrect code is entered (or there is no Internet), an error dialog appears. Retrieving and parsing the JSON is done outside the EDT.

Another area of major savings was in the view(GriffonWeatherServiceView.groovy). I was able to declare the functionality of a label/value pair and repeatedly invoke the defined closures.

import net.miginfocom.swing.MigLayout
application(title:'Griffon Weather', pack:true, size:[700,400], pack:true, locationByPlatform:true, layout:new MigLayout()) {
    label("Airport Code:")
    textField(id:'tf',columns:5, constraints:'spanx',
        actionPerformed:{controller.getJSON('http://ws.geonames.org/weatherIcaoJSON?ICAO=K'+tf.text?.toUpperCase())
        })
    def createHorizontalBox = { fieldName, boundParam ->
        label(fieldName, constraints:"newline")
        label(id:fieldName.replace(' ','_')+'Label', foreground: java.awt.Color.BLUE,
               text:bind(source:model, sourceProperty:boundParam), constraints:'wrap'
        )
    
    }
    def createHorizontalBoxWithSuffix = { fieldName, boundParam, suffix ->
        label(fieldName, constraints:'newline')
        label(id:fieldName.replace(' ','_')+'Label', foreground: java.awt.Color.BLUE,
            text:bind(source:model, sourceProperty:boundParam, converter:{it + suffix}), constraints:"wrap, spanx"
        )
    }
        
    createHorizontalBox('Station: ', "stationName")
    createHorizontalBox('Clouds: ', "clouds")
    createHorizontalBoxWithSuffix('Wind Direction: ', "windDirection", " degrees")
    createHorizontalBoxWithSuffix('Wind Speed: ', "windSpeed", " knots")
    createHorizontalBoxWithSuffix('Temperature: ', "temperature", " C degrees")
    createHorizontalBoxWithSuffix('Dew Point: ', "dewPoint", " C degrees")
    createHorizontalBoxWithSuffix("Humidity: ", "humidity", "%")
    createHorizontalBoxWithSuffix("Sea Level Pressure: ", "seaLevelPressure", "mb")
   // createHorizontalBox("METAR Observation: ", "observation")
        
}

On startup, I declared a JXBusyLabel to live on our GlassPane and indicate when the system is busy. From Startup.groovy:

import java.awt.Dimension
import java.awt.geom.*
import org.jdesktop.swingx.JXBusyLabel
import org.jdesktop.swingx.painter.BusyPainter

def busyLabel = new JXBusyLabel(app.appFrames[0].getSize())
busyLabel.setOpaque(false)
busyLabel.setBusy(false)
busyLabel.getBusyPainter().setHighlightColor(java.awt.Color.DARK_GRAY)
busyLabel.getBusyPainter().setPointShape(new Ellipse2D.Float(1,1,10,10))

app.appFrames[0].setGlassPane(busyLabel)

 

 

Griffon BOF at Devoxx 2008

Generally, I prefer giving BOFs to regular sessions because there are more informal, intimate, and people feel less intimidated to stop you and ask questions. The downside is that most conferences seem to schedule them after 7 PM when most people are first getting a chance to relax so attendance can suffer a bit. Maybe a good place for them would be partway into lunch hour. Anyways...

Being the last BOF of the day but possibly bolstered by the carry-over from the Grails BOF before it, the room was pretty full with about 30-40 people. There was lots of interest and questions about bind/@Bindable and how Griffon stacks up to JavaFX. Several noted how SwingBuilder looks similar to JavaFXScript to which I pointed out that SwingBuilder was around first. Some were especially surprised at how much Java you could use with Griffon, eventually evolving to groovier code rather than having to make a break with all Java code and learn a totally new language. I could see the lightbulbs lighting up when they realized that not only does Groovy extend their ROI on learning Java but Griffon does the same for Grails. It's a giant sybiotic chain.

Thanks to Griffon co-despot Andres Almiray, I was able to demo SwingPad, a Griffon app to prototype Swing UIs without having to declare and initialize a SwingBuilder...very similar to GfxPad. 24 hours before our 0.0 release, we had basic Netbeans support (thanks to Geertjan Wielaga) and this was certainly a big surprise for this release. I only had a day to play around with it and change around my slides.

The cold I had been recovering from made an appearence and somehow I segued for a couple minutes on European Fanta vs. America Fanta.

Here are my slides:

 

Devoxx 2008

Devoxx 2008 is over so expect to see a deluge of wrap-up blog post (like this one ;-) ). Even if you are on the West coast, I think Devoxx is worth the trip. Unlike JavaOne, you can actually find hotels for less than $90 per night, decent food is reasonably priced and a full access pass is about 1/3 of a CommunityOne + JavaOne pass. As expected, there was a major JavaFX push. IMHO, It bordered on insanity. Developers are born cynics. If you have 5 sessions and the presenters are repeat the same slogan phases, it comes off a little forced.

As always, the JavaPosse was a blast, though technical difficultes forced @joeracer to be Tor's interpreter for most of the session. Much thanks to Atlassian for the beer.

There were lots of sessions of interest to Groovyists. Guillaume gave a 3hr talk on Groovy and Grails on the first of the University days. Andy Glover led a Tools in Action session on using REST with Grails. John F. Smart talked about easyB. There was also a BOF on Grails by Fabrice Robini and of course my Griffon BOF.

I'll preface the next line by saying that I know that probably no one else does it yet. It would really be nice if the BOFs were recorded. Maybe not with the painstaking effort of the main sessions but there were a lot of cool BOFs I went to that the people at home won't get to enjoy.