Jul 08 2008

How to write a Groovy DSL for Rio

Tag: groovy, riojeje @ 4:14 pm

Rio currently uses deployment descriptor files written in XML, which indicates the services you’d like to deploy, which cybernodes your service could run into, how you’d like to scale or relocate your services, etc.

That XML format, although quite extensible is also quite verbose, so a Domain Specific Language (DSL for short), means a lot of sense in that context in order to reduce verbosity.

In order to ease the code for such a DSL, Rio deployment descriptors (called OpStrings) parsing was rewritten using Groovy. This is something which turned out to be quite easy actually and is mostly done. I spent about 4 or 5 days on this task, including writing some Groovy Tests in order to reduce the risk of regression. This work is interesting because I was able to at least cut the code to half its initial size and it is now more easy to read, hence maintain.

Armed with this test suite, I started to work on the DSL. The option I have taken is to transform the DSL syntax into the current XML file and then use the usual XML parser. The trade off is that it minimize the work needed to be done but of course it would be better to use the DSL in order to create the Java model already using when parsing the XML format (this is still being discussed).

So here is how it looks like. Below is the XML format taken from one of Rio examples:


<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
<!DOCTYPE opstring SYSTEM "java://org/jini/rio/dtd/rio_opstring.dtd">
<opstring>
    <OperationalString Name="Calculator">
        <Groups>
            <Group>rio</Group>
        </Groups>

        <Resources id="impl.jars">
            <JAR>calculator/lib/calculator.jar</JAR>
        </Resources>

        <Resources id="client.jars">
            <JAR>calculator/lib/calculator-dl.jar</JAR>
        </Resources>

        <ServiceBean Name="Calculator">
            <Interfaces>
                <Interface>calculator.Calculator</Interface>
                <Resources ref="client.jars"/>
            </Interfaces>
            <ImplementationClass Name="calculator.service.CalculatorImpl">
               <Resources ref="impl.jars"/>
            </ImplementationClass>

            <Associations>
                <Association Name="Add" Type="requires" Property="add"/>
                <Association Name="Subtract" Type="requires" Property="subtract"/>
                <Association Name="Multiply" Type="requires" Property="multiply"/>
                <Association Name="Divide" Type="requires" Property="divide"/>
            </Associations>

            <Maintain>1</Maintain>
        </ServiceBean>

        <ServiceBean Name="Add">
            <Interfaces>
                <Interface>calculator.Add</Interface>
                <Resources ref="client.jars"/>
            </Interfaces>
            <ImplementationClass Name="calculator.service.AddImpl">
                <Resources ref="impl.jars"/>
            </ImplementationClass>
            <Maintain>1</Maintain>
        </ServiceBean>

        <ServiceBean Name="Subtract">
            <Interfaces>
                <Interface>calculator.Subtract</Interface>
                <Resources ref="client.jars"/>
            </Interfaces>
            <ImplementationClass Name="calculator.service.SubtractImpl">
                <Resources ref="impl.jars"/>
            </ImplementationClass>
            <Maintain>1</Maintain>
        </ServiceBean>

        <ServiceBean Name="Multiply">
            <Interfaces>
                <Interface>calculator.Multiply</Interface>
                <Resources ref="client.jars"/>
            </Interfaces>
            <ImplementationClass Name="calculator.service.MultiplyImpl">
                <Resources ref="impl.jars"/>
            </ImplementationClass>
            <Maintain>1</Maintain>
        </ServiceBean>

        <ServiceBean Name="Divide">
            <Interfaces>
                <Interface>calculator.Divide</Interface>
                <Resources ref="client.jars"/>
            </Interfaces>
            <ImplementationClass Name="calculator.service.DivideImpl">
                <Resources ref="impl.jars"/>
            </ImplementationClass>
            <Maintain>1</Maintain>
        </ServiceBean>

    </OperationalString>
</opstring>

Here is now the DSL in action for the same example:


opstring(name:'Calculator') {
    groups('rio')

    resources(id: 'impl.jars', 'calculator/lib/calculator.jar')
    resources(id: 'client.jars', 'calculator/lib/calculator-dl.jar')

    service(name: 'Calculator') {
        interfaces {
            classes('calculator.Calculator')
            resources(ref: 'client.jars')
        }
        implementation(class: 'calculator.service.CalculatorImpl') {
            resources(ref: 'impl.jars')
        }
        associations {
            association(name: 'Add', type: 'requires', property: 'add')
            association(name: 'Subtract', type: 'requires', property: 'subtract')
            association(name: 'Multiply', type: 'requires', property: 'multiply')
            association(name: 'Divide', type: 'requires', property: 'divide')
        }
        maintain 1
    }

    service(name: 'Add') {
        interfaces {
            classes('calculator.Add')
            resources(ref: 'client.jars')
        }
        implementation(class: 'calculator.service.AddImpl') {
            resources(ref: 'impl.jars')
        }
        maintain 1
    }

    service(name: 'Subtract') {
        interfaces {
            classes('calculator.Subtract')
            resources(ref: 'client.jars')
        }
        implementation(class: 'calculator.service.SubtractImpl') {
            resources(ref: 'impl.jars')
        }
        maintain 1
    }

    service(name: 'Multiply') {
        interfaces {
            classes('calculator.Multiply')
            resources(ref: 'client.jars')
        }
        implementation(class: 'calculator.service.MultiplyImpl') {
            resources(ref: 'impl.jars')
        }
        maintain 1
    }

    service(name: 'Divide') {
        interfaces {
            classes('calculator.Divide')
            resources(ref: 'client.jars')
        }
        implementation(class: 'calculator.service.DivideImpl') {
            resources(ref: 'impl.jars')
        }
        maintain 1
    }
}

What’s interesting is that if you have a close look at the above syntax, you will notice that the Add, Sutract, Divide and Multiply services are configured quite the same. Wouldn’t it be nicer to define the configuration once for all those services?

This is something which is accomplished for free because the DSL is a Groovy script, so any Groovy syntax is valid in it:


opstring(name:'Calculator') {
    groups 'rio'

    resources id:'impl.jars', 'calculator/lib/calculator.jar'
    resources id:'client.jars', 'calculator/lib/calculator-dl.jar'

    service(name: 'Calculator') {
        interfaces {
            classes 'calculator.Calculator'
            resources ref:'client.jars'
        }
        implementation(class:'calculator.service.CalculatorImpl') {
            resources ref:'impl.jars'
        }
        associations {
            association name:'Add', type:'requires', property:'add'
            association name:'Subtract', type:'requires', property:'subtract'
            association name:'Multiply', type:'requires', property:'multiply'
            association name:'Divide', type:'requires', property:'divide'
        }
        maintain 1
    }

    ['Add', 'Subtract', 'Multiply', 'Divide'].each { s ->
        println "Service is $s"
        service(name: s) {
            interfaces {
                classes "calculator.$s"
                resources ref:'client.jars'
            }
            implementation(class: "calculator.service.${s}Impl") {
                resources ref:'impl.jars'
            }
            maintain 1
        }
    }
}

Our OpString file has now been reduced from 71 lines to 34 ones!

The only drawback in that case is that using the DSL you loose the autocompletion provided by any good XML editor.

So how does the code look like for that DSL:


class GroovyDSLOpStringParser implements OpStringParser {
    def OpStringParser xmlParser = new XmlOpStringParser()
    def static final Logger logger = Logger.getLogger(GroovyDSLOpStringParser.class.name);

    public List<OpString> parse(Object source, ClassLoader loader, boolean verify, String[] defaultExportJars,
                                String codebaseOverride, String[] defaultGroups, boolean processingOverrides,
                                Object loadPath) {
        logger.info "Parsing source $source"
        ExpandoMetaClass.enableGlobally()

        def tempFile = File.createTempFile('rio-dsl', 'xml')
        def writer = new FileWriter(tempFile)
        def builder = new groovy.xml.MarkupBuilder(writer)
        Script dslScript = new GroovyShell().parse(source)

        dslScript.metaClass = createEMC(dslScript.class, {
            ExpandoMetaClass emc ->
            emc.opstring = { Map attributes, Closure cl ->
                builder.printer.println('<!DOCTYPE opstring SYSTEM "java://org/jini/rio/dtd/rio_opstring.dtd">')
                builder.opstring {
                    OperationalString(Name: attributes.name) { cl() }
                }
                writer.close()
                xmlParser.parse(tempFile, loader, verify, defaultExportJars, codebaseOverride, defaultGroups,
                                processingOverrides, loadPath)
            }
            emc.groups = { String... groups ->
                builder.Groups {
                    groups.each { Group(it) }
                }
            }
            emc.cluster = { String... machines ->
                builder.Cluster {
                    machines.each { Machine(it) }
                }
            }
            emc.service = { Map attributes, Closure cl ->
                builder.ServiceBean(Name: attributes.name) { cl() }
            }
            emc.serviceExec = { Map attributes, Closure cl ->
                builder.ServiceExec(Name: attributes.name) { cl() }
            }
            emc.spring = { Map attributes, Closure cl ->
                builder.SpringBean(Name: attributes.name, config: attributes.config) { cl() }
            }
            emc.interfaces = { Closure cl ->
                builder.Interfaces { cl() }
            }
            emc.implementation = { Map attributes, Closure cl ->
                builder.ImplementationClass(Name: attributes.class) { cl() }
            }
            emc.execute = { Map attributes ->
                builder.Exec(nohup: attributes.nohup ? 'yes' : 'no') {
                    if (attributes.inDirectory)
                        WorkingDirectory(attributes.inDirectory)
                    def String[] cmd = attributes.command.split()
                    CommandLine(cmd[0])
                    if (cmd.size() - 1 > 0)
                        cmd[1..cmd.size() - 1].each { InputArg(it) }
                }
            }
            emc.classes = { String... interfaceClasses ->
                interfaceClasses.each { builder.Interface(it) }
            }
            emc.resources = { String... resources ->
                builder.Resources {
                    resources.each { JAR(it) }
                }
            }
            emc.resources = { Map attributes ->
                builder.Resources(attributes)
            }
            emc.resources = { Map attributes, String... resources ->
                builder.Resources(id: attributes.id) {
                    resources.each { JAR(it) }
                }
            }
            emc.configuration = { String configuration ->
                builder.Configuration(configuration)
            }
            emc.associations = { Closure cl ->
                builder.Associations { cl() }
            }
            emc.association = { Map attributes ->
                builder.Association(Name: attributes.name, Type: attributes.type,
                                    Property: attributes.property, MatchOnName: attributes.matchOnName ? 'yes' : 'no')
            }
            emc.maintain = { Integer maintain ->
                builder.Maintain(maintain)
            }
            emc.maxPerMachine = { Integer max ->
                builder.MaxPerMachine(max)
            }
            emc.maxPerMachine = { Map attributes, Integer max ->
                if (attributes.type)
                    builder.MaxPerMachine(max, type: attributes.type)
                else
                    builder.MaxPerMachine(max)
            }
            emc.software = { Map attributes ->
                builder.SystemRequirements {
                    SystemComponent(Name: 'SoftwareSupport') {
                        Attribute(Name: 'Name', Value: attributes.name)
                        Attribute(Name: 'Version', Value: attributes.version)
                    }
                }
            }
            emc.software = { Map attributes, Closure cl ->
                builder.SystemRequirements {
                    SystemComponent(Name: 'SoftwareSupport') {
                        Attribute(Name: 'Name', Value: attributes.name)
                        Attribute(Name: 'Version', Value: attributes.version)
                        builder.SoftwareLoad(removeOnDestroy: attributes.removeOnDestroy ? 'yes' : 'no') { cl() }
                    }
                }
            }
            emc.download = { Map attributes ->
                builder.Download(InstallRoot: attributes.installRoot,
                                 Unarchive: attributes.unarchive ? 'yes' : 'no', Source: '') {
                    Location(attributes.source)
                }
            }
            emc.postInstall = { Map attributes, Closure cl ->
                builder.PostInstall(RemoveOnCompletion: attributes.removeOnCompletion ? 'yes': 'no') { cl() }
            }
            emc.serviceLevelAgreements = { Closure cl ->
                builder.ServiceLevelAgreements { cl() }
            }
            emc.sla = { Map attributes, Closure cl ->
                builder.SLA(ID: attributes.id, Low: attributes.low, High: attributes.high) { cl() }
            }
            emc.policy = { Map attributes ->
                builder.PolicyHandler(type: attributes.type, max: attributes.max,
                                      lowerDampener: attributes.lowerDampener, upperDampener: attributes.upperDampener)
            }
            emc.logging = { Closure cl ->
                builder.Logging { cl() }
            }
            emc.logger = { String name, Level level = Level.INFO, Closure cl ->
                builder.Logger(Name: name, Level:level.toString()) { cl() }
            }
            emc.handler = { String name, Level level = Level.INFO ->
                builder.Handler(ClassName: name, Level:level.toString())
            }
        })
        List<OpString> opstrings = dslScript.run()
        return opstrings
    }

    public parseElement(Object element, GlobalAttrs global, ParsedService sDescriptor, OpString opString) {
        throw new UnsupportedOperationException()
    }

    static ExpandoMetaClass createEMC(Class clazz, Closure cl) {
        ExpandoMetaClass emc = new ExpandoMetaClass(clazz, false)
        cl(emc)
        emc.initialize()
        return emc
    }

}

As you can see, the above code uses the XML MarkupBuilder which makes it very easy to generate the XML format. After that, it’s just a matter of delegating the real work to the XML parser!