Creating a Java::Geci Generator
Learn more about creating a Java::Geci generator in your Java projects.
Join the DZone community and get the full member experience.
Join For FreeA few days back, I wrote about the Java::Geci architecture, code generation philosophy, and the different possible ways to generate Java source code.
In this article, I will talk about how simple it is to create a generator with Java::Geci
.
Hello, World Generator
HelloWorld1
The simplest generator is a Hello, World!
generator. This will generate a method that prints Hello, World!
to the standard output. To create this generator, the Java class has to implement the Generator
interface. The whole code of the generator is:
package javax0.geci.tutorials.hello;
import javax0.geci.api.GeciException;
import javax0.geci.api.Generator;
import javax0.geci.api.Source;
public class HelloWorldGenerator1 implements Generator {
public void process(Source source) {
try {
final var segment = source.open("hello");
segment.write_r("public static void hello(){");
segment.write("System.out.println(\"Hello, World\");");
segment.write_l("}");
} catch (Exception e) {
throw new GeciException(e);
}
}
}
This really is the whole generator class. There is no simplification or deleted lines. When the framework finds a file that needs the method hello()
, then it invokes process()
.
The method process ()
queries the segment named “hello.” This refers to the following lines in the source code:
//<editor-fold id="hello">
//</editor-fold>
The segment
object can be used to write lines into the code. The method write()
writes a line. The method write_r()
also writes a line, but it also signals that the lines following this one have to be indented. The opposite is write_l()
, which signals that this line and the consecutive lines should be tabbed back to the previous position.
To use the generator, we should have a class that needs it. This is:
package javax0.geci.tutorials.hello;
public class HelloWorld1 {
//<editor-fold id="hello">
//</editor-fold>
}
We also need a test that will run the code generation every time we compile the code and thus run the unit tests:
package javax0.geci.tutorials.hello;
import javax0.geci.engine.Geci;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static javax0.geci.api.Source.maven;
public class TestHelloWorld1 {
@Test
@DisplayName("Start code generator for HelloWorld1")
void testGenerateCode() throws Exception {
Assertions.assertFalse(new Geci()
.only("^.*/HelloWorld1.java$")
.register(new HelloWorldGenerator1()).generate(), Geci.FAILED);
}
}
When the code has executed, the file HelloWorld1.java
will be modified and will get the lines inserted between the editor folds:
package javax0.geci.tutorials.hello;
public class HelloWorld1 {
//<editor-fold id="hello">
public static void hello(){
System.out.println("Hello, World");
}
//</editor-fold>
}
This is an extremely simple example that we can develop a bit further.
HelloWorld2
One thing that is sub-par in the example is that the scope of the generator is limited in the test calling the only()
method. It is a much better practice to let the framework scan all the files and select the source files that somehow, themselves, signal that they need the service of the generator. In the case of the “Hello, World!” generator, it can be the existence of the hello
segment as an editor fold in the source code. If it is there, the code needs the method hello().
Otherwise, it does not. We can implement the second version of our generator that way. We can also modify the implementation not simply by implementing the interface Generator
but rather y extending the abstract class AbstractGeneratorEx
. The postfix Ex
in the name suggests that this class handles exceptions for us. This abstract class implements the method process()
and calls the to-be-defined processEx()
, which has the same signature as process()
, but it is allowed to throw an exception. If that happens, then it is encapsulated in a GeciException,
just as we did in the first example.
The code will look like the following:
package javax0.geci.tutorials.hello;
import javax0.geci.api.Source;
import javax0.geci.tools.AbstractGeneratorEx;
import java.io.IOException;
public class HelloWorldGenerator2 extends AbstractGeneratorEx {
public void processEx(Source source) throws IOException {
final var segment = source.open("hello");
if (segment != null) {
segment.write_r("public static void hello(){");
segment.write("System.out.println(\"Hello, World\");");
segment.write_l("}");
}
}
}
This is even simpler than the first one; although, it is checking the segment existence. When the code invokes source.open("hello")
, the method will return null
if there is no segment named hello
in the source code. The actual code using the second generator is the same as the first one. When we run both tests in the codebase, they both generate code that is, fortunately, identical.
The test that invokes the second generator is:
package javax0.geci.tutorials.hello;
import javax0.geci.engine.Geci;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static javax0.geci.api.Source.maven;
public class TestHelloWorld2 {
@Test
@DisplayName("Start code generator for HelloWorld2")
void testGenerateCode() throws Exception {
Assertions.assertFalse(new Geci()
.register(new HelloWorldGenerator2())
.generate(), Geci.FAILED);
}
}
Note that this time we did not need to limit the code scanning calling the method only()
. Also, the documentation of the method only(RegEx x)
says that this is in the API of the generator builder as a last resort.
HelloWorld3
The first and second versions of the generator are working on text files and do not use the code we modify, which is actually Java. The third version of the generator will rely on this fact, and that way, it will be possible to create a generator that can be configured in the class that needs the code generation.
To do that, we can extend the abstract class AbstractJavaGenerator
. This abstract class finds the class that corresponds to the source code and also reads the configuration encoded in annotations on the class as we will see. The abstract class implementation of processEx()
invokes the process(Source source, Class klass, CompoundParams global).
If the source code is a Java file, there is already a compiled class (sorry, compiler, we may modify the source code now, so there may be a need to recompile) and the class is annotated appropriately.
The generator code is the following:
package javax0.geci.tutorials.hello;
import javax0.geci.api.Source;
import javax0.geci.tools.AbstractJavaGenerator;
import javax0.geci.tools.CompoundParams;
import java.io.IOException;
public class HelloWorldGenerator3 extends AbstractJavaGenerator {
public void process(Source source, Class<?> klass, CompoundParams global)
throws IOException {
final var segment = source.open(global.get("id"));
final var methodName = global.get("methodName", "hello");
segment.write_r("public static void %s(){", methodName);
segment.write("System.out.println(\"Hello, World\");");
segment.write_l("}");
}
public String mnemonic() {
return "HelloWorld3";
}
}
The method process()
(an overloaded version of the method defined in the interface) gets three arguments. The first one is the very same Source
object, as shown in the first example. The second one is the Class
that was created from the Java source file we are working on. The third one is the configuration of the framework that was reading from the class annotation. This also needs the support of the method mnemonic()
. This identifies the name of the generator. It is a string used as a reference in the configuration. It has to be unique.
A Java class, itself, needs to be modified by a generator and has to be annotated using the Geci
annotation. The Geci
annotation is defined in the library javax0.geci.annotations.Geci
. The code of the source to be extended with the generated code will look like the following:
package javax0.geci.tutorials.hello;
import javax0.geci.annotations.Geci;
@Geci("HelloWorld3 id='hallo' methodName='hiya'")
public class HelloWorld3 {
//<editor-fold id="hallo">
//</editor-fold>
}
Here, there is a bit of a nuisance. Java::Geci
is a test phase tool, and all the dependencies are test dependencies. The exception is the annotations library. This library has to be a normal dependency because the classes that use the code generation are annotated with this annotation, and therefore, the JVM will look for the annotation class during run time, even though there is no role of the annotation during run-time. For the JVM, test execution is just at runtime — there is no difference.
To overcome this, Java::Geci
lets you use any annotations as long as the name of the annotation interface is Geci
and it has a value
, which is a String
. This way, we can use the third "hello world" generator the following way:
package javax0.geci.tutorials.hello;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@HelloWorld3a.Geci(value = "HelloWorld3 id='hallo'", methodName = "hiyaHuya")
public class HelloWorld3a {
//<editor-fold id="hallo">
//</editor-fold>
@Retention(RetentionPolicy.RUNTIME)
@interface Geci {
String value();
String methodName() default "hello";
}
}
In the previous example, the parameters id
and methodName
were defined inside the value
string (which is the default parameter if you do not define any other parameters in an annotation). In that case, the parameters can easily be misspelled and the IDE does not give you any support for the parameters simply because the IDE does not know anything about the format of the string that configures Java::Geci
. On the other hand, if you have your own annotations, you are free to define any named parameters. In this example, we defined the method methodName
in the interface. Java::Geci
is reading the parameters of the annotation as well as parsing the value
string for parameters. This way, some generators may use their own annotations to help users with parameters defined as annotation parameters.
The last version of our third “Hello, World!” application is perhaps the simplest:
package javax0.geci.tutorials.hello;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class HelloWorld3b {
//<editor-fold id="HelloWorld3" methodName = "hiyaNyunad">
//</editor-fold>
}
There is no annotation on the class, and there is no comment that would look like an annotation. The only thing is an editor-fold
segment that has the idHelloWorld3
, which is the mnemonic of the generator. If it exists, the AbstractJavaGenerator
realizes that and reads the parameters from there (it reads extra parameters that are not present on the annotation even if the annotation is present). And not only does it read the parameters, but it also calls the concrete implementation so the code is generated. This approach is the simplest and can be used for code generators that need only one single segment to generate the code into and when they do not need separate configuration options for the methods and fields that are in the class.
Summary
In this article, I described how you can write your own generator. We also dived into how annotations can be used to configure the class that needs generated code. Keep in mind that some of the features discussed in this article may not be in the release version, but you can download and build the (b)leading edge version from https://github.com/verhas/javageci.
Published at DZone with permission of Peter Verhas, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments