Java

Ant 1.7: Using Antlibs

_침묵_ 2006. 8. 30. 19:21
사용자 삽입 이미지
   
 Published onONJava.com(http://www.onjava.com/)
 http://www.onjava.com/pub/a/onjava/2006/08/09/ant-1-7-using-antlibs.html
 See thisif you're having trouble printing code examples


Ant 1.7: Using Antlibs

byKev Jackson
08/09/2006

A new release ofAntis just around the corner, so it's a good time to introduce one of the coolest new features that Java developers will soon be able to play with: antlibs. These are a better way for Java developers to create and distribute custom Ant tasks, types, and macros, and a much better way for the Ant developers to distribute the optional tasks included with the Ant distribution.

So, what was wrong with normaltaskdefs that desperately needed to be fixed?

  • Classpath lookups fortaskdefclasses--people still have problems getting JUnit to work properly with Ant.
  • No standard way to distribute custom tasks.
  • Optional tasks are tightly coupled to Ant core.

Antlibs help to alleviate these problems:

  • By default, antlibs are placed in$ANT_HOME/liband specified using XML namespaces.
  • The antlib format provides a good standard method of distribution (single .jar containing tasks, types, and macros, together with an XMLdescriptor).
  • Optional tasks can be de-coupled from the Ant core code and released on a different schedule.

Using an Antlib

If you already have an antlib, it can be used in a build file in a few different ways. Use<typedef file="example-antlib.xml"/>if the antlib is just a directory of classes and anexample-antlib.xmldescriptor. This is the most similar to a normal external Ant task.

Another approach is to use:

<typedef resource="com/mycompany/ant/example-antlib.xml"
uri="example:/mycompany.com"/>

This is appropriate if the antlib is a .jar file of classes and the antlib descriptor. This can then be used with anxmlnsdeclaration as follows:

<example:task xmlns:example="example:/mycompany.com">
...
</example>

This assumes that there's a task calledtaskdefined in theexample-antlib.xmlfile.

Finally, there's the most convenient way of using antlibs. If the following conditions are met, Ant will automatically load all definitions of tasks and types declared in the antlib:

  • The antlib .jar is placed in$ANT_HOME/lib
  • The antlib .jar contains a file calledantlib.xml
  • Thebuild.xmlis defined as:
<project xmlns:example="antlib:com.mycompany.ant">
<target name="test">
<example:task/>
</target>
</project>

Antlib Overview

Before we start creating our own antlib, let's ask ourselves: what exactly is an antlib, and what are the ingredients of one? To begin with, an antlib is simply a collection of classes bundled with an XML descriptor file. Typically, an antlib is distributed as a .jar file, but this isn't a strict requirement. The root element of the XML file is<antlib>. Any classes can go into the makeup of an antlib, but only some classes can be declared in theantlib.xmlfile. The following are allowed to be declared in theantlib.xml:

  • <typedef>
  • <taskdef>
  • <macrodef>
  • <presetdef>
  • <scriptdef>
  • Any class that subclassesorg.apache.tools.ant.taskdefs.AntLibDefinition

So now that we have an idea of what goes into an antlib, let's get our hands dirty and make one.

Creating a New Antlib

Let's start with a simple goal: wrap a command-line executable to give us a nicer interface than<exec>. For this I've decided to pick one of the many open source version control systems,Arch. First, let's get Arch installed (if it isn't already): find the correct package for your system and follow theinstallinstructions. Finally, typetla helpand you should see something similar to Figure 1.

사용자 삽입 이미지

Figure 1. GNU Arch installed

For Ant tasks that wrap around a command-line tool, a good starting point is to build a base class that handles the main setup of the environment, and then create a subclass for each command you want to support. For example, the Ant SVN library has anAbstractSvnTask, and then aSvntask that handles any commands, along withSvnRevisionDiffandSvnTagDiff, which are specialized for performing thesvn diffcommand.

Because GNU Arch is a similar tool toSubversion(ignoring the whole distributed versus client/server argument here), the same strategy of writing a base class and then creating specialized subclasses should work just as well. The only problem is that the AntSvndoesn't (yet) ship with ant.

However, we don't need to re-invent the wheel, since Ant already contains a task forCVS, which behaves in an identical manner to theSvntask (indeed, theSvntask was modeled closely on theCvstask). It also wraps a command line tool and it deals with version control, an almost perfect fit!

With the strategy of "baseclass + specialized subclasses" chosen, and an example to model our code on, we can start writing our antlib.

Because we are creating an antlib, we should put the code in a separate Java package/namespace; indeed an entirely new project seems appropriate here. I'm usingEclipse, but the choice of IDE (or to even use an IDE) has no bearing on the way an antlib is developed. For our package, I've decided onorg.apache.ant.tla, and following the example of theAbstractCvsTask, I'm naming my base classAbstractTlaTask.

The most important part of the code is shown below, therunCommand()method; all of the other code inAbstractTlaTasksets the environment up before calling this method to actually perform the work. It isn't essential for all antlibs to have arunCommand()method, but in our case, as we have to interface with a command-line tool, this is one way to achieve that level of interaction, which works well with theAbstractCvsTask. As you can see, when executing system commands we have to deal with a lot of potentialExceptions. Check out thesample codeto see the full source of thisTask.

protected void runCommand(Commandline toExecute)
throws BuildException {
Environment env = new Environment();
Execute exe = new Execute(
getExecuteStreamHandler(), null
);
exe.setAntRun(getProject());
exe.setCommandline(
toExecute.getCommandline()
);
exe.setEnvironment(env.getVariables());
try {
String actualCommandLine = executeToString(exe);
log(
actualCommandLine, Project.MSG_VERBOSE
);
int retCode = exe.execute();
log(
"retCode=" + retCode, Project.MSG_DEBUG
);
if (failOnError && Execute.isFailure(retCode)) {
throw new BuildException(
"tla exited with error code "
+ retCode + StringUtils.LINE_SEP
+ "Command line was [" + actualCommandLine + "]",
getLocation()
);
}
} catch (IOException e) {
if (failOnError) {
throw new BuildException( e, getLocation() );
} else {
log(
"Caught exception: " + e.getMessage(),
Project.MSG_WARN
);
}
} catch (BuildException e) {
if (failOnError) {
throw (e);
} else {
Throwable t = e.getException();
if (t == null) {
t = e;
}
log(
"Caught exception: " + t.getMessage(),
Project.MSG_WARN
);
}
} catch (Exception e) {
if (failOnError) {
throw new BuildException( e, getLocation() );
} else {
log(
"Caught exception: " + e.getMessage(),
Project.MSG_WARN
);
}
}
}

Now that we have code that can handle passing a command to the system, let's look at one of the commands that we must implement to have a semi-useful wrapper for Arch. The first thing that the antlib must be able to do is toregister-archive. Figure 2 shows the format of the command we need to pass to the system. The archive is in the form of a URL. We can also see that passing an archive name is an optional parameter that our task should support.

사용자 삽입 이미지

Figure 2. The format for theregister-archivecommand

Because we have most of the support code written inAbstractTlaTask, theRegisterArchiveis fairly straightforward. As you can see in the code below, we override theexecutemethod and include the parameters that are required. Before running the command,validateis called to ensure that the required parameters are present.

package org.apache.ant.tla;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.types.Commandline;
public class RegisterArchive extends AbstractTlaTask {
private String archive;
private void validate() throws BuildException {
if (null == getRepoURL() || getRepoURL().length() == 0) {
throw new BuildException(
"You must specify a url for the repo."
);
}
if (null != archive && archive.indexOf("@") == -1) {
throw new BuildException(
"If you specify an archive name"
+ ",you must specify it correctly"
+ "see the GNU arch documentation"
);
}
}
public void execute() throws BuildException {
validate();
Commandline c = new Commandline();
c.setExecutable("tla");
c.createArgument(true).setValue( "register-archive" );
if (null != archive && archive.length() > 0) {
c.createArgument().setValue(archive);
}
c.createArgument().setValue( this.getRepoURL() );
this.addConfiguredCommandline(c);
super.execute();
}
public String getArchive() {
return archive;
}
public void setArchive(String archive) {
this.archive = archive;
}
}

Compiling the Antlib

Now that we have a basic antlib prepared, we need to add the "special sauce" that makes it an antlib, and then we can get down to testing our creation! For all antlibs, we must include an XML document with our code (in the package with the class files).

<?xml version="1.0" encoding="utf-8"?>
<antlib>
<taskdef
name="registerarchive"
classname="org.apache.ant.tla.RegisterArchive"
/>
</antlib>

Since we currently only have one task defined in our antlib, we only need one entry in theantlib.xml. We don't include theAbstractTlaTaskas it is only a support class and we don't want the user to be able to access it directly. Finally, we need tojarthe antlib and place it into our$ANT_HOME/libdirectory or in our classpath.

Notealthough the antlib facility is being used extensively for Ant 1.7+, the ability to load an antlib is available in the current release version, Ant 1.6.5.

So we're done right? We've got our new antlib .jar on our classpath and we can use the defined task just like any other<taskdef>. But does it really work?

Testing Tasks and Antlibs

Traditionally, Java developers useJUnitto create their unit tests. However, testing Ant tasks and antlibs is easier if you use the facilities provided by Ant.

Instead of subclassingTestCase, Ant provides a wrapperBuildFileTest, which allows us to test an Ant task using a build file as the driver. This is a much better test of the code since it is being exercised in a similar way to how it will operate in real life. However, some people may object to calling these tests unit tests, as they are more like integration or system tests.

For ourRegisterArchivetask, we'll just bang out a quick build file to test it by eye-balling (don't worry, the sample codedoesinclude a realBuildFileTest). For a quick test and aBuildFileTest, you can use the same build file. For the tests, I've selected the Arch repository mentioned in the Arch documentation.

<?xml version="1.0" encoding="utf-8"?>
<project name="tla-test"
basedir="../../../"
default="register"
xmlns:tla="antlib:org.apache.ant.tla">
<target name="register">
<tla:registerarchive
repoURL=
"http://www.atai.org/archarchives/atai@atai.org--public/"
/>
</target>
</project>

The output of running Ant with our new antlib and the build file above looks like this:

Spikefish:~/projects/ant-tla/trunk kj$ ant -f src/etc/testcases/registerarchive.xml
Buildfile: src/etc/testcases/registerarchive.xml
register:
[tla:registerarchive] Registering archive: atai@atai.org--public
BUILD SUCCESSFUL
Total time: 3 seconds

Testing with AntUnit

Another new facility provided by Ant is theAntUnitantlib. Unlike a JUnitTestCaseor aBuildFileTest, AntUnit allows you to specify your tests without using any Java code. AntUnit itself is provided as an antlib, so it must be placed in your$ANT_HOME/libdirectory, or specified on the classpath. In a recursively friendly way, AntUnit is used to test both Ant and AntUnit!

Since we have a build file that we have already used for ad hoc testing, let's modify it to use AntUnit and introduce a much more repeatable test. First, we must declare the AntUnit namespace, so the header of our build file becomes:

<project name="tla-test"
basedir="../../../"
default="go"
xmlns:tla="antlib:org.apache.ant.tla"
xmlns:au="antlib:org.apache.ant.antunit">

AntUnit works in a similar way to JUnit and looks for targets which start withtest, so we must change ourregistertarget totest-register. Like JUnit, AntUnit also needs some way of asserting if the test passed. In this case, we'll use theassertLogContainsmacro to check if the output is what we are expecting. Here's what our newtest-registerlooks like:

<target name="test-register">
<tla:registerarchive
repoURL=
"http://www.atai.org/archarchives/atai@atai.org--public/"
/>
<au:assertLogContains text="Registering archive:"/>
</target>

The final addition is the driving targetgo. We need to set up AntUnit in this target:

<target name="go">
<au:antunit>
<fileset dir="src/etc/testcases"
includes="au-registerarchive.xml"/>
<au:plainlistener/>
</au:antunit>
</target>

As you can see below, our test passes. Adding another test is simply a case of adding a new target with a name beginning withtest. No compilation, no Java: a much simpler way of testing Ant tasks.

Spikefish:~/projects/ant-tla/trunk kj$ ant -f src/etc/testcases/au-registerarchive.xml
Buildfile: src/etc/testcases/au-registerarchive.xml
go:
[au:antunit] Build File: /Users/kj/projects/ant-tla/trunk/src/etc/testcases/au-registerarchive.xml
[au:antunit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 3.378 sec
[au:antunit] Target: test-register took 3.329 sec
BUILD SUCCESSFUL
Total time: 7 seconds

Refactoring Optional or Custom Tasks

Finally, lets look at how much work there is involved in refactoring a standard custom task to change it into an antlib.

Let's start with thecodeof the original VSS task. What is the minimum amount of code required to change it into an antlib? Actually, just adding one file,antlib.xml, is all that is required:

<antlib>
<taskdef name="vss"
classname="org.apache.ant.vss.MSVSS"
/>
<taskdef name="add"
classname="org.apache.ant.vss.MSVSSADD"
/>
<taskdef name="checkin"
classname="org.apache.ant.vss.MSVSSCHECKIN"
/>
... (code elided)
</antlib>

Of course, we also want to decouple the optional task from the main Ant source so that we can ship fixes to the optional tasks (now antlibs) independently from releasing a full Ant distribution. In reality, all this requires is a change of package namespace and, along with theantlib.xmlfile, the optional task becomes a decoupled antlib!

Recap

We have seen just two of the main new features of Ant 1.7: antlibs and AntUnit.

The antlib feature allows developers of Ant tasks to ship fixes and updates independently of the main Ant distribution, and the AntUnit antlib is a much quicker method of creating unit tests for Ant tasks.

In this article we've written the start of an antlib for the SCM system Arch, showing how easy it is to develop new tasks with the antlib mechanism. We also tested this antlib with an AntUnit test.

Resources

Kev Jacksonis a software developer and a committer on the Apache Ant project.


Return toONJava.com.

Copyright © 2006 O'Reilly Media, Inc.

'Java' 카테고리의 다른 글

Getting High Performance from Your Desktop Client  (0) 2006.09.08
Sun Java Development Kit on FC5  (0) 2006.08.29
Runtime.exec()  (1) 2006.07.08