Home Page Blog Archive Index

Java Article

Supercharging BeanShell with Ant

Author: Pankaj Kumar
Last Revision Date: 7/26/05, Publication Date: 7/26/05

Abstract

This article explains how to execute any Ant task from within a BeanShell script, bringing the rich and growing Ant task library to BeanShell programmers and, in the process, vastly increasing its potential for serious scripting. This kind of bridging is good for Ant as well, allowing Ant tasks to be used within familiar control and looping constructs.






Read Review

Motivation

Long before Java based scripting languages became the in thing and started hogging serious media attention, BeanShell, a free and nifty tool created by Patrick Niemeyer, allowed us all the benefits of scripting languages with the familiar Java syntax and access to Java class libraries. Personally, I have used it many times over the years for quick prototyping and experimentation tasks.

Although BeanShell enhances productivity for Java programmers as it is, the lack of a rich cross-platform script library has limited its usefulness, especially for general purpose admin and automation tasks. In fact, I have seen Ant scripts being used for such purposes. Such uses of Ant, although frowned upon, have been justified under scenarios where availability of JDK and Ant can be safely assumed. Ant comes with a huge collection of cross-platform automation tasks, either built-in or through integration with other tools, and most Java programmers are familiar with these tasks. However, Ant is not designed for general scripting and such usage are awkward at best.

Many solutions have been devised to address this problem. One solution is to embed one of many scripting languages supported by Ant through Script task. As BeanShell is one of those languages, you can execute BeanShell code from within an Ant script. Another solution is Jelly, a java and XML based scripting and processing engine. Jelly tags allow conditional statements and looping constructs for Ant tasks and target to create general purpose scripts.

Although both of these solutions work, I find the resulting programs too verbose, awkward to write (and understand) and, in general, inelegant. What would be really cool is a seamless way to call Ant tasks from BeanShell scripts. Not finding anything like this on the Web, so one weekedn I explored the possibility of creating such an integration myself. Fortunately, BeanShell's mechanism to invoke arbitrary commands and Ant's Task API made the task really simple.

Running Ant Tasks From BeanShell Prompt

Within few hours I had the guts of the integration ready through a scripted object, which I named ant. Those familiar with BeanShell would know that this scripted object defintion should reside in file ant.bsh. With this file in place, I could intialize the scripted object ant at BeanShell prompt, assuming availability of BeanShell jar in the working directory and Ant installation at ANT_HOME:

C:\>java -cp bsh-2.0b1.jar;%ANT_HOME%\lib\ant.jar bsh.Interpreter
BeanShell 2.0b1.1 - by Pat Niemeyer (pat@pat.net)
bsh % source("ant.bsh");
bsh % ant = ant();
and execute Echo Ant task:
bsh % ant.echo().message("How do you do?").execute();
     [echo] How do you do?
bsh %
Compare this invocation with the equivalent Ant script fragment:
<echo message="How do you do?"/>
Unarguably, the BeanShell syntax is no simpler than the corresponding Ant syntax. However, it does have a direct and simple mapping with the Ant syntax. I kept it this way to maximize the reuse of Ant task documentation and keep my code simple. Let me illustrate this with another Ant script fragment:
<copy todir="tmp" filtering="true">
  <fileset dir="." includes="*.xml"/>
  <filterset>
    <filtersfile file="filters.props"/>
  </filterset>
</copy>
This script copies all files with extension xml in the current directory to tmp sub-directory, replacing tokens within '@' symbol (such as @author@) to token values specified in file filters.props.

The corresponding BeanShell statements are:

bsh % copy = ant.copy().todir("tmp").filtering("true");
bsh % copy.fileset().dir(".").includes("*.xml");
bsh % copy.filterset().filtersfile().file("filters.props");
bsh % copy.execute();
It is possible to shorten the previous sequence of commands with the following single command:
bsh % copy = ant.copy().todir("tmp").filtering("true").fileset()
.dir(".").includes("*.xml").parent.filterset().filtersfile()
.file("filters.props").execute();
Here is another BeanShell command making use of Ant task to perform a SSH remote execution:
bsh % ant.sshexec().host("hostname").username("user").password("passwd")
.command("ls").execute();
Note: You should replace hostname, user and passwd in the previous command with values appropriate for your environment. Also, you would need jsch.jar to execute sshexec task as mentioned in Library Dependencies section of Ant documentation.

Now that we have a feel for how the scripted object ant works, let us look at the complete set of rules to map Ant script fragments to BeanShell statements.

Rules to Write BeanShell Statements

As you might have already deduced from previous examples, BeanShell statements for Ant task invocations are written by observing following simple rules:

  1. Expression 'ant.taskname()' creates an element object (more on element objects later ) for Ant task taskname and returns the newly created object. Example: statement 'echo = ant.echo();' sets variable echo to the element object for Ant Echo task.
  2. Expression 'elemobj.attrname(attrvalue)' sets the attribute attrname to value attrvalue for elemobj element object, applying conversion rules on the argument as defined by the Ant Task API. Example: statement 'echo.message("How are you?");' sets attribute message to string "How are you?".
  3. Expression 'elemobj.elemname()' creates inner element elemname as child of elemobj and returns the created element object. Example: statement 'copy.fileset();' creates element object for inner element fileset.
  4. Expression 'elemobj.parent' returns the parent of elemobj. Example: expression 'copy.fileset().parent' returns copy element object.
  5. Expression 'elemobj.execute()' executes the task represented by elemobj or its ancestor. This allows execute() to be invoked on any child element object.
  6. Expressions can be chained together to perform multiple operations in one statement. Example: Statement 'ant.echo().message("How do you do?").execute();' creates element object for Echo task, sets its attribute message and invokes operation execute() on the task.
  7. Environment variables can be accessed through expression ant.getenv(envvar).

The following diagram illustrates element objects, corresponding Ant objects, their associations and flow of control during execute() operation for the previous example involving the copy task:

Figure: Objects and interactions in the example copy task execution

Pay attention to this diagram. This will come handy in understanding the code presented in the next section.

Looking at The Code

Rather than presenting the defintion of scripted object ant piece-meal, let me present the whole ant.bsh file and then explain what it does.

/*
 * Copyright (c) 2005 Pankaj Kumar (pankaj.kumar@gmail.com)
 * This software is being made available on "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
 * express or implied.
 */
ant(){
  if (getResource("/org/apache/tools/ant/Project.class") == null){
    print("WARNING: Cannot initialize Ant.");
    print("WARNING: Perhaps ant.jar is not in CLASSPATH ...");
    return null;
  }
  project = new org.apache.tools.ant.Project();
  project.setCoreLoader(null);
  project.init();
  
  logger = new org.apache.tools.ant.DefaultLogger();
  logger.setOutputPrintStream(System.out);
  logger.setErrorPrintStream(System.err);
  logger.setMessageOutputLevel(org.apache.tools.ant.Project.MSG_INFO);

  project.addBuildListener(logger);
  project.setBaseDir(new File("."));
  tasks = new Properties();
  propFilename = "/org/apache/tools/ant/taskdefs/defaults.properties";
  tasks.load(tasks.getClass().getResourceAsStream(propFilename));
   
  // Set environment.
  env_prop = new org.apache.tools.ant.taskdefs.Property();
  env_prop.setProject(project);
  env_prop.setEnvironment("myenv");
  env_prop.execute();
  
  invoke(String tname, Object[] args){
    clazz = tasks.getProperty(tname);
    if (clazz == null){
      throw new Exception("no such task: " + tname);
    }
    tobj = Class.forName(clazz).newInstance();
    tobj.setProject(project);
    tobj.setTaskName(tname);
    return element(tobj, null);
  }
  
  // need separate method for ant(). May be a BeanShell bug.
  ant(){
    return invoke("ant", new Object[] { });
  }
  
  convType(String arg, Class type){
    if (type.isPrimitive()){
      if (type.toString().equals("boolean")){
        ucArg = arg.toUpperCase();
        return ((ucArg.equals("TRUE") || ucArg.equals("ON") || 
            ucArg.equals("YES")) ? Boolean.TRUE : Boolean.FALSE);
      } else if (type.toString().equals("int")){
        return Integer.valueOf(arg);
      } else if (type.toString().equals("short")){
        return Short.valueOf(arg);
      } else {
        print("unknown type: " + type);
        return null;
      }
    } else {
      ctor = type.getConstructor(new Class[] {arg.getClass()});
      return ctor.newInstance(new Object[] { arg });
    }
  }
              
  element(aobj, parent){
    this.aobj = aobj;
    this.parent = parent;
    java.lang.reflect.Method[] methods = aobj.getClass().getMethods();
    
    invoke(String aname, Object[] args){
      if (args.length == 0){ // treat aname as an inner element
        for (method:methods){
          mName = method.getName();
          if (mName.equalsIgnoreCase("create" + aname)){
            return element(method.invoke(aobj, args), this);
          } else if (mName.equalsIgnoreCase("add" + aname)||
              mName.equalsIgnoreCase("addConfigured" + aname)){
            Class[] ptypes = method.getParameterTypes();
            if (ptypes.length == 1){
              args = new Object[] { ptypes[0].newInstance() };
              method.invoke(aobj, args);
              return element(args[0], this);
            }
          }
        }
        print("no such inner element: " + aname);
      } else if (args.length == 1){
        for (method:methods){
          if (method.getName().equalsIgnoreCase("set" + aname) ||
              (aname.equalsIgnoreCase("text") && 
               method.getName().equals("addText")) ){
            Class[] ptypes = method.getParameterTypes();
            if (ptypes.length != 1){
              print("no such attribute: " + aname);
              return null;
            }
            // May need for type conversion
            if (!ptypes[0].equals(args[0].getClass())){ 
              args[0] = convType(args[0], ptypes[0]);
            }
            method.invoke(aobj, args);
            return this;
          }
        }
        print("no such attribute: " + aname);
      } else {
        print("invalid signature for: " + aname);
      }
      return null;
    }
    // Convenience method, so that one can call 
    // task().innerelem().....execute()
    // in place of task().innerelem().....parent.execute()
    execute(){
      if (parent == null)
        aobj.execute();
      else
        parent.execute();
    }
    return this;
  }

  String getenv(String key){
    return project.getProperty("myenv." + key);
  }
  
  String getenv(String key, String def){
    value = project.getProperty("myenv." + key);
    return (value == null ? def : value);
  }

  return this;
}

As you can see from the above code, initialization of scripted object ant performs following functions:

  1. Verifies presence of ant.jar in CLASSPATH. returns null if verification fails.
  2. Creates org.apache.tools.ant.Project object and initializes it.
  3. Creates a default logger, sets its output level to INFO and assigns it to the project object.
  4. Loads information about all Ant tasks in property object tasks.
  5. Loads environment variables and their values. This information is later used by method ant.getenv().

Invocation of ant.taskname() takes advantage of BeanShell's invoke() method to dynamically determine the class for taskname() and create the corresponding object. This object is then wrapped around scripted object ant.element(). Similarly, invocations of attrname(attrvalue) and elemname() on element objects result in execution of its invoke() method, creating appropriate structures and performing required datatype conversions.

One Plus One is More Than Two

The combination of BeanShell and Ant has more power than the mere sum of their individual powers. This may sound as hyperbole, but is indeed true and can be corroborated through examples:

Quick Experimentation

You no longer need to write a full-fledged Ant script file to experiment with a task or option that you haven't used before. Just type-in the command at BeanShell prompt and see what happens.

Reflective Exploration

Access to Ant objects allow you to explore their attributes and sub-elements using BeanShell's built-in javap() command.

bsh % copy = ant.copy();
bsh % javap(copy.aobj);

Recall that member aobj points to the actual Ant object and the javap() command lists all the public members of the class.

Accessing Environment Variables

Scripted object ant allows access of environment variable values within a BeanShell script on all platforms where Ant is supported.

bsh % print(ant.getenv("OS"));
Windows_NT
bsh % print(ant.project.getProperties().entrySet());
... voluminous output skipped ...
bsh %

Luanching External Programs

BeanShell command exec() can be used to launch an external program, but it is much less versatile than the Ant exec task. For example, The Ant exec task allows you to

  • launch the program in the background without waiting for its completion,
  • specify a timeout after which the launched program should be killed,
  • set the current directory for the external program,
  • redirect the output to a file,
  • store the error code returned by the external program in a property,
and so on.

Conclusion

In this article I demonstrated how Ant tasks can be created, configured and executed within BeanShell scripts using a simple scripted object. Such uses allow creation of powerful scripts with very little effort and effectively address limitations of either BeanShell or Ant used alone.

Resources

  1. Downloadable file(s) discussed in this article: ant.bsh
  2. BeanShell Project website: http://beanshell.org/
  3. Ant Project website: http://ant.apache.org/

© 2005 by Pankaj Kumar. All Rights Reserved. You may use the content of this article, including the source code, in your programs and documentation. In general, you do not need to contact me for permission unless you are reproducing a significant portion of the article or code for commercial purposes. For example, writing a program that uses the code for personal or company use does not require permission. Republishing this article, even with proper attribution, does require permission. Similarly, incorporating the code in a program that you sell for profit requires permission.

You are welcome to send me comments, suggestions or bug fixes. I will look into those as and when time permits and may even publish the good ones on this page.

You can contact me by sending e-mail to pankaj.kumar@gmail.com.