Saturday, July 12, 2008

Write Custom Ant Tasks

I know that there are several documents over the internet including those on the Apache Ant manual that describes adding custom tasks to your ant scripts. My focus in this article is to give a brief understanding and provide a simple guideline to add custom task definition with a few examples and provide references for more complex problems.

This article will talk about introducing your own ant task definitions which could be used in your Ant scripts for custom logic execution. To give you a brief overview of how this is accomplished, I would say that Apache Ant is built on top of a set of libraries which allow you to make use of task definitions in the Ant scripts to get your job done in an easier way. If you observe, your Ant installation comes bundled with a basic set of libraries (ANT_HOME/lib) which enables us to write useful Ant scripts. Like wise we could have our own library or a set of libraries defined and use them in the Ant scripts.

Framework Overview:

All Ant tasks inherit from “org.apache.tools.ant.Task”. This is an Abstract class which extends “org.apache.tools.ant.ProjectComponent”. The basic rule is that custom task definitions must inherit from “org.apache.tools.ant.Task” so that we could use them in the Ant Scripts. Alternatively, we could make use of other predefined tasks provided by Ant like “org.apache.tools.ant.taskdefs.JDBCTask” for tasks meant to talk to the database, “org.apache.tools.ant.taskdefs.MatchingTask” that should be used by all those tasks that require to include or exclude files based on pattern matching, “org.apache.tools.ant.taskdefs.Pack/Unpack” for processing ZIP archives and many more such tasks.

So a basic custom Task would look as given below:

package com.test;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;

public class PrintTask extends Task
{
private String strMessage;

public void execute() throws BuildException
{
System.out.println(strMessage);
}

public void setMessage(String strMessage)
{
this.strMessage = strMessage;
}
}

Method execute is used to execute the custom logic. This is where we would carry out custom logic to be executed by the task. In the above case, we are just trying to print text to the console. We also have a setter method “setMessage” which sets up the member variable “strMessage”.

At this point, let’s look at the Ant script and how the setter method is used to print text to the console.

build.xml

<?xml version="1.0"?>

<project name="TestProject" default="print" basedir=".">
<taskdef name="printtask" classname="com.test.PrintTask "/>
<target name="print">
<printtask message="Hello World!"/>
</target>
</project>

Before we analyze the ant script, we need to make sure that we compile the custom task written above and add the same to the classpath. An easy alternative would be to jar the task and place the jar file under “ANT_HOME/lib”.

If we analyze the ant script above we have added a “taskdef” element which has attributes “name” which is the name of the custom task and “classname” which is the qualified class name of the custom task. Once we have done this, we could use the custom task like any other pre existing ant tasks. To print to the console we make a call to the newly introduced taskdef with an attribute “message” with the value to print. Its that easy. :-)

Running “ant” on the “TestProject” should give you:

Buildfile: build.xml

print:
[printtask] Hello World!

BUILD SUCCESSFUL


Total time: 0 seconds

Lets look at adding some more functionality to the above task. Say, for example we need to have some conditional logic in the task based on which we need to setup an ant property to use further down in the ant scripts. How do we do that?

Here is how:

package com.test;


import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;

public class PrintTask extends Task
{
private String strMessage;
private String strPropertyName;

public void execute() throws BuildException
{
System.out.println(strMessage);

// set up the ant property here
getProject().setUserProperty(strPropertyName, "testval");
}

public void setMessage(String strMessage)
{
this.strMessage = strMessage;
}

public void setPropertyName(String strPropertyName)
{
this.strPropertyName = strPropertyName;
}

}

We have enhanced the basic example above, to include an additional member variable which lets the task know which property to setup. In the execute() method we have made use of getProject().setUserProperty(). getproject() returns us an instance of the current ant project being executed, in the form of “org.apache.tools.ant.Project”. There are several things we could do with the “Project” object. We use it to set up a user property by the name “test.property” with a value “test” for the current project.

Here is how the build.xml would look like:

build.xml

<?xml version="1.0"?>

<project name="TestProject" default="print" basedir=".">
<property name="test.property" value="testval" />
<echo message="test.property before assignment = ${test.property}"/>
<taskdef name="printtask" classname="com.test.PrintTask "/>
<target name="print">
<printtask message="Hello World!" propertyname=”test.property”/>
<echo message="test.property after assignment = ${test.property}"/>
</target>
</project>

We have enhanced the “build.xml” to include a property definition by the name “test.property” which defaults to “testval”. For the example purpose we print the value for the property twice, i.e before and after assignment. The custom print task sets up the value for the property as described above. This also shows that the custom ant task overrides the property’s default value.

Running the above example should give you this:

Buildfile: build.xml
[echo] test.property before assignment = testval

print:
[printtask] Hello World!
[echo] test.property after assignment = test

BUILD SUCCESSFUL
Total time: 0 seconds


Easy isn’t it ? :-)

Let’s do another variant of “org.apache.tools.ant.Task”now that we have seen some of the standard Ant custom tasks. The“org.apache.tools.ant.taskdefs.JDBCTask” is a handy Task provided by Apache Ant that we could use to talk to a RDBMS using Apache Ant. Here is how we would do it:

This example assumes that you have a MYSQL database setup, and has a database with a table as given below:

Table: Products

= = = = = = = = =
[ Id ] [ version]
= = = = = = = = =
[ 1 ] [ 1.1 ]
= = = = == = = =


Having said this, its really easy to use with any other RDBMS. We just need to change the driver and URL configurations to get it up and running.

Lets write the Ant Task now:

package com.test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;


import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.JDBCTask;

public class GetProductVersion extends JDBCTask {

private String strProductId;
private String strPropertyName;

private String getProductVersion() throws SQLException

{
String retval = "0";
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try

{
conn = getConnection();
stmt = conn.createStatement();
rs = stmt.executeQuery("select version from products where id = '" + strProductId + "'");

if(rs.first())
retval = rs.getString(1);
}
finally

{
if(rs != null) rs.close();
if(stmt != null) stmt.close();
if(conn != null) conn.close();
}
return retval;
}

public void setProductId(String strProductId)
{
this.strProductId = strProductId;
}


public void setPropertyName(String strPropertyName)
{
this.strPropertyName = strPropertyName;
}

public void execute() throws BuildException
{
if(strProductId == null)
throw new BuildException("ProductId must be specified", getLocation());

try

{
getProject().setUserProperty(strPropertyName, getProductVersion());
}
catch(SQLException sqlEx)

{
throw new BuildException(sqlEx, getLocation());
}
}

}

In the above task we have made use of the JDBCTask, which makes easier for us to talk to the RDBMS using ANT. The custom task doesn’t need to worry about the connection and the drivers to make the connection. These have been taken care by the JDBCTask. The execute method just implements custom logic passed on parameters passed in.

Here is how we would use the Task in the Ant Script:

build.xml

<?xml version="1.0"?>
<project name="TestProject" default="jdbctest" basedir=".">
<taskdef name="GetProductVersion" classname="com.test.GetProductVersion"/>
<target name="jdbctest">
<GetProductVersion userid="${db.userid}"
productid="1"
password="${db.password}"
url="${db.url}"
driver="${db.driver}"
propertyname="prod.version" />
<echo message="prod.version in the DB = ${prod.version}" />
</target>
</project>

What we have here is a “taskdef” for “GetProductVersion”. If you observe in the task above, we have passed in several other parameters like userid, password, url and driver other than the productid and the propertyname for which we have customized the task. These are utilized by the JDBCTask and thus allow us to just code in the SQL blocks without worrying about stuff like driver and connection. That’s why I mentioned that its easy to make use of any RDBMS. Assuming you have provided accurate values for the task params, this is what the output should look like:

Buildfile: build.xml

jdbctest:
[echo] prod.version in the DB = 1.1

BUILD SUCCESSFUL
Total time: 0 seconds


Hope this article gives you a heads up on Ant Task Customization. Having said this we could do a lot more customization and make use of Nested elements for custom tasks. Read more on that from the Ant developer Manual. The reason I don’t intend to write examples for SubAnt and supporting Nested Task elements is because, the Ant tutorial gives us a satisfactory description along with example for the same. Anybody more interested in Ant projects, could also read about the Ant Contrib. project and its features. I have been using it in many of my projects so far.


Cheers!
Kris