Skip to content

kasparthommen/java-code-gen

Repository files navigation

Java Code Generator Annotations

badge

TL;DR

What does this library provide?

This library provides the following code-generating annotations:

  • @Instantiate generates concrete instantiations of generic classes analogous to C++ templates:

/* you write... */
@Instantiate(String.class)
class MyList<T> {
    private T[] array;
    // ...
}
/* ... and you'll get */
class MyListString {
    private String[] array;
    // ...
}
  • @Derive generates new classes from existing ones by applying string or regex replacements to the source code:

/* you write... */
@Derive(name = "MyFloatList", replace = @Replace(from = "double", to = "float"))
class MyDoubleList {
    private double[] array;
    // ...
}
/* ... and you'll get */
class MyFloatList {
    private float[] array;
    // ...
}

Why should I use it?

The main advantage of this library over generic template engines such as StringTemplate, Velocity or FreeMaker is:

Your template is actual code!

Thus, instead of having to write a placeholder-sprinkled, engine-specific template file, your "template" is a normal Java class (with annotations). The benefits are as follows:

  • The "template" is source code rather than a resource file

  • The "template" can be unit tested

  • The "template" enjoys IDE syntax highlighting - no template engine-specific plugins required

  • The "template" can be auto-formatted, linted and refactored by your IDE

Instantiate generic classes with @Instantiate

Motivation

Consider the following generic class (which, of course, would require a lot more work before it’s a reasonable list implementation):

package com.kt.codegen.demo.list1;

class MyList<T> {
    private T[] array;

    MyList(int size) {
        this.array = (T[]) new Object[size];
    }

    T get(int index) {
        return array[index];
    }
}

You can annotate it with @Instantiate to e.g. create a concrete String instantiation, analogous to C++ templates:

package com.kt.codegen.demo.list2;

import com.kt.codegen.Instantiate;

@Instantiate(String.class)
class MyList<T> {
    private T[] array;

    MyList(int size) {
        this.array = (T[]) new Object[size];
    }

    T get(int index) {
        return array[index];
    }
}

This will generate the following class:

// generated from com.kt.codegen.demo.list2.MyList
package com.kt.codegen.demo.list2;

class MyListString {
    private String[] array;

    MyListString(int size) {
        this.array = (String[]) new Object[size];
    }

    String get(int index) {
        return array[index];
    }
}

Nice, but the annotation processor only operates on a source code level and simply replaces occurrences of T with String. This leads to a guaranteed class cast exception in the expression (String[]) new Object[size]. Can we fix this? Yes, with custom string replacements, see below.

Custom String Replacements

Simply replacing a generic type with a concrete type like we just did doesn’t usually get us all the way, but fret not, there are custom string replacements:

package com.kt.codegen.demo.list3;

import com.kt.codegen.Instantiate;
import com.kt.codegen.Replace;

@Instantiate(value = String.class,
             replace = @Replace(from = "(T[]) new Object[size]", to = "new String[size]"))
class MyList<T> {
    private T[] array;

    MyList(int size) {
        this.array = (T[]) new Object[size];
    }

    T get(int index) {
        return array[index];
    }
}

Now the generated string list is safe:

// generated from com.kt.codegen.demo.list3.MyList
package com.kt.codegen.demo.list3;

class MyListString {
    private String[] array;

    MyListString(int size) {
        this.array = new String[size];
    }

    String get(int index) {
        return array[index];
    }
}

Primitives

How about adding a primitive version of our list? Simple: just add a double instantiation:

package com.kt.codegen.demo.list4;

import com.kt.codegen.Instantiate;
import com.kt.codegen.Replace;

@Instantiate(value = String.class,
             replace = @Replace(from = "(T[]) new Object[size]", to = "new String[size]"))
@Instantiate(value = double.class,
             replace = @Replace(from = "(T[]) new Object[size]", to = "new double[size]"))
class MyList<T> {
    private T[] array;

    MyList(int size) {
        this.array = (T[]) new Object[size];
    }

    T get(int index) {
        return array[index];
    }
}

This will additionally geenrate the following class:

// generated from com.kt.codegen.demo.list4.MyList
package com.kt.codegen.demo.list4;

class MyListDouble {
    private double[] array;

    MyListDouble(int size) {
        this.array = new double[size];
    }

    double get(int index) {
        return array[index];
    }
}

Note that the class is called MyListDouble instead of MyListdouble (note the different case of the "d") to make the two types explicit in the class name.

Multiple Type Parameters

If your generic class has more than one type parameter then you’ll simply have to provide the necessary number of concrete types for each instantiation:

package com.kt.codegen.demo.map;

import com.kt.codegen.Instantiate;

import java.time.Instant;

@Instantiate({String.class, Instant.class})  // <-- two concrete types
class MyMap<K, V> {                          // <-- two type parameters
    private K[] keys;
    private V[] values;

    // ...
}

Notes

  • For projects that don’t follow the Maven directory layout you can specify the relative source directory with @SourceDirectory on the source class.

  • If normal string replacement won’t cut it, you can set @Replace.regex to true.

  • You can specify multiple replacements with replace = {@Replace(…​), @Replace(…​), …​}.

  • I you prefer prepending the concrete type(s) to the class rather than the default appending style (i.e., StringMyList rather than MyListString) then set @Instantiate.append to false.

Generate derived classes with @Derive

Say you are working on a primitive collection library. You have just finished writing a double list implementation:

package com.kt.codegen.demo.double1;

public class MyDoubleList {
    private double[] array;

    MyDoubleList(int size) {
        this.array = new double[size];
    }

    // ...
}

Now you have a couple of options to create lists for other primitive types:

  1. You copy and paste the class a couple of times followed by a search/replace frenzy. This is cumbersome, time-consuming, and will eventually lead to implementations drifting apart because you’ll forget to apply that one fix to the float implementation.

  2. You fire up a generic template engine, convert this nice, working, unit-tested, syntax-highlighted, auto-formatted, error-checked class into a template text file that immediately loses all those nice properties, and you start configuring the template engine.

  3. Or you annotate the class as follows:

package com.kt.codegen.demo.double2;

import com.kt.codegen.Derive;
import com.kt.codegen.Replace;

@Derive(name = "MyFloatList", replace = @Replace(from = "\\bdouble\\b", to = "float", regex = true))
@Derive(name = "MyLongList", replace = @Replace(from = "\\bdouble\\b", to = "long", regex = true))
public class MyDoubleList {
    private double[] array;

    MyDoubleList(int size) {
        this.array = new double[size];
    }

    // ...
}

This will generate two derived classes:

// generated from com.kt.codegen.demo.double2.MyDoubleList
package com.kt.codegen.demo.double2;

public class MyFloatList {
    private float[] array;

    MyFloatList(int size) {
        this.array = new float[size];
    }

    // ...
}

And:

// generated from com.kt.codegen.demo.double2.MyDoubleList
package com.kt.codegen.demo.double2;

public class MyLongList {
    private long[] array;

    MyLongList(int size) {
        this.array = new long[size];
    }

    // ...
}

Notes

  • The relative source directory can also be changed using @SourceDirectory.

  • Custom string replacements can be specified in @Derive.replace.