You have a task that your Ant build process needs to perform
and none of the built-in or dozens of optional tasks fits the bill.
If at this point you're thinking that Ant won't work for you, then
the authors of Ant have some wonderful news. The framework they use
to run built-in tasks is also available for your own task.
If that piques your interest, you'll be happy to know that in
the next few paragraphs, I promise you'll have all the information
you need to use this framework. If you haven't used Ant yet, read the
excellent article by Joey Gibson, "A (Brief) Introduction to Ant"
(JDJ, Vol. 7, issue 11), before venturing on this journey.
The framework that supports Ant loosely defines a contract
that describes the responsibilities of the task provider. Briefly
stated, the task provider provides a class that implements the
desired behavior and introduces this task and its supporting class to
Ant, exploiting one of the means available for that purpose. The
framework is responsible for discovering the task's supporting class
and then managing that class's life cycle. Although a task is fronted
with a single class, other classes (which may or may not be known to
Ant) may be used to help support the completion of the task. Let's
take an in-depth look at the custom task contract defined by Ant.
For a class to be considered a custom task it must implement
the method, public void execute(). That's it! If you're wondering
what the catch is, it's pretty much the same as with everything else.
The more information you provide, the less work the framework has to
do in order to work with your task. Providing additional information
allows the framework to do more work for you. With this in mind,
let's describe the portion of the contract we're concerned with here.
The description of the providers' responsibilities are as follows:
Named task must be supported with a Java class.
The class must implement public void execute().
The class may extend import org.apache.tools.ant.Task.
The class may implement public void init().
The class must implement a set method for each attribute of the task.
The class should implement the default constructor.
The name task must be registered with either the taskdef task
or via the addition of an entry into the task.properties file.
The execute method should throw a org.apache.tools.ant.
BuildException.Complementary to the providers' responsibilities are the
frameworks' responsibilities:
Call the default constructor for the provider's supporting class.
Call the public void init() method once.
Call the appropriate set method for every task attribute.
Call the public void execute() throws BuildException method
once for every occurrence of the corresponding task found in the
build file.
In addition to those assuming these responsibilities, the
provider must define a set of XML tags to be used when exploiting the
custom task. From here, we could proceed to a fairly dry explanation
of how all this fits together, but let's see how it all works in
practice instead. I'll kick start the process with the following
story.
Most applications built today contain a number of
"off-the-shelf" components that complement code developed in-house.
These components can be supplied by a combination of vendors and
infrastructure teams that work alongside the application development
team. Given that each group (especially third-party suppliers) will
have its own delivery schedule over which you most likely won't have
any influence, it would be unheard of not to have to deal with
version management. Lack of a concise plan to manage versions often
results in confusion among configuration management staff that can
result in them having to spend a considerable amount of time sorting
out which versions of which components (a.k.a. JARs) are to be used
for each build. A tactical solution to this problem is to use a
version map to describe the build. Let's first build a sample map and
then a custom task to use this map to drive a build process.
To keep things somewhat simplified so we aren't too
distracted from our main goal - learning how to build a custom task -
we'll purposely limit the functionality of the version map to pulling
artifacts from CVS. The test application will be a toy chat
application that consists of four layers (see Figure 1).
The JMS bus was acquired from a third-party vendor (SwiftMQ
in this instance). Our internal infrastructure group is responsible
for the channel component. Finally, the application development team
has built chat client and topic server. During the development
process, they've also identified a common layer shared by the clients
and the server. In accordance with our earlier constraints, the JMS
implementation can be found in the file system. The JAR file for the
channel, as well as the source for commons, the chat client, and the
topic server will be pulled from CVS. Let's use XML to build the
version map.
<map application="chat" version="1.0">
<component source="cvs" name="channel" version="cvs_label" dest="./lib"/>
<component source="cvs" name="common" version="cvs_label"
dest="./com mon"/>
<component source="cvs" name="chat_server" version="cvs_label"
dest="./serv er"/>
<component source="cvs" name="chat_client" version="" dest="./client"/>
</map>
The map contains all the information needed to identify all
the attributes that are necessary to locate a component. Note: This
mechanism does rely on a stable policy of how code artifacts are to
be stored.
As mentioned earlier, we're required to register the custom
task. This can be completed using one of two techniques. The taskdef
task maps an alias for a task to its implementation. The second
technique inserts the definition into the task.properties file found
in the Ant distribution. A quick examination of the file will reveal
how to do this. I'm going to avoid the discussion as to which
approach is best, but there are implications from using either
mechanism to register your custom task that go beyond technical
issues. Let's look at the build file fragment found in the following
code.
<taskdef name="versionmap" class="com.jpt.ant.task.VersionMap">
<target name="version">
<versionmap file="chat.xml" dest="." />
</target>
This is telling Ant that we'd like to define the custom task
aliased as versionmap and implemented in the class
com.jpt.ant.task.VersionMap. The task versionmap takes two
attributes, the name of the file containing the map and a destination
directory to work in.
Again referring back to the contract, we see that we should
create a class that implements our custom behavior. Though it's not
necessary, it makes life a lot easier to have our class extend
org.apache.tools.ant.Task. Extending this class allows us to inherit
common task behavior. We'll have to decide on an implement for the
methods public void init() and public void execute(). In addition,
the XML defines the two attributes, file and dest. Fields and supporting access methods must be implemented to support these attributes. Let's look at the custom
task class template found in Listing 1.
With this, we've pretty much covered the entire contract. All
that's left is to fill in the task's behavior. Let's start with the
init method. The init method gets called once and this occurs fairly
early on in the build process. Consequently, we should embed behavior
that we only want executed once and that can also occur early in the
build process. Since our custom task doesn't have any behavior that
fits this description, we allow the call to percolate up to the super
classes implementation.
A common technique used for coding the execute method is to
defer the bulk of the implementation to a helper method called
execute0. Doing this allows us to easily handle the inherited
attribute, failonerror, in the execute method. The main logic that we
need is to read each entry in the version map and use the information
to trigger a CVS get (see Listing 2). Note how the functionality of
other tasks is being used to help us achieve our goal.
In the execute0() method, the XML file is converted into a
map. An iterator runs through each entry in the map and calls its
execute method using the task as a parameter. The execute method of
the task is then free to make a call back on the getSourceFromCVS
method. It's this method that creates an instance of a CVS (from the
optional tasks list) task and then configures it. The executeTask
method sets the technical parameters on the task before executing it.
Our simplified custom task is now complete.
Where to Go from Here
Nested attributes can add a lot of flexibility to your custom
task. Each Ant data type object is supported by a class and,
consequently, the development path is fairly similar to one that's
followed by custom tasks. In addition to tasks and data types, you
can also build specialized build listeners. Though their development
path is slightly different than that of data types and tasks, it's
still not all that difficult. I've always found that it's best to
look into Ant's code when I'm looking for examples or techniques to
help me develop custom tasks, data types, and listeners. Happy AntingS
References
Ant 1.5.1 source code: www.apache.org
Williamson, A., et al. (2003). Ant Developer's Handbook. Sams.
About The Author
Kirk Pepperdine is the chief technical officer at Java Performance
Tuning.com and has been focused on object technologies and
performance tuning for the last 15 years. Kirk is a co-author of Ant
Developer's Handbook (Sams).
kirk@javaperformancetuning.com
"Customizing Ant"
Vol. 8, Issue 9, p. 38
Listing 1 Code template for a custom task
import org.apache.tools.ant.Task;
import org.apache.tools.ant.BuildException;
public class VersionMap extends Task {
private String file;
private String dest;
public VersionMap() {
}
public void setFile(String file) {
this.file = file;
}
public void setDest( String dest) {
this.dest = dest;
}
public void execute() throws BuildException {
try {
execute0();
} catch (BuildException be) {
if (failonerror)
throw be;
else
log(be.getMessage());
}
}
}
Listing 2 Triggering a get from CVS
public void execute0() throws BuildException {
try {
this.createSourceDir( this.dest);
VersionMap map = VersionMap.createFrom( this.file);
Iterator iter = map.iterator();
while ( iter.hasNext())
((VersionMapEntry)iter.next()).processMapEntry( this);
} catch (Exception e) {
throw new BuildException(e);
}
}
private void executeTask(Task task) throws BuildException {
task.setProject(this.getProject());
task.setLocation(this.getLocation());
task.setOwningTarget(this.getOwningTarget());
task.init();
task.execute();
}
public void getSourceFromCVS(String moduleName, String version, String dest)
throws BuildException {
Cvs task = new Cvs();
task.setCommand("checkout");
task.setPackage(moduleName);
task.setTag(version);
task.setDest(dest);
this.executeTask(task);
}
All Rights Reserved
Copyright © 2004 SYS-CON Media, Inc.
E-mail: info@sys-con.com
Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.
|