Fixing Bugs in HtmlUnit

For the last week or two I’ve been busy at my day job with some less technical things: iteration planning, specification clarification, architecture documentation and other miscellaneous putting-out-of-fires. The net result has been a desire to find some other outlet for my technical creative impulses ;-) Enter HtmlUnit.

I’ve been a committer to the project for a while, but have struggled to find time to work on it. These past couple of weeks, however, have seen a flurry of activity on my part, mainly fixing bugs. One interesting bug involves supporting the jQuery JavaScript library. We already run a portion of the Prototype unit tests as part of HtmlUnit’s own unit testing — the goal being to eventually be able to “certify” certain versions of HtmlUnit as being compatible with certain other versions of Prototype. I’m working towards doing the same for jQuery, and seem to be getting there slowly but surely.

In other news, it looks like I may soon have a second outlet for my “creative impulses”…

Mavenizing OC4J Client Libraries With Groovy

The project I’m currently working on uses Spring on both the server side and the client side to wire everything together. We’ve been coding away for three or four months, doing all of our client/server integration testing with lightweight standalone RMI using Spring’s excellent remoting support. However, the time has come to shoehorn our stuff into the production application server: OC4J. This change involves adding a thin stateless session bean layer over our Spring-managed beans.

The largest unforeseen issue which we’ve encountered thus far has been that in order to perform JNDI lookups from the remote client, the OC4J client libraries need to be on the classpath. We’re using Maven 2 for build management, so normally this wouldn’t be an issue: just install the JAR into the local repository. Unfortunately, the OC4J client JAR declares a custom “Class-Path” entry in its manifest file, using relative paths. This constitutes a dependency declaration of sorts, but not one which Maven can work with.

The solution was to “translate” the manifest classpath dependency mechanism into Maven’s POM dependency mechanism. The following Groovy script targets a single JAR file (in this case the central OC4J client JAR), reads the classpath manifest entry, and adds each referenced JAR to the local Maven repository before finally adding the targeted JAR to the repository. Each JAR added to the repository is a copy of the original JAR, the only difference being that the “Class-Path” manifest attributes are removed from the copies. The script also generates a POM file for each JAR, assigning it a custom dependency configuration based on the removed “Class-Path” manifest attributes.

This was a quickie implementation, so there are a number of limitations: it uses the runtime environment, so it only works on Windows; the base path needs to be configured carefully, or the generated artifact IDs will contain some oddities; the dependency processing is not recursive, so multi-level dependency trees are not supported; etc. All of the limitations are easily fixable, I just don’t need the extra functionality ;-)

My apologies for the formatting, which doesn’t quite fit the site template…

// Installs the target JAR (see "jar" variable below) into the Maven 2
// repository, as well as any manifest classpath dependencies. All manifest
// classpath dependencies are used to build appropriate Maven POMs.
//
// http://maven.apache.org/plugins/maven-install-plugin/index.html
//
// @author Daniel Gredler

import java.util.jar.*;
import java.util.zip.*;

String groupId = "com.oracle.oc4j.client"
String version = "10.1.3.2.0"

String home = "C:javaoc4j-client-10.1.3.2.0\"
File base = new File(home)
File dir = new File(base, "j2ee/home")
File jar = new File(dir, "oc4jclient.jar")
String[] paths = getClassPath(jar)

paths.each() {
	File f = new File(dir, it)
	if(f.exists()) installJar(f, groupId, version, dir, home)
}

installJar(jar, groupId, version, dir, home)

println "Finished!"

def getClassPath(file) {
	JarFile jar = new JarFile(file)
	if(!jar.manifest) return null
	String cp = jar.manifest.mainAttributes.getValue("Class-Path")
	if(!cp) return null
	return cp.split("s+")
}

def getArtifactId(file, home) {
	String artifactId = file.canonicalPath
	if(artifactId.startsWith(home)) artifactId = artifactId.substring(home.length())
	if(artifactId.endsWith(".jar")) artifactId = artifactId.substring(0, artifactId.length() - 4)
	artifactId = artifactId.replaceAll("\", "-")
	println "Artifact name mapping: ${file.canonicalPath} -> ${artifactId}"
	return artifactId
}

def createPom(groupId, artifactId, version, dependencies, dir, home) {
	File pom = File.createTempFile("temp", ".pom")
	FileWriter writer = new FileWriter(pom)
	writer.write("<project>\r\n")
	writer.write("t<modelVersion>4.0.0</modelVersion>rn")
	writer.write("t<groupId>${groupId}</groupId>rn")
	writer.write("t<artifactId>${artifactId}</artifactId>rn")
	writer.write("t<version>${version}</version>rn")
	if(dependencies) {
		writer.write("t<dependencies>rn")
		dependencies.each() {
			File jar = new File(dir, it)
			if(jar.exists()) {
				String aid = getArtifactId(jar, home)
				writer.write("tt<dependency>rn")
				writer.write("ttt<groupId>${groupId}</groupId>rn")
				writer.write("ttt<artifactId>${aid}</artifactId>rn")
				writer.write("ttt<version>${version}</version>rn")
				writer.write("tt</dependency>rn")
			}
			else {
				println "WARNING: Not including dependency ${jar.canonicalPath}, because the file does not exist."
			}
		}
		writer.write("t</dependencies>rn")
	}
	writer.write("</project>rn")
	writer.close()
	return pom
}

def makeCopyWithoutClassPathManifestEntry(file) {

	JarFile jar = new JarFile(file)
	Manifest manifest = (jar.manifest ? new Manifest(jar.manifest) : new Manifest())
	manifest.mainAttributes.remove(new Attributes.Name("Class-Path"))

	InputStream input = new BufferedInputStream(new FileInputStream(file))
	File copy = File.createTempFile(getNamePrefix(file), getNameSuffix(file))

	BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(copy));
	JarOutputStream output = new JarOutputStream(bo, manifest)

	int b
	byte[] buffer = new byte[4096]
	jar.entries().each() {
		if(!it.name.endsWith("MANIFEST.MF")) {
			output.putNextEntry(new ZipEntry(it.name))
			InputStream is = jar.getInputStream(it)
			while((b = is.read(buffer)) != -1) output.write(buffer, 0, b)
			is.close()
		}
	}

	input.close()
	output.close()
	println "Copied ${file.canonicalPath} -> ${copy.canonicalPath}"
	return copy
}

def getNamePrefix(file) {
	if (file.name.contains(".")) return file.name.substring(0, file.name.lastIndexOf("."))
	else return file.name
}

def getNameSuffix(file) {
	if (file.name.contains(".")) return file.name.substring(file.name.lastIndexOf("."))
	else return null
}

def installJar(file, groupId, version, dir, home) {
	File copy = makeCopyWithoutClassPathManifestEntry(file)
	String artifactId = getArtifactId(file, home)
	String path = copy.canonicalPath
	String[] dependencies = getClassPath(file)
	int dependencyCount = (dependencies ? dependencies.length : 0)
	File pom = createPom(groupId, artifactId, version, dependencies, dir, home)
	String pomPath = pom.absolutePath
	installJar(groupId, artifactId, version, path, pomPath, dependencyCount)
}

def installJar(groupId, artifactId, version, path, pomPath, dependencyCount) {
	String cmd = "cmd /c mvn install:install-file -DgroupId=${groupId} -DartifactId=${artifactId} -Dversion=${version} -Dpackaging=jar -Dfile=\"${path}\" -DpomFile=\"${pomPath}\" -DcreateChecksum=true"
	println "Installing ${artifactId} with ${dependencyCount} dependencies using command: ${cmd}"
	println ""
	Runtime.getRuntime().exec(cmd)
}

Maven 1 Frustrations

I just spent the past hour attempting to install Maven 1.0.2 in order to get some HtmlUnit work done.

Lesson the first: don’t point your JAVA_HOME environment variable to a path which includes spaces. This was a good rule of thumb back in 1997, but come on! Ten years later, and we’re still dealing with these issues? The Maven batch file is full of double-quoted variables and values, but apparently it’s just an effort to instill a false sense of security in unwary developers.

Lesson the second: do not, under any circumstances, add a trailing slash to your MAVEN_HOME environment variable. Doing so (ie, using “C:\java\maven-1.0.2\” instead of “C:\java\maven-1.0.2″) will cause Maven to throw the Java usage message at you, instead of doing something useful like, say, compiling your project.

It’s too bad that Mergere seems to be having some trouble. Both versions of Maven could really use the polish that comes with a professional [read: for money] operation a la JBoss…

UPDATE: Enter Sonatype!

Unit Testing Spring-Based Swing Apps

Unit and integration testing with Spring is fairly straightforward. You can even extend one of the convenient Spring test classes if you need to get multiple beans to interact during the course of a test. Abbot, on the other hand, is an excellent framework for testing Swing GUIs, providing its own convenient base test class, ComponentTestFixture. The trouble with mixing these two frameworks is that you can only extend from one of the available base test classes. After trying a couple of different approaches to unit testing a Spring-based Swing application, the following solution seems to work fairly well:

public class MyTest extends ComponentTestFixture {

    private ClassPathXmlApplicationContext applicationContext;

    @Override
    public void runBare() throws Throwable {
        try {
            this.applicationContext = this.createContext();
            super.runBare();
        } finally {
            if (this.applicationContext != null) {
                this.applicationContext.close();
            }
        }
    }

    private ClassPathXmlApplicationContext createContext() {
        String[] s = new String[] { /* my config files */ };
        ClassPathXmlApplicationContext context =
            new ClassPathXmlApplicationContext(s);
        context.registerShutdownHook();
        return context;
    }

    @Override
    protected void fixtureSetUp() throws Throwable {
        super.fixtureSetUp();
        // Custom initialization.
    }

    // Test methods.

}

« Previous entries