/sundrio

Misc compile time tools

Primary LanguageJavaApache License 2.0Apache-2.0

Sundrio: Code generation toolkit for Java

  • Build
  • Maven Central

Overview

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.

Features

Note: Currently, builder generators, dsl generators etc are bound to annotation processing, but we are currently working on decoupling them from apt.

Code Model

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>

A hello world example

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();
   }

Adapters

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 TypeElement via annotation processing
  • Adapting existing source

Reflection adapter

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>

Annotation processor adapter

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>

Source adapter

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.

Code manipulation

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.

Using the builders

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();
}

Using the visitor pattern

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;
}

Modules

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

Compiling

The project is meant to be compiled using java 8. The project internally is using com.sun:tools which is found under $JAVA_HOME/lib/tools.jar for all java version before 11. To avoid referencing to that path, which is known to cause issues, its required to install it your maven local repository.

mvn install:install-file -Dfile=$JAVA_HOME/lib/tools.jar -DgroupId=com.sun -DartifactId=tools -Dversion=8 -Dpackaging=jar

Note: To just use this project no action is required as the dependency is only needed to compile sundrio itself.

Projects using sundrio