How to eliminate cut-and-paste closures when using JSONBuilder
Does anyone know of a way to avoid repeating a closure when emitting the same object more than once using the latest version of the Grails JSONBuilder?
I have a Group
domain object that contains sets of Members
and Leaders
. I would like to find a way to emit a person without having to cut and paste the closure.
def builder = new JSONBuilder()
def result = builder.build {
array {
Group.list().each { Group group ->
g = {
id = group.id
name = group.name
members = array {
group.members?.person?.sort().each { Person person ->
m = { // Person closure copy #1
id = person.id
firstName = person.firstName
lastName = person.lastName
}
}
}
leaders = array {
group.leaders?.person?.sort().each { Person person ->
l = { // Person closure copy #2
id = person.id
firstName = person.firstName
lastName = person.lastName
}
}
}
}
}
}
}
I have tried defining the closure separately but that leads to errors such as: exception: No such property: id for class:
.
Some notes:
1) The domain objects in the example are greatly simplified. I'm using JSONBuilder instead of render Group.list() as JSON
or render Group.list().encodeAsJS开发者_C百科ON
because I need to control what parts of my objects are encoded.
2) I'll accept authoritative answers that explain why this can't be done.
After repeated failures using closures, I have a solution. It doesn't use a closure directly but instead uses a closure that returns a map.
class Person {
...
def toMap = {
def map = [:]
map["id"] = this.id
map["firstName"] = this.firstName
map["lastName"] = this.lastName
return map
}
}
def builder = new JSONBuilder()
def result = builder.build {
array {
Group.list().each { Group group ->
g = {
id = group.id
name = group.name
members = array {
group.members?.person?.sort().each { Person person ->
m(person.toMap())
}
}
leaders = array {
group.leaders?.person?.sort().each { Person person ->
l(person.toMap())
}
}
}
}
}
}
The m(person.toMap())
syntax isn't intuitive but it works and allows me to avoid repeating myself. This blog entry provides the details and explains the origin of the current Grails JSONBuilder.
You can execute another closure in the same "context" by setting the "delegate" for the closure. Closures aren't multi-thread safe (only one delegate at a time) so you will have to clone the closure each time if the closures are shared in a singleton class or static variable.
This is just an idea of refactoring the code, it might not work (I didn't test it). You can replace assignment (=) with "setProperty" and DSL method calls with "invokeMethod" if you have to dynamicly decide the property name or method name in the DSL. (reference http://groovy.codehaus.org/api/groovy/lang/Closure.html)
def personClosure = { Person person, varname ->
setProperty(varname, {
id = person.id
firstName = person.firstName
lastName = person.lastName
})
}
def groupMembersClosure = { memberList, memberListVarName, memberVarName ->
personClosure.delegate = delegate
setProperty(memberListVarName, array {
memberList?.person?.sort().each personClosure, memberVarName
})
}
def builder = new JSONBuilder()
def result = builder.build {
array {
Group.list().each { Group group ->
g = {
id = group.id
name = group.name
groupMembersClosure.delegate = delegate
groupMembersClosure(group.members, 'members', 'm')
groupMembersClosure(group.leaders, 'leaders', 'l')
}
}
}
}
精彩评论