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