Scripting a Java Project with Groovy

by 3/30/2007 09:04:00 AM 0 comments

Scripting a Java Project with Groovy

For java projects, after you've finished writing the the API, business logic, O/R mapping layer, etc, you have to get down to the nitty, gritty details of making the application actually run. I often find that I need to script together different API's to perform some task. For example, I might want to call some of my libraries that add a time-code track to a QuickTime movie. I could write a Java class to do this but generally this is cumbersome and creates a fairly heavy-weight development process (write-compile-test-deploy); and honestly, isn't really much fun. And even after I've written the class to do the particular task, I still need to write a shell script to execute the classes main method. For desktop applications this means creating a launcher, such as a JNLP file or shell script.
Recently, I've started poking around with Groovy . I've been very impressed with it's scripting ability. Here, I will present what I've done to script together different API's using Groovy. Below, I present a technique that allows you to script a Java applications without requiring you to install anything outside of the project. (i.e. you don't need to install groovy on the system that's running your scripts.) In order to build this project you will need to have the following installed: Java, Ant
First, I create an Ant build.xml file. This particular project doens't actually compile any code, the build file is just going to use Maven to fetch the dependencies I need for my project and store them in a lib directory. This technique is described here.
My project is set up like so:
PRJ_HOME
|~~ bin         [Shell scripts]
|~~ conf        [Configuration - e.g. log4j.xml]
|~~ lib         [Java jars]
`~~ scripts    
    `~~ groovy  [Groovy scripts]
My build.xml looks like this:
<?xml version="1.0"?>
<!-- build file for eits-db program -->
<!-- Created by:  Brian Schlining -->

<project name="eits-db" default="init" basedir="." xmlns:artifact="antlib:org.apache.maven.artifact.ant">
  <property name="lib" value="lib" />
  <property name="groovy" value="scripts/groovy" />
  
<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
artifact.install
   this target is not necessary if you put ivy.jar in your ant lib directory
   if you already have ivy 1.4 in your ant lib, you can simply remove this
   target and the dependency the 'go' target has on it
-->

  <property name="artifact.jar.dir" value="${basedir}/.ant" />
  <property name="artifact.jar.file" value="${artifact.jar.dir}/maven.artifact-ant-2.0.2-dep.jar" />
  
  <target name="artifact.install" description="Install artifact lib">
      <mkdir dir="${artifact.jar.dir}"/>
      <!--
         download maven antlib from web site so that it can be used even without any
         special installation
       -->
      <available file="${artifact.jar.file}" property="artifact.exists"/>
      <echo message="Installing Maven Antlib..."/>
      <get src="http://www.apache.org/dist/maven/binaries/maven-artifact-ant-2.0.2-dep.jar"
           dest="${artifact.jar.file}" usetimestamp="true" ignoreerrors="${artifact.exists}"/>
      
      <typedef resource="org/apache/maven/artifact/ant/antlib.xml" uri="antlib:org.apache.maven.artifact.ant">
          <classpath>
              <pathelement location="${artifact.jar.file}" />
          </classpath>
      </typedef>
      
  </target>
  
  <target name="init" description="Setup project and fetch dependencies from internet" depends="artifact.install">
      <mkdir dir="${lib}" />
      <artifact:pom id="pom" file="pom.xml" />
      <echo>Fetching dependencies for ${pom.artifactId}-${pom.version}</echo>
      <artifact:dependencies filesetId="dependency.fileset" useScope="runtime">
          <pom refid="pom"/>
      </artifact:dependencies>
      <copy todir="${lib}" verbose="true">
          <fileset refid="dependency.fileset" />
          <mapper type="flatten" />
      </copy>
  </target>
  
  <target name="clean" description="Cleanup the project" >
      <delete dir="${lib}" />
  </target>

</project>
Next, I'll declare my dependencies in a Maven pom.xml file. For this example the 2 dependencies we really care about are groovy-all-1.0.jar and commons-cli-1.0.jar. Both of these are required in order to script from the command line with groovy.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>eits</groupId>
    <artifactId>eits-db</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>EITS Database Tools</name>
    <url>http://foo.bar.org/projects/${project.name}</url>
    <dependencies>
 <!-- Other dependencies not included for brevity -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>groovy</groupId>
            <artifactId>groovy-all</artifactId>
            <version>1.0</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>commons-cli</groupId>
            <artifactId>commons-cli</artifactId>
            <version>1.0</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
</project>
Next, I'll need to create a launcher script to set up the environment and execute and of my Groovy scripts. I'm developing on Mac OS X, so I'll create a bash script. The script is pretty simple, it sets the application classpath to include all jars in PRJ_HOME/lib. if your working on windows, setting up the classpath at runtime is a bit more of a pain; the details for it can be found here.
#!/bin/sh
# GSH: Execute groovy script using the projects library directory.

PRJ_HOME=`dirname "$0"`/..

CP=${PRJ_HOME}/scripts/groovy:$PRJ_HOME/conf:$PRJ_HOME/lib
PRJ_LIB=$PRJ_HOME/lib
for jar in $(ls $PRJ_LIB/*.jar)
do
    CP=$CP:$jar
done

java -classpath "$CP" groovy.ui.GroovyMain "$@"
With this in place, all I need to do script any of my java apps is create a Groovy script in PRJ_HOME/scripts/groovy then run 'gsh myscript' on the command line. Groovy does allow for command line switches using a groovy builder called 'CliBuilder'. Discussion about CliBuilder is way beyond what I want to cover here, but there's and example of it's use in the script below. And of course, the groovy script:
/**
 * addtimecode.groovy 
 *
 * This script is used takes a QuickTime movie as input, adds a timecode track 
 * and saves it out to a different 'flattened' QuickTime movie file
 */
 
import org.mbari.movie.Timecode
import org.mbari.qt.FileExistsException
import org.mbari.qt.QT
import org.mbari.qt.TimeUtil
import org.mbari.qt.VideoStandard

/*
 * Setup command line interface
 */
def cli = new CliBuilder(usage: "run_groovy ../scripts/groovy/addtimecode -i [input move] -o [output movie] -t [hh:mm:ss:ff]")
cli.h(longOpt: "help", "usage information")
cli.i(argName: "input", longOpt: "input", args:1, required: true, "movie file to read")
cli.o(argName: "output", longOpt: "output", args:1, required: true, "movie file to write")
cli.t(argName: "timecode", longOpt: "timecode", args:1, required: true, "starting timecode (hh:mm:ss:ff) - assuming NTSC (29.97 fps)")

/*
 * Parse the command line options
 */
def options = cli.parse(args)
if (!options) {
    return
}
if (options.h) {
    println "Script for adding a time-code track to QuickTime movies"
    cli.usage()
}

// Create a file reference
def inputFile = new File(options.i)
URL inputUrl = inputFile.toURL()

// Add a timecode track
def timecode = new Timecode(options.t)

// Export to the output file
def exitCode = 0
try {
    def movie = QT.openMovieFromUrl(inputUrl)
    TimeUtil.addTimeCodeTrack(movie, timecode, VideoStandard.NTSC)
    QT.flattenMovie(movie, new File(options.o), false)
}
catch (FileExistsException e) {
    println "The file ${inputFile} already exists. You must delete it first or save to a different file."
    exitCode = 1
}
catch (Exception e) {
    println "An error occurred during processing. \n  ERROR: ${e.class} \n  MESSAGE:${e.message}"
    exitCode = 2
}

System.exit(exitCode)
To execute this script I would run the following on the command line: gsh addtimecode -i /Some/movie.mov -o /Some/newmovie.mov -t 00:12:34:01.

Brian Schlining

Developer

Cras justo odio, dapibus ac facilisis in, egestas eget quam. Curabitur blandit tempus porttitor. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.

0 comments: