Generators are classes that implement the Generator
interface.
This interface is simple and does not specify anything about the configuration.
The actual configuration of a generator depends on how it is implemented.
Their implementation is out of the control of the Java::Geci library. It is only a recommendation to use the tools and structures that are provided in the form of some generators and libraries.
There are guidelines for the code generator development that also includes configuration management for code generators. This document describes how to develop a code generator that follows these guidelines, and how a generator following those guidelines can be configured.
The advantage following these guidelines is:
-
There are tools readily available for code generators to handle configuration data. Using these tools the code generator code can focus on the core functionality without the need for excessive configuration. (There is still a lot of configuration, but it is auto-generated, so the maintenance of the code is marginal.)
-
Developers who use the code generator do not need to learn the specific configuration handling of the actual code generator. They understand the configuration keys and their meaning. The coding to set those values are the same for all code generators.
From now on in this document when talking about a code generator we assume that the actual implementation fully follows these guidelines.
Code generators can be configured with many scopes. The smaller scope usually overrides the larger scope. For example, a configuration value can be set
-
default values are coded in the generator code by the programmer and they have a scope for the whole lifetime of the specific release of the code generator running different times on many different machines at different geo-locations. (Or in space.)
-
for the generator object whole lifecycle, which means that the generator will use the set value for all the source files it processes, unless there is some value that the source code annotations and other configurations override. This level is managed by using the builder pattern when the generator is instantiated, usually in an expression that is the argument to the Java::Geci
register()
method. -
for the source level that will control how the execution of code generation for one specific source file. Such a value is usually configured using the annotation the class, and/or in the arguments of the
editor-fold
segment or in a comment that looks like the@Geci
annotation. -
for the field, method or other managed member level. Such a value is configured using the annotation on the specific member.
Java::Geci provides support for configuration management on the generator object lifestyle scope and on the class and member scope.
Generators define an inner class
private static class Config {
...private fields defined with default values assigned to them...
}
Note that the name has to be Config
in order to use the code generation support (See more about it later).
The class can also be non-static and in special cases there is a reason to do that, but unless you want it to be non-static make it static.
Usually the only thing that you code into this class are the fields. In some cases you also write setters, which are used instead of direct field assignment when the code handling the configuration is generated using the config builder. There is no strict rule that would forbid to have other methods in this class, but essentially you want to code only things that belong some way to the configuration handling. For example the
cloner
generator has two configuration parameterscopyMethod
andsuperCopyMethod
. One is the name of the copy method it generates the other one is the name of the method with the same functionality in the parent class. They usually have the same name. The default iscopy()
. However, when the developer wants to define a different name then it is enough to specifycopyMethod
and they have to definesuperCopyMethod
only when that is different from the configuredcopyMethod
. To handle this configuration logic theConfig
class in the cloner has the following code:
private String getSuperCopyMethod() {
return superCopyMethod != null && superCopyMethod.length() != 0 ?
superCopyMethod : copyMethod;
}
This code does not affect the functionality of the configuration code generated by the config builder code generator.
Generators also have a private
, preferably final
field
private final Config config = new Config();
Generators also define a method named builder()
that returns a builder object, which is a (non-static) inner class of the generator class.
This builder class has a method for each of the fields declared in the class Config
.
For example, there is a code generator in the core
package named ConfigBuilder
and it has the following code
private static class Config {
private String aggregatorMethod = "add";
private String buildMethod = "build";
private String builderFactoryMethod = "builder";
private String builderName = "Builder";
private String filter = "private & !static & !final";
}
then the Builder
class will be
public class Builder {
public Builder aggregatorMethod(String aggregatorMethod) {
config.aggregatorMethod = aggregatorMethod;
return this;
}
public Builder buildMethod(String buildMethod) {
config.buildMethod = buildMethod;
return this;
}
public Builder builderFactoryMethod(String builderFactoryMethod) {
config.builderFactoryMethod = builderFactoryMethod;
return this;
}
public Builder builderName(String builderName) {
config.builderName = builderName;
return this;
}
public Builder filter(String filter) {
config.filter = filter;
return this;
}
public ConfigBuilder build() {
return ConfigBuilder.this;
}
}
Each filed named xyz
has a corresponding method with the same name that sets the value of the field.
The method build()
returns the configured generator object.
Following this structure, the generator can be instantiated using the
ConfigBuilder.builder().filter("(private|protected) & !static & !final"). ... .build()
setting all the configuration parameters that can be used by the generator.
When the generator is attending to a source file reading it and then generating code it also reads the configuration from the editor-fold
and from the annotations.
A well-designed generator will read and interpret the configuration key xyz
if that appears in the Config
class and is String
type.
The String
configuration values that are defined in the Config
class can be overridden in the source code.
The scope of these values will be the code generation of the actual source file and they have no effect on the code generation on the next source code processing.
Note that generally a
Generator
can work on any source file, like on XML, JSON or even on binary files and usually generate Java code. If the generator works on anything else but Java source code then it is totally up to the generator implementation if it reads the configuration values if any from the source file it uses to work on. From now on we assume that the generator class directly or through other abstract classes extends theAbstractJavaGenerator
defined in thetools
module.
When the generator starts processing a Java source file it tries to read the Geci annotation of the class. An annotation is a Geci annotation if:
- the name of the annotation is
Geci
- the annotation interface is meta-annotated with a Geci annotation.
Meta-annotated means if we get all annotations, recursively, we eventually get a
Geci
annotation.
Every Geci annotation has a mnemonic defining which generator it is configuring.
This mnemonic should present in the string of the value()
parameter of the annotation.
In this case, this is the first word in the string value separated by one or more spaces from the parameters.
For example, the annotation:
@Geci("accessor filter='private | protected'")
has the mnemonic accessor
and the parameter, it has only one, is filter
.
When a Geci annotation is not named Geci
then the name of the annotation can also be used to identify the generator it is configuring.
In that case, the name of the annotation can be the mnemonic of the generator.
The first character of the annotation is changed to lowercase in this case before it is used as the mnemonic of the generator.
The example above can be converted to use an annotation named Accessor
if one exists and is annotated using a Geci annotation:
@Accessor("filter='private | protected'")
There can be several @Geci
annotations on a class.
The generator takes only the one into account, the one specifying the mnemonic of the generator.
The other annotations are ignored.
If there is no Geci annotation on a class then the generator reads the source code and tries to find a line that is a comment line (starting with //
characters) and contains something that is syntactically is a Geci annotation.
In this case, you can ONLY use the name Geci
as an annotation look like the name because the code is scanned as a series of lines and not via reflection.
The code will treat any line that is a commented @Geci
annotation if it is followed by a line that looks like the start of the class.
If there are more than one such comments in front of a class then only the one will be taken into account that has the mnemonic at the start of the string.
Also, note that you cannot use @Geci(value="...")
format.
The line scanning expects only @Geci("...")
format in the comment.
In general, the use of the annotation in a comment is a last resort feature and it is recommended to use normal annotations instead.
When the generator has collected the parameters from the annotation on the class or from the comment that looks like an annotation (and never from both) then it finds a line that looks like
//<editor-fold id="mnemonic" ...>
and merges the parameters from this line into the already collected parameters. It is possible not to annotate a class at all and have the generator triggered to generate code for the class if the source code contains the line as above.
Note that the parameters in the annotation (commented or real annotation) use a single apostrophe (
'
) to enclose the values of the parameters when they are specified inside thevalue
string. Normal annotation arguments andeditor-fold
parameters use double quotes ("
). Escaping single apostrophe in annotation value and escaping double quotes ineditor-fold
parameters is not possible. This way a parameter defined in the annotationvalue
cannot contain the apostrophe character and a parameter defined in theeditor-fold
cannot contain the double quote character. Regular annotation parameters are interpreted by the Java compiler and thus they can contain any character.
Configuration parameter names can be mistyped.
When using the builder for the generator object lifetime scope the compiler will not allow the programmer to enter any wrong configuration name.
When using annotation parameters (and not encoding the parameter into the value
string) then again, the compiler will warn the programmer when there is a typo in the name of a parameter.
When the parameter is defined in a comment or in the value
string or in the editor-fold
segment the compiler has no means to detect the spelling error.
To warn the programmer, in this case, the generators check that all the parameters defined in the source code are expected by the generator and in case there is a configuration value that the generator does not understand then they will throw a GeciException
.
The actual check is done by the abstract class CompoundParams
.
It is performed only when some caller invokes the setConstraints()
method or when a CompoundParams
object is created uniting other such objects and at least one has constraints.
The constraints currently are set by the class AbstractJavaGenerator
getting the set of allowed configuration keys invoking the method implementedKeys()
.
The concrete implementation has to provide a method implementedKeys()
overriding the one implemented in the abstract class.
This method should return a set of strings containing all the configuration parameters the generator can handle.
(Note that the set should also include the parameter name id
).
The default implementation returns null
and in this case there is no check.
This is not a good practice.
The method implementedKeys()
is automatically created by the config builder code generator that generators are encouraged to use.
The checking ignores the parameter desc
because this parameter can be used in the editor-fold
line to specify a string that the IDE displays when the fold is closed.
You can use desc
freely, the generators will ignore it.
(Unless one generator explicitly uses that as a parameter.
It is also not recommended.)
Writing the generator code to properly handle the configuration parameters will result in a lot of boilerplate code even when using the library support.
You need the builder class, the method implementedKeys()
and some other utility methods.
All these can be generated automatically knowing the names of the configuration parameters.
The generator ConfigBuilder
does this code generation.
When developing a code generator the developer has to create the private static class Config
class with all the fields that can be configured and insert an
//<editor-fold id="configBuilder">
//</editor-fold>
segment into the code and run the configBuilder
from some tests:
@Test
void buildGenerators() throws Exception {
Assertions.assertFalse(
new Geci().source("...").register(ConfigBuilder.builder().build()).generate(),
Geci.FAILED
);
}
The generator will create
- the
config
field, - the
builder()
method, which is the factory method for the builder class - the builder class itself with all the building methods
- the method
implementedKeys()
returning all configurable keys - a
private Config localConfig(CompoundParams params)
method.
Note that the building methods will assign the argument string to the field that has the same name as in the example above.
The generator however will check if there is a setter method (named set
+ capitalized field name) and in case it is there it invokes the setter instead of directly accessing the field.
This behaviour makes it possible to directly alter some values in the generator.
For example declaring the Config
class non-static the setter have access to the boolean field declaredOnly
defined in the AbstractFieldGenerator
and AbstractMethodGenerator
classes (whichever the actual generator extends).
The code is generated in a similar way in the method localConfig()
thus it is safe to declare the configuration field, which is not used more than to trigger the code generation for the invocation of the setter, to be final
.
Now that we have already discussed the first four items in the above list, but we have not discussed the method localConfig()
.
This method is to help the development of the generator in a way that is coherent with the guidelines laid out here.
The method localConfig(CompoundParams params)
creates a new instance of the Config
class and fills it with the parameters from the instance referenced by the field config
and the parameters specified in the argument variable params
.
If a configuration parameter is defined in the params
parameter set then that is used, otherwise the builder configured or default value from config
is used.
When a generator needs some parameter it is a good practice to pass the parameters read by the framework to this method and use the returned value to get the actual value of the parameter.
When the generator uses the configuration from the class level as well as field or method (or other members) level then the combined parameters has to be passed as params
.
The hierarchy and configuration inheritance is handled automatically by this structure and there is no need to manually program the decision what parameter to use at a certain point in the code generator.
Note that this method will copy the non-string values from the config
object but will not touch the final
fields (because that is not possible, and they are there in case the generator class developer wants to store some constant values there).
The title may be confusing, but the config builder is just a code generator that uses the configuration structure described in this document and can be configured. The following parameters can be configured
-
filter
filters the fields that are used from theConfig
class. -
builderName
is the name of the builder class. The default isBuilder
. -
builderFactory
the name of the static method that returns a new builder object. The default isbuilder
. -
buildMethod
the name of the method that returns the generator instance after it was configured calling the chained builder methods. -
configAccess
the access modifier of theconfig
field. The default value isprivate
, but this may need to beprotected
in case the generator is created in part of some inheritance. An example is the accessor generator that uses this feature. -
generateImplementedKeys
can be set tofalse
to avoid the generation of theimplementedKeys()
method. You should set this configuration parameter tofalse
is you want to allow any configuration key to be used by the code generation. The usual behaviour is that this method returns the set of the configuration keys that can be used for the given generator and in case there is a typo in the configuration in the source code then it results an error message. -
configurableMnemonic
can specify the string for the mnemonic of the generator. When this is specified and not empty string then the methodmnemonic()
will be created and a builder function of the same name will also be created so the actual mnemonic of the instance created using the builder can be configured to be different from the default. The default behaviour is not to create the configuration for this extra value and not to create themnemonic()
method overriding the one in the super class. Note that the value for the builder configured value is stored in a field in the generator class and not inside theConfiguration
inner class and thus it cannot be configured in an annotation. It can be configured only in the builder.Making the mnemonic configurable lets the user of the generator to use the generator if there are multiple generators with the same mnemonic and to avoid collision of the mnemonics. It is also possible to use multiple instances of the same generator on one specific source. An example can be the
repeated
generator.