Mar 19th 2008 05:07 am Using Gant to execute a groovy script within the Grails context

(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.

Posted by tednaleid / grails and groovy

11 Responses to “Using Gant to execute a groovy script within the Grails context”

  1. Mike Stephen on 20 Mar 2008 at 10:18 am #

    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. Armin Heinzer on 20 Mar 2008 at 1:20 pm #

    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. tednaleid on 21 Mar 2008 at 4:33 am #

    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. John Hurst on 31 Mar 2008 at 9:47 pm #

    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. BarryMac on 20 May 2008 at 4:55 pm #

    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. rollo on 16 Jun 2008 at 4:08 am #

    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. Tim Russell on 05 Jul 2008 at 1:27 pm #

    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. tednaleid on 05 Jul 2008 at 1:54 pm #

    @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. Tim Russell on 07 Jul 2008 at 3:42 pm #

    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. tednaleid on 08 Jul 2008 at 2:14 pm #

    @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. Michael Tkachev on 21 Jul 2008 at 8:29 am #

    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.

Comments RSS

Leave a Reply