Developer: J2EE and Open Source

Ant 1.6 for Task Writers
by Stefan Bodewig

Take advantage of the changes in Ant 1.6 internals to write a task or even a library of tasks.

Published July 2005

In my last article , I focused on using some of the new features of Ant 1.6 that can help you to better control or reuse your build setups. This article will show you that Ant 1.6 has also changed internally and how you can take advantage of those changes when you write a task or even a library of tasks.

Key Improvements in Ant 1.6

One of the major changes in Ant 1.6 is that Ant has become XML namespace aware. This shouldn't affect your build files or how you write tasks too much, unless you've been using a colon in your task's name — which has been a bad practice and discouraged even before XML namespaces existed. Ant's namespace usage may be puzzling at times and thus the first section tries to point out some pitfalls as well.

Tightly related to XML namespace support is the concept of Ant libraries (antlibs for short). You can group your custom tasks and types into a single library and put them into an XML namespace of their own. You don't have to worry about choosing unique names for your tasks any longer. If you pick your namespace URI according to a naming convention explained later you can even make Ant discover your antlib automatically, you only need to declare their namespace in your build file.

If you wanted to write a selector or a filterreader to use within Ant's tasks, you had to use a custom format in your build file that made using your selectors or readers less convenient than using their built-in cousins. Writing custom conditions has even been impossible since adding a new condition required a change in one of Ant's core classes.

Starting with version 1.6, Ant supports a new way to specify nested elements in tasks. Much like the older TaskContainer interface that told Ant your task would support any Ant task as nested element you can now make your task support arbitrary subclasses of a common base class or implementations of an interface. Many existing Ant tasks have been retrofitted to support the new way so you can now easily add conditions to Ant.

Example: an rsync task

As a running example let's write a task that provides a thin layer on top of the rsync command line interface. The implementation only serves as an example, a real task would certainly be more sophisticated and provide more control over the command line arguments, check error conditions and so on.

The task is supposed to distribute one master directory to an unknown number of slaves using rsync. Since different tasks may want to connect to the same slaves, slaves are implemented as a data type that can be re-used via Ant's id/refid system.

A simplistic slave would look like

public class Slave extends ProjectComponent {

    private String refid;
    private String hostAndDir;

    public Slave() {}

    public void setRefid(String id) {
        if (hostAndDir != null) {
            throw new BuildException("Can't mix hostanddir with refid");
        }
        refid = id;
    }

    public void setHostanddir(String had) {
        if (refid != null) {
            throw new BuildException("Can't mix hostanddir with refid");
        }
        hostAndDir = had;
    }

    public String getHostAndDir() {
        if (refid != null) {
            Slave s = (Slave) getProject().getReference(refid);
            return s.getHostAndDir();
        }
        return hostAndDir;
    }
}


and a very simple rsync task could be

 
public class Rsync extends Task {

    private Commandline cmd = new Commandline();
    private File master;
    private ArrayList slaves = new ArrayList();

    public Rsync() {
        cmd.setExecutable("rsync");
        cmd.createArgument().setValue("-vauCz");
        cmd.createArgument().setValue("--delete");
    }

    public void setMaster(File f) {
        cmd.createArgument().setFile(f);
    }

    public void addSlave(Slave s) {
        slaves.add(s);
    }

    public void execute() {
        Iterator iter = slaves.iterator();
        while (iter.hasNext()) {
            Slave s = (Slave) iter.next();
            Commandline c = (Commandline) cmd.clone();
            c.createArgument().setValue(s.getHostAndDir());
            Execute exe = new Execute(new LogStreamHandler(this, 
                                                           Project.MSG_INFO,
                                                           Project.MSG_WARN),
                                      null);
            exe.setCommandline(c.getCommandline());
            try {
                exe.execute();
            } catch (IOException e) {
                throw new BuildException(e, getLocation());
            }
        }
    }
}


To make the task and type visible to Ant you'd use

   <typedef name="slave" classname="org.example.Slave"/>
   <taskdef name="deploy" classname="org.example.Rsync"/>


in the build file and make sure Ant finds your classes.

If you want to provide the classpath dynamically, you have to make sure that Ant reuses the same classloader. I.e. use type/taskdef's loaderref attribute like

   <typedef name="slave" classname="org.example.Slave" loaderref="deploy">
     <classpath>
       <pathelement location="path-to-my.jar"/>
     </classpath>
   </typedef> 
   <taskdef name="deploy" classname="org.example.Rsync" loaderref="deploy"/>


If you don't do so, Ant will load the Slave class twice using different classloaders — which in turn would lead to ClassCastExceptions later.

You can then use the task like

    <slave hostanddir="slave1:/www" id="slave1"/>
    <slave hostanddir="slave2:/www" id="slave2"/>
    <deploy master="some-dir">
      <slave refid="slave1"/>
      <slave hostanddir="slave3:/www"/>
    </deploy>


Users don't really need to know that the task uses rsync under the covers so it's named <deploy> instead of <rsync>.

XML Namespace

Now imagine that your application server provider offers a <deploy> task that lets you re-deploy your application to the application server without restarting the server process.

Prior to Ant 1.6 you had to rename one of the two tasks if you wanted to use both inside the same build file. Starting with Ant 1.6 you can qualify the tasks and types by XML namespaces[1]. For Ant's purpose an XML namespace simply consists of an URI identifying it and a prefix that you want to use for it in your XML file.

Simply pick a prefix and an URI and you are set:

  <project ... xmlns:mylib="org.example">
    <typedef name="slave" classname="org.example.Slave" uri="org.example"/>
    <taskdef name="deploy" classname="org.example.Rsync" uri="org.example"/>
    <mylib:slave hostanddir="slave1:/www" id="slave1"/>
    <mylib:slave hostanddir="slave2:/www" id="slave2"/>
    <mylib:deploy master="some-dir">
      <mylib:slave refid="slave1"/>
      <mylib:slave hostanddir="slave3:/www"/>
    </mylib:deploy>
  </project>


here "mylib" is the prefix and "org.example" the URI.

You can establish mappings between prefixes and URIs at every element individually, but I find it most convenient to declare them at the <project> element. The only exception would be an Ant library that is assembled and used within the same build process (see below).

You can even change the default namespace (the one without a prefix) at any level you want to save some typing, for example:

    <deploy master="some-dir" xmlns="org.example">
      <slave refid="slave1"/>
      <slave hostanddir="slave3:/www"/>
    </deploy>


Be very careful when you use this. If you change the default namespace too often it can make the build file unreadable.

There are a couple of things to note:

  • The <deploy> task is not aware of XML namespace at all. It didn't have to change in order to support namespaces. You could put the application server provider's <deploy> task into a namespace of its own as easily.
  • The rules that tie namespace URIs to prefixes are defined by XML namespaces and not by Ant.
  • The URI is completely opaque, you can use whatever you want with a single exception: Ant reserves all URIs starting with "ant:" for itself.
  • URIs starting with "antlib:" are used for Ant library auto-discovery (see below).
Pitfalls

There are some common pitfalls with Ant's namespace support that should be noted.

DynamicConfigurator is an interface that allows task writers to accept arbitrary nested elements and attributes without writing methods of the required signatures. For backwards compatibility reasons this interface could not be changed to become namespace aware, Ant will always pass the qualified name (prefix plus element name) to the element creation methods. Ant 1.6.2 will include a namespace aware DynamicConfiguratorNS interface for more advanced requiremenets.

All elements discovered via Ant's reflection rules are considered part of the same namespace as the parent element by Ant. This is the reason that we need the "mylib" prefix for the slave element in

    <mylib:deploy master="some-dir">
      <mylib:slave refid="slave1"/>
    </mylib:deploy>


is what you would expect. But assume that we wanted to support a nested <dirset> as an alternative to the master attribute in <mylib:deploy>. We'd add

    public void addDirset(org.apache.tools.ant.types.DirSet ds) {
        ...
    }


to the task. Since Ant discovers the nested "dirset" element via reflection, it considers it being part of the same namespace that is used by the task and thus one must write

    <mylib:deploy master="some-dir">
      <mylib:dirset dir="some-dir"/>
    </mylib:deploy>


even though <dirset> is already defined as a data-type in the default namespace. To make things worse, Ant has added some new reflection rules (see below) that allows task writers to use

    public void add(org.apache.tools.ant.types.DirSet ds) {
        ...
    }


instead of addDirset. This alternative form lets the task support any named type that is a subclass of DirSet as a nested element. Here, using <mylib:dirset> wouldn't work since there is no <typedef> for "dirset" in the "org.example" namespace, only a <dirset> using Ant's core namespace would be allowed.

To make this situation less confusing, Ant 1.6.2 will allow both <mylib:dirset> or <dirset> when you use addDirSet or addConfiguredDirSet so that task writers can recommend using the unqualified form and don't have to expose the add method mechanism used by the task to the build file writer.

Ant will only set attributes on elements if they are in the same namespace as the element (or don't have any prefix at all). All attributes of a different namespace are ignored by Ant. This means that — unlike elements — you can use attributes coming from a namespace with no special meaning for Ant inside your build file.

Ant Libraries

So far all slaves have to be enumerated for each <deploy> task. Even though one can use references to <slave>s defined somewhere else, this still is a manual and error-prone task. A better approach is a slave collection type that can be used to define a collection in a single place and use a reference to it wherever it is needed.

Since we plan to extend this later, we use an interface to describe an abstract slave collection type

public interface SlaveCollection {
    Collection getSlaves();
}


and provide a simple list implementation

public class SlaveList extends ProjectComponent implements SlaveCollection {
    private String refid;
    private ArrayList slaves = new ArrayList();

    public SlaveList() {}

    public void setRefid(String id) {
        if (slaves.size() > 0) {
            throw new BuildException("Can't mix nested slaves with refid");
        }
        refid = id;
    }

    public void addSlave(Slave s) {
        if (refid != null) {
            throw new BuildException("Can't mix nested slaves with refid");
        }
        slaves.add(s);
    }

    public Collection getSlaves() {
        if (refid != null) {
            SlaveCollection sc = (SlaveCollection) getProject().getReference(refid);
            return sc.getSlaves();
        }
        return slaves;
    }
    
}


We then change Rsync to contain:

    public void addConfiguredSlaveList(SlaveList sc) {
        slaves.addAll(sc.getSlaves());
    }


"addConfigured" instead of "add" since we want the list to be fully functional at this point - otherwise getSlaves would always return an empty list.

Putting things together

  <project ... xmlns:mylib="org.example">
    <typedef name="slave" classname="org.example.Slave" uri="org.example"/>
    <typedef name="slavelist" classname="org.example.SlaveList" uri="org.example"/>
    <taskdef name="deploy" classname="org.example.Rsync" uri="org.example"/>
    <mylib:slave hostanddir="slave1:/www" id="slave1"/>
    <mylib:slave hostanddir="slave2:/www" id="slave2"/>
    <mylib:slavelist id="some-dir-slaves">
      <mylib:slave refid="slave1"/>
      <mylib:slave hostanddir="slave3:/www"/>
    </mylib:slavelist>
    <mylib:deploy master="some-dir">
      <mylib:slavelist refid="some-dir-slaves"/>
    </mylib:deploy>
  </project>


with three more or less identical type/taskdefs at the top. If we added loaderref into the mix there is even more chance of getting them out of sync. The resource or file attributes added to type/taskdef in Ant 1.4 would allow us to define multiple types (slave and slavelist in our example) using a single <typedef> with the help of a properties file, but we'd still have to keep the <taskdef> and <typedef> in sync.

Ant libraries provide a mechanism to group related tasks and types into a library. Ant libraries (or antlibs) use a simple XML descriptor to describe their contents. The root element is <antlib> and a few Ant tasks can be used inside it, most importantly <typedef> and <taskdef>. The example would use

  <antlib>
    <typedef name="slave" classname="org.example.Slave"/>
    <typedef name="slavelist" classname="org.example.SlaveList"/>
    <taskdef name="deploy" classname="org.example.Rsync"/>
  </antlib>


as descriptor and

    <typedef file="our-descriptor.xml" uri="org.example">
      <classpath>
        <pathelement="path-to-my.jar"/>
      </classpath>
    </typedef> 


to define all three elements in a single step inside the build file. This also ensures they'll end up in the same namespace and be loaded by the same classloader.

For classes that are available to Ant when Ant starts, we don't need the nested <classpath> element. For those we can even omit the <typedef> completely if we follow a simple naming convention for namespace URI and descriptor file name.

When Ant finds a namespace declaration for a namespace URI starting with "antlib:" it will treat the rest of the URI as a Java package name and try to load a descriptor named antlib.xml as resource from that package. I.e. for a namespace URI "antlib:org.example" Ant would try to load org/example/antlib.xml from the classloader that has loaded Ant.

So when we bundle up our rsync task and the two types in a single jar file, we put the descriptor shown above into a file named antlib.xml and add it to the jar file as well (inside the org/example directory). The build file can then be reduced to

  <project ... xmlns:mylib="antlib:org.example">
    <mylib:slave hostanddir="slave1:/www" id="slave1"/>
    <mylib:slave hostanddir="slave2:/www" id="slave2"/>
    <mylib:slavelist id="some-dir-slaves">
      <mylib:slave refid="slave1"/>
      <mylib:slave hostanddir="slave3:/www"/>
    </mylib:slavelist>
    <mylib:deploy master="some-dir">
      <mylib:slavelist refid="some-dir-slaves"/>
    </mylib:deploy>
  </project>


and there is no <typedef> left inside the build file at all.

This means Ant libraries can ship as self-contained jars. Task writers then tell users to make it available to Ant (place it in ANT_HOME/lib or $HOME/.ant/lib on Unix or %userprofile%\.ant\lib on Windows are the most common choices) and start using it by simply declaring the matching namespace.

Ant's own built-in tasks and types are part of the "antlib:org.apache.tools.ant" namespace and are treated exactly like any other antlib - the only difference is that you don't need to declare the namespace, Ant will load the descriptor automatically.

Ant's optional tasks proved that it may be non-critical if a task or types cannot be loaded. The user may not have the libraries necessary to run a particular optional task, but as long as he never tries to use the task, there is no problem. To support such optional tasks in third-party antlibs as well, a new attribute onerror has been added to <typedef> that can be used to tell Ant that it should continue if it fails to load a specific type or task.

Apart from <taskdef> and <typedef>, <macrodef> and <presetdef> are allowed inside antlib descriptors. This means you can define tasks as macros or as variants of existing tasks without making this design decision visible to the user. You can even write tasks of your own for use within the descriptor if your task extends a given base class in Ant.

If you build an antlib and want to use it within the same build file, you should not use Ant's auto-discovery since this may make Ant load an old version of your library and will probably lead to class loader problems later. In this case don't declare the namespace mapping on the project element and use an explicit <typedef> to load your antlib when it is ready to be used.

New Reflection Rules

Polymorphism in Ant 1.5

If you look into the SlaveList example you see the limited form of polymorphism that has been supported by Ant 1.5. The getSlaves method in SlaveList expects the project reference to be an implementation of SlaveCollection and doesn't assume it was a SlaveList. If we add yet another type <slavesfile> that reads the desired slave configuration from a file (details omitted) we can use it via reference:

    <mylib:slavesfile file="some/file" id="from-file"/>
    <mylib:deploy master="some-dir">
      <mylib:slavelist refid="from-file"/>
    </mylib:deploy>


Here Ant creates a slavelist instance that's only used to delegate to a different implementation of SlaveCollection. You have to resort to the same trick if you want to use Ant's built-in <classfileset>, you "tunnel" it through a <fileset>.

Polymorphism in Ant 1.6

In order to add real support for polymorphic elements, Ant has added two new methods to its reflection logic.

    public void add(X);
    public void addConfigured(X);


Any subclass (or implementation, if X is an interface) of X can be used as a nested element, as long as it is defined as an Ant type either via a <typedef> or an implicitly loaded antlib. In some way, this is an extension of Ant 1.4's TaskContainer interface, it has been generalized to arbitrary types.

If we replace the addSlave and addConfiguredSlaveList methods in Rsync with

    public void add(Slave s) {
        slaves.add(s);
    }

    public void addConfigured(SlaveCollection sc) {
        slaves.addAll(sc.getSlaves());
    }


all examples above will still work — since <mylib:slave> and <mylib:slavelist> are defined as types — but <mylib:deploy> will now accept arbitrary named subclasses of Slave and implementations of SlaveCollection as nested elements. One could now write

    <mylib:deploy master="some-dir">
      <mylib:slavesfile file="some/file" id="from-file"/>
    </mylib:deploy>


directly. In particular anybody can write an implementation of SlaveCollection of her own, <typedef> it and nest it into <mylib:deploy> without making any change to the <deploy> task.

This is not only useful for new tasks that want to provide plug-in points of their own, it has also been applied to Ant's core. The <condition> task's implementation now has a method of the signature add(Condition) so it immediately supports pluggable conditions. The same is true for tasks and types supporting nested <selector>s or <filterchain>s and their support for pluggable Selector or FilterReader implementations.

A custom condition that determines whether the current moon phase is full moon could look like

package org.example;

import java.util.Calendar;
import org.apache.tools.ant.util.DateUtils;
import org.apache.tools.ant.taskdefs.condition.Condition;

public class FullMoon implements Condition {
    public boolean eval() {
        return DateUtils.getPhaseOfMoon(Calendar.getInstance()) == 4;
    }
}


and

  <typedef name="fullmoon" classname="org.example.FullMoon"/>
  <condition property="full-moon" value="true">
    <fullmoon/>
  </condition>
  <property name="full-moon" value="false"/>
  <echo>Today is full moon: ${full-moon}</echo>


will tell you about the moon phase.

Closely related to the new reflection rules are the adapter and adaptto attributes of <typedef>. Just like TaskAdapter enables arbitrary classes to be used as Ant tasks as long as they provide an execute method of the appropriate signature, you can now define adapter classes for other Ant interfaces or types. This is useful if you want to make your classes extend from a different base class than is required by the method signature or to decouple your own implementations from Ant as far as possible.

Say we want to use conditions as file selectors, maybe we want to include some files only on full moon — or more seriously depending on the operating system running Ant. The following class will adapt arbitrary Condition implementations to FileSelectors:

public class ConditionToSelector implements TypeAdapter, FileSelector {

    private Project p;
    private Object realThing;

    public void setProject(Project p) {
        this.p = p;
    }

    public Project getProject() {
        return p;
    }

    public void setProxy(Object o) {
        realThing = o;
    }

    public Object getProxy() {
        return realThing;
    }

    public void checkProxyClass(Class proxyClass) {
        if (!Condition.class.isAssignableFrom(proxyClass)) {
            throw new BuildException(proxyClass + " is not a condition");
        }
    }

    public boolean isSelected(File basedir, String filename, File file) {
        return ((Condition) getProxy()).eval();
    }

}


With

  <typedef name="os-selector" 
           classname="org.apache.tools.ant.taskdefs.condition.Os"
           adapter="org.example.ConditionToSelector"/>


we can then use

  <fileset dir="scripts">
    <or>
      <and>
        <os-selector family="dos"/>
        <filename name="**/*.bat"/>
      </and>
      <and>
        <os-selector family="unix"/>
        <filename name="**/*.sh"/>
      </and>
    </or>
  </fileset>


to select batch files or shell scripts depending on the current operating system.

Conclusion

If you have a set of related tasks and types you should seriously consider bundling them in an Ant library. Use XML namespaces to avoid naming clashes and to allow auto-discovery of your library.

If you are writing tasks that may provide convenient extension points, use the new reflection rules instead of the older methods to define nested elements.

Footnotes:
[1] http://www.w3.org/TR/REC-xml-names/


Stefan Bodewig ( stefan.bodewig@freenet.de) is an Ant committer since 2000 and a member of the project management committees of the Apache Ant, Gump and Jakarta projects. In real life he is a senior software developer at BoST interactive in Cologne, Germany.