Tuesday, March 2, 2010

Ant targets with different classpaths

I recently ran into a problem where an Ant task could not find a given Java class. There are a number of solutions listed in the Ant FAQ page, all passing through adding the corresponding JAR files to the Ant lib directory or the the classpath before invoking Ant.

It turns out the particular task from the Ant target I imported was invoking an XSL transformation containing function defined in an external class, like this:

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xalan2="http://xml.apache.org/xalan"
xmlns:fixquote="com.package.ExternalXsltFunction"
...



The original ExternalTask task was defined in A.jar, but com.package.ExternalXsltFunction was defined in external.jar. Since com.package.ExternalXsltFunction is dynamically loaded only when the internal XSL transformation is invoked, Ant simply ignores and discards its containing JAR file while trying to load ExternalTask.




<taskdef name="externaltask" classname="com.package.ExternalTask">
<classpath>
<pathelement name=”A.jar”/>
<pathelement name=”external.jar”/> <—Makes no difference, ExternalXsltFunction is not loaded when ExternalTask is loaded.
<classpath/>
</taskdef>



The solution was to create an extension of the venerable “antcall” task, adding a “classpath” element to it. This new task, let’s call it “antcallwithclassloader”, sets the classpath to the thread of execution, invokes the target, then resets the thread classpath back to its original value, like this (all comments stripped out):




package com.myproject.ant;

import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.CallTarget;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;

public class CallTargetWithClasspath extends CallTarget {

private Path classpath;

private Reference classpathRef;

@Override
public void execute() throws BuildException {
Path resolvedClassPath = null;
if (classpathRef != null) {
resolvedClassPath = (Path)classpathRef.getReferencedObject();
} else {
resolvedClassPath = classpath;
}
AntClassLoader acl = new AntClassLoader(getProject(), resolvedClassPath);
acl.setThreadContextLoader();
super.execute();
acl.resetThreadContextLoader();
}

public Path getClasspath() { return classpath; }

public void setClasspath(Path targetClasspath) { this.classpath = targetClasspath; }

public Path createClasspath() {
classpath = new Path(getProject());
return classpath;
}

public Reference getClasspathRef() { return classpathRef; }

public void setClasspathRef(Reference classpathRef) { this.classpathRef = classpathRef; }

}




With this task, now added to a “myanttools.jar” file, I could successfully refactor my Ant script so that the problematic task was moved to its own target and was invoked with its required classpath, like this:




    <taskdef name="my.antcall" classname="com.myproject.ant.CallTargetWithClasspath" >
<classpath>
<pathelement location="myanttools.jar"/>
</classpath>
</taskdef>


    <target name="wrapper.target" depends="init">
<my.antcall target="refactored.target">
<classpath>
<pathelement path=”external.jar”/> <—- Adds B.jar to the classpath before invoking target
</classpath>
</my.antcall>
</target>


    <target name="refactored.target">
<!—- The caller set external.jar in the classpath –>
<!-- and ExternalXsltFunction is visible to the classloader -–>



<externaltask …/>
</target>