Build scripts with Groovy and Ant

Enhance your automated build process with powerful and flexible Groovy scripts

Ant has been widely celebrated as the future of build processing. In fact, it represents a great improvement over previous make tools: it is extendible, multiplatform, and standard-based. Some Ant characteristics particularly make it the tool of choice:

  • It is simple to use, even for junior developers, thanks to its XML syntax
  • It is reasonably fast
  • It is extensible
  • It offers good support for J2EE packages and practices
  • It is integrated out-of-the-box with several popular tools, such as JUnit, and with most important servers in the market

It has achieved first place in nearly all existing J2EE projects for everything related to process automation: building, packaging, testing, and deploying software.

However, as a consequence of this wide adoption and usage in such different (and sometimes unexpected) scenarios, Ant has some disadvantages over a more traditional building process based on scripts. First of all, though you can do most tasks with Ant, in some situations, it is simpler and quicker to use a more traditional scripting language (like Python, Perl, or Ruby) to perform specific jobs like connecting to servers and applications or processing files. As a result, you often see a mixture of XML build files and script batch files in project automation tools.

Moreover, the XML syntax doesn't allow us to use an object-oriented language's most common and useful characteristics, such as inheritance and polymorphism; in simple terms, you cannot extend (in the object-oriented meaning) an XML build file or write polymorphic XML files. These limitations result in every complex project having a complex build system.

The problem seems to be the approach: with a script-centric system, you have a flexible and powerful instrument, but you need to know the script language very well; on the contrary, with an XML and platform-independent approach, you have simplicity and reusability, but a bit less power.

But we want it all, and now. What's the solution? A script language with a native and simple support for XML syntax. Groovy.

The GroovyMarkup

Groovy is a new dynamic scripting language for the JVM, similar to famous scripting languages like Python and Ruby, but, at the same time, easy to learn and use by Java developers. The good news is that its standardization is underway in the Java Community Process as Java Specification Request 241, so in the future, it is going to be the second (after Java) official language for the JVM.

Note: Since this article is not an introduction to Groovy, it won't cover the basics of this language; you can find a list of introductory articles in Resources.

Groovy natively supports different markup languages like XML, HTML, and SOAP, thanks to a feature called GroovyMarkup. This means it defines a specific syntax to express tree-like structures. For example, the simple XML structure:

<book isbn="1234-5678" category="A-123">
   <author>Paul Jones</author>
   <title>The zen and the art of the scripting</title>
   <publisher>XYZ Pub. Co.</publisher>
   <year>2004</edition> 
</book>

can be described with the following Groovy script:

xml = new groovy.xml.MarkupBuilder() 
book = xml.book(isbn: "1234-5678", category: "A-123") {
   author("Paul Jones") 
   title("The zen and the art of the scripting")
   publisher("XYZ Pub. Co.")
   year("2004")
}
println book #to see the result

Though simple, the example clearly shows how the Groovy syntax allows us to build a complex XML tree using all the facilities offered by an object-oriented scripting language: classes, loops, and if/then conditions.

How is this nice feature related to Ant and software project automation? Well, Ant build files are XML files.

Groovy and Ant

Integrating Groovy with Ant is no more difficult than using the GroovyMarkup, since we also deal with XML in this case. Groovy provides a helper class called AntBuilder to define the build file directly inside our scripts.

Consider a simple project with the structure presented in the figure below.

Directory structure for our DemoProject. Click on thumbnail to view full-sized image.

This simple project's Ant build file is the following:

<?xml version="1.0"?>
   <project name="DemoProject" basedir="." default="build">
      <property name="src_dir" value="src"/>
      <property name="lib_dir" value="lib"/>
      <property name="build_dir" value="classes"/>
      <property name="dist_dir" value="dist"/>    
      <property name="file_name" value="whoami"/>
      <path id="master-classpath">
         <fileset dir="${lib_dir}">
            <include name="*.jar"/>
         </fileset>
         <pathelement path="${build_dir}"/>
      </path>
      <target name="clean">
         <delete dir="${build_dir}"/>
         <delete dir="${dist_dir}"/>
      </target>
      <target name="build" description="Compile main source tree java files">
         <mkdir dir="${build_dir}"/>
         <javac destdir="${build_dir}" debug="true" failonerror="true">
            <src path="${src_dir}"/>
            <classpath refid="master-classpath"/>
         </javac>
      </target>
      <target name="dist" depends="clean, build">
         <mkdir dir="${dist_dir}"/>
         <jar basedir="${build_dir}" destfile="${dist_dir}/${file_name}.jar" />
      </target>   
   
   </project>

The build.xml file shown above allows us to do all the basic operations needed in every project. It defines three targets:

  • clean: Deletes all folders containing compiled code
  • build: Compiles every Java source file in the src folder and puts the results in the classes folder
  • dist: Creates a jar file named whoami.jar in the dist directory

Now let's examine how the same build procedure can be expressed using Groovy:

class Build {
   ant = new AntBuilder()
   base_dir = "path/to/my/DemoProject/"
   src_dir = base_dir + "src"
   lib_dir = base_dir + "lib"
   build_dir = base_dir + "classes"
   dist_dir = base_dir + "dist"
   file_name = "whoami"
   classpath = ant.path {
      fileset(dir: "${lib_dir}"){
         include(name: "*.jar")
      }
      pathelement(path: "${build_dir}")
   }
   clean() {
      ant.delete(dir: "${build_dir}")
      ant.delete(dir: "${dist_dir}")
   }
   build() {
      ant.mkdir(dir: "${build_dir}")
      ant.javac(destdir: "${build_dir}", srcdir: "${src_dir}", classpath: "${classpath}")       
   }
   jar() {
      clean()
      build()
      ant.mkdir(dir: "${dist_dir}")
      ant.jar(destfile: "${dist_dir}/${file_name}.jar", basedir: "${build_dir}")
   }
   static void main(args) {
      b = new Build()
      b.run(args)
   }
   void run(args) {
      if ( args.size() > 0 ) { 
         invokeMethod(args[0], null )
      }
      else {
         build()
      } 
   }
}

The properties defined at the beginning of build.xml now become our class fields, while the various tasks are now regular Java methods. These methods contain calls to the standard Ant tasks using the GroovyMarkup as discussed in the previous section. For instance, a call to the javac task is now equivalent to a call to the AntBuilder class's javac() method.

Two additional methods (main() and run()) are added to the Build class to make it executable. run()'s purpose is to take the argument passed by the user as the target to execute; if no argument is passed, the default target build is performed.

To try your Groovy script, first save it as build.groovy and then simply run it, typing groovy build.groovy jar in a shell window; the output will be the same as a traditional Ant execution, and a jar package of your application will be created.

Though this example is simple, the main advantage of this approach is clear: you can now mix ordinary Ant tasks with custom script procedures in your build scripts, making it possible to perform every arbitrary complex task you need while using only one tool.

Other (perhaps more important) advantages are related to the object-oriented nature of the Build class; I address them in the following section.

Advanced use

With a language like Groovy that supports dynamic typing, polymorphism is a simple and straightforward game; the interesting thing is that polymorphism is actually possible without inheritance.

Let's suppose we want to start and stop a Web application with our build procedure. We can begin writing a separate Groovy script for starting/stopping a Web application powered by Tomcat, the open source servlet container:

class Tomcat
{
   ant = null;
   url = "http://localhost:8080/manager"
   username = "tomcat"
   password = "tomcat"
   path = "/tomcat-docs"
   start() {
    ant.taskdef(name: "start", classname: "org.apache.catalina.ant.StartTask")
    ant.start(url: "${url}", username: "${username}", password: "${password}", path: "${path}")   
   }
   stop() {
    ant.taskdef(name: "stop", classname: "org.apache.catalina.ant.StopTask")
    ant.stop(url: "${url}", username: "${username}", password: "${password}", path: "${path}")      
   }
}

The simple script has two methods for starting and stopping a Web application. Internally, it uses the taskdef Ant task to import two tasks shipped with Tomcat called StartTask and StopTask. Obviously, all these tasks are invoked using the GroovyMarkup syntax.

In the same way, we can define other scripts for starting and stopping applications with other servlet containers (Jetty, for example). Keep in mind to use the same conventions, providing:

  • An ant field
  • A start() method
  • A stop() method

That's the way we can write polymorphic classes with Groovy without using inheritance. Inheritance is only a means to achieving polymorphism, but in the context of dynamic languages, it is not strictly necessary.

Now we need to enhance our Build class so it can start and stop applications of an arbitrary server. Inside Build's code, we add the following snippet:

...
server_script = base_dir+"/tomcat.groovy"
server = initServer()
initServer()
{
   Class clazz = new GroovyClassLoader().parseClass(new java.io.File(server_script))
   GroovyObject obj = (GroovyObject) clazz.newInstance()
   obj.ant = ant
   return obj   
}
start()
{           
   server.start()
}
stop()
{   
   server.stop()
}
...

The server_script field points to the script that contains the actual implementation of functionalities related to the adopted application server; if we want to change our server, we simply need to modify the script name. Note the init_server method: it loads the script, creates an instance of a GroovyObject (the base class of every object defined with Groovy), and initializes the ant field with the current AntBuilder object.

The two most important methods, start() and stop() delegate to only the right server script the responsibility for starting and stopping the Web application.

You can adopt the same technique for abstracting the behavior of other components like version control systems (CVS, Subversion, or others), databases, and issue tracking systems.

The result of this approach is a more general and well-written build script that is also more reusable and maintainable.

Conclusion

The combined use of Groovy and Ant shows some interesting benefits, especially in projects with complex and highly automated build processes. Thanks to its flexibility and power, the Groovy-Ant combination can become the platform of choice for build automation in the near future, thus simplifying the tangle of XML files and custom build scripts (often written in several different languages) frequently adopted in our projects.

However, it is important to remark that Groovy's implementation is not yet as stable and reliable as required in production environments, since it is still undergoing considerable development.

So, let's wait for Gravy (Groovy+Ant) to become the next big thing in our software automation routines.

Filippo Diotalevi is an IT consultant in Milan, Italy, where he works mainly as a J2EE application developer. His chief areas of interest are software design, patterns, aspect-oriented programming, and agile methodologies. He has authored several technical articles and is founder of the Java User Group Milano.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies