Using Gant to Execute a Groovy Script Within the Grails Context (Updated)

| Comments

UPDATE: A new version of this script has been merged into the Grails 1.3.6 release so it’ll be part of core Grails with the next version of grails. You’ll only need to get it manually for versions earlier than that, see this post for the version that was merged into grails.

In a previous post I showed a script that I had created to allow the execution of a groovy script within a grails application context (including access to domain objects, controllers, services, etc). A couple of people reported a problem with the script where they were getting lazy initialization exceptions. I finally tracked this issue down to one where many-to-many relationships are being used between two domain classes.

Here is an updated script that fixes this issue by setting up the hibernate session in the Gant script.


import org.codehaus.groovy.grails.commons.GrailsClassUtils as GCU
import org.springframework.orm.hibernate3.SessionFactoryUtils
import org.springframework.orm.hibernate3.SessionHolder

grailsHome ="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)

target(runScript: "Main implementation that executes the specified script after starting up the application environment") {
    if (argsMap["params"].size() == 0) {
        event("StatusError", ["Required script name parameter is missing"])
        System.exit 1
    classLoader = new URLClassLoader([classesDir.toURL()] as URL[], rootLoader)
    argsMap["params"].each { scriptFile ->
        executeScript(scriptFile, classLoader)

def configureHibernateSession() {
    // without this you'll get a lazy initialization exception when using a many-to-many relationship
    def sessionFactory = appCtx.getBean("sessionFactory")
    def session = SessionFactoryUtils.getSession(sessionFactory, true)
    TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session))

def executeScript(scriptFile, classLoader) {
    File script = new File(scriptFile)
    if (script.exists()) {
        def shell = new GroovyShell(classLoader, new Binding(ctx: appCtx, grailsApplication: grailsApp))
    } 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:

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 that exercises the many-to-many relationship:

def theTalisman = new Book(title: "The Talisman").save()
def stephenKing = new Author(name:"Stephen King").addToBooks(theTalisman).save()
def peterStraub = new Author(name:"Peter Straub").addToBooks(theTalisman).save()def book = Book.findByTitle("The Talisman")
println "Found ${book.title} with authors = ${book.authors*.name}"

If you had these domain classes: Author.groovy

class Author {
static hasMany = [books:Book]
String name


class Book {
static belongsTo = Author
static hasMany = [authors:Author]
String title

You could then run the script:

grails run-script userScripts/createBook.groovy

And you’d see the output:

Found The Talisman with authors = ["Stephen King", "Peter Straub"]

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.

Without the new fix that configures the hibernate transaction on the session (configureHibernateSession()), you’d see this error instead:

failed to lazily initialize a collection of role: Book.authors, no session or session was closed

Thanks to Peter Wolf on the GUM mailing list for reporting an issue and testing things out as well as Armin Heinzer for the pointer to the suggested fix on the grails-user list.