Thursday, 11 January 2007

Groovy at work - developing a GUI build tool

Suppose you have a complicated Ant build which builds your latest code from SVN. It is very complicated and running it requires a set of parameters to be passed into it. This involves creating and maintaining a set of properties files and probably also batch files which start the build as you don't want to manually type in all props every time. Also, source files need to be updated before running the build. Does it not sound like a tough manual and repeatable work?

How about a simple GUI where you could type in (or even choose from drop downs, if possible) required properties and run the build by clicking one button? Yes, why not, but we don't want to waste two weeks or more writing such a tool, in Swing for example. Ok, how about Groovy and its brilliant features, like SwingBuilder, AntBuilder and closures? Hmm, that sounds much better!

Recently I have developed such a GUI build tool using Groovy. It took me 2 days to code it. Now guess what features it has:
  • full checkout from SVN to a local directory
  • project update before each build
  • running the old Ant build, written in XML
  • intercepting build output for error messages
  • reading tool properties from an external XML config
All in about 350 lines of groovy code! Most of the code are SwingBuilder closures where I am building the GUI and AntBuilder doing the build job. The most interesting parts however are:

Defining external svn ant task
To get that to work some additional jars are needed. Assuming they are under local lib directory, the svn ant task can be defined like that:

def PATH = "task.path"
ant.path(id:PATH) {
ant.pathelement(location:"lib/ganymed.jar")
ant.pathelement(location:"lib/svnkit.jar")
ant.pathelement(location:"lib/svnant.jar")
ant.pathelement(location:"lib/svnClientAdapter.jar")
ant.pathelement(location:"lib/svnkit-javahl.jar")
}
ant.taskdef(
resource:"org/tigris/subversion/svnant/svnantlib.xml",
classpathref:PATH)

Accessing SVN
Now, having the svn ant task defined, accessing svn (checkout, update etc.) is as easy as:

def svnCall = { task ->
ant.svn(username:USER, password:PASS) {
task()
}
}
where the task can be svn checkout:

svnCall {
ant.checkout(url:url, destpath:dest)
}
or svn update:

svnCall {
ant.update(dir:dest)
}

As you can see there is no code duplication. The ant.svn call is common for all svn tasks. I am passing a closure as a parameter into it, which then can call ant.checkout or ant.update. Closures - it's brilliant, isn't it?

Running an external Ant build and reading output info.
The XML ant build needs to be started from command line (no, we are not going to rewrite it using Gant, not yet ;-) ). How do we do this in Grooovy?

def build = "cmd /c ant -f build.xml
-propertyfile build.properties target".execute()
The build variable is actually a Process instance, so we can now intercept the in and err streams:

build.in.eachLine { line ->
//consume each line
}

build.err.eachLine { line ->
//consume each line
}

Why intercepting the err stream? Well, if the build fails Ant writes BUILD FAILED + the error to that stream. Then I can show an info message using JOptionPane as MsgBOX:

import javax.swing.JOptionPane as MsgBOX
...
MsgBOX.showMessageDialog(gui, message, title,
MsgBOX.INFORMATION_MESSAGE)

The build tool
To give you an idea what the build tool looks like, here is a screenshot (I have removed real labels/data) :

Title borders, drop downs, text fields, labels and box layout - all in 2 days? Yep, plus event handling for all the combo boxes, which make up some sort of hierarchy, actually. This makes the tool a bit complicated, but not the code! Event handling is as easy as clicking a button (actionPerformed as a closure):

def comboBox = swing.comboBox(items:items,
actionPerformed:action)
and the hierarchy is read from an external XML file, using Groovy's XML parser:

def XML = new File(xmlFile).getText()
root = new XmlParser().parseText(XML)
Nice and simple, isn't it?

Summary
It was such a pleasure writing the tool, in no time actually. I have to admit that it was my first bigger piece of Groovy code, which now really works fine in my company! What else can I say - thank you, Groovy! You saved me at least a week of swing coding in Java!

4 comments:

Mike Wolfson said...

Cool post. I am writing a very similar GUI (to read info from an XML file, and generate Servlet Requests). While my functionality isn't the same, my GUI component layout is very similar. I am struggling getting the layout working for me (I am not very Swing competent). I would love to see your code (not sure if you can share). When I try to put multiple panels on a Frame, only the last one displays. I realize you aren't a support group for Groovy GUI, but you have done exactly what I want to do. At any rate. I found this post very interesting, and wanted to thank you for that. If you can share how you did the layout, it would be cool.
MSW (Phoenix, AZ, USA)

Marcin Domanski said...

Firs of all, thanks for your comment. In fact, that was the first one on my first post, which I wrote over a year ago! ;-)

Unfortunately I can't share the code, but I think I can show you how to get the right layout. All you need is the BoxLayout, which does the trick of laying out in rows or columns, as in my example. And here is another one, which I've just hacked for you :) It's a simple frame with 3 titled panels with labels and text fields inside, and the panels placed one under the other.

//-------------------------------------------
import groovy.swing.SwingBuilder
import javax.swing.BoxLayout
import javax.swing.JFrame
import javax.swing.border.EtchedBorder
import javax.swing.border.TitledBorder


def swing = new SwingBuilder()

def frame = swing.frame(title:'Frame', size:[300, 200], defaultCloseOperation:JFrame.EXIT_ON_CLOSE) {
boxLayout(axis: BoxLayout.Y_AXIS)

border = new TitledBorder( new EtchedBorder(), 'Titled border')
panel(border: border, preferredSize:[100, 30], size:[100, 30]) {
label(text: "Enter some text: ")
textField(columns: 4)
}
panel(border: border, preferredSize:[100, 30], size:[100, 30]) {
label(text: "Enter some text: ")
textField(columns: 4)
}
panel(border: border, preferredSize:[100, 30], size:[100, 30]) {
label(text: "Enter some text: ")
textField(columns: 4)
}
}

frame.show()
//-------------------------------------------

And that's it!

Hope that helps. Please feel free to drop me a line if you have any trouble getting the right thing.

Thanks,
Marcin

Sergey Bondarenko said...

You can also write something like "task.delegate = ant" in your svnCall closure.
Thus you will be able to write innner tasks without need to specify "ant." prefix.

For instance:
svnCall {
update(dir:dest)
}

Anonymous said...

Thank you, I've recently been looking for info approximately this topic for a while and yours is the best I have came upon till now. But, what concerning the conclusion? Are you sure concerning the source?

Have a look at my website; Florida directory white pages