Thursday, July 3, 2008

Grails Bootstrapping

So outside of the project I've been describing in various posts thus far, I have a Grails application that I've developed for my real job. The application manages personnel data related to the industry I'm in. The data can originate from our website, or through a phone screening partner. The phone screening partner manages an Excel spreadsheet for all of the data that they collect, and it's my job to merge that data back into our central database.

Seems simple enough:
  1. Create an SSH tunnel to our production database
  2. Configure a Grails DataSource with an arbitrary environment configuration ('prodtunnel')
  3. Develop a script that parses the spreadsheet, binds each row to a Grails domain object and then persists the object to the production database by leveraging Grails' dynamic GORM save() methods
Sounds easy enough, right? Well - it turns out that getting GORM to work outside of a running application (and an integration test) was tougher than I planned.

My first swipe went something like this:
  1. Create a GANT script.
  2. Follow the documentation here
  3. Writing a one-liner just to ensure that Grails was properly bootstrapped and that I would be able to use GORM for persistence.
Here's that initial script:

Ant.property(environment: "env")
grailsHome = Ant.antProject.properties."env.GRAILS_HOME"
includeTargets << new File("${grailsHome}/scripts/Bootstrap.groovy")

target('default': "First try") {
//copy and paste from $GRAILS_HOME/scripts/Shell.groovy
depends(configureProxy, packageApp, classpath)
classLoader = new URLClassLoader([classesDir.toURI().toURL()] as URL[], rootLoader)
Thread.currentThread().setContextClassLoader(classLoader)
loadApp()
configureApp()
println "Subject count: ${Subject.count()}"
}


When I ran the script, here's what I was greeted with

No signature of method: static Subject.count() is applicable for
argument types: () values: {}


So what's wrong? Well, Groovy scripts are not interpreted line by line (and as a whole, Groovy IS NOT an interpreted language). From GinA:


Groovy syntax is line orientated, but the execution of Groovy code is not. Unlike other scripting languages, Groovy code is not processed line-by-line in the sense that each line is interpreted separately.


So what does that mean in the context of the problem I've presented? Well, even though I've bootstrapped the the Grails environment and THEN called my the dynamic GORM method on my domain class - it doesn't matter. The JVM has already loaded the Subject class because the Java byte code has already been generated from the script by the time the script runs. That's the problem, and now here's my solution:

Ant.property(environment: "env")
grailsHome = Ant.antProject.properties."env.GRAILS_HOME"

includeTargets << new File("${grailsHome}/scripts/Bootstrap.groovy")


target('default': "Working edition") {
//we need one arg, the script to run. Follow a convention here,
//the argument is the name of the script to run minus the file
//suffix and 'Script' naming convention. For example, running:
//>grails ScriptRunner Merge
//will run $PROJECT_ROOT/test/local/MergeScript.groovy with the
//fully bootstrapped environment
if (!args) {
throw new RuntimeException("[fail] This script requires an argument to the script to run.")
}
//copy and paste from $GRAILS_HOME/scripts/Shell.groovy
depends(configureProxy, packageApp, classpath)
classLoader = new URLClassLoader([classesDir.toURI().toURL()] as URL[], rootLoader)
Thread.currentThread().setContextClassLoader(classLoader)
loadApp()
configureApp()
new GroovyScriptEngine(Ant.antProject.properties."base.dir", classLoader)
.run("test/local/${args}Script.groovy", null)
}

MergeScript.groovy:

println "Subject count: ${Subject.count()}"

Console after running the script:

Subject count: 505

The key to the solution is using the class loader created by the Grails bootstrapping process and then passing that to my target script. I hope this saves someone some time out there :) If anyone's got something more elegant or cleaner - please post a comment!

Update: here's a bit more information on the same subject.

2 comments:

npiv said...

This was incredibly helpful to my project, thanks a bunch

Anonymous said...

Good to know. Thank you.