Groovy Markup BeanBuilder


I've continued experimenting with Groovy. I'd still like to be able to perform application configuration via Groovy scripts rather than XML. As others commented after my 'Groovy Killed the XML File?' posting Groovy Markup might be the answer. So I started looking at the AntBuilder and BuilderSupport classes to see how they worked and started a Builder implementation to connect JavaBeans (or GroovyBeans) together. A google search for "groovy beanbuilder" yielded a single result, a reference to BeanBuilder in aTODO.txt file in the groovy cvs repository, but nothing in the source distribution I downloaded. So I rolled my own.

BeanBuilder employs the Groovy Markup syntax to enable a tree of JavaBean, or GroovyBean instances to be created and wired together.

The following example illustrates BeanBuilder markup in a groovy script:

builder = new BeanBuilder(this) Object o = builder.bean { beanInstance { mapInstance { key1 { bean { } } key2 { list { } } } } }

Each node name (bean, mapInstance, etc.) is handled according to the type of it's containing element. The above markup is equivalent to the following Java code:

map map = new HashMap(); map.put("key1", new BeanBuilderBean()); map.put("key2", new ArrayList()); BeanBuilderBean bean = new BeanBuilderBean(); bean.setMapInstance(map);

The markup outermost element is always a reference to builder.XXX, where XXX is a reference to a bean property, or bean creator method (see below) on the builder delegate. The delegate is passed in the BeanBuilder constructor. If the no-argument constructor is used then the builder instance is it's own delegate. As you will see a delegate can really enhance the expressiveness of the bean markup.

If the containing element is a bean then the builder looks for a setXXX() method or an addXXX() method on the containing instance (XXX is the node name in the markup). If one of these methods is found the builder creates an instance of the class of the method argument and invokes the setXXX() or addXXX() on the containing bean.

If the setter method argument is an interface or is declared as type Object the type can be set explicitly by supplying a class attribute in the markup:

Object o = builder.bean { objectInstance(class:BeanBuilderBean.class) { : : } }

The builder recognizes Collection, Set and Map interfaces as setter arguments and supplies an instance of one of the implementation classes (ArrayList, TreeSet and HashMap). If the setter argument is of type Object and no class attribute is declared an exception will be thrown if you attempt to nest any markup inside the element.

Simple bean properties (numbers, strings, etc.) must be set via attributes in the node definition:

Object o = builder.bean { objectInstance(class:BeanBuilderBean.class, stringInstance: 'hello world') { : : } }

The structured markup is reserved for setting nested bean properties.

If the containing element is a bean and no set/add method is found for the node name in the markup then the builder looks for a method named createXXX(), first on the object created for the parent node, then on the builder delegate object (passed in the BeanBuilder constructor) and finally on thee builder object itself. If such a method is found it is invoked and the return value is used as the bean to which any nested markup is applied. Implementing these creator methods on your builder delegate allows you to create your own 'vocabulary' for the markup. For example, the method:

public BeanBuilderBean createBean() { return new BeanBuilderBean(); }

implemented on the builder delegate allows 'bean' to be used as a node tag thus:

Object o = builder.bean { }

The BeanBuilder class implements several custom creator methods which extend the tag vocabulary to include the collection classes:

Object o = builder.bean { collectionInstance { map { } list { } set { } collection { } } }

The above markup initializes the bean's collectionInstance property with a Collection instance with 4 elements of type Map, List, Set and Collection. You can implement any, or all, of these methods, e.g. createMap(), in your builder delegate class to provide your own custom implementation of the interface.

If the parent node represents a Map or a Collection, nested markup is handled differently. For a Collection class the collection elements can be declared in one of 2 ways, by declaring an element node and providing a class attribute:

Object o = builder.collection { element(class:BeanBuilderBean.class) { } }

or, as described above, using one of your custom node tag names to initialize the collection elements:

Object o = builder.collection { bean { } }

Map entries can be declared with an 'entry' node:

Object o = builder.testMap { entry(key:'myBean') { bean { } } }

The entry node must have a key attribute. Any bean resulting from the markup nested inside the entry element is use to set the value of the map entry.

If the map keys are strings than the map entries can be declared thus:

Object o = builder.map { myBean { bean { } } }

the bean is stored in the map with the key 'myBean'. What about keys in a Map that are also beans? MH.

List and Set elements are declared using the same markup as described above for collections.

I used the groovy-1.0-beta-5 source distribution and added classes to the groovy.util package in the main and test source directories. You can download the source: groovy-beanbuilder.tgz. All of the examples in this document are taken from the BeanBuilderTest.groovy test case script. I placed the classes and scripts in the 'groovy.util' package only so I could execute the tests using the groovy testing mechanism in the groovy source distribution. BeanBuilder is NOT part of the groovy source tree.

Conclusions

The BeanBuilder markup looks clean and extending the markup language via creator methods on the builder delegate should allow the markup for a particular implementation to be very expressive, with a minimum of custom code.

The markup gets compile or edit time checking only for markup structure, not for type-safety since node tags do not resolve to a bean implementation until the groovy code is executed. However, since the code is a script it can be tested prior to deployment.

The implementation handles only trees of beans, not graphs. A bean created in one part of the markup cannot, yet, be referenced by another bean elsewhere in the markup. Update: This entry: Shared References in BeanBuilder shows the implementation of shared bean references added to BeanBuilder.

I'm getting to like Groovy the more I try it out. I wonder how long before we're all coding Groovy instead of Java?