Template Groovy Configurator

Template Groovy Configurator is an implementation of ConfigurationInterface allowing the use of Groovy-based Domain Specific Language. A central concept of the language is a template - a named piece of configuration information. The templates are defined in configuration files by a following statement:

template Dog init {

            [
                color:"black",
                length:60,
                head:[
                    eyes:"black",
                    ears:"small" 
                ],
                favoriteToys:[
                    "teddy",
                    "ball",
                    "bone" 
                ]
            ]
}

This particular statement creates a template called "Dog". The {...} part of the statement is a Groovy closure. Whatever the closure returns is the content of the defined configuration.

  • Note 1: Groovy closures can either use the return statement, or if the return statement is omitted (as in the example), result of the last statement is returned.
  • Note 2: Content of the configuration can be retrieved by the ConfigurationInterface using it's name - i.e. "Dog", if the content is a Map (defined in Groovy using the square braces and colons [:], the members of the map can be retrieved using dot notation, such as "Dog.head.eyes" would return "black".

Single configuration file can contain any number of template definitions. Note also, that since the definition of the template content is a Groovy closure, any valid Groovy code (and therefore any valid Java code) can be inside the definition.

Modification and Derivation

There are two basic operations that can be performed on the templates - modification and derivation. In abstract terms, the derivation operation is used to create a hierarchy of templates (DAG in general), where the derived template derives it's content from the parent template. Closely related is the modification template, which is used to append modifiers to the template. The modifiers are then used to modify the content of the template. By combining derivation and modification operations we can arrive at structures such as depicted in Figure 1.

What we see in Figure 1. is, that template A was defined in file template1.groovy, then template B was derived from A and modified by mod3 in file template2.groovy and finally template A was modified by mod2 in template3.groovy. The resulting configuration is A modified by mod1 and mod2 , and B derived from A with modifications mod1 and mod2 modified by mod3. This illustrates how the modifications stack on the template and how derived template depends on another template with all its modifications.

It is also important to say, how the parsing and processing of the configurations works. The configuration files are parsed sequentially, creating new templates, adding modifications to the modification stack of each template and creating derivation links between the templates. Only after all the files are parsed, the configurations are processed - modifications are evaluated and contents are passed over the derivation links. After that the configurations are ready to be accessed via the ConfigurationInterface.

Now, let us have a look on some examples.

Modify

Modify can be used in three variants based on flexibility of Groovy syntax:

//variant 1
template Dog modify color:"white" 

//variant 2
template Dog modify (
        head:[
            ears:"large" 
        ]
    )

//variant 3
template Dog modify {
    it.length = 100
    return it
}

Variants 1. and 2. are equal in meaning and are just different use of syntax. Their result is that the map given to modify as an argument is merged with the content of the modified template. This use of modify requires the template content to be a map so the merge can be performed. Note, that during the merge, values with the same key are replaced by the new values.

Different meaning has variant 2. In this variant the argument of modify is a closure, which gets as input the content of the template. The output of the closure is then used as a new content of the template. Note, that in the example it is crucial to use the return statement to return the modified content - otherwise, new content of the template would be the result of the last statement witch is 100 (it is in Groovy a default parameter name of a closure).

  • Note 1: If more than one modifier is applied to a template, the modifiers are executed in the order they were defined and the input of a modifier is the output of the previous one.

DeriveFrom

Derivation is performed by a keyword deriveFrom. On its own, the only effect of the derivation is that two templates are chained together, so the content of the second one is the same as the content of the first one with all modification applied. This is usually not very interesting and therefore the deriveFrom command is usually chained with the modify command defined above. The code looks like this:

template Retriever deriveFrom Dog modify {

    it.color = "golden" 
    it.canSwim = true

    return it

}

Now the flow of the data is such that the content of Dog with all its modifications applied is placed into the content of Retiever to which is then applied the modification defined above. The way derivation is used above results in a configuration Retirever which has all parameters derived from Dog except for color which is overridden and a new parameter canSwim. Now if a modification is applied to the Dog template, it accordingly propagates to the Retirever template.

Another use of derivation is to take one template as parameters to another one. Example follows:

template BoxParams init {
    [
        width:10,
        height:15,
        depth:5,
    ]
}

template Box deriveFrom BoxParams modify { params ->
    return [
        volume:params.width * params.height * params.depth    
        ]
}

In this example, the result of derivation and modification is completely different structure then the original template, but whenever the BoxParams template is modified, the modifications propagate into the Box template.

It is also possible to derive a template from multiple templates, but this feature will be described in the next section.

Advanced Operations

Clone

The clone operator has similar functionality to deriveFrom with one major difference. When clone is used, the content of the original template (with all so far defined modifications) is copied to the new template's content, but no derivation link is created, therefore when the original template is modified after the clone command was used, the modification is not propagated to the new template.

Derive Multiple

Operation deriveFrom can take more than one template name as parameters. Such case is illustrated in following example:

template AnimalHome deriveFrom Dog, Cat composeBy { animals ->

    return [
        homeName:"Lonely Animals",
        fullCapacity:10,
        animals:animals
    ]

} modify {

    it.capacity = it.fullCapacity - it.animals.size()

    return it
}

First, a composition closure is used to compose the content of all parents of the new template. The composition closure is defined by a composeBy keyword. Composition closure receives a list of contents of the parent templates as an input. It's output is then used as a content of the new template - as with simple derivation the content can be modified by one or more modification closures. If the composeBy closure is omitted, default map sum composition is used.

Coderive

Operation coderiveFrom is used to add new parent to an existing template, resulting in a situation similar as if deriveFrom had been used on multiple templates. The composition closure can be again modified by using the composeBy operation.

template AnimalHome coderiveFrom Retriever

In the example above, the Retriever template would be added to the set of AnimalHome parents, resulting in a different value of the capacity field.

In-lining Processing of Configurations

It may be reasonable in some situations to want to parse and process some configurations in a separate configuration space (not mixing with the currently parsed templates) and place the result of such processing into a content of the currently parsed template. This functionality can be obtained using the process keyword. Use of process is demonstrated in the following example:

//plane1.groovy
template PlaneParams init {
            [
                name:"Plane2",
                id:1034,
            ]
}
//planetemplate.groovy
template Plane deriveFrom PlaneParams modify { params ->
            [
                name:params.name,
                address:"${params.id}/${params.name}",
            ]
}
//planes.groovy
template Planes init {
            [
                Plane1: process("plane1.groovy", "planetemplate.groovy"),
                Plane2: process('template PlaneParams init name:"Plane2",id:1034', "planetemplate.groovy") 
            ]
}

In this example, some plane parameters are defined in plane1.groovy which are then used by planetemplate.groovy. In planes.groovy is then shown, how in-line processing can be used. The Map of planes is populated by configurations defined in the respective files. Also note, that not only files have to be used - in-line code is also processed, thus enabling dynamic creation of configurations. It may be interesting to see what is the actual content of the resulting Planes template (output of toString()):

    {
    Plane1={
        PlaneParams={
            name=Plane1, id=1033
            }, 
        Plane={
            name=Plane1, address=1033/Plane1
            }
        }, 
    Plane2={
        PlaneParams={
            name=Plane2, id=1034
            }, 
        Plane={
            name=Plane2, address=1034/Plane2
            }
        }
    }

derive_modify.png (22.7 KB) Štolba Michal, 09/19/2012 01:23 PM