Mar 31st 2008 01:11 am Using Gant to execute a groovy script within the Grails context (updated)

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.

scripts/RunScript.groovy:

import org.codehaus.groovy.grails.commons.GrailsClassUtils as GCU
import org.springframework.orm.hibernate3.SessionFactoryUtils
import org.springframework.orm.hibernate3.SessionHolder
import org.springframework.transaction.support.TransactionSynchronizationManager
 
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()
	configureHibernateSession()
    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))
        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 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
}

Book.groovy

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.

Posted by tednaleid / command line and grails and groovy

13 Responses to “Using Gant to execute a groovy script within the Grails context (updated)”

  1. Josh Reed on 11 Apr 2008 at 12:23 am #

    Hey Ted,

    Thanks for this work. I ran into a few problems when I was trying to cut and paste this into my Grails install.

    1) On line 4, it looks like Wordpress ate a line break. It should end after: TransactionSynchronizationManager

    2) Some of the double quotes got converted to nice HTML double quotes. When you cut and paste, make sure you just do a global search and replace to standard double quotes ”

    3) Finally, the regex in the parseArguments target doesn’t work for me. If I type:
    grails run-script userScripts/populateDatabase.groovy
    This matches the nameOnlySwitch. Just looking at the regex, this makes sense to me because you have “-?(.*)” which I interpret as “zero or one dashes followed by anything”. If I switch it to “-(.*)” or “-+(.*)” (one or more dashes) this falls through and the script name gets picked up. Maybe this is another cut and paste artifact.

    Thanks for the good work.

    Cheers,
    Josh

  2. Josh Reed on 11 Apr 2008 at 12:48 am #

    Just a quick follow up, the reason why things were failing was because the double dash got converted to an mdash. So it really should be two separate dashes and then the question mark, which makes more sense from a regex standpoint.

    Cheers,
    Jsoh

  3. tednaleid on 15 Apr 2008 at 3:12 am #

    Ack…Thanks Josh. I’ve been having some trouble getting wordpress to not screw up my code.

    I believe that I’ve fixed it above, but here’s a pastie link if that causes problem s for anyone.

  4. Armin Heinzer on 16 Apr 2008 at 2:58 pm #

    I tried this new version. I could read and create new objects, but I was not able to update an existing object which was read from database before. Such updates were ignored:

    b = Book.get(1)
    b.name += ‘ updated’
    b.save()

    I added following lines to the script and now updates works as well:


    import org.codehaus.groovy.grails.support.*

    configureHibernateSession()

    // added new
    appCtx.getBeansOfType(PersistenceContextInterceptor).each { k,v ->
    v.init()
    }

    argsMap[”params”].each { scriptFile ->
    executeScript(scriptFile, classLoader)
    }

    // added new
    appCtx.getBeansOfType(PersistenceContextInterceptor).each { k,v ->
    v.flush();
    v.destroy()
    }

    May be this is not the most elegant solution (I am not a grails expert yet), but at least it fixed my problem.

    Armin

  5. along on 27 May 2008 at 10:07 am #

    there is something wrong:
    includeTargets >> new File ( “${grailsHome}/scripts/Package.groovy” )
    includeTargets >> new File ( “${grailsHome}/scripts/Bootstrap.groovy” )

    here “>>” should be “<<”

  6. tednaleid on 27 May 2008 at 9:10 pm #

    @along

    Thanks for the note! It got messed up when I initially posted it and wordpress ate the less than signs, I manually edited it and put the wrong ones in, oops. Should be fixed above now.

  7. Oliver on 30 May 2008 at 6:06 am #

    Hi

    Excellent work. Thanks for sharing it with us. One question though: In which grails environment is the script running? Development? If so, how is it possible to specify a different environment?

  8. tednaleid on 30 May 2008 at 9:23 am #

    @Oliver

    By default, scripts run in the development environment. You can run any script in another environment by using the -Dgrails.env <env_name>parameter.

    Here’s an example running in the “test” environment (where environments are defined in the grails-app/conf/Config.groovy file)

    grails -Dgrails.env=test run-script userScripts/createBook.groovy

    You can take a look at the environments section of the Grails documentation for more information.

  9. Oliver on 31 May 2008 at 4:59 am #

    Thanks. Another question: It is currently not supported to pass arguments to the script because you basically treat all arguments as seperate script files right?

  10. tednaleid on 31 May 2008 at 1:10 pm #

    @Oliver, yes, the current script doesn’t pass any arguments to the script and simply treats all parameters as separate script files.

    It wouldn’t be that hard to change it to have it instead only parse a single file and pass the parameters along to the calling script.

    The call to parseArguments would simply grab the first parameter as the file name and create a variable that contains the remaining arguments.

    Then, executeScript would add the remaning parameters as a new item in the Binding object.

    Here’s an updated script with these changes (I called it RunScriptWithParms.groovy):

    import org.codehaus.groovy.grails.commons.GrailsClassUtils as GCU
    import org.springframework.orm.hibernate3.SessionFactoryUtils
    import org.springframework.orm.hibernate3.SessionHolder
    import org.springframework.transaction.support.TransactionSynchronizationManager
     
    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()
    	configureHibernateSession()
    	scriptFile = argsMap["params"].getAt(0)
    	// remove our script name from the params so when we call the script it's not in there
    	argsMap["params"].remove(scriptFile)
        executeScript(scriptFile, classLoader, argsMap)
    }
     
    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, argsMap) {
        File script = new File(scriptFile)
        if (script.exists()) {
            def shell = new GroovyShell(classLoader, new Binding(ctx: appCtx, grailsApplication: grailsApp, argsMap: argsMap, args: args))
            shell.evaluate(script.text)
        } else {
            event("StatusError", ["Designated script doesn't exist: $scriptFile"])
        }
    }
     
    // this argument parsing target was submitted as a patch to grails and is availble in Init.groovy as of 1.0.3
    // see 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 have this groovy script (userScripts/showParams.groovy):

    println "in showParams script, argsMap is: $argsMap"
    println "in showParams script, raw args are: $args"

    And call it with this command line:

    grails run-script-with-parms userScripts/showParams.groovy --foo=zazz -b=quux

    You’ll get this as a result with the args passed in:

    in showParams script, argsMap is: ["params":[], "foo":"zazz", "b":"quux"]
    in showParams script, raw args are: userScripts/showParams.groovy
    --foo=bar
    --foo=zazz
    -b=quux
  11. Steve on 01 Jun 2008 at 3:08 pm #

    I tried your code to load my script, but my script wasn’t able to read existing entries in the database (which works when loaded into grails console).

  12. tednaleid on 02 Jun 2008 at 12:05 am #

    @Steve Did it give you some sort of error, or were the items just not found? If they weren’t found, I’d be sure that you’re calling the script with the right environment (noted in the comments above). If you’re getting an error, does the stuff mentioned above by Armin Heinzer fix things for you?

  13. Steve on 11 Jun 2008 at 6:31 pm #

    Sorry for the delay. I ended up just running my script in the grails console. I’m not familiar enough with grails to try what was suggested by Armin.

Trackback URI | Comments RSS

Leave a Reply