In this post, I focus on writing a Groovy script that takes an existing Ant build file (often called
build.xml
) as input and generates a Groovy script that executes the same tasks defined in the build file. A few things about Groovy make this generation of code easier and I mention them as relevant in the post.Writing code that generates code has been a hallmark of the lazy efficient developer for many years. Code that writes code can be used to replace mundane code writing with efficient and highly repeatable code generation. Code generators can take a while to build, but if the code they generate is large or commonly generated, the cost can be recovered in terms of future savings. Code generation is very common in the DBA/SQL world, but it is also common in software development. Hamlet D'Arcy's presentation Code Generation on the JVM: Writing Code that Writes Code covers various techniques for writing code that generates code on the JVM. I earlier reviewed the JavaOne 2010 version of his presentation given by Andres Almiray.
Groovy is particularly effective at writing Java and other Groovy code. I have demonstrated this with a few examples in posts such as Groovy: Java Enum Generation from XML and Reproducing "code too large" Problem in Java. In this post, I again look at using Groovy to generate other code. In this case, I generate Groovy code that performs similar functionality as an input Ant build file.
If one only wanted to execute the targets of the Ant file, I probably would just use Ant. If I wanted to run the targets as-is from an existing Ant file, I would probably use the approach I discussed in Executing Ant Build File Targets from Groovy. However, if I would like my Groovy code to execute some new targets in addition to or adapted from existing Ant build files, a good starting point would be a script like that I cover in this post. The code in this post could be extended to convert an existing Ant build file into Groovy code that could then be altered and enhanced as desired. This might be easier than trying to write the Groovy build code from scratch.
The source Ant build file used for the example in this post is called
build.xml
and is contained in the next code listing. It's a simple build file with some basic targets, but it is enough to demonstrate the concept of generating a Groovy script that performs the same as an Ant build file. Note that what the application this Ant file builds is not so important for what is covered here, but its related blog post is Effective Java NullPointerException Handling.build.xml
<?xml version="1.0" encoding="UTF-8"?> <project name="AvoidingNullPointerExceptionInJava" default="all" basedir="."> <description>Avoiding NullPointerException in Java</description> <property name="javac.debug" value="true" /> <property name="src.dir" value="src" /> <property name="dist.dir" value="dist" /> <property name="classes.dir" value="classes" /> <property name="javadoc.dir" value="${dist.dir}/javadoc" /> <property name="jar.name" value="AvoidingNullPointerExceptionInJava.jar" /> <property name="jar.filesonly" value="true" /> <path id="javadoc.example.classpath" /> <target name="-init"> <mkdir dir="${classes.dir}" /> <mkdir dir="${dist.dir}" /> </target> <target name="compile" description="Compile the Java code." depends="-init"> <javac srcdir="${src.dir}" destdir="${classes.dir}" classpathref="javadoc.example.classpath" debug="${javac.debug}" /> </target> <target name="jar" description="Package compiled classes into JAR file" depends="compile"> <jar destfile="${dist.dir}/${jar.name}" basedir="${classes.dir}" filesonly="${jar.filesonly}"> </jar> </target> <target name="all" description="Compile Java source, assemble JAR, and generate documentation" depends="jar, javadoc" /> <target name="javadoc" description="Generate Javadoc-based documentation"> <mkdir dir="${javadoc.dir}" /> <javadoc doctitle="Avoiding NullPointerException in Java Examples" destdir="${javadoc.dir}" sourcepath="${src.dir}" classpathref="javadoc.example.classpath" private="true" author="Dustin" /> </target> <target name="clean" description="Remove generated artifacts."> <delete dir="${classes.dir}" /> <delete dir="${dist.dir}" /> </target> </project>
The Ant build file is specified in XML. The good news here is that Groovy makes XML parsing very easy with either XmlSlurper or XmlParser (I use the former in this post). Indeed, one can easily parse this entire XML file with Groovy with very little challenge. However, the use of Ant's
ProjectHelper
and Project
classes in conjunction with Groovy's XML parsing make the script generation a little cleaner because those two Ant classes do some of the heavy lifting for us in terms of providing project basics (name, description, default and other targets).convertAntBuildFileToGroovy.groovy
#!/usr/bin/env groovy /** * convertAntBuildFileToGroovy.groovy * * This is an example of how Groovy can be used to convert an Ant build.xml file * into a Groovy code equivalent. */ import org.apache.tools.ant.Project import org.apache.tools.ant.ProjectHelper def inputBuildFileName = args[0] def outputScriptName = args.length > 1 ? args[1] : "antbuild.groovy" def groovyScriptFile = new File(outputScriptName) println "Parsing alleged Ant build XML file:\n\t${inputBuildFileName}" println "Writing Groovy script:\n\t${outputScriptName}" def projectXml = new XmlSlurper().parse(inputBuildFileName) def antFile = new File(inputBuildFileName) def project = new Project() project.init() ProjectHelper.projectHelper.parse(project, antFile) def projectName = project.name def projectDesc = project.description def defaultTarget = project.defaultTarget def basedir = project.baseDir println "Ant project named: '${projectName}'" println "Ant project description: '${projectDesc}'" println "Ant project base directory: '${basedir}'" def properties = new HashMap<String,String>() def antProperties = project.properties antProperties.each { key, value -> properties.put(key, escapeFilePath(value)) } groovyScriptFile.write(buildScriptStart(outputScriptName, inputBuildFileName, projectName, properties)) println "Ant project targets:" def targets = new HashMap<String,NodeChild>() projectXml.target.each { def targetName = it.@name as String targets.put(targetName, it) println "\t${targetName}" } groovyScriptFile << buildTargetExecutionMethod(targets.keySet(), defaultTarget) targets.each { key, value -> groovyScriptFile << buildMethodForTarget(value, properties) } import groovy.util.slurpersupport.Attributes import groovy.util.slurpersupport.NodeChild /** * Replace any Ant properties in the provided String (indicated with ${} syntax) * with any Ant defined properties provided in the antProperties parameter and * matching the string. * * @param originalString String with potential Ant substitution tokens (${}). * @param antProperties Map of possible property name matches for substitution * token. * @return Original String with substitution tokens replaced with Ant properties. */ def String replaceAntPropertiesWithValues( String originalString, Map<String,String> antProperties) { def newValue = "" if (originalString.contains('${')) { newValue = originalString originalString.eachMatch(~/\$.*?}/) { def newString = antProperties.get(it.substring(2, it.size()-1)) newValue = newValue.replace(it, newString) } } else { newValue = originalString } return newValue } /** * Build the code for the initial part of the Groovy script. * * @param scriptName Name of the script. * @param buildFileName Name of build file which is source for generated script. * @param antProjectName Name of project in Ant build file. * @param antProperties Properties explicitly specified in Ant build file. * @return Groovy code for initial portion of script. */ def String buildScriptStart( String scriptName, String buildFileName, String antProjectName, Map<String, String> antProperties) { def groovyShebang = '#!/usr/bin/env groovy' def antBuilder = "ant = new AntBuilder()" def scriptStart = new StringBuilder() scriptStart << groovyShebang << "\n\n" scriptStart << "//\n// ${scriptName}\n//\n// " scriptStart << "Build script adapted from Ant build file\n" scriptStart << "// ${buildFileName}\n" scriptStart << "// and project ${antProjectName}\n" scriptStart << "//\n\n" << antBuilder << "\n\n" scriptStart << "def properties = ${antProperties.inspect()}\n\n" return scriptStart } /** * Build the set of methods that will be called to execute script equivalents * of Ant targets in XML file. * * @param targetNames Names of Ant targets. * @param defaultTarget The default target for the Ant project. * @return Groovy code calling method equivalents of XML targets. */ def String buildTargetExecutionMethod(Set<String> targetNames, String defaultTarget) { println "Target Names: ${targetNames}" def targetExecStr = new StringBuilder() targetExecStr << "if (args.length < 1)\n" targetExecStr << "{\n" targetExecStr << " ${buildMethodName(defaultTarget)}\n" targetExecStr << "}\n" targetExecStr << "else\n" targetExecStr << "{\n" targetExecStr << " args.each\n" targetExecStr << " {\n" targetNames.each { if (!it.startsWith('-')) { targetExecStr << " if (it.equals('${it}'))\n" targetExecStr << " {\n" targetExecStr << " ${buildMethodName(it)}\n" targetExecStr << " }\n" } } targetExecStr << " }\n" targetExecStr << "}\n" return targetExecStr } /** * Build code for a Groovy method from the provided 'target' element. * * @param targetElement NodeChild representing an Ant XML target element. * @return Code for a Groovy method corresponding to the provided Ant XML * target element. */ def String buildMethodForTarget(NodeChild targetElement, Map<String,String> antProperties) { def methodComment = targetElement.@description def methodCode = new StringBuilder() if (methodComment && !methodComment.isEmpty()) { methodCode << "\n/**\n * ${methodComment}\n */\n" } methodCode << "def void ${buildMethodName(targetElement.@name as String)}\n" methodCode << "{\n" methodCode << buildDependencyCalls(targetElement.@depends) targetElement.mkdir.each { methodCode << " " << buildAntMakeDirectoryCall(it, antProperties) << "\n" } targetElement.delete.each { methodCode << " " << buildAntDeleteCall(it, antProperties) << "\n" } targetElement.javac.each { methodCode << " " << buildAntJavacCall(it, antProperties) << "\n" } targetElement.jar.each { methodCode << " " << buildAntJarCall(it, antProperties) << "\n" } targetElement.javadoc.each { methodCode << " " << buildAntJavadocCall(it, antProperties) << "\n" } methodCode << "}\n" return methodCode } /** * Build a Groovy-friendly method name from the provided Ant target name. * * @param targetName Name of Ant target. * @return Groovy-friendly method name corresponding to provided Ant target name. */ def String buildMethodName(String targetName) { return "run${(targetName - "-").capitalize()}Target()" } /** * Build Groovy code calling dependent methods. * * @param dependencyAttribute Ant Target's depends attribute. * @return Groovy code calling dependencies. */ def String buildDependencyCalls(Attributes dependencyAttribute) { def methodsCalled = new StringBuilder() def dependencyStr = dependencyAttribute as String if (!dependencyStr.isEmpty()) { def dependencies = dependencyStr.replace(" ", "").split(",") dependencies.each { methodsCalled << " ${buildMethodName(it)}\n" } } return methodsCalled } /** * Builds a Groovy Ant mkdir call from provided directory name. * * @param mkdirElement Ant XML element representation for mkdir task. * @param antProperties Properties for Ant project. * @return String containing Groovy call for using AntBuilder to create directory. */ def String buildAntMakeDirectoryCall(NodeChild mkdirElement, Map<String,String> antProperties) { def evaluatedDirectoryName = replaceAntPropertiesWithValues( mkdirElement.@dir as String, antProperties) def mkdirCall = new StringBuilder() def antBaseDir = antProperties.get('basedir') mkdirCall << "ant.mkdir(dir: '${mergeAntBaseDirWithRelativeDir(antBaseDir, evaluatedDirectoryName)}')" return mkdirCall } /** * Builds a Groovy Ant delete call from provided directory name. * * @param mkdirElement Ant XML element representation for delete task. * @param antProperties Properties for Ant project. * @return String containing Groovy call for using AntBuilder to delete. */ def String buildAntDeleteCall(NodeChild deleteElement, Map<String,String> antProperties) { def evaluatedDirectoryName = replaceAntPropertiesWithValues( deleteElement.@dir as String, antProperties) def deleteCall = new StringBuilder() def antBaseDir = antProperties.get('basedir') deleteCall << "ant.delete(dir: '${mergeAntBaseDirWithRelativeDir(antBaseDir, evaluatedDirectoryName)}')" return deleteCall } /** * Builds a Groovy Ant Javac Task call from the provided build's "javac" element. * * @param jarElement Ant XML element representation for javac task. * @param antProperties Properties for Ant project. * @return Groovy code for invoking Javac Ant task as XML element did. */ def String buildAntJavacCall(NodeChild javacElement, Map<String,String> antProperties) { def javacCall = new StringBuilder() def antBaseDir = antProperties.get('basedir') def javacSrcDir = replaceAntPropertiesWithValues( javacElement.@srcdir as String, antProperties) def javacDestDir = replaceAntPropertiesWithValues( javacElement.@destdir as String, antProperties) def javacDebug = replaceAntPropertiesWithValues( javacElement.@debug as String, antProperties) javacCall << "ant.javac(srcdir: '${mergeAntBaseDirWithRelativeDir(antBaseDir, javacSrcDir)}', " javacCall << "destdir: '${mergeAntBaseDirWithRelativeDir(antBaseDir, javacDestDir)}', " javacCall << "debug: '${javacDebug}')" return javacCall } /** * Builds a Groovy Ant Jar Task call from the provided build's "jar" element. * * @param jarElement Ant XML element representation for jar task. * @param antProperties Properties for Ant project. * @return Groovy code for invoking Jar Ant task as XML element did. */ def String buildAntJarCall(NodeChild jarElement, Map<String,String> antProperties) { def antJarCall = new StringBuilder() def destfile = replaceAntPropertiesWithValues( jarElement.@destfile as String, antProperties) def classBasedir = replaceAntPropertiesWithValues( jarElement.@basedir as String, antProperties) def filesonly = replaceAntPropertiesWithValues( jarElement.@filesonly as String, antProperties) def antBaseDir = antProperties.get('basedir') antJarCall << "ant.jar(destfile : '${mergeAntBaseDirWithRelativeDir(antBaseDir, destfile)}', " antJarCall << "basedir : '${mergeAntBaseDirWithRelativeDir(antBaseDir, classBasedir)}', " antJarCall << "filesonly : '${filesonly}')" return antJarCall } /** * Builds a Groovy Ant Javadoc Task call from the provided build's "javadoc" element. * * @param jarElement Ant XML element representation for javadoc task. * @param antProperties Properties for Ant project. * @return Groovy code for invoking Javadoc Ant task as XML element did. */ def String buildAntJavadocCall(NodeChild javadocElement, Map<String,String> antProperties) { def antJavadocCall = new StringBuilder() def doctitle = javadocElement.@doctitle def destdir = replaceAntPropertiesWithValues( javadocElement.@destdir as String, antProperties) def sourcepath = replaceAntPropertiesWithValues( javadocElement.@sourcepath as String, antProperties) def javadocPrivate = javadocElement.@private def author = javadocElement.@author def antBaseDir = antProperties.get('basedir') antJavadocCall << "ant.javadoc(doctitle: '${doctitle}', " antJavadocCall << "destdir: '${mergeAntBaseDirWithRelativeDir(antBaseDir, destdir)}', " antJavadocCall << "sourcepath: '${mergeAntBaseDirWithRelativeDir(antBaseDir, sourcepath)}', " antJavadocCall << "private: '${javadocPrivate}', " antJavadocCall << "author: '${author}')" return antJavadocCall } /** * Combine the two provided directories (base and relative) into single directory * path. * * @param antBaseDir Base directory for Ant project. * @param relativeDir Relative directory. * @return Consolidated directory formed by base + relative directory. */ def String mergeAntBaseDirWithRelativeDir(String antBaseDir, String relativeDir) { return "${antBaseDir}${File.separator}${File.separator}${relativeDir}" } /** * Escapes the provided file path appropriately for a Java/Groovy String that * will need to be processed by Java/Groovy APIs. The returned String has two * escaped Windows/DOS path separators. * * @param filePath File path to be converted to Java/Groovy escaped String for * DOS/Windows file separator. * @return String with any Windows/DOS escaped file path separators converted * to two escaped Windows/DOS file path separators. */ def String escapeFilePath(String filePath) { return filePath.replace("\\", "\\\\") }
The next screen snapshot shows the output of this script as it generates a new Groovy script.
The generated Groovy script,
antbuild.groovy
is shown next. All of the code in this code listing was generated code.antbuild.groovy (Generated Script)
#!/usr/bin/env groovy // // antbuild.groovy // // Build script adapted from Ant build file // C:\java\examples\avoidingNullPointerExceptions\build.xml // and project AvoidingNullPointerExceptionInJava // ant = new AntBuilder() def properties = ["java.vm.version":"21.0-b04", "ant.core.lib":"C:\\groovy-1.8.0\\lib\\ant-1.8.2.jar", "sun.jnu.encoding":"Cp1252", "java.vendor.url":"http://java.oracle.com/", "java.vm.info":"mixed mode, sharing", "user.dir":"C:\\java\\examples\\groovyExamples\\antToGroovy", "sun.cpu.isalist":"pentium_pro+mmx pentium_pro pentium+mmx pentium i486 i386 i86", "java.awt.graphicsenv":"sun.awt.Win32GraphicsEnvironment", "sun.os.patch.level":"Service Pack 2", "tools.jar":"C:\\Program Files\\Java\\jdk1.7.0\\lib\\tools.jar", "java.io.tmpdir":"C:\\Users\\Dustin\\AppData\\Local\\Temp\\", "user.home":"C:\\Users\\Dustin", "java.awt.printerjob":"sun.awt.windows.WPrinterJob", "java.version":"1.7.0-ea", "file.encoding.pkg":"sun.io", "java.vendor.url.bug":"http://java.sun.com/cgi-bin/bugreport.cgi", "javac.debug":"true", "file.encoding":"Cp1252", "sun.java.command":"org.codehaus.groovy.tools.GroovyStarter --main groovy.ui.GroovyMain --conf C:\\groovy-1.8.0\\conf\\groovy-starter.conf --classpath .;C:\\Program Files\\Java\\jre6\\lib\\ext\\QTJava.zip;. convertAntBuildFileToGroovy.groovy C:\\java\\examples\\avoidingNullPointerExceptions\\build.xml", "line.separator":"\r\n", "java.vm.specification.vendor":"Oracle Corporation", "dist.dir":"dist", "java.vm.vendor":"Oracle Corporation", "java.class.path":"C:\\groovy-1.8.0\\lib\\groovy-1.8.0.jar", "sun.io.unicode.encoding":"UnicodeLittle", "user.variant":"", "user.language":"en", "user.name":"Dustin", "os.arch":"x86", "java.runtime.version":"1.7.0-ea-b134", "sun.boot.class.path":"C:\\Program Files\\Java\\jdk1.7.0\\jre\\lib\\resources.jar;C:\\Program Files\\Java\\jdk1.7.0\\jre\\lib\\rt.jar;C:\\Program Files\\Java\\jdk1.7.0\\jre\\lib\\sunrsasign.jar;C:\\Program Files\\Java\\jdk1.7.0\\jre\\lib\\jsse.jar;C:\\Program Files\\Java\\jdk1.7.0\\jre\\lib\\jce.jar;C:\\Program Files\\Java\\jdk1.7.0\\jre\\lib\\charsets.jar;C:\\Program Files\\Java\\jdk1.7.0\\jre\\lib\\modules\\jdk.boot.jar;C:\\Program Files\\Java\\jdk1.7.0\\jre\\classes", "sun.desktop":"windows", "script.name":"C:\\java\\examples\\groovyExamples\\antToGroovy\\convertAntBuildFileToGroovy.groovy", "sun.cpu.endian":"little", "groovy.starter.conf":"C:\\groovy-1.8.0\\conf\\groovy-starter.conf", "awt.toolkit":"sun.awt.windows.WToolkit", "sun.boot.library.path":"C:\\Program Files\\Java\\jdk1.7.0\\jre\\bin", "java.vm.name":"Java HotSpot(TM) Client VM", "java.home":"C:\\Program Files\\Java\\jdk1.7.0\\jre", "javadoc.dir":"dist/javadoc", "ant.java.version":"1.7", "java.endorsed.dirs":"C:\\Program Files\\Java\\jdk1.7.0\\jre\\lib\\endorsed", "basedir":"C:\\java\\examples\\avoidingNullPointerExceptions", "sun.management.compiler":"HotSpot Client Compiler", "ant.project.name":"AvoidingNullPointerExceptionInJava", "java.runtime.name":"Java(TM) SE Runtime Environment", "jar.name":"AvoidingNullPointerExceptionInJava.jar", "ant.file.type.AvoidingNullPointerExceptionInJava":"file", "java.library.path":"C:\\Program Files\\Java\\jdk1.7.0\\bin;.;C:\\Windows\\Sun\\Java\\bin;C:\\Program Files\\Java\\jdk1.7.0\\bin", "file.separator":"\\", "java.specification.vendor":"Oracle Corporation", "java.vm.specification.version":"1.7", "ant.file.AvoidingNullPointerExceptionInJava":"C:\\java\\examples\\avoidingNullPointerExceptions\\build.xml", "groovy.home":"C:\\groovy-1.8.0", "sun.java.launcher":"SUN_STANDARD", "ant.version":"Apache Ant(TM) version 1.8.2 compiled on December 20 2010", "user.timezone":"", "program.name":"", "os.name":"Windows Vista", "path.separator":";", "jar.filesonly":"true", "classes.dir":"classes", "java.ext.dirs":"C:\\Program Files\\Java\\jdk1.7.0\\jre\\lib\\ext;C:\\Windows\\Sun\\Java\\lib\\ext", "sun.arch.data.model":"32", "os.version":"6.0", "java.specification.name":"Java Platform API Specification", "ant.project.default-target":"all", "user.script":"", "user.country":"US", "java.class.version":"51.0", "java.vendor":"Oracle Corporation", "java.vm.specification.name":"Java Virtual Machine Specification", "java.specification.version":"1.7", "src.dir":"src"] if (args.length < 1) { runAllTarget() } else { args.each { if (it.equals('clean')) { runCleanTarget() } if (it.equals('jar')) { runJarTarget() } if (it.equals('compile')) { runCompileTarget() } if (it.equals('javadoc')) { runJavadocTarget() } if (it.equals('all')) { runAllTarget() } } } /** * Remove generated artifacts. */ def void runCleanTarget() { ant.delete(dir: 'C:\\java\\examples\\avoidingNullPointerExceptions\\classes') ant.delete(dir: 'C:\\java\\examples\\avoidingNullPointerExceptions\\dist') } def void runInitTarget() { ant.mkdir(dir: 'C:\\java\\examples\\avoidingNullPointerExceptions\\classes') ant.mkdir(dir: 'C:\\java\\examples\\avoidingNullPointerExceptions\\dist') } /** * Package compiled classes into JAR file */ def void runJarTarget() { runCompileTarget() ant.jar(destfile : 'C:\\java\\examples\\avoidingNullPointerExceptions\\dist/AvoidingNullPointerExceptionInJava.jar', basedir : 'C:\\java\\examples\\avoidingNullPointerExceptions\\classes', filesonly : 'true') } /** * Compile the Java code. */ def void runCompileTarget() { runInitTarget() ant.javac(srcdir: 'C:\\java\\examples\\avoidingNullPointerExceptions\\src', destdir: 'C:\\java\\examples\\avoidingNullPointerExceptions\\classes', debug: 'true') } /** * Generate Javadoc-based documentation */ def void runJavadocTarget() { ant.mkdir(dir: 'C:\\java\\examples\\avoidingNullPointerExceptions\\dist/javadoc') ant.javadoc(doctitle: 'Avoiding NullPointerException in Java Examples', destdir: 'C:\\java\\examples\\avoidingNullPointerExceptions\\dist/javadoc', sourcepath: 'C:\\java\\examples\\avoidingNullPointerExceptions\\src', private: 'true', author: 'Dustin') } /** * Compile Java source, assemble JAR, and generate documentation */ def void runAllTarget() { runJarTarget() runJavadocTarget() }
The above generated script can then be run to execute the same targets as in the original Ant build file. The next series of screen snapshots indicate running the generated Groovy script. The first image shows the "default" case where no target name is specified and the functionality is equivalent to that when the Ant build file's default is used. The other images demonstrate passing various target names to the generated script. Although the generated code is a little verbose in handling these options, it's not a big deal because it is generated code.
As the above images indicate, the generated Groovy script can now be used to execute targets in a manner very similar to running them via Ant directly.
The original script generated this script using many nice features of Groovy. These useful features demonstrated in that script include closures, AntBuilder, XML slurping, and advanced String functionality.
It is the use of AntBuilder that makes the generated script relatively short and easy to generate. A second purpose of this post after demonstrating generation of a Groovy build script from a source Ant build file is to also demonstrate
AntBuilder
used for various tasks such as making directories, cleaning, compiling, archiving, and generating Javadoc. The last shown Groovy code listing (the generated Groovy script) is contains several examples of AntBuilder
in action.I am confident that the original script could be made even leaner, but it does its job for the simple example
build.xml
file. The script only covers tasks and task attributes that exist in this particular source XML file, but could be easily extended to cover more Ant tasks and more attributes for the already covered Ant tasks.Conclusion
Groovy is useful for all types of tasks related to development. In this post, I've shown how Groovy can be used to generate more Groovy code that mimics what an input Ant build file does. From this point, one could modify and enhance the generated script to suit particular script-related needs.
No comments:
Post a Comment