22.3. Code Generator Example for Groovy

The build tree containing the Java code generator example, built using the Groovy framework, can be found in the tree located at java. The code generator itself is at java/code-generator. This example discusses the code generator, and it also serves as a general example for several aspects of the Groovy framework.

In any situation in which code generators are used, there will always be one build item that provides the code generator and one or more build items that use the code generator. In some cases, the provider and user will be the same build item, but in the most general case, the code generator will usually be provided by a build item whose sole purpose is to provide the code generator. In the Java world, a useful and common way to perform code generation is through providing a custom ant task, which is what we do here.

We'll start our examination by looking at the build item that provides the code generator. We provide a simple build item whose name is just code-generator. (In a real implementation, you would obviously want to give this a more specific name.) Its Abuild.conf and Abuild.groovy files reveal a very ordinary build item:

java/code-generator/Abuild.conf

name: code-generator
platform-types: java

java/code-generator/Abuild.groovy

parameters {
    java.includeAntRuntime = 'true'
    java.jarName = 'CodeGenerator.jar'
    abuild.rules = 'java'
}

The only thing unusual about this Abuild.groovy file is that it sets the parameter java.includeAntRuntime to true. This is necessary for any build item that uses ant's API, such as when adding a new ant task. This build item generates a JAR file, but unlike most Java build items, this JAR file is not intended to be added to the compile-time, run-time, or package-time class paths. Instead, we just assign it to a specific interface variable so that it can be used in the taskdef statement of the rules implementation. Here is the Abuild.interface file:

java/code-generator/Abuild.interface

# Provide a variable that names our code generator so we can use it in
# taskdef.  Since this jar just creates an ant task, we don't need to
# add it to the classpath.

declare code-generator.classpath filename
code-generator.classpath = $(ABUILD_OUTPUT_DIR)/dist/CodeGenerator.jar

Next, let's look at the example task itself. This is just a regular Java implementation of an ant task. There's nothing abuild-specific about it, but we'll show it here for completeness. This task just creates a class with a public negate method that returns the opposite of the number passed in. The task takes the fully qualified class name and the source directory into which the class should be written as arguments. Here is the implementation:

java/code-generator/src/java/com/example/codeGenerator/ExampleTask.java

package com.example.codeGenerator;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.BuildException;

public class ExampleTask extends Task
{
    private File sourceDir;
    public void setSourceDir(File dir)
    {
        this.sourceDir = dir;
    }

    private String fullClassName;
    public void setClassName(String name)
    {
        this.fullClassName = name;
    }

    public void execute() throws BuildException
    {
        if (this.sourceDir == null)
        {
            throw new BuildException("no sourcedir specified");
        }
        if (this.fullClassName == null)
        {
            throw new BuildException("no fullclassname specified");
        }
        Pattern fullClassName_re = Pattern.compile(
            "^((?:[a-zA-Z0-9_]+\\.)*)([a-zA-Z0-9_]+)$");

        Matcher m = fullClassName_re.matcher(this.fullClassName);
        if (! m.matches())
        {
            throw new BuildException("invalid fullclassname");
        }

        String packageName = m.group(1);
        String className = m.group(2);

        if (packageName.length() > 0)
        {
            packageName = packageName.substring(0, packageName.length() - 1);
        }

        File outputFile = new File(
            this.sourceDir.getAbsolutePath() + "/" +
            this.fullClassName.replace('.', '/') + ".java");
        File parentDir = outputFile.getParentFile();

        if (parentDir.isDirectory())
        {
            // okay
        }
        else if (! parentDir.mkdirs())
        {
            throw new BuildException("unable to create directory " +
                                     parentDir.getAbsolutePath());
        }

        if (! outputFile.isFile())
        {
            try
            {
                FileWriter w = new FileWriter(outputFile);
                if (packageName.length() > 0)
                {
                    w.write("package " + packageName + ";\n");
                }
                w.write("public class " + className + "\n{\n");
                w.write("    public int negate(int n)\n    {\n");
                w.write("        return -n;\n    }\n");
                w.write("}\n");
                w.close();
            }
            catch (IOException e)
            {
                throw new BuildException("IOException: " + e.getMessage());
            }

            log("created " + outputFile.getAbsolutePath());
        }
    }
}

To make this task available for use by other Java build items, we have to provide an implementation of the appropriate rules. The implementation of the rules can be found in the file rules/java/codegenerator.groovy. This means that people can use these rules by ensuring that codegenerator appears in abuild.Rules, almost certainly along with java. As is always advisable, we also provide a rules help file, which is rules/java/codegenerator-help.txt, is shown here:

java/code-generator/rules/java/codegenerator-help.txt

** Help for users of abuild.rules = 'codegenerator' **

Set the parameter codeGenerator.classname to the fully qualified
classname to be generated.

To generate multiple classes, instead set the parameter
codeGenerator.codegen to a list of maps, each of which has a
'classname' key.

The implementation of the rules is heavily commented, but we'll also provide some additional discussion following the text. You may wish to follow the implementation as you read the text in the rest of this section. Here is the code generator implementation:

java/code-generator/rules/java/codegenerator.groovy

// This code provides the "codegen" task, provided by this build item,
// to generate a class named by the user of the build item.

// Create a class to contain our targets.  From inside our class,
// properties in the script's binding are not available.  By doing our
// work inside a class, we are protected against a category of easy
// coding errors.  It doesn't matter if the class name collides with
// other classes defined in other rules.

class CodeGenerator
{
    def abuild
    def ant

    CodeGenerator(abuild, ant)
    {
        this.abuild = abuild
        this.ant = ant

        // Register the ant task.  The parameter
        // 'code-generator.classpath' is set in Abuild.interface.
        ant.taskdef('name': 'codegen',
                    'classname': 'com.example.codeGenerator.ExampleTask',
                    'classpath': abuild.resolve('code-generator.classpath'))
    }


    def codegenTarget()
    {
        // By using abuild.runActions, it is very easy for your custom
        // targets to support production of multiple artifacts.  This
        // method illustrates the usual pattern.

        // Create a map of default attributes and initialize this map
        // by initializing its members from the values of
        // user-supplied parameters.  In this case, the 'classname'
        // key gets a value that comes from the
        // 'codeGenerator.classname' parameter.  If the
        // codeGenerator.classname parameter is not set, the key will
        // exist in the map and will have a null value.
        def defaultAttrs = [
            'classname': abuild.resolveAsString('codeGenerator.classname')
        ]

        // Call abuild.runActions to do the work.  The first argument
        // is the name of a control parameter, the second argument is
        // a closure (here provided using Groovy's method closure
        // syntax), and the third argument is the default argument to
        // the closure.  If the control parameter is not initialized,
        // runActions will call the closure with the default
        // attributes.  Otherwise, the control parameter must contain
        // a list.  Each element of the list is either a map or a
        // closure and will cause some action to be performed.  If it
        // is a map, any keys in defaultAttrs that are not present in
        // the map will be added to the map.  Then the default closure
        // will be called with the resulting map.  If the element is a
        // closure, the closure will be called, and the default
        // closure and attributes will be ignored.
        abuild.runActions('codeGenerator.codegen', this.&codegen, defaultAttrs)
    }

    def codegen(Map attributes)
    {
        // This is the method called by abuild.runActions as called
        // from codegenTarget when the user has not supplied his/her
        // own closure.  Since defaultAttrs contained the 'classname'
        // key, we know that it will always be present in the map,
        // even when the user supplied his/her own map.

        // In this case, we require classname to be set.  This means
        // the user must either have defined the
        // codeGenerator.classname parameter or provided the classname
        // key to the map.  If neither has been done, we fail.  In
        // some cases, it's more appropriate to just return without
        // doing anything, but in this case, the only reason a user
        // would select the codegenerator rules would be if they were
        // going to use this capability.  Also, in this example, we
        // ignore remaining keys in the attributes map, but in many
        // cases, it would be appropriate to remove the keys we use
        // explicitly and then pass the rest to whatever core ant task
        // is doing the heart of the work.

        def className = attributes['classname']
        if (! className)
        {
            ant.fail("property codeGenerator.classname must be defined")
        }
        ant.codegen('sourcedir': abuild.resolve('java.dir.generatedSrc'),
                    'classname': className)
    }
}

// Instantiate our class and add codegenTarget as a closure for the
// generate target.  We could also have added a custom target if we
// wanted to, but rather than cluttering things up with additional
// targets, we'll use the generate target which exists specifically
// for this purpose.

def codeGenerator = new CodeGenerator(abuild, ant)
abuild.addTargetClosure('generate', codeGenerator.&codegenTarget)

In the Groovy programming language, a script is a special type of class that has access to read and modify variables in the binding. This is a powerful facility that makes it easy to communicate between the script and the caller of the script. Abuild makes use of the binding to provide the ant project and other state to rules implementations. However, one downside is that any undeclared variable becomes part of the binding, which may not be what you intended. To minimize unintended consequences of using undeclared variables, we recommend the practice of doing most of the work inside a class. For any script that contains any code other than a single class implementation, Groovy automatically creates a class named after the file. Since our rules implementation defines a class and then also contains other code, we set the name of the class explicitly to something other than the name of the file. This this case, the base part of the file name is codegenerator, but we name the class inside of it CodeGenerator. Our class's constructor takes as arguments a reference to the abuild object (see Section 19.10.1, “Interface to the abuild Object”) and to the ant project (see Section 19.7.2, “The Ant Project”). This is a common pattern suitable for use for just about any rule implementation. The constructor performs global setup. In this case, we just call ant.taskdef, as would be done by virtually any task-providing custom rules. Other things that would be appropriate to do in your constructor would be initializing additional fields of your object or doing any other types of operations that would be common in class constructors. The main things you should not do are to perform operations that depend on users' parameter settings or on state of an in-progress build since, at the this is loaded, not all initialization is necessarily in place.

Right after the constructor, we have the implementation of codegenTarget. This method will be used as the closure for the target provided by these rules. This target follows the pattern expected to be used for all but the most trivial rules: it sets up a default attributes list whose fields are initialized from parameters intended to be set in users' Abuild.groovy files. Here, we initialize the classname field from the codeGenerator.classname parameter. This is what makes it possible for the user to specify the name of the class to be generated by setting that parameter or, alternatively, by providing lists of maps containing the classname key. Once we provide our default attributes, we can just call abuild.runActions. The abuild.runActions method takes three arguments: the name of the control parameter, a closure that implements the required actions, and the default attributes. The closure, which here uses the Groovy method closure syntax of object.&method syntax, will be invoked with a map. This map will always have any keys in it that are defined in the defaultAttrs argument.

The next significant chunk of code here is the codegen method. This is the method that actually does the work. Everything up to this point has just been scaffolding. The codegen method takes a single parameter: a map of attributes. Any key provided in the default attributes is known to be defined. Any other keys can be used at the discretion of the code. A common convention, which is used by most of abuild's built-in targets, is to take extra attributes and just pass them along to whichever underlying ant task is doing the real work. In this case, we simply ignore extra attributes since the work is being done by a custom task, and we have already handled all available options. In this code, we set the className variable to a field of the attributes element. Other common idioms would be to set something conditionally upon whether a key is present or to set something and also to delete the attribute. For examples of these, please refer to the implementation of the built-in java rules (Appendix J, The java.groovy and groovy.groovy Files). The actual implementation of our code generator target just does a few sanity checks and then invokes the task using the task we've provided. Notice that we use java.dir.generatedSrc as the directory in which to write the generated class. This is what all code generators should do. By using abuild.resolve to get the value at this point, we ensure that any changes the user may have made to the value of that parameter will be properly accounted for. Resolving the parameter as needed is a better implementation choice than reading the parameter value in the constructor and stashing it in a field as it prevents rules from ignoring late changes to the value of the parameter.

Finally, we come to the code that resides outside the CodeGenerator class. This code just creates an instance of the class, passing to it the abuild and ant objects from the binding, and then adds the codegenTarget method as a closure for the generate target, again using Groovy's method closure syntax. Sometimes you might want to do other checks here, such as making sure other required rules have been loaded. Abuild's built-in groovy rules do this. The implementation of those rules is included in Appendix J, The java.groovy and groovy.groovy Files.

Now that we've seen how the rules are implemented, we can see how the rules are used. The good news is that using the rules is much simpler than implementing them. This is as it should be: creation of rules is a much more advanced operation that needs to be performed by people with more in-depth knowledge of abuild. Using rules should be very simple. Our library build item in java/library makes use of the code generator. To do this, it must declare a dependency on the code-generator build item, as you can see in the Abuild.conf file:

java/library/Abuild.conf

name: library
platform-types: java
deps: code-generator

and it must set the required parameter to generate the class. In this case, we use com.example.library.Negator as the fully qualified class name, as you can see by looking at the Abuild.groovy file:

java/library/Abuild.groovy

parameters {
    java.jarName = 'example-library.jar'

    // Generate a Negator class using code-generator.  If we wanted to
    // create multiple classes, we could instead set
    // codeGenerator.codegen to a list of maps with each map
    // containing a classname key.  For an example of setting a
    // parameter to a list of maps, see ../executable/Abuild.groovy.
    codeGenerator.classname = 'com.example.library.generated.Negator'

    // Use both java and codegenerator rules.
    abuild.rules = ['java', 'codegenerator']
}

We also include one statically coded Java source file which, along with the generated class, will be packaged into example-library.jar. Here, for completeness, is the text of the additional source file, which just wraps around the generated class:

java/library/src/java/com/example/library/Library.java

package com.example.library;

import com.example.library.generated.Negator;

public class Library
{
    private int value = 0;
    private Negator n = new Negator();

    public Library(int value)
    {
        this.value = value;
    }

    public int getOppose()
    {
        return n.negate(value);
    }
}

Finally, as for any well-behaved Java build item that exports a JAR file, we add the JAR file to the regular compile-time class path as well as to the manifest class path. We do this here by following archiveName/archivePath convention first discussed in Section 3.6, “Building a Java Library” and by adding the archive file to both abuild.classpath and abuild.classpath.manifest. Here is the Abuild.interface file:

java/library/Abuild.interface

declare library.archiveName string = example-library.jar
declare library.archivePath filename = \
   $(ABUILD_OUTPUT_DIR)/dist/$(library.archiveName)
abuild.classpath = $(library.archivePath)
abuild.classpath.manifest = $(library.archivePath)

The example in Section 22.4, “Multiple Wrapper Scripts” creates an executable that tests this library.