ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Ant 1.7: Using Antlibs
Pages: 1, 2

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



register-archive command
Figure 2. The format for the register-archive command

Because we have most of the support code written in AbstractTlaTask, the RegisterArchive is fairly straightforward. As you can see in the code below, we override the execute method and include the parameters that are required. Before running the command, validate is 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 the antlib.xml. We don't include the AbstractTlaTask as it is only a support class and we don't want the user to be able to access it directly. Finally, we need to jar the antlib and place it into our $ANT_HOME/lib directory or in our classpath.

Note although 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 use JUnit to create their unit tests. However, testing Ant tasks and antlibs is easier if you use the facilities provided by Ant.

Instead of subclassing TestCase, Ant provides a wrapper BuildFileTest, 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 our RegisterArchive task, we'll just bang out a quick build file to test it by eye-balling (don't worry, the sample code does include a real BuildFileTest). For a quick test and a BuildFileTest, 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 the AntUnit antlib. Unlike a JUnit TestCase or a BuildFileTest, 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/lib directory, 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 with test, so we must change our register target to test-register. Like JUnit, AntUnit also needs some way of asserting if the test passed. In this case, we'll use the assertLogContains macro to check if the output is what we are expecting. Here's what our new test-register looks 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 target go. 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 with test. 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 the code of 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 the antlib.xml file, 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 Jackson is a software developer and a committer on the Apache Ant project.


Return to ONJava.com.