Set up the build environment

Apache Ant builds itself, we are using Ant too (why we would write
a task if not? :-) therefore we should use Ant for our build.

We choose a directory as root directory. All things will be done
here if I say nothing different. I will reference this directory
as root-directory of our project. In this root-directory we
create a text file names build.xml. What should Ant do for us?

This buildfile uses often the same value (src, classes, MyTask.jar), so we should rewrite that
using <property>s. On second there are some handicaps: <javac> requires that the destination
directory exists; a call of "clean" with a non existing classes directory will fail; "jar" requires
the execution of some steps before. So the refactored code is:

and we can compile and jar it with ant (default target is "jar" and via
its depends-clause the "compile" is executed before).

Use the Task

But after creating the jar we want to use our new Task. Therefore we need a
new target "use". Before we can use our new task we have to declare it with
<taskdef> [2]. And for easier process we change the default clause:

Integration with TaskAdapter

Our class has nothing to do with Ant. It extends no superclass and implements
no interface. How does Ant know to integrate? Via name convention: our class provides
a method with signature public void execute(). This class is wrapped by Ant's
org.apache.tools.ant.TaskAdapter which is a task and uses reflection for
setting a reference to the project and calling the execute() method.

Setting a reference to the project? Could be interesting. The Project class
gives us some nice abilities: access to Ant's logging facilities getting and setting
properties and much more. So we try to use that class:

Deriving from Ant's Task

Ok, that works ... But usually you will extend org.apache.tools.ant.Task.
That class is integrated in Ant, get's the project-reference, provides documentation
fields, provides easier access to the logging facility and (very useful) gives you
the exact location where in the buildfile this task instance is used.

Accessing the Task's Project

The parent project of your custom task may be accessed through method getProject(). However, do not call this from the custom task constructor, as the return value will be null. Later, when node attributes or text are set, or method execute() is called, the Project object is available.

Here are two useful methods from class Project:

String getProperty(String propertyName)

String replaceProperties(String value)

The method replaceProperties() is discussed further in section Nested Text.

Attributes

Now we want to specify the text of our message (it seems that we are
rewriting the <echo/> task :-). First we well do that with an attribute.
It is very easy - for each attribute provide a public void set<attributename>(<type>
newValue) method and Ant will do the rest via reflection.

Oh, what's that in execute()? Throw a BuildException? Yes, that's the usual
way to show Ant that something important is missed and complete build should fail. The
string provided there is written as build-failes-message. Here it's necessary because
the log() method can't handle a null value as parameter and throws a NullPointerException.
(Of course you can initialize the message with a default string.)

But here properties are not resolved! For resolving properties we have to use
Project's replaceProperties(String propname) : String method which takes the
property name as argument and returns its value (or ${propname} if not set).

Thus, to replace properties in the nested node text, our method addText() can be written as:

Nested Elements

There are several ways for inserting the ability of handling nested elements. See
the Manual [4] for other.
We use the first way of the three described ways. There are several steps for that:

We create a class for collecting all the info the nested element should contain.
This class is created by the same rules for attributes and nested elements
as for the task (set<attributename>() methods).

The task holds multiple instances of this class in a list.

A factory method instantiates an object, saves the reference in the list
and returns it to Ant Core.

Then we can use the new nested element. But where is xml-name for that defined?
The mapping XML-name : classname is defined in the factory method:
public classname createXML-name(). Therefore we write in
the buildfile

Test the Task

We have written a test already: the use.* tasks in the buildfile. But its
difficult to test that automatically. Common (and in Ant) used is JUnit for
that. For testing tasks Ant provides a JUnit Rule org.apache.tools.ant.BuildFileRule.
This class provides some for testing tasks useful methods:
initialize Ant, load a buildfile, execute targets, capturing debug and run logs ...

In Ant it is usual that the testcase has the same name as the task with a prepending
Test, therefore we will create a file HelloWorldTest.java. Because we
have a very small project we can put this file into src directory (Ant's own
testclasses are in /src/testcases/...). Because we have already written our tests
for "hand-test" we can use that for automatic tests, too. But there is one little
problem we have to solve: all test supporting classes are not part of the binary
distribution of Ant. So you can build the special jar file from source distro with
target "test-jar" or you can download a nightly build from
http://gump.covalent.net/jars/latest/ant/ant-testutil.jar [5].

For executing the test and creating a report we need the optional tasks <junit>
and <junitreport>. So we add to the buildfile:

Back to the src/HelloWorldTest.java. We create a class with a public
BuildFileRule field annotated with JUnit's @Rule annotation. As per
conventional JUnit4 tests, this class should have no constructors, or a default no-args
constructor, setup methods should be annotated with @Before, tear down methods
annotated with @After and any test method annotated with @Test.

Debugging

Try running Ant with the flag -verbose. For more information, try flag -debug.

For deeper issues, you may need to run the custom task code in a Java debugger. First, get the source for Ant and build it with debugging information.

Since Ant is a large project, it can be a little tricky to set the right breakpoints. Here are two important breakpoints for version 1.8:

Initial main() function: com.apache.tools.ant.launch.Launcher.main()

Task entry point: com.apache.tools.ant.UnknownElement.execute()

If you need to debug when a task attribute or the text is set, begin by debugging into method execute() of your custom task. Then set breakpoints in other methods. This will ensure the class byte-code has been loaded by the Java VM.

Resources

This tutorial and its resources are available via
BugZilla [6].
The ZIP provided there contains

this initial version of this tutorial

the buildfile (last version)

the source of the task (last version)

the source of the unit test (last version)

the ant-testutil.jar (nightly build of 2003-08-18)

generated classes

generated jar

generated reports

The last sources and the buildfile are also available
here [7] inside the manual.