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