Writing Ant TasksApache Ant is an increasingly popular open source, cross-platform build tool written in Java. Ant's build files are written in XML and generally consist of a project and a set of interdependent targets. These targets contain one or more tasks that can perform all kinds of functions, such as compiling Java source code, creating .zip, .gzip, or .bzip2 archives, cleaning up old files, and so on.
A nice feature of Ant is that it is designed to allow you to add your own tasks and use them in an build. This article shows you the basics of writing an Ant task and how to get a task to work.
This article assumes that you are familiar with Ant and with the Java programming language. It walks you through the creation of a simple Ant task in Java, and then looks at a more complex task written by James Clark for Jing, an open source RELAX NG and Schematron validator written in Java. Both RELAX NG and Schematron are schema languages for XML. (I won't demonstrate the features of Schematron in this article). The example code and other files mentioned in this article are available for download from the Resources section below and at www.wyeast.net/task.zip. The examples have been tested with Ant version 1.6.1 (the latest version of Ant at the time of this writing), Java version 1.4.2_03, and Microsoft Windows XP Professional version 5.1.2600. (I'll be using the command-line version of Ant, though some GUIs for the tool are available.)
Before getting started, download and
install Ant version 1.6.1 or later on your system, if it is not already
there. When you extract the files from the Ant archive (in .zip, .gzip, or
.bzip2 format, depending on what you download), you'll notice a
bin directory in the distribution. Place the bin
directory in your path, and then type ant -version at a
command or shell prompt. If you get back a response like Apache Ant
version 1.6.1 compiled on February 12 2004, you're in business.
In the example archive, you'll find a file named Add.java. This file contains an Ant task that simply reports the sum of two integers. The code is not complex, but it provides a skeleton for your own tasks. Here's a listing of the file:
package net.wyeast.ant;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
public class Add extends Task {
private int op1;
private int op2;
private int sum;
// The method executing the task
public void execute()
throws BuildException {
sum = op1 + op2;
System.out.println("The sum of the " +
"operands is " + sum + ".");
}
// The setter for the "op1" attribute
public void setOp1(int op1) {
this.op1 = op1;
}
// The setter for the "op2" attribute
public void setOp2(int op2) {
this.op2 = op2;
}
}
|
Related Reading
Ant: The Definitive Guide |
You don't have to use a package name as I did (net.wyeast.ant),
though it's axiomatic to disambiguate class names that could
possibly collide with other class names in an open environment.
Only two Ant classes are imported: org.apache.tools.ant.BuildException
and org.apache.tools.ant.Task, an abstract class.
The Add class extends the Ant Task class. Other
options are possible, but Task is the base class for all Ant tasks. By the way, because only the class Task
is declared as abstract, you do not need to implement all of its methods.
The other imported class is BuildException. You'll notice
that the execute method in Add, which overrides
the method of the same name from Task, throws a
BuildException. A BuildException is thrown
if something goes wrong with the build or if the task cannot be
properly configured.
The three private variables of type int,
op1, op2, and sum, are used to
perform the arithmetic you see in the execute method. The
execute method is mandatory: it's the method Ant uses to
execute a task. The Add task adds two
operands and then prints the sum with System.out.println.
But where do these operands come from during a build? They are picked up
from the values of the attributes op1 and op2 in
the file build.xml file, which you will see in a moment. The two setter
methods setOp1 and setOp2 correspond with the
attributes op1 and op2. Setter methods such as
these process attribute values, which is the main way an Ant task gets input
and passes it on to an underlying program.
Now it's time to compile and package the task code into a .jar. To compile
Add.java, you'll need to add ant.jar
to your classpath so the compile can pick up the Task and
BuildException classes. You'll find this .jar in
the lib subdirectory in the Ant distribution. At a prompt
in the working directory where you extracted the files from the
example archive, type something similar to the
following line, taking care to use the correct path to
ant.jar, wherever it might be on your system:
javac -classpath ant.jar net/wyeast/ant/Add.java
Once you are successful at compiling Add.java, package
Add.class into a .jar file:
jar cvf add.jar net/wyeast/ant/Add.class
The class or classes you use for the task apparently must be in a .jar
file to work -- I've tried just using bare classes in a variety of ways
with no success. For convenience, place a copy of add.jar
in the lib subdirectory mentioned earlier. This is where all
of the Ant .jar files happily live. With add.jar there, Ant will be
able to find the class in it at build time. Granted, as long
as you make sure add.jar is in your classpath (using Ant's
-lib option), Ant will be able to find it, but I
find it's most convenient to drop .jars into lib.
With the code compiled and the .jar in place, your are ready to try out the task. In the example archive you will also find the following Ant build file, build.xml:
<?xml version="1.0"?>
<project default="main">
<taskdef name="adder" classname="net.wyeast.ant.Add"/>
<target name="main">
<adder op1="23" op2="77"/>
<target>
<project>
This is a small example of a build file, but it suits the needs of the
moment. It contains only one target, called main. The
default attribute on project is required and
names the default target to be used; that is, main, the only
target in the file. The main target contains the task
adder, which is dependent on taskdef.
The taskdef element preceding the target defines a name
for the task and the class name where the code for the task lives.
The value of the name
attribute names the task. That name need not match the name
of the class, although it must match the name of the task element
that triggers it. Also, taskdef does not need to precede
the target or targets that use the task.
The value of the classname attribute gives the name of
the class that executes the task. The class net.wyeast.ant.Add is
found in add.jar, which you previously copied to the Ant lib
subdirectory. If add.jar is not there (or
elsewhere in the classpath), this task will simply not work.
The adder child element of target executes
the task using its two attributes mentioned earlier, op1 and
op2. While in the working directory, test out the task by
typing ant at the command or
shell prompt. Ant automatically invokes the instructions in the file
build.xml because build.xml is the default
Ant build file name (you can use build files with different names if
you use the equivalent -f, -file, or
-buildfile option with Ant). After you type ant,
you should get the following response:
Buildfile: build.xml
main:
[adder] The sum of the operands is 100.
BUILD SUCCESSFUL
Total time: 1 second
There you are. You now know the basics of how to write an Ant task in Java, compile and package it as a .jar, and put the .jar in a place where Ant can find it. You also learned how to define a task and invoke it within an Ant build file. Now that you understand the process, it's a good time to do some experimenting. Some things you could try include:
op1 and op2 in build.xml.taskdef and its element name.setOp3, and then placing the corresponding attribute op3 in the invocation of the task in build.xml (also, add a variable to handle the attribute value and add it to the sum).After you play around with the build file and code, try writing an entirely new task yourself with a different and more interesting result than adding integers!
|
Now let's explore a more complex task. James Clark has written an Ant task for Jing. As I mentioned earlier, Jing is an open source, Java validator for the RELAX NG and Schematron schema languages for XML. Jing is reliable and provides good performance, and is a good choice for those wanting to use Ant to automate the validation of documents with an XML pipeline -- the execution of a succession of XML tasks.
To use this task, you need to download the Jing binaries and place jing.jar in the Ant lib subdirectory (or elsewhere in the classpath using -lib). You can also download the Jing source code (see src.zip), where you will find JingTask.java. For your convenience, I have included the source file JingTask.java in the example archive, along with
the Jing license (copying.txt).
JingTask.java is too long to list here in its entirety,
but I will highlight parts of the code in general terms in the
discussion that follows.
The JingTask.java source should provide some hints on writing more complex Ant tasks. In Add.java, all of the task code is neatly self-contained within that class. Of course, in most tasks, the task code comes from external code for an existing program that must be imported into the task code. To illustrate, near the top of JingTask.java, you will notice that it imports many more classes than Add.java, 18 to be exact: eight from Jing, five from Ant, two from SAX, and three from Java itself:
import com.thaiopensource.util.PropertyMapBuilder;
import com.thaiopensource.validate.Flag;
import com.thaiopensource.validate.SchemaReader;
import com.thaiopensource.validate.ValidationDriver;
import com.thaiopensource.validate.schematron.SchematronProperty;
import com.thaiopensource.validate.rng.CompactSchemaReader;
import com.thaiopensource.validate.rng.RngProperty;
import com.thaiopensource.xml.sax.ErrorHandlerImpl;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import java.io.File;
import java.io.IOException;
import java.util.Vector;
JingTask extends Ant's Task class, and
declares a set of private variables, mostly for the management of files
or filesets:
/**
* Ant task to validate XML files using RELAX NG
* or other schema languages.
*/
public class JingTask extends Task {
private File schemaFile;
private File src;
private final Vector filesets = new Vector();
private PropertyMapBuilder properties =
new PropertyMapBuilder();
private boolean failOnError = true;
private SchemaReader schemaReader = null;
The filesets variable of type Vector will be
used for code that manipulates sets of files within the context of a task (search for filesets in JingTask.java to see how this code is pieced together). The properties variable of type PropertyMapBuilder is used with Jing's ValidationDriver.
Next, the task sets up an error handler, LogErrorHandler,
an extension of Jing's ErrorHandlerImpl, which in turn is an implementation of SAX's ErrorHandler interface:
private class LogErrorHandler extends
ErrorHandlerImpl {
int logLevel = Project.MSG_ERR;
public void warning(SAXParseException e)
throws SAXParseException {
logLevel = Project.MSG_WARN;
super.warning(e);
}
public void error(SAXParseException e) {
logLevel = Project.MSG_ERR;
super.error(e);
}
public void printException(Throwable e) {
logLevel = Project.MSG_ERR;
super.printException(e);
}
public void print(String message) {
log(message, logLevel);
}
}
The message constants MSG_ERR and MSG_WARN come
from Ant's Project class. The warning,
error, and printException methods are called
from the superclass ErrorHandlerImpl. The print
method calls the log method, which you can find in Task
and in Task's superclass, Ant's ProjectComponent.
When the JingTask constructor is defined, it sets
the RELAX NG property so that it checks ID/IDREF/IDREFS by default,
if they exist in the instance:
public JingTask() {
RngProperty.CHECK_ID_IDREF.add(properties);
}
The execute method overrides the method
of the same name from Ant's Task class.
This is the most complex part of the code. Basically, this implementation of
execute sets up the task for the retrieval of
instance and schema files for Jing and, unlike the Add
task, does a fair amount of error handling. If the expected files
or filesets are not found, this part of the code will throw
a SAX, I/O, or build exception, depending on the problem.
At the beginning, the method checks to see if the values in
the rngFile attribute or the schemaFile
attribute are in place, then if either a file attribute
or a fileset element exists:
if (schemaFile == null)
throw new BuildException("There must be an
rngFile or schemaFile attribute",location);
if (src == null && filesets.size() == 0)
throw new BuildException("There must be a file
attribute or a fileset child element",
location);
If a value for rngFile is not present, there
must be a value for schemaFile; if file
is not present, the fileset element must be a child
of the jing element.
Following these tests, an error handler is instantiated and an
error flag (hadError) is set up. Next, a try
block contains the code to feed file names to Jing using ValidationDriver. After the try block, several catch blocks
and an if statement stand sentinel to handle any
errors.
After the execute method, JavaTask
defines setter methods for eight possible attributes, namely
setCheckid, setCompactsyntax,
setFailonerror, setFeasible,
setFile, setPhase,
setRngfile, and setSchemafile.
The following table shows the correspondence between these
methods, Jing's command-line options, and Ant task attributes.
Table 1. Jing methods and attributes
| Method | Attribute | Option Description |
|---|---|---|
setCheckid | checkid | Checks for
ID/IDREF/IDREFS compatibility. Corresponds to -i. Possible
values are true or false, with default
true. |
setCompactsyntax | compactsyntax | Turns
compact syntax checking on or off. Corresponds to -c. Possible
values are true or false, with default
false. |
setFailonerror | failonerror | A
value of false means that processing continues after an
error occurs. |
setFeasible | feasible | Checks if document
is feasibly valid.
Corresponds to -f. Possible values are true or
false with default false. |
setFile | file | Name of XML file to validate. |
setPhase | phase | Schematron phase to be used during internal schema creation or validation (not demonstrated here). |
setRngFile | rngfile | Name of RELAX NG schema file. |
setSchemafile | schemafile | Name of Schematron file (not demonstrated here). |
The final bit of code in JingTask.java is the
addFileset method, a utility method for
fileset functionality. Search for filesets
in the code to see how it works.
Following is another build file from the archive (build-jing.xml) that exercises the Jing task:
<?xml version="1.0"?>
<project default="jing">
<taskdef name="jing" classname="com.thaiopensource.relaxng.util.JingTask"/>
<target name="jing">
<jing file="time.xml" rngfile="time.rng"/>
</target>
</project>
taskdef identifies the name of the task (jing)
and the name of the class that holds the code for the task
(com.thaiopensource.relaxng.util.JingTask). The only child
of target is the jing element, with the
attributes file and rngfile. The jing
element triggers the Jing task, telling Jing to validate
time.xml against time.rng.
Now invoke Ant with build-jing.xml. This assumes that you have placed jing.jar in the Ant lib directory. In the working directory, type this command:
ant -f build-jing.xml
This will be the result if all files are in place:
Buildfile: build-jing.xml
jing:
BUILD SUCCESSFUL
Total time: 1 second
If Jing runs quietly, that indicates success. Two other Jing-related
build files are in the example archive: build-jing-compact.xml,
which validates a file against a compact syntax schema, and build-jing-set.xml, which validates
a set of files against a RELAX NG schema. Test both these files yourself
with ant -f. The inner workings of these build files should
now be self-evident.
After reading this article, the basic steps of writing an Ant task should no longer be a mystery to you. To write a task for a program as complex as Jing, however, you need to have an intimate understanding of the code so that you know how to feed the program what it needs to run properly. You'll also want to add error handling of some sort for an industrial-strength task.
To expand your understanding of creating tasks for Ant, you'll can review the section in Ant's official online manual that describes how to write your own task, as well as the tutorial on writing tasks. The Ant site also provides a list of external tasks (tasks written for Ant by third parties) that may be of interest.
Michael Fitzgerald is Principal at Overdue Books, a publishing and writing consultancy.
Return to ONJava.com.
Copyright © 2009 O'Reilly Media, Inc.