MetaBuilder is a builder that builds builders.

MetaBuilder

As if there weren't enough options already for constructing your own builders in Groovy, along comes another: MetaBuilder. Quite literally, MetaBuilder is a builder that builds builders. Through some simple examples, this article will show you how you can put MetaBuilder to work for you in just three easy steps. To follow along, simply visit SourceForge to get the MetaBuilder distribution and include groovytools-builder-x.x.x.jar in your classpath.

Oh, and you'll also need Groovy 1.5 and Java 1.5, too (smile)

MetaBuilder in Three Easy Steps

  1. Create an instance of MetaBuilder
  2. Define your domain specific language (DSL)
  3. Build your objects

Create an instance of MetaBuilder

There's almost nothing to this step but just to call a constructor:

import groovytools.builder.*

MetaBuilder mb = new MetaBuilder(getClass().getClassLoader())

You don't have to pass MetaBuilder a class loader, but in some cases, especially in scripts that declare new classes such as this one, it's necessary. This can be done by setting the classLoader property or by passing the class loader in MetaBuilder's constructor, as shown above.

Define Your Domain Specific Langauge (DSL)

MetaBuilder provides a DSL, implemented as a Groovy builder, for defining your own builders. If you are already familiar with builders in Groovy, it takes just one simple example to get you started. Continuing where we left off above, let's start defining a customer class and customer builder:

class Customer {
    def name
    def dateOfBirth
    def ssn
    def phone
}

mb.define {
    customer(factory: Customer) {
        properties {
            name()
            dateOfBirth()
            ssn()
        }
    }
}

In the previous snippet, define is used to tell MetaBuilder that we are going to create some new definitions, or schema, for the objects that our new builder can create. MetaBuilder keeps track of our definitions and it's even possible to reuse and extend these definitions, as we'll see later.

customer is the name of the schema and the factory attribute tells MetaBuilder what object to create whenever a customer is to be built. properties contains a list of property names.

MetaBuilder will throw an excception if a build script attempts to use unspecified or mispelled properties. So, for example, even though phone is a member of Customer, MetaBuilder won't allow you to use it unless you add it to your schema.

Build Your Objects

Building objects is now just a matter of telling MetaBuilder that is what you want to do:

def aCustomer = mb.build {
    customer {
        name = 'J. Doe'
        dateOfBirth  = '1/1/1900'
        ssn  = '555-55-5555'
    }
}

// this is equivalent
aCustomer = mb.build {
    customer ( name: 'J. Doe', dateOfBirth: '1/1/1900', ssn: '555-55-5555')
}

// you can even mix up the styles:
aCustomer = mb.build {
    customer ( name: 'J. Doe', dateOfBirth: '1/1/1900') {
        ssn = '555-55-5555'
    }
}

Great! If you've been following along in your own IDE, you hopefully now have the basics down and are ready to take a look at some advanced techniques.

MetaBuilder.build() returns the last object constructed. If your build script creates multiple objects, use the buildList method instead to return all of them.

Catching Errors

What would happen if you used a property that was not in the schema or mistyped a legitimate property name? For example:

aCustomer = mb.build {
    customer {
        name = 'J. Doe'
        dob  = '1/1/1900'      // should have been 'dataOfBirth'
        ssn  = '555-55-5555'
        phone = '1-555-555-5555' // not allowed
    }
}

If you executed this, MetaBuilder will throw the following exception:

groovytools.builder.PropertyException: Property 'dob': property unkown

Assuming you fix dob only and retry, MetaBuilder will throw another exception:

groovytools.builder.PropertyException: Property 'phone': property unkown

Despite the fact that phone is an actual property of your class, MetaBuilder only goes by what's in the schema. That kind of checking can protect private or sensitive properties. Read on to see how MetaBuilder can do even more with some additional information.

Controlling the Build

The purpose of this section is to go a bit deeper into MetaBuilder's capabilities.

More on factory

Let's take another look at the factory attribute used earlier. By simply specifying the factory attribute in your schemas, you tell MetaBuilder how to build the right object every time. MetaBuilder was designed to accept as wide a variety of values as possible. For example, you can specify the factory attribute as any of the following:

Feel free to consult MetaBuilder Meta-Schema for all the gory details on each of the attribute values MetaBuilder accepts.

This next example demonstrates how one might use closure to create Customer objects:

mb.define {
    customer2(factory: { new Customer() } )) {
        properties {
            name()
            dateOfBirth()
            ssn()
        }
    }
}

def aCustomer2 = mb.build {
    customer2 {
        name = 'J. Doe'
        dateOfBirth  = '1/1/1900'
        ssn  = '555-55-5555'
    }
}

Property Attributes

MetaBuilder supports a number of useful attributes on properties. These include the following:

Reusing and Extending Schema: the schema Attribute

Use the schema attribute to tell MetaBuilder that you want to reuse a schema. In the next example, we'll create a new Phone class and update our schema to use it:

class Phone {
    def type
    def number
}

mb.define {
    phone (factory: Phone) {
        properties {
            type(check: ['home','cell','work'], def: 'home')
            number(req: true, check: ~/\d{3}-\d{3}-\d{4}/)
        }
    }
}

Now, let's make phone a required property on our customer schema:

mb.define {
    customer4(factory: Customer) {
        properties {
            name(req: true, min: 1)
            dob(property: 'dateOfBirth')
            ssn(check: ~/\d{3}-\d{2}-\d{4}/)
            phone(schema: 'phone', req: true)
        }
    }
}

def aCustomer4 = mb.build {
    customer4 {
        name = 'J. Doe'
        dob  = '1/1/1900'
        ssn  = '555-55-5555'
        phone {
            type = 'home'
            number = '123-456-7890'
        }
    }
}

Collections

So far, we've only looked at examples of properties, but MetaBuilder also supports collections. You define collections like you define properties, just provide a list of them and set attributes as needed.

class Customer2 extends Customer {
    def phoneList = []
    def addresses = [:]
}

mb.define {
    customer5(factory: Customer2) {
        properties {
            name(req: true, min: 1)
            dob(property: 'dateOfBirth')
            ssn(check: ~/\d{3}-\d{2}-\d{4}/)
            phone(schema: 'phone', req: true)
        }
        collections {
            phoneList(min: 1) {
                phone(schema: phone)
            }
            addresses (key: 'type', min: 1, max:2) {
                address() {
                    properties {
                        type(check: ['billto', 'shipto'], def: 'billto')
                        street()
                        city()
                        state()
                        zip()
                    }
                }
            }
        }
    }
}

The above definition extends Customer and adds a list of phone numbers and a map of addresses. Like properties, collections are mapped to properties of an object by the name.

Note how address is defined directly within the collection. Nesting definitions can make the definitiona bit more brief, but comes at the risk of creating definitions that aren't as easily reused.

Another thing to note is the use of the key attribute in the addresses collection. Presence of the key attribute tells MetaBuilder that the parent-child relationship is indexed. In the following example, you can see that customer5 has both a list of phone and map of addresses using the address's type
and the key.

def aCustomer5 = mb.build {
    customer5 {
        name = 'J. Doe'
        dob  = '1/1/1900'
        ssn  = '555-55-5555'
        phone {
            type = 'home'
            number = '123-456-7890'
        }
        phoneList {
            phone(type: 'work', number: '111-222-3333')
            phone(type: 'cell', number: '444-555-6666')
        }
        address {
            type   = 'billto'
            street = '1234 Some St.'
            city   = 'Some City'
            zip    = '12345'
        }
        address {
            type   = 'shipto'
            street = '1234 Some Other St.'
            city   = 'Some Other City'
            zip    = '12345'
        }
    }
}

Collections support a number of useful properties:

Where to Go From Here

This whirlwind tour of MetaBuilder really only just scratched the surface of its features and capabilities. If you get stuck, be sure to check out the MetaBuilder Meta-Schema, which describes the entire MetaBuilder feature set.

Also included in the release are a number of tests and examples that are also useful to look at.

Mailing List

Developer(s)