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
syntax, will be invoked with a map. This map will always have
any keys in it that are defined in the
object
.&method
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.