Writing things like:
-
Nested Builders
Pod pod = new PodBuilder() .withNewMetadata() .withName("my-pod") .endMetadata() .withNewSpec() .addNewContainer() .withName("nginx") .withImage("quay.io/sundrio/nginx") .withImagePullPolicy("IfNotPresent") .endContainer() .endSpec() .build();
-
Domain Specific Languages
DockerClient client = new DockerClient() client.image().build().usingDockerFile("src/main/docker/Dockerfile.jvm") client.image().withName("nginx").tag().inRepository("quay.io/sundrio/nginx").force().withTagName("1.0"); client.image().withName("quay.io/sundrio/nginx").push().withTag("1.0").toRegistry(); close();
-
and pretty much anything useful that requires a lot of boilerplate ...
is a great experience the first time, but a real burden from there after.
Existing tooling is something that works in very strict context, e.g. via annotation processing, maven/gradle tooling etc, but is rarelly portable to an other (are coupled with the context). For example, some tools work via annotation processing, other works via maven/gradle plugins, other programmatically, but it's quite rare to see tools that can handle all three styles / contexts.
Sundrio, provides an abstract way of representing java code, that allows you to represent, manipulate and generate code, regardless of the context. In addition, it provides adapters that can be used adapt/convert existing representations to the sundrio model.
On top of this model, it provides tools that perform tasks, like builder generators, dsl generations and more.
-
Java
-
Maven
Note: Currently, builder generators, dsl generators etc are bound to annotation processing, but we are currently working on decoupling them from apt.
The java code model is a fluent api that allows you to:
- create
- refactor/manipulate
- render java code.
To add it to your project:
<dependency>
<groupId>io.sundr</groupId>
<artifactId>sundr-model</artifactId>
<version>${project.version}</version>
</dependency>This example demonstrate how we can create a Greeter interface with a helloWorld method and then print it`s code in the system output.
TypeDef greeter = new TypeDefBuilder()
.withKind(Kind.Inteface)
.withName("Greeter")
.addNewMethod()
.withName("helloWorld")
.endMethod()
.build();
System.out.println(greeter.render());The output of the code above is expected to be:
interface Greeter {
void helloWorld();
}Having an api to create java source code programmatically is pretty handy at times, however it's more common to manipulate existing code or classes.
So and Adapter api is provided and some core adapter implementations for:
- Adapting classes via reflection
- Adapting
TypeElementvia annotation processing - Adapting existing source
The reflection adapter allows you to create TypeDef instances, from Class instances:
TypeDef runnable = Adapters.adaptType(Runnable.class, AdapterContext.getContext());To add it to your project:
<dependency>
<groupId>io.sundr</groupId>
<artifactId>sundr-adapter-reflect</artifactId>
<version>${project.version}</version>
</dependency>The annotation processor adapter allows you to create TypeDef instances, from TypeElement instances.
A TypeElement is the way annotation processing facilities of the java compiler represent types.
AptContext aptContext = AptContext.create(processingEnv.getElementUtils(), processingEnv.getTypeUtils());
TypeDef runnable = Adapters.adaptType(Runnable.class, AdapterContext.getContext());Note: An instance of javax.annotation.processing.ProcessingEnvironment (see processingEnv above) is passed to all annotation processors by the compiler.
To add it to your project:
<dependency>
<groupId>io.sundr</groupId>
<artifactId>sundr-adapter-apt</artifactId>
<version>${project.version}</version>
</dependency>The source adapter allows you to parse java source files into TypeDef instances.
The java parser used is under the hood is the Github Java Parser.
So, the adapters practically converts TypeDeclaration / CompilationUnit instances (github java parser) into TypeDef ones.
try (FileInputStream is = new FileInputStream(new File("/path/to/Runnable.java"))) {
CompilationUnit cu = JavaParser.parser(is);
TypeDef runnable = Adapters.adaptType(cu, AdapterContext.getContext());
}A utility to simplify the step above is also provided:
TypeDef runnable = Adapters.adaptType(new File("/path/to/Runnable.java"), AdapterContext.getContext());To add it to your project:
<dependency>
<groupId>io.sundr</groupId>
<artifactId>sundr-adapter-source</artifactId>
<version>${project.version}</version>
</dependency>The dependency above comes with the Github Java Parser as a transitive dependency. If you would rather to have that dependency shaded istead:
<dependency>
<groupId>io.sundr</groupId>
<artifactId>sundr-adapter-source-nodeps</artifactId>
<version>${project.version}</version>
</dependency>As all hello worlds, the example is as simple as it get, yet the code model is so rich that allows manipulating:
- superclasses & interfaces
- properties
- methods
- inner classes
- generic parameters
Control is really find grained up to the point of statements, which at the moment are treated as strings.
So far we briefly covered ways for creating TypeDef instances that are representing Java code.
Code generation usually requires the manipulation of the code.
As already demonstrated, the code model comes with a rich set of fluent builders that allow manipulation of code.
For example let's take the Runnable interface (as demonstrated above) and give it a much more interesting name:
TypeDef runnable = Adapters.adaptType(Runnable.class, AdapterContext.getContext());
TypeDef forrest = new TypeDefBuilder(runnable).withName("Forrest").build();
System.out.println(forrest);The code above should have an output:
@FunctionalInterface
public interface Forrest {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
Not all code manipulations are as simple as the example above.
Let's consider a more realistic use case, of generating DTO class for our plain old java objects.
Let's assume an imaginary Person class:
public class Person extends Entity {
private final firstName;
private final lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
//More code ...
}We want to generate a DTO that
- will have no superclass
- will have no methods
- all fields will be public
Lucky for us, we don't need to traverse all properties one by one, we can just define a visitor for Property objects, that will perform the task for us.
So how does this work? Each time we pass a visitor to the Builder it will traverse the object graph and if it finds an object that matches the type of the visitor, it will be passed to the Visitors visit method.
In our case:
TypeDef person = Adapters.adaptType(Person.class, AdapterContext.getContext());
TypeDef dto = new TypeDefBuilder(person)
.withName(person.getName() + "DTO")
.withMethods(Collections.emptyList())
.withConstructors(Collections.emptyList())
.withExtendsList(Collections.emptyList())
.accept(PropertyBuilder.class, property -> {
property.withNewModifiers().withPublic().endModifiers()
}).build();
System.out.println(dto.render());The output should be something like:
public class PersonDTO {
public String firstName;
public String lastName;
}The project also includes so modules that put the code model & adapters into the test. In other words they use code model in order to generate things like:
- Fluent nested hierarchical builders
- Domain specific languages
- Template based code
The project is meant to be compiled using java 11.