Using Gant to execute a groovy script within the Grails context

2008/03/19

(3/30/08 I’ve posted an update to this script that fixes the problems noted in the comments)

I’ve been working with Gant recently and wanted the ability to be able to execute a script within the grails context, but not add it as part of a controller or needing to paste it every time in a grails console/shell.

After a little playing around, I came up with this:

import org.codehaus.groovy.grails.commons.GrailsClassUtils as GCU
 
grailsHome = Ant.project.properties."environment.GRAILS_HOME"
 
includeTargets << new File ( "${grailsHome}/scripts/Package.groovy" )
includeTargets << new File ( "${grailsHome}/scripts/Bootstrap.groovy" )
 
target('default': "Execute the specified script after starting up the application environment") {
    depends(checkVersion, configureProxy, packageApp, classpath)
    runScript()
}
 
target(runScript: "Main implementation that executes the specified script after starting up the application environment") {
    parseArguments()
    if (argsMap["params"].size() == 0) {
        event("StatusError", ["Required script name parameter is missing"])
        System.exit 1
    }
    compile()
    classLoader = new URLClassLoader([classesDir.toURL()] as URL[], rootLoader)
    Thread.currentThread().setContextClassLoader(classLoader)
    loadApp()
    configureApp()
    argsMap["params"].each { scriptFile ->
        executeScript(scriptFile, classLoader)
    }
}
 
def executeScript(scriptFile, classLoader) {
    File script = new File(scriptFile)
    if (script.exists()) {
        def shell = new GroovyShell(classLoader, new Binding(ctx: appCtx, grailsApplication: grailsApp))
        shell.evaluate(script.text)
    } else {
        event("StatusError", ["Designated script doesn't exist: $scriptFile"])
    }
}
 
// this argument parsing target has actually been submitted as a patch to Init.groovy after some feedback
// on the grails user mailing list and will hopefully be in the next release of grails.
// Vote it up if you like it: http://jira.codehaus.org/browse/GRAILS-2663
 
argsMap = [params: []]
 
target(parseArguments: "Parse the arguments passed on the command line") {
    args?.tokenize().each {  token ->
        def nameValueSwitch = token =~ "--?(.*)=(.*)"
        if (nameValueSwitch.matches()) { // this token is a name/value pair (ex: --foo=bar or -z=qux)
            argsMap[nameValueSwitch[0][1]] = nameValueSwitch[0][2]
        } else {
            def nameOnlySwitch = token =~ "--?(.*)"
            if (nameOnlySwitch.matches()) {  // this token is just a switch (ex: -force or --help)
                argsMap[nameOnlySwitch[0][1]] = true
            } else { // single item tokens, append in order to an array of params
                argsMap["params"] << token
            }
        }
    }
    event("StatusUpdate", ["Done parsing arguments: $argsMap"])
}

If you save that in your application’s “scripts” directory as “RunScript.groovy” you can execute it like this:

grails run-script [path-to-script-1] [path-to-script-2]...[path-to-script-n]

The paths are relative to the root of the grails application directory. To see it’s potential use, if you had a directory in your app called “userScripts” that contained a script “createBook.groovy”:

def a = new Author(name: "Arthur C. Clarke")
a.save()
 
def b = new Book(title: "The Nine Billion Names of God", author: a)
b.save()
 
if (!b.hasErrors()) {
    println "${a.name}'s ${b.title} saved successfully!"
}
 
println "Current book list: ${b.list()}"

and if you had an Author domain class:

class Author {
    String name
 
    public String toString() {
        return "Author[name: $name]"
    }
}

and a Book domain class:

class Book {
    String title
    Author author
 
    public String toString() {
        return "Book[author: ${author}, title: $title]"
    }
}

You could then run the script:

grails run-script userScripts/createBook.groovy

And you’d see the output:

Arthur C. Clarke's The Nine Billion Names of God saved successfully!
Current book list: [Book[author: Author[name: Arthur C. Clarke], title: The Nine Billion Names of God]]

Since you’re running in the grails context, you also have access to all of your service and controller classes. Anything that you can do inside grails can also be scripted using this target.

There are 11 comments in this article:

  1. 2008/03/20Mike Stephen say:

    This is really awesome work!

    It seems to me that this is an easy way to reuse your GORM and other grails classes in batch programs.

    Mike.

  2. 2008/03/20Armin Heinzer say:

    Great work, exactly what I has been looking for!

    In my script, I got the error: “failed to lazily initialize a collection of role: …, no session or session was closed”, but this seems to be the same problem like in the Shell (in the Console it works): http://www.nabble.com/domain-relationships-in-shell—console-td15434264.html#a15435468

    Armin

  3. 2008/03/21tednaleid say:

    Thanks guys, I’m glad you found it useful!

    @Mike: Yep, using it in a batch program is exactly what drove me to make this in the first place :)

    @Armin: Do you have a script that you can share that’s exhibiting a problem? My understanding of the bug that you linked to shouldn’t affect the way that this run-script gant task works.

    I’ve run into the issue that you speak of when I try to save an object, but then forget to check .hasErrors() on it after the save to see if it was successful.

    The linked issue seems to be due to the normal grails shell executing one statement at a time and associating a different hibernate context with each. Since context info isn’t shared across multiple statements in the grails shell, they fail. The way run-script is compiling the grails script with a single “evaluate”, I’d think it wouldn’t suffer from this. The entire script should be run with the same hibernate session in scope.

    I recreated the Customer->Sites example given in the post and was able to run the example script without hitting an issue.

    I created and ran 2 test scripts with this command:

    grails run-script testScript.groovy testScript2.groovy

    And got this output:

    testScript all done! Customer : 1, Cust sites: [Site : 1] and Site : 1
    In script 2: [Customer : 1]

    script1.groovy:
    def s = new Site(name: “foo”)
    s.save()
    if (s.hasErrors()) {
    println “Site Errors: ${s.errors}”
    }
    def cust = new Customer(name: “bar”, sites: [s])
    cust.save()
    if (cust.hasErrors()) {
    println “Cust errors: ${cust.errors}”
    }
    println “testScript all done! $cust, Cust sites: ${cust.sites} and $s”

    script2.groovy:
    println “In script 2: ${Customer.list()}”

    Again, if you have an example that demonstrates the problem, I’d love to see it.

  4. 2008/03/31John Hurst say:

    Thank you, thank you, thank you!

    I am fairly new to Grails, and for some strange reason I expected to be able to do this “out of the box” with scripts. I see I have a long way to go to understand the Grails infrastructure.

    I think “run-script” needs to become part of the standard infrastructure!

    Thanks again,

    John Hurst
    Wellington, New Zealand

  5. 2008/05/20BarryMac say:

    I was just working away in the grails console and wanted to be able to run the script in the grails context from just the command line to process some csv files.

    Perfect, thanks a million!

    I agree with the comment it should be built into grails!

  6. 2008/06/16rollo say:

    Runs perfectly, thanks a lot
    But next time, pls don’t forget to espcape HTML characters like gt and lt when putting code in HTML page ;-)
    Rollo

  7. 2008/07/5Tim Russell say:

    Hi Ted,

    I’ve found this extremely helpful. I’m very surprised that something like this isn’t already part of the grails core. I wonder if anyone else has run into some issues dealing with command line arguments: specifically, the removal of equals signs (=) from within parameters.

    Here’s my test script. Versions are:
    – Welcome to Grails 1.0.3 – http://grails.org/
    – Groovy Version: 1.5.6 JVM: 1.6.0_02-b06

    import org.codehaus.groovy.grails.commons.GrailsClassUtils as GCU
    grailsHome = Ant.project.properties.”environment.GRAILS_HOME”
    includeTargets << new File ( “${grailsHome}/scripts/Init.groovy” )
    target(‘default’: “The description of the script goes here!”) {
    println “arguments are: \n” + args + “\n”
    }

    When I run “grails test-run argumentOne argumentTwo –id=15 argumentFour”, I get:

    arguments are:
    argumentOne
    argumentTwo
    –id
    15
    argumentFour

    It seems that some piece of the initialization, in turning the args variable from an array to a newline-delimited string, is treating that equals sign in “–id=15″ as a separator rather than part of the argument.

    Has anyone seen a way to either a) get it to stop doing that or b) get at the original command line parameters?

    Thanks!

  8. 2008/07/5tednaleid say:

    @Tim

    I’m glad you found it useful!

    Regarding the issue you’re asking about, I’m actually not seeing the same behavior that you’re seeing with the “=” getting stripped out. I get the normal java behavior of splitting on a space (which leaves the “-id=15″ intact. In the original script above, I actually have a “parseArguments” Gant target that takes advantage of the = being there. This method is part of grails 1.0.3, so you should have it available if you call “parseArguments()” in your script.

    Here’s a slightly modified version of your script that demonstrates it:

    grailsHome = Ant.project.properties."environment.GRAILS_HOME"
     
    includeTargets << new File ( "${grailsHome}/scripts/Init.groovy" )
    target('default': "The description of the script goes here!") {
    	parseArguments() // populates the argsMap variable
    	println "arguments are:\n $args"
    	println "argsMap = $argsMap"
    }

    when I run this command (slightly modified to have 2 dashes in front of the “id” for posix compliance):

    grails test-run argumentOne argumentTwo --id=15 argumentFour

    Here is the output:

    Welcome to Grails 1.0.3 - http://grails.org/
    Licensed under Apache Standard License 2.0
    Grails home is set to: /usr/local/grails		
     
    Base Directory: /Users/ted/Documents/workspace/testargs
    Note: No plugin scripts found
    Running script /Users/ted/Documents/workspace/testargs/scripts/TestRun.groovy
    Environment set to development
    Done parsing arguments: ["params":["argumentOne", "argumentTwo", "argumentFour"], "id":"15"] ...
    arguments are:
     argumentOne
    argumentTwo
    --id=15
    argumentFour
    argsMap = ["params":["argumentOne", "argumentTwo", "argumentFour"], "id":"15"]

    I’m currently running the same versions of grails/groovy that you are:

    % groovy -v
    Groovy Version: 1.5.6 JVM: 1.5.0_13-119
    % grails version
    Welcome to Grails 1.0.3 - http://grails.org/
    ...

    Do you have the release version of 1.0.3 running? I’d also try just creating a clean app (“grails create-app testargs”) and pasting in the test script to see if you get the same behavior.

  9. 2008/07/7Tim Russell say:

    Looks like I figured it out…

    I now feel it necessary to mention that I’m running Windows XP… and unfortunately this plays a role in what’s occurring. After digging through the Init.groovy code, tracing back to where the system property grails.cli.args is set and doing some more playing in that, I decided to go even further back — to the batch file that actually launches grails on Windows machines (startGrails.bat). Even that batch file was seeing the equals sign as a space. I then created a dummy batch file:

    echo %1 %2 %3

    And ran it:

    test.bat –id=15 b c

    I’d expect the output to be the same parameters I passed in: “–id=15 b c”. But Windows has a better idea:

    –id 15 b

    To wrap things up, everything works fine if I wrap my equals-containing parameter in double-quotes, like so:

    grails test-run argumentOne argumentTwo “–id=15″ argumentFour

    Finally I get the desired result:

    arguments are:
    argumentOne
    argumentTwo
    –id=15
    argumentFour
    argsMap = ["params":["argumentOne", "argumentTwo", "argumentFour"], “id”:”15″]

  10. 2008/07/8tednaleid say:

    @Tim

    Thanks for the follow up message, that makes sense. I used to do a fair amount of programming on windows, but I always used the cygwin shell, which I think would treat this in the same way that it does on my Mac.

  11. 2008/07/21Michael Tkachev say:

    Hi

    I’ve found problem with run-script and many-to-many relations (lazy initialisation exception). And there is fix – change shell execution routine to following:

    appCtx.getBeansOfType(PersistenceContextInterceptor).each { k,v ->
    v.init()
    }
    def shell = new GroovyShell(classLoader, new Binding(ctx: appCtx, grailsApplication: grailsApp))
    shell.evaluate(script.text)
    appCtx.getBeansOfType(PersistenceContextInterceptor).each { k,v ->
    v.flush()
    v.destroy()
    }
    And add
    import org.codehaus.groovy.grails.support.*
    to resolve PersistenceContextInterceptor.

    Thanks, Michael.

Write a comment: