ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Writing Ant Tasks Writing Ant Tasks

by Michael Fitzgerald
06/02/2004

Apache 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.)

Writing a Simple Task

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
By Jesse E. Tilly, Eric M. Burke

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.

Exercising the Task

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:

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!

The Jing Task

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

MethodAttributeOption Description
setCheckidcheckidChecks for ID/IDREF/IDREFS compatibility. Corresponds to -i. Possible values are true or false, with default true.
setCompactsyntaxcompactsyntaxTurns compact syntax checking on or off. Corresponds to -c. Possible values are true or false, with default false.
setFailonerrorfailonerrorA value of false means that processing continues after an error occurs.
setFeasiblefeasibleChecks if document is feasibly valid. Corresponds to -f. Possible values are true or false with default false.
setFilefileName of XML file to validate.
setPhasephaseSchematron phase to be used during internal schema creation or validation (not demonstrated here).
setRngFilerngfileName of RELAX NG schema file.
setSchemafileschemafileName 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.

Conclusion

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.

Resources

Michael Fitzgerald is Principal at Overdue Books, a publishing and writing consultancy.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.