The problem:
I wanted an easy way of building a graph of objects based on the NavElement class below from some sort of configuration.
class NavElement {In the past, XML would have been the obvious format for the configuration, which would then be turned into java objects via something like commons digester.
NavElement parent
String name
String controller
String action
List children = []
boolean selected
boolean equals(other) {
// omitted
}
int hashCode() {
// omitted
}
}
<navbar>However, now we are Groovy, some new options present themselves. Firstly, we can represent the XML in groovy code and use MarkupBuilder to turn the config code into XML, if we prefer.
<navelement name="Create">
<navelement name="Article" controller="article" action="create">
</navelement>
<navelement name="'Admin'" role="administrator">
<navelement name="Create" controller="user" action="create">
<navelementt name="List" controller="user" action="list">
</navelement>
</navbar>
navBar {Obviously there is no advantage to this in this particular case - we are not dynamically building the closure for example. In fact now we now have the extra, albeit trivial, task of converting the groovy config closure to XML.
navElement(name:'Create')
navElement(name:'Article', controllerName:'article', action:'create')
}
navElement(name:'Admin') {
navElement(name:'Create', controllerName:'user', action:'create')
navElement(name:'List Users', controllerName:'user', action:'create')
}
}
However, if we could use the same groovy config closure to generate the object graph directly with our own custom builder, we can save ourselves this extra step AND the need for marshalling code from XML to our object graph. We can also make the groovy config closure less verbose. Let's look at how we might simplify groovy config closure.
navBar {Now we need to implement a builder that can turn this config closure into our object graph. To do this, the groovy api provides an abstract BuilderSupport class to extend and implement. There are a couple of examples about this class on the groovy website.
create {
Article(controllerName:'article', action:'create')
}
Admin {
Create( controllerName:'user', action:'create')
'List Users'(controllerName:'user', action:'create')
}
}
Make a builder
Builder Support
So here is the Builder implementation for our example.
class NavBuilder extends BuilderSupport {
public static NavBuilder newInstance() {
return new NavBuilder();
}
protected void setParent(Object parent, Object child) {
child.parent = parent
}
protected Object createNode(Object name) {
createNode name, null, null
}
protected Object createNode(Object name, Object value) {
createNode name, null, value
}
protected Object createNode(Object name, Map attrs) {
createNode name, attrs, null
}
protected Object createNode(Object name, Map attrs, Object value) {
def node = new NavElement(name: name)
if (attrs) {
attrs.each {property, val ->
if (node.properties.keySet().contains(property)) {
node."${property}" = val
}
}
}
if (getCurrent()) {
getCurrent().children << node
}
return node
}
}
There are a few things to take note of
1. The createNode methods
These methods are called as each node in the closure is evaluated. In our example, which is fairly simple, we delegate all the over-loaded methods to the most complex version. This simply news up a NavElement based on the node name and sets the properties on the new NavElement. You'll note we do nothing with value parameter. We rely on the BuilderSupport class to take care of the recursive calls to the createNode for the nested elements. I have highlighted in the closure snippet below how the parameters, name, attributes and value, map to the closure elements for a particular node.
navBar(controllerName:'home', action:'index') {
Admin {
Create( controllerName:'user', action:'create')
'List Users'(controllerName:'user', action:'create')
}
}
2. The getCurrentNode method
This returns the current node, which returns the current node in the closure that is being evaluated. In our example, we need to use this method to add any nodes that are being created to the children of the currentNode.
3. The setParent method. This is a convenient hook to allow us to set the parent of any node we are creating.
That's it! Here is a bit of test code that proves it works
class NavBuilderTests extends GroovyTestCase {
void testBuildingOfNavElements() {
navBuilder = new NavBuilder()
navBar =
navBuilder.navBar() {
publisher()
create {
'create article'(controller:'articleController', action:'create')
}
}
assertEquals ('navBar', navBar.name)
assertNotNull navBar.children
assertEquals 2, navBar.children.size()
assertEquals ('publisher', navBar.children[0].name)
assertEquals ('navBar', navBar.children[0].parent.name)
def createNavElement = navBar.children[1]
assertEquals ('create', createNavElement.name)
assertEquals ('navBar', createNavElement.parent.name)
assertEquals 1, createNavElement.children.size()
def articleCreateNavElement = createNavElement.children[0]
assertEquals ('create article', articleCreateNavElement.name)
assertEquals ('articleController', articleCreateNavElement.controller)
assertEquals ('create', articleCreateNavElement.action)
assertEquals ('create', articleCreateNavElement.parent.name)
}
}
No comments:
Post a Comment