Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Create Your Own Extensible Apache Ant Framework

DZone's Guide to

Create Your Own Extensible Apache Ant Framework

· DevOps Zone
Free Resource

Download “The DevOps Journey - From Waterfall to Continuous Delivery” to learn learn about the importance of integrating automated testing into the DevOps workflow, brought to you in partnership with Sauce Labs.

The technology I'm going to talk about here might be a little old fashioned, but I think I'll be presenting something interesting to some people, specially those starting to automate their projects' usual tasks, like compiling, testing, deploying, etc; it seems like everyone starting to do this chooses Apache Ant as their main tool, and that tool is exactly what I use in this article.

I'm going to show you a way to create an extensible framework for defining and executing chained Ant tasks. A full version of the framework can be downloaded here.

A typical scenario for the framework is the following: you need to execute a sequence of operations in order to deploy your project, say download it from a VCS, compile it, test it, generate documentation, back up the database, package everything and deploy it. You want to do all this by typing a simple command that triggers the sequence. For instance you want to say: execute deploy. And that's it. The complete chain of operations up to deploying your app is executed, one after another.

Well, we are going to achieve this using Ant as our main tool.

RATIONALE

First, we need to notice that these tasks are not isolated, but they depend on each other. You can't start compiling your code before you have downloaded it from your VCS. So there is a dependency among these tasks that can be pre-elaborated. Ant allows us to establish dependencies among tasks easily. Some tasks might be isolated (no dependencies on other tasks), but in general tasks depend on each other.

The second thing we must be aware of is that, if we are going to create a framework, we must try to cope with (closely) all the cases that might present to us. I mean, you can't expect everyone will download their source code from a Git repo, because there are some people still using Subversion. Or compile their project with javac, because there are other compilers and languages. So we must create a framework that accepts all kind of tasks. But people do perform general tasks, like 'Download from a VCS', or 'Compile code', or 'Backup the database' either it is PostgreSQL or MongoDB. So these tasks can be at least grouped in categories.

The third thing to be concerned of is that people do not need to execute ALL the tasks in one run, or at all. There are people who don't do tests. Or apps that don't need a data backend. So there must be a way to define exactly what tasks you want to run.

Up to here we need three things:

  • Create task categories, and specific tasks inside these categories, like: category 'Backup the database' conveys tasks 'Dump PostgreSQL database' or 'Backup SQLite database'; or 'Deploy app' means 'Upload to Http server' or 'Transfer to FTP server'.
  • Define the tasks dependencies. Or better, the dependencies between the tasks categories, like: 'Compile code' goes after 'Download from a VCS'.
  • Define exactly what tasks we want to execute out of the entire universe of specific tasks, like: I want to 'Export code from Github', 'Compile code with gcc' and 'Deploy app to Http server' (no testing, really?).

So, let's get our hands dirty.

TASKS AND CATEGORIES

Our categories and their tasks are going to be established based on directories. I'm going to refer to the base directory as './'. We are going to have a directory for tasks called './tasks/'. So, if we have the category 'compile' and the task 'compile-javac' (yes, let's start to use more robotic names; computers like them :-)), then we'll have a directory like './tasks/compile/compile-javac/'.

Each specific task will define its own way of executing itself inside a build.xml file that defines the task, and is located in the task's own directory. So we will have a file like ./'tasks/compile/compile-javac/build.xml'. 

For now, let's define specific tasks as dummy tasks. They will only print an explanation of what they are supposed to do. Our ./'tasks/deploy/ftp-trasfer/build.xml' file would look like this:

<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="ftp-transfer" default="default">
    <target name="default">
		<echo message="doing deploy - ftp transfer"/>
	</target>
</project>

The build file for a specific task is a self-contained build file; that is, it is defined as a regular Ant build file with targets that contain all the invocations the task needs to perform its duty, for example, executing pg_dump utility to backup a PostgreSQL database.

DEFINING DEPENDENCIES AMONG CATEGORIES

Now we need to define dependencies among tasks categories. These dependencies will be expressed as regular Ant targets' dependencies, each target corresponding to each category. 

Dependencies will be defined inside the file './tasks/dependencies.xml', which is an Ant build file. For now, let's see an example of what this file could contain:

<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="dependencies" default="do.all">    

	<target name="do.export">
		 <ant dir="${basedir}/export/${export.specific.task}"
			     inheritAll="false"/>
	</target>

	<target name="do.compile"
			depends="do.export">
		 <ant dir="${basedir}/compile/${compile.specific.task}"
			     inheritAll="false"/>
	</target>
</project>

What we do in each target here is invoke a remote target; we invoke the target declared as default in the build.xml file located at a specific task's directory. For instance, the target 'do.compile' executes the default target in the directory './tasks/compile/${compile.specific.task}', where 'compile.specific.task' will be the name of the specific task to execute (i.e. 'compile-javac').

DEFINING THE TASKS WE WANT TO EXECUTE

Defining the tasks we want to execute is as simple as loading inside './tasks/dependencies.xml' a properties file that defines these tasks, and executing each target depending on the condition that a task for its corresponding category has been defined. In Ant it can be done like this:

<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="dependencies" default="do.all">
    
<!-- Load specific tasks from specific-tasks.properties -->
	<dirname property="basedir" file="${ant.file.dependencies}"/>
	<property file="${basedir}/specific-tasks.properties"/>
	
    
	<target name="do.export" 
   if="export.specific.task">
		 <ant dir="${basedir}/export/${export.specific.task}"
			     inheritAll="false"/>
	</target>

	<target name="do.compile"
			depends="do.export"
   if="compile.specific.task">
		 <ant dir="${basedir}/compile/${compile.specific.task}"
			     inheritAll="false"/>
	</target>
</project>

Notice how I load the file './tasks/specific-tasks.properties' and then create if clauses to execute the targets or not. Also, notice that I follow certain conventions for naming the properties that define the specific tasks; this is good practice, but you could name them as you want as long as you check them against the correct name in each target.

An example of a specific-tasks.properties file is like this:

export.specific.task=git-export
compile.specific.task=javac-compile
test.specific.task=junit-test
build.specific.task=custom-build
database.specific.task=postgres-dump
doc.specific.task=javadoc
deploy.specific.task=ftp-transfer

WIRING EVERYTHING TOGETHER: THE FRAMEWORK

All we have now is a bunch of Ant files and targets. Yes, './tasks/dependencies.xml' has some nice stuff inside, because it is capable of executing sequential specific tasks by iterating through category targets in a chained manner. But how do we trigger the chain?

We only need to invoke the 'trailing' category we want to execute. If you want to execute tasks up to testing, you would invoke the 'test' category. For this, I created a batch script called 'invoke.bat' that contains the following:

@echo off
set script_dir=%~dp0
set script_dir=%script_dir%##
set script_dir=%script_dir:\##=##%
set script_dir=%script_dir:##=%
set ANT_HOME=%script_dir%/3p/ant
set PATH=%PATH%;%ANT_HOME%/bin
set ant_lib_extras=%script_dir%/3p/ant/lib/extras
set category=%1
set target=do.%category%
cd ..
ant -quiet -lib "%ant_lib_extras%"  -buildfile tasks/dependencies.xml %target%
pause
NOTE: The framework has Ant bundled within it so that it can be sure that Ant exists and avoid some work to you. It also contains a Linux version of the script.

You can execute the script like this: invoke deploy

The output of running it with the sample specific-tasks.properties file above should be like:

[echo] doing export - git export
[echo] doing compile - javac compile
[echo] doing test - junit test
[echo] doing build - custom build
[echo] doing database - postgres dump
[echo] doing doc - javadoc
[echo] doing deploy - ftp transfer

If you run invoke test, then the output is like:

[echo] doing export - git export
[echo] doing compile - javac compile
[echo] doing test - junit test

PSEUDO-CODE MIGHT EXPLAIN BETTER

For a better understanding of what's happening under the hood some pseudo code might work:

function invoke(category) {
    // Let Ant figure out the execution order of category targets up to category
    orderedCategoriesList = AntCreateOrderedCategoriesExecutionUpTo(category, './tasks/dependencies.xml');

    foreach(cat in orderedCategoriesList) do
        if isset '$cat.specific.task' in './tasks/specific-tasks.properties' then
	    execute default target in './tasks/$cat/${$cat.specific.task}/build.xml'
	else continue
    end
}

EXTENDING THE FRAMEWORK

You can create new tasks inside the categories already defined by just creating a new directory for that task inside the corresponding category folder, and create its 'build.xml' file.

To create a new category you should create a folder with the name of the category inside the './tasks' directory, and the folders and 'build.xml' file for each specific task. Then you should create a target for this category in 'dependencies.xml', and define or reassemble all the dependencies.

To invoke a new task or category, just edit 'specific-tasks.properties' and run the script.

CONCLUSIONS

The framework is based on Ant's basic features, so it's very simple. It relies on a pre-elaborated directory structure to define categories and tasks hierarchies; the 'dependencies.xml' to define dependencies between tasks categories; and a simple script that manages to trigger the chain of tasks you ask for. You can try more on your own by downloading the framework.

Discover how to optimize your DevOps workflows with our cloud-based automated testing infrastructure, brought to you in partnership with Sauce Labs

Topics:

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}