Ant hooks using Groovy, INIX, and XMLTask

I revisit the Ant Hook scripts using Groovy and change it to use INIX file for hook storage and use XML transform via XMLTask to insert the hook feature into a legacy build script.

In a prior post, Ant hooks using Groovy, continued, I used the Ant BuildListener interface to add a hooks feature that invokes a Groovy script mapped to build events. Then I implemented an “around advise” capability.

Many issues with this approach, but two stand out.

Drawbacks
  1. Existing Ant scripts must be modified to add this in.
  2. Hook scripts are now a bunch of files that must be managed.
Approach
  1. Hook scripts are stored in INIX files
  2. A meta build step is used to insert the hook feature into a build script.

INIX storage
Below, the Groovy hook scripts from the previous blog post are stored in the sections of a INIX file (a form of INI file). The section begin tag is composed of a unique path, optional fragment identifier, and a query string. Just like a URL. The query string indicates when this hook is applied and if the target in the script should be skipped (applicable for ‘before’ hooks). The last section in the file will be discussed later.

hooks.inix
[>hook/root/compile?when=after,skip=false]
    println " hook: root,{target=${event.target.name},when=post,event=$event}"
[<]

[>hook/demo1/deploy?when=before,skip=true]
    println "  hook: {project=${event.project.name},target=${event.target.name},when=pre,event=$event}"
[<]

[>hook/demo1/compile?when=before,skip=false]
    println "  hook: {project=${event.project.name},target=${event.target.name},when=pre,event=$event}"
[<]

[>fragment]
    <path id="libs">    
        <fileset dir="lib">
            <include 
                name="groovy-all-2.2.1.jar" />
        </fileset> 
        
        <pathelement location="src/main/groovy"/>
    </path>    
     
    <!-- Groovy library -->
    <taskdef name="groovy"
       classname="org.codehaus.groovy.ant.Groovy"
       classpathref="libs"/> 
    
    <!-- sets a BuildListener to the project -->
    <scriptdef name="set-listener" 
        language="Groovy"
        classpathref="libs" 
        src="src/main/groovy/com/octodecillion/util/ant/Hook.groovy"> 
        <attribute name="path"/>
    </scriptdef>
     
    <!-- install the listener -->
    <set-listener path="hooks.inix"/>   
     
[<fragment]

Ant build file transform
In the above inix file, the last section contains the XML fragment of the build script hook support. We read in this section and use Groovy’s Templating support to replace any values. Thus, by running a metabuild or bootstrap process we can take that fragment and insert it into a target Ant script.

Since an Ant script is XML, an XML transform is used by the XMLTask Ant task. This task allows us to indicate with an XPath expression where to insert the fragment. In a Groovy blog post, we of course, will use Groovy to do this via the Groovy AntBuilder. Below, the InsertHooks class implements this.

InsertHooks.groovy
package com.octodecillion.util.ant

import static com.octodecillion.util.inix.Inix.EventType.END

import com.octodecillion.util.inix.Inix

import org.apache.tools.ant.*

import static groovy.io.FileType.FILES

/**
 * Insert the XML into Ant script to enable hooks.
 *   
 * @author josef betancourt
 */
class InsertHooks{

    def ant
    def DEBUG = false
    static final String srcFilePath='build.xml'
    static final String destFilePath="build-hooked.xml"
    static final String INIXFILE = 'hooks.inix'    
    static final String XMLTASK = 'com.oopsconsultancy.xmltask.ant.XmlTask'
    
    static main(args){
        new InsertHooks().execute()
    }
    
    /** An Ant task entry point */
    public void execute() throws BuildException{
        def ant = new AntBuilder()
        
        try {
            
            def fragment = loadFragment(INIXFILE)
            if(!fragment){
                throw new BuildException("'fragment' from $INIXFILE is invalid")
            }        
            
            def engine = new groovy.text.SimpleTemplateEngine()
            def template = engine.createTemplate(fragment)             
            def xml = template.make([hookFilePath:INIXFILE])
              
            ant.path(id: "path") {
                fileset(dir: 'lib') {
                   include(name: "**/xml*.jar")
                }
            }
     
            ant.taskdef(name:'xmltask',classname:
                XMLTASK,
                classpathref: 'path')
             
            def xpath = '(//target)[1]' 
            ant.xmltask(source:srcFilePath,dest:destFilePath, 
                expandEntityReferences:false,report:false){
                insert(position:"before",path:xpath,xml:xml)                 
            }
                 
            new File(destFilePath).eachLine{
                println it
            }
            
        } catch (Exception e) {
            e.printStackTrace()
            throw new BuildException(e.getMessage(), e)
        }
    }
    
    def loadFragment(String path){
        def text = ''
        def inix = new Inix(path)
        def theEvent = inix.next()
         
        while(theEvent && theEvent != Inix.EventType.END ){
            Inix.Event event = inix.getEvent()

            if(event.isSection("fragment")){
                text = event.text
                break
            }
            
            theEvent = inix.next()
        }
        
        return text
    }    
    
}

// end Script

Hooking Up
Now the target Ant script has the ability to invoke the script below via a ScriptDef. The script wires in the Ant BuildListener interface implementation, and then loads the hooks from the inix file. When the Ant script is run, the BuildListener’s invokeTargetHook method is invoked. This in turn executes the correct hook scripts.

Hook.groovy
package com.octodecillion.util.ant

import groovy.transform.TypeChecked;
import groovy.transform.TypeCheckingMode;

import java.util.List;
import java.util.Map;
import java.util.regex.Pattern

import com.octodecillion.util.inix.Inix

import org.apache.tools.ant.BuildEvent
import org.apache.tools.ant.BuildException
import org.apache.tools.ant.Project
import org.apache.tools.ant.SubBuildListener;

import static groovy.io.FileType.FILES

// wire in the listener
def path = binding.attributes.get('path')
if(!path){
    throw new BuildException("'path' to hook inix not set")
}

def listener = new HookListener(project,path)
listener.project = project
project.addBuildListener(listener)

// end wiring

/**
 * Ant build listener that invokes groovy hook scripts.
 *  
 * @author josef betancourt
 *
 */
@TypeChecked
class HookListener implements SubBuildListener {
    Project project
    boolean DEBUG = false
    
    /**                          */
    def HookListener(Project project, String path){
        this.project = project
        loadInix(path)                
    }    
    
    /** load scripts in inix file */
    def loadInix(String path){
        debugln("load inix")
        def inix = new Inix()
        inix.reader = new BufferedReader(
            new FileReader(new File(path)))
         
        def theEvent = inix.next()
        def found = false
         
        while(theEvent && theEvent != Inix.EventType.END ){
            def event = inix.getEvent()

            if(isHook(event)){
                found = true
                def key = [event.path[2],((String)(event.params['when'])).
                    toUpperCase()].join('/')
                    
                String txt = event.text
                String skString = event.params['skip']
                boolean sk = (skString.compareTo('true')==0 ? true : false)
                debugln "key=$key, ${event.params['skip']}, skip=$sk"
                    
                def node = new HookNode(txt, sk)
            
                def prj = event.path[1]                
                if(!hooks[prj]){
                    hooks[prj] = [:]
                }    
                
                hooks[prj].put(key,node);            
            }
            
            theEvent = inix.next()
        }
        
        dumpHooks()        
        
    }

    /** invoked by Ant build */
    @Override
    public void targetStarted(BuildEvent event) {
        die("targetStarted invoked with null event", !event)
        invokeTargetHook(event, When.BEFORE)
    }
    
    /** invoked by Ant build */
    @Override
    public void targetFinished(BuildEvent event) {
        die("targetFinished invoked with null event", !event)
        invokeTargetHook(event, When.AFTER)
    }

    /** Invoke the target's hook script */
    def invokeTargetHook(BuildEvent event, When when){
        def b = new Binding()
        b.setProperty("event",event)
        b.setProperty("hook",this)
        
        def shell = new GroovyShell(b)
        
        def hookName = "${event.target.name}/$when"
        def pHook = hooks[event.project.name][hookName]
        def rHook = hooks['root'][hookName]    
        debugln("invokeTargetHook: $hookName\npHook:  $pHook\nrHook:  $rHook")
            
        boolean skipSet = false
        
        if(pHook){
            skipSet = pHook.skip    
            debugln("skipSet=$skipSet")
            shell.evaluate(pHook.text)
            
            if(!override && rHook){
                skipSet = skipSet ? skipSet : rHook.skip
                shell.evaluate(rHook.text)
            }            
            
        }else if(rHook){
            skipSet = rHook.skip            
            shell.evaluate(rHook.text)        
        }
        
        if( skipSet && (pHook || rHook) && (when == When.BEFORE) ){
            createSkipforTarget(event)
        }        
    } 

    /**   */
    private createSkipforTarget(BuildEvent event) {
        debugln "setting skip: ${event.target.name}_skipTarget"
        event.project.setProperty("${event.target.name}_skipTarget", "true")
        event.target.setUnless("${event.target.name}_skipTarget")
    }
    
    /** throw exception if flg is true */
    private die(Object msgObject, boolean flg){
        if(flg){
            throw new IllegalArgumentException(String.valueOf(msgObject))            
        }        
    }    
    
    @TypeChecked(TypeCheckingMode.SKIP)
    private isHook(ev){        
        ev.path && ev.path[0] == 'hook'        
    }
    
    private dumpHooks() {
        if(!DEBUG){
            return            
        }
        
        hooks.each{
            it.each{ node ->
                debugln(node)
            }
        }        
    }
    
    private debugln(Object msg){
        if(DEBUG){
            println(msg)
        }
    }
    
    String TARGETHOOK = "target"
    def override = true;
    
    enum When{
        BEFORE('before'),AFTER('after')
        String name
        
        When(s) {this.name = s}
    }
    
    private class HookNode {
        String text
        boolean skip
        public HookNode(String text, boolean skip){
            this.text = text
            this.skip = skip
        }
        
        def String toString() {return "s:$skip"};
    }
    
    Map<String, Map<String,HookNode>> hooks = [:]
    
    //@formatter:off
    @Override
    public void subBuildFinished(BuildEvent event) {}
    @Override
    public void subBuildStarted(BuildEvent event) {}
    @Override
    public void buildFinished(BuildEvent event) {}
    @Override
    public void buildStarted(BuildEvent event) {}
    @Override
    public void messageLogged(BuildEvent event) {}
    @Override
    public void taskFinished(BuildEvent event) {}
    @Override
    public void taskStarted(BuildEvent event) {}
    //@formatter:on
} // end class HookListener
// end Script

Demo
Below is a non-hooked Ant script.

build.xml
<project name="demo1" default="build" basedir=".">

<target name="compile">
    <echo>******* 'compile': Hello compile world!</echo>  
</target> 

<target name="deploy">
    <echo>******* 'deploy': Deploying ... ${skipSet}</echo>
</target>

<target name="build" depends="compile,deploy">
    <echo>******* 'build': Building ... </echo>
</target>

</project>

When we run the InsertHooks scripts, a new Ant script is created, build-hooked.xml. The console output (for demonstration) is:

Output

build-hooked.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project basedir="." default="build" name="demo1">

    <path id="libs">
<fileset dir="lib">
<include name="groovy-all-2.2.1.jar"/>
</fileset>

<pathelement location="src/main/groovy"/>
</path>

<!-- Groovy library -->
<taskdef classname="org.codehaus.groovy.ant.Groovy" classpathref="libs" name="groovy"/>

<!-- sets a BuildListener to the project -->
<scriptdef classpathref="libs" language="Groovy" name="set-listener" src="src/main/groovy/com/octodecillion/util/ant/Hook.groovy">
<attribute name="path"/>
</scriptdef>

<!-- install the listener -->
<set-listener path="hooks.inix"/>

<target name="compile">
    <echo>******* 'compile': Hello compile world!</echo>  
    </target> 
    
    <target name="deploy">
        <echo>******* 'deploy': Deploying ... ${skipSet}</echo>
    </target>
    
    <target depends="compile,deploy" name="build">
        <echo>******* 'build': Building ... </echo>
    </target>

</project>

Then we subsequently run the build-hooked.xml script, the console output is shown below. Note that the “deploy” target is skipped, only the output of the hook will show.

Buildfile: C:\Users\jbetancourt\workspace-4.3\AntAroundAdvice2\build-hooked.xml
compile:
  hook: {project=demo1,target=compile,when=pre,event=org.apache.tools.ant.BuildEvent}
     [echo] ******* 'compile': Hello compile world!
 hook: root,{target=compile,when=post,event=org.apache.tools.ant.BuildEvent}
deploy:
  hook: {project=demo1,target=deploy,when=pre,event=org.apache.tools.ant.BuildEvent}
build:
     [echo] ******* 'build': Building ... 
BUILD SUCCESSFUL
Total time: 3 seconds

Summary
Shown was an improvement of the Groovy Ant Hooks idea. If nothing else, shown was some Groovy code that used AntBuilder, XMLTask, and Inix file reading.

The actual use case for the need of a Ant hook is probably very narrow, such as a build server that reuses legacy Ant build scripts, or the targeting of legacy build to new requirements.

Further reading

Similar Posts:

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.

2 thoughts on “Ant hooks using Groovy, INIX, and XMLTask”

Leave a Reply

Your email address will not be published. Required fields are marked *