java-primitive-specializations-generator
java-primitive-specializations-generator copied to clipboard
JPSG is temporarily extracted from Koloboke tree to make new releases.
Java Primitive Specializations Generator
Java Primitive Specializations Generator (JPSG) is a template processor with the primary goal of producing code specialized for any Java primitive types and for object (generic) types, while Java doesn't have generics over primitives.
Source code of the Koloboke collections library is generated with JPSG. The difference between JPSG and similar template processing engines powering other libraries of collections for primitive types in Java (fastutil, Eclipse, Trove, HPPC - all have their own template processing engine) is that JPSG's template files can be valid Java source files that correspond to one of the specializations produced from the respective template.
This allows to make the development of templates more convenient and less error prone by configuring directories with template files as source directories in your IDE so that IDE features like autocomplete and inspections will work in the template files. On the other hand, IDE will think that there are duplicate classes in the project (because a template file's name (class name) and package are the same as in one of the generated specializations), so you will lose the ability to build the project from IDE (you will need to use Maven or Gradle every time to build the project).
This is possible because JPSG captures and replaces actual type occurrences in code
instead of relying on patterns that can't be part of Java code. For example, the following file
CharIntPair.java
is a valid JPSG template:
/* with char|byte|short|int|long|float|double|object key
int|byte|char|short|long|float|double|object value */
/* if !(object key object value) */ // Excludes ObjectObjectPair, because there is Pair already
interface CharIntPair/*<>*/ extends Pair<Character, Integer> {
/* if !(object key) */ char getCharKey(); /* endif */
/* if !(object value) */ int getIntValue(); /* endif */
}
JPSG produces the following specialization FloatObjectPair.java
from the above template, along
with 62 others:
interface FloatObjectPair<V> extends Pair<Float, V> {
float getFloatKey();
}
As you can see in this example, the second thing that enables JPSG's templates to be valid Java code
is that control structures such as /* if ... */ ... /* endif */
use Java comment syntax. (More on
this in the section about /* if */
blocks in the tutorial below.)
The above example highlights another difference between JPSG and some other template processing
engines: JPSG allows specialization for primitive types as well as object (generic) types. This
makes it possible to produce all possible specializations of types such as Pair<A, B>
,
Map<K, V>
, Triple<A, B, C>
, etc. from a single source template, while with some other template
processing engines usually 4 or 8 different templates are required to cover all {object, primitive}
combinations across all dimensions.
Although primarily designed for specializing Java types, JPSG also supports specialization over
arbitrarily defined (non Java type) "dimensions". For example, for the following template file
Foo.java
:
/* with Foo|Bar myDimension */
class Foo {
void getFoo() {
int foo = /* if Foo myDimension */0// elif Bar myDimension //1// endif */;
return foo;
}
}
JPSG produces Foo.java
:
class Foo {
void getFoo() {
int foo = 0;
return foo;
}
}
and Bar.java
:
class Bar {
void getBar() {
int bar = 1;
return bar;
}
}
Dimensions for specializing Java type options and dimensions for specializing non Java type options can be mixed in a single JPSG template file.
JPSG is available as a Gradle plugin and a Maven plugin.
Tutorial
Template files
By convention, in both Gradle and Maven JPSG plugins JPSG template files are located in
src/main/javaTemplates/com/mycompany/MyShortSpecializedType.java
, as well as
src/test/javaTemplates/...
. In other words, conventional template directories javaTemplates/
are
sibling to conventional java/
directories. The package structure is reflected in the directory
tree structure as well as in ordinary sources. Gradle plugin additionally supports
resourceTemplates/
, for example
src/main/resourceTemplates/META-INF/services/com.mycompany.MyShortSpecializedType
.
A template file usually begins with a /* with */
construction that defines specialization
dimensions of the template. For example:
/* with char|byte|short|int|long|float|double|object key
int|byte|char|short|long|float|double|object value */
/* license header */
package com.mypackage;
...
Here there are two dimension definitions: dimension "key" has "char|byte|short|int|long|float|double|object" options, dimension "value" has "int|byte|char|short|long|float|double|object" options. Another example:
/* with integer|long|double|obj tType long|double|integer|obj uType Immutable|Mutable mutability */
...
Here dimension "tType" has "integer|long|double|obj" options, dimension "uType" has "long|double|integer|obj" and dimension "mutability" has "Immutable|Mutable" options.
Here is the formal BNF syntax of dimension definitions:
<dimensionName> := alphanumeric string
<javaTypeOption> := "bool" "ean"? | "byte" | "char" "acter"? | "short" | "int" "eger"? |
"long" | "float" | "double" | "obj" "ect"?
<nonJavaTypeOption> := alphanumeric string which isn't a <javaTypeOption>
<javaTypeOptions> := ( <javaTypeOption> "|" )* <javaTypeOption>
<nonJavaTypeOptions> := ( <nonJavaTypeOption> "|" )* <nonJavaTypeOption>
<options> := (<javaTypeOptions> | <nonJavaTypeOptions>)
<dimension> := <options> " " <dimensionName>
<dimensions> := ( <dimension> " "+ )* <dimension>
The first option in the |
-delimited list defined for each dimension is the source option. In the
previous example, the source option of dimension "tType" is "integer", "long" of "uType" dimension
and "Immutable" of "mutability" dimension.
JPSG searches for occurrences of the source options in the contents and the file name of the
template file and replaces them to produce specializations corresponding to all combinations of
options for all dimensions (a cartesian product), except, maybe, some combinations that are
filtered. See the section about /* if */
blocks below for more info about filtering.
The file name can be customized further with a special /* define ClassName */
construction. See
the section about /* define */
blocks below.
If there is no /* with */
block in the beginning of a template file, JPSG attempts to deduce
dimensions from the name of the template file, based on defaultTypes
configuration (available in
both Gradle and Maven plugins) that defaults to "byte|char|short|int|long|float|double", i. e. all
primitive numeric Java types. For example, for a template file named FloatShortIntTriple.java
deduced dimension definitions will be:
float|byte|char|short|int|long|double t
short|byte|char|int|long|float|double u
int|byte|char|short|long|float|double v
JPSG doesn't deduce dimensions with non Java type options from the template file name to enforce
giving these dimensions more meaningful names than "t", "u", and "v". /* with */
block should be
used to specify non Java type dimensions. And if such block is present,
JPSG doesn't try to deduce more dimensions from the template file name combine them with
explicitly defined dimensions.
If there is no /* with */
block in the beginning of a template file and its file name doesn't
include parts that correspond to java primitive type names, JPSG assumes that there are no file-wide
dimensions for specialization. In this case JPSG generates exactly one output file from the template
with the same name. It's not pointless because such template file could have inner generation,
for example:
class PrimitiveUtils {
/* with byte|char|short|int|long|float|double primType */
... some code that repeats for each primitive type
/* endwith */
}
See the section about /* with */
blocks below.
Specialization rules
Each dimension can have either all Java type options or all non Java type options (see BNF
definitions of <javaTypeOptions>
and <nonJavaTypeOptions>
above). That is,
int|long|Foo myDimension
is an illegal dimension definition for JPSG because there are both Java
type options "int", "long" and a non Java type option "Foo". JPSG specializes dimensions with Java
type options (also called Java type dimensions) differently than dimensions with non Java type
options.
Specialization of Java type dimensions
Java type options are "bool", "boolean", "byte", "short", "char", "character", "int", "integer", "long", "float", "double", "obj", and "object". Java type dimension specifies a subset of those options. "bool"/"boolean", "char"/"character", "int"/"integer", and "obj"/"object" can't both appear in one dimension definition (but there is a difference between the short and long forms - see below.) "bool"/"boolean" and "obj"/"object" can't be source options, i. e. they can't go first in the options list of a dimension.
JPSG analyzes the template and replaces primitive type appearances in all possible forms (the following examples assume that the source option is "char" or "character"):
- Primitive Java type occurrences:
char value = ...
,public char myFunction();
- Boxed primitive class occurrences:
List<Character> myValues = ...
,/** @see Character#MAX_VALUE */
- Lowercase: "char" and "character", title case: "Char" and "Character", and upper case: "CHAR" and
"CHARACTER" appearances in any Java identifiers (variable names, method names, class names, block
labels, etc), as well as in all other (non Java) contexts in the template file, such as in
comments and inside string literals. Plural forms: "chars", "characters" are recognized.
Examples:
Object myChar = ...
,int numChars = ...
,void sortCharacters();
,interface CharFunctor ...
,int CHARACTER_LIMIT = ...
,/** Get a char value. */
.
Specialization to "obj"/"object" by default replaces both primitive Java type occurrences (char
)
and boxed primitive class occurrences (Character
) with a single letter generic parameter: the
first letter of the dimension name. For example, for dimension char|object key
, template
char getChars() { List<Character> list = getList(); return list.get(0); }
is specialized to "object" as
K getObjects() { List<K> list = getList(); return list.get(0); }
For this reason, dimensions that have "object" or "obj" options must have names all starting with different letters.
If some primitive type occurrence or a boxed primitive class occurrence should be replaced with
Object
rather than a generic parameter reference, that occurrence should be prepended with
a special /* raw */
modifier in the template:
/* raw */char getChars() { List</* raw */Character> list = getList(); return list.get(0); }
is specialized to "object" as
Object getObjects() { List<Object> list = getList(); return list.get(0); }
Choosing either of the options in "bool"/"boolean", "char"/"character", "int"/"integer", and
"obj"/"object" pairs affects how the specialization for the corresponding option appears in
identifiers: short getShort();
(assuming the source option is "short") could be specialized as
either int getInt();
or int getInteger();
, depending on whether "int" or "integer" option is
specified for the dimension. If the source option also allows two potential variants (i. e. the
source option is either "char", "character", "int" or "integer", because booleans and objects can't
be source options), the occurrences in the template that correspond to the specified source option
are recognized as neutral, other occurrences are recognized as deliberately long or short:
Source option | Occurrence in template | Recognized as | Target option | Specialization outcome |
---|---|---|---|---|
"char" | CharCursor |
neutral | "bool" | BoolCursor |
"char" | CharCursor |
neutral | "boolean" | BooleanCursor |
"char" | CharacterCursor |
long | "bool" | BooleanCursor |
"char" | CharacterCursor |
long | "boolean" | BooleanCursor |
"character" | CharCursor |
short | "bool" | BoolCursor |
"character" | CharCursor |
short | "boolean" | BoolCursor |
"character" | CharacterCursor |
neutral | "bool" | BoolCursor |
"character" | CharacterCursor |
neutral | "boolean" | BooleanCursor |
It's often convenient to choose "short" (also "char" and "byte", if the template has several Java
type dimensions, such as Map, Pair, Triple, etc.) as the source option, because short
is rarely
used in Java code, so there will be little type occurrences that need to be excluded from
specialization (see the section about /* with */
blocks below). If identifiers in different styles
(getChar
vs getCharacter
- see above) appear in the code, choosing "char"/"character" and
"int"/"integer" as source options might allow to express the specialization logic without /* if */
and /* define */
blocks.
Non Java type dimensions
Options of non Java type dimensions intended for specialization must start with a capital letter:
/* with Foo|Bar myDimension */
, not /* with foo|bar myDimension */
(but lowercase non Java type
options can be used to mask outer dimensions - see the section about /* with */
blocks below
, and in conjunction with generics structures like /*<>*/
- see the
corresponding section below). Options can include several
words in camel case: /* with NoElements|SomeElements numElements */
. JPSG replaces textual
occurrences of source non Java type options in three forms:
- Camel case: "NoElements", "Foo".
- Lower case: "noElements", "foo".
- Upper case with underscores between words: "NO_ELEMENTS", "FOO".
Block control structures
There are three kinds of block control structures in JPSG: /* with */
, /* if */
and
/* define */
. They should be paired with corresponding /* endwith */
, /* endif */
and
/* enddefine */
, like /* with ... */ void myMethod() { ... } /* endwith */
. The exceptions are
/* with */
blocks in the very beginning of template files (see the section about template files
above), possibly directly followed by an /* if */
block - these two shouldn't be closed.
Block control structures should be correctly nested in the template file with respect to each other.
/* with */
blocks
The syntax of /* with */
blocks is already described in the section about template files above.
The only difference between a /* with */
block in the beginning of a template file and
/* with */
blocks inside a template file is that the latter should be paired with a closing
/* endwith */
. Code between the tags is repeated and specialized for each combination of options
of all dimensions defined in the /* with */
block (a cartesian product).
/* with */
blocks can be nested. If a nested /* with */
block has a definition of a dimension
with the same name as some dimension defined in an outer /* with */
block (including the one in
the beginning of the template file), the inner definition shadows the outer definition. This allows
to exclude some option occurrences from specialization if they don't belong to the specialization
domain:
/* with int|long element */
void sort(int[] array) {
/* with not element */int/* endwith */ len = array.length;
...
}
/* endwith */
In the example above, we don't want array's length to become long
when we sort arrays of longs.
Such occurrences that don't belong to the specialization domain are enclosed between
/* with not myDimensionName */.../* endwith */
. There is nothing special about "not" - this is
just a sole non Java type option of a myDimensionName
definition that shadows the dimension in
some outer /* with */
block. It could have been /* with quoted myDimensionName */
,
/* with notSpecialized myDimensionName */
, etc - whatever you find more readable.
/* if */
blocks
JPSG matches conditions in /* if */
blocks against the dimension options in the context (also
called dimensions context) to generate different variants of code (or none at all). Here are some
examples that show different features of /* if */
blocks:
/* if int|long|double elem */
java.util.function.ToIntFunction f = ...
/* elif object elem //
Function<E> f = ...
// elif byte|char|short|float elem //
com.mypackage.ToIntFunction f = ...
// endif */
/* if !(obj key) || !(obj value) //
ByteShortPair//<>// pair = ...
// elif obj key obj value */
Pair/*<>*/ pair = ...
/* endif */
There can be some /* elif */
branches between /* if */
and /* endif */
, each with it's own
condition. Note that there are no "else" branches. If the condition in the /* if */
doesn't match
the current dimensions context, nor any of the conditions in the /* elif */
branches, the whole
/* if */ ... /* endif */
is skipped by JPSG. No code from inside it appears in the specialization
output file.
Here is the formal BNF syntax of conditions allowed in /* if */
and /* elif */
(it references
<dimensions>
formalized above, i. e. syntactically equivalent to dimension definitions that appear
in /* with */
blocks):
<simple-condition> := <dimensions>
<negated-condition> := "!(" <simple-condition> ")"
<condition-in-parens> := "(" <simple-condition> ")" | <negated-condition>
<disjunction-condition> := ( <condition-in-parens> " || " )+ <condition-in-parens>
<conjunction-condition> := ( <condition-in-parens> " && " )+ <condition-in-parens>
<condition> := <simple-condition> | <negated-condition> | <disjunction-condition> | <conjunction-condition>
<simple-condition>
has the following semantics: a condition that has dimension_1
..
dimension_N
with options option_1_1
.. option_1_M1
, option_2_1
.. option_2_M2
, ...,
option_N_1
.. option_N_Mn
respectively matches a specialization context
dimension_in_context_1
= target_option_1
, ..., dimension_in_context_K
= target_option_K
if
and only if for each of i
between 1 and N, if there is dimension_in_context_j
equal to
dimension_i
, then target_option_j
appears to the list option_i_1
.. option_i_Mi
.
Putting that a little more intuitively, a condition like
option_1_1|...|option_1_M1 dimension_1
option_2_1|...|option_2_M2 dimension_2
...
option_N_1|...|option_N_Mn dimension_N
should be read as "if dimension_1 is option_1_1 or option_1_2 or ... option_1_M1, and dimension_2 is ..., and dimension_N is ...".
Semantics of <negated-condition>
, <disjunction-condition>
, and <conjunction-condition>
correspond to semantics of syntactically identical operations with boolean values in Java code.
Conditions with nested parentheses are not supported in JPSG, but it's possible to express any condition in a disjunctive normal form.
So the conditions from the examples in the beginning of this section have the following meanings:
-
int|long|double elem
: if elem is "int" or "long" or "double". -
object elem
: if elem is "object". -
!(obj key) || !(obj value)
: if key is not "obj" or value is not "obj". -
obj key obj value
: if key is "obj" and value is "obj".
All control structures in JPSG can start and end with //
as well as /*
and */
. This is
particularly useful in /* if */
blocks, where choosing how each if
and elif
branch starts and
ends allows to "comment out" some branches to keep the template valid Java code. For example, here
is an /* if */
block from one of the examples above:
int foo = /* if Foo myDimension */0// elif Bar myDimension //1// endif */;
Note that the elif
part starts and ends with //
, and the endif
part starts with //
and ends
with */
, so that 1
in the elif
branch is commented out in the template file, if it is treated
as Java code. Alternatively, the if
branch could have been commented out as:
int foo = /* if Foo myDimension //0// elif Bar myDimension */1/* endif */;
You may choose to always use /*
and */
because in the end of the day JPSG template files are not
compiled with javac
, but then your IDE might think that there are errors in the template files, if
the IDE is configured to recognize the template files as Java code.
In the beginning of a template file, right after a /* with */
block, there may be an /* if */
block. When the condition in this /* if */
block doesn't match a dimensions context from the
power set determined by the preceding /* with */
block, a specialization file for this dimensions
context is not generated. An example from the beginning of this document:
/* with char|byte|short|int|long|float|double|object key
int|byte|char|short|long|float|double|object value */
/* if !(object key object value) */ // Excludes ObjectObjectPair, because there is Pair already
interface CharIntPair ...
Here the dimension context "key=object,value=object" doesn't match the condition
!(object key object value)
, therefore ObjectObjectPair.java
specialization is not generated.
/* if */
block in the beginning of a template file can't have /* elif */
branches and must not
have a closing /* endif */
.
/* define */ blocks
/* define */
blocks are used to create shortcuts for some lengthy template code (usually involving
/* if */
blocks) and use them in multiple places. For example:
/* with byte|char|short|int|long|float|double|object key
short|byte|char|int|long|float|double|object value */
/* define MapType //
// if object key object value //MutableMap<Object, Object>
// elif object key //MutableObjectShortMap<Object>
// elif object value //MutableByteObjectMap<Object>
// elif !(object key) && !(object value) //MutableByteShortMap
// endif //
// enddefine */
/*MapType*/MutableByteShortMap/**/ myMap1 = new /*MapType*/MutableByteShortMap/**/();
/*MapType*/MutableByteShortMap/**/ myMap2 = new /*MapType*/MutableByteShortMap/**/();
The word after define
is the name of the definition. After the closing enddefine
, JPSG replaces
/* <definition-name> */
and the following code until /**/
with the template code between
/* define // ... // enddefine */
. The opening tag may also start or end with //
and the closing
tag may also be /*//
, //*/
, or ////
, as well as for other control structures in JPSG. Spaces
around the <definition-name>
are optional: the definition use may also be /*<definition-name>*/
.
If there is no closing /**/
after the definition use, only the construction
/* <definition-name> */
is replaced without "consuming" any following code until /**/
.
Note: the text or code between /* <definition-name> */
and /**/
must not contain /
character.
If a definition called "ClassName" appears somewhere in the template file, JPSG uses the replacement
with leading and trailing whitespace removed concatenated with ".java" as a specialization file
name. For example, consider the following template ByteShortPair.java
:
/* with byte|short|char|int|long|float|double|object key
short|byte|char|int|long|float|double|object value */
/* define ClassName //
// if !(object key object value) //ByteShortPair// elif object key object value //Pair// endif //
// enddefine */
interface /*ClassName*/ByteShortPair/**//*<>*/ {
byte getKey();
short getValue();
}
JPSG produces a specialization file called Pair.java
rather than ObjectObjectPair.java
from this
template, along with other 63 specializations.
/* define */
blocks can also have one text parameter, like macros in some programming languages.
Consider the following template:
/* with Preconditions|Assert|No parameterChecks */
/* define checkNonNull x */
/* if Preconditions parameterChecks //
Preconditions.checkNonNull(x);
// elif Assert parameterChecks //
assert x != null;
// endif */ /* comment // if parameterChecks=No, not emitting anything //*/
/* enddefine */
void myFirstMethodPreconditionsChecks(String arg1, String arg2) {
/* checkNonNull arg1 */
/* checkNonNull arg2 */
}
void myOtherMethodPreconditionsChecks(MyClass my) {
/* checkNonNull my */
}
/* endwith */
JPSG produces the following specialization from this template:
void myFirstMethodPreconditionsChecks(String arg1, String arg2) {
Preconditions.checkNonNull(arg1);
Preconditions.checkNonNull(arg2);
}
void myOtherMethodPreconditionsChecks(MyClass my) {
Preconditions.checkNonNull(my);
}
void myFirstMethodAssertChecks(String arg1, String arg2) {
assert arg1 != null;
assert arg2 != null;
}
void myOtherMethodAssertChecks(MyClass my) {
assert my != null;
}
void myFirstMethodNoChecks(String arg1, String arg2) {
}
void myOtherMethodNoChecks(MyClass my) {
}
The above example also features a definition named "comment" that always exists in the JPSG's
context. It has empty body, so it is replaced with nothing. It is useful to consume the text between
/* comment //
and the following //*/
not letting it into the generated code. This effectively
works as template-level comments.
Auxiliary non-block structures and modifiers
Generics structures: /*<>*/
, /*<extends>*/
, /*<super>*/
, /*<?>*/
Generics structures are useful when generating code for both primitive and object (generic) types
from the same template. /*<>*/
has already been used in several examples above in this tutorial.
JPSG replaces /*<>*/
with generics corresponding to first letters of the dimension names, for all
Java type dimensions that are specialized to "object" or "obj" in the current context. If some Java
type dimension is specialized to a primitive type, it's omitted from the generics list. If all Java
type dimensions are specialized to primitive types, JPSG replaces /*<>*/
(three other variants of
the generics structure) with an empty string. Generics are arranged in the same order as the
corresponding dimensions are defined in the template. Consider the example from the beginning of
this document:
/* with char|byte|short|int|long|float|double|object key
int|byte|char|short|long|float|double|object value */
/* if !(object key object value) */
interface CharIntPair/*<>*/ extends Pair<Character, Integer> {
...
}
Here are the lines that JPSG will produce for different specializations:
-
ByteBytePair extends Pair<Byte, Byte>
-
ObjectBytePair<K> extends Pair<K, Byte>
-
ByteObjectPair<V> extends Pair<Byte, V>
-
ObjectObjectPair<K, V> extends Pair<K, V>
- not actually generated in this particular example because of the exclusive condition in the beginning of the file, but could have been generated if the condition wasn't there.
/*<extends>*/
, /*<super>*/
, /*<?>*/
follow the same rules, but they are replaced with
<? extends T, ? extends U, ...>
, <? super U, ? super T, ...>
and <?, ?, ...>
respectively. T
and U
are just examples - JPSG derives the letters from the dimension names, see the section about
specialization of Java type dimensions.
If there is a single-value dimension called "view" in the context and its value corresponds to the name of some Java type dimension in the context, generics structures consider only that dimension. This feature is useful in a specific case of Map-like templates with KeySet and Values views, for example:
/* with byte|short|char|int|long|float|double|object key
short|byte|char|int|long|float|double|object value */
class MyByteShortMap/*<>*/ implements Map/*<>*/ {
...
/* with key view */
class KeySetView implements ByteSet/*<>*/ {
...
}
/* endwith */
/* with value view */
class ValuesView implements ShortCollection/*<>*/ {
...
}
/* endwith */
}
is specialized to code like the following when "key=object" and "value=object":
class MyObjectObjectMap<K, V> implements Map<K, V> {
...
class KeySetView implements ObjectSet<K> {
...
}
class ValuesView implements ObjectCollection<V> {
...
}
}
Note that JPSG replaces /*<>*/
with <K>
rather than <K, V>
in the line
class KeySetView implements ObjectSet/*<>*/ {
and with <V>
in the line
class ValuesView implements ObjectCollection/*<>*/ {
because of the "view" dimensions specified.
/* raw */
modifier
The /* raw */
modifier was already mentioned in the section about specialization of Java type
dimensions above, it tells JPSG to replace a source
primitive type occurrence with Object
rather than a generic parameter reference.
/* bits */
modifier and floating wrapping
In some cases, when specializing to float
and double
types you may need to refer to int
and
long
primitive types as the "bits" counterparts of float
and double
(int
and long
are the
return types of Float.floatToIntBits()
and Double.doubleToLongBits()
, respectively). To achieve
that a standalone primitive occurrence should be prepended with a /* bits */
modifier, for
example:
/* with byte|int|long|float|double t */
/* bits */byte unwrapByte(byte v) { return /* unwrap t */v; }
/* bits */byte unwrapRawByte(byte v) { return /* unwrapRaw t */v; }
byte wrapByte(/* bits*/byte bits) { return /* wrap t*/bits; }
/* endwith */
JPSG generates code like the following from the above template:
byte unwrapByte(byte v) { return v; }
byte unwrapRawByte(byte v) { return v; }
byte wrapByte(byte bits) { return bits; }
int unwrapInt(int v) { return v; }
int unwrapRawInt(int v) { return v; }
int wrapInt(int bits) { return bits; }
long unwrapLong(long v) { return v; }
long unwrapRawLong(long v) { return v; }
long wrapLong(long bits) { return bits; }
int unwrapFloat(float v) { return Float.floatToIntBits(v); }
int unwrapRawFloat(float v) { return Float.floatToRawIntBits(v); }
float wrapFloat(int bits) { return Float.intBitsToFloat(bits); }
long unwrapDouble(double v) { return Double.doubleToLongBits(v); }
long unwrapRawDouble(double v) { return Double.doubleToRawLongBits(v); }
double wrapDouble(long bits) { return Double.longBitsToDouble(bits); }
This example presents auxiliary floating point wrapping/unwrapping structures which are often useful
at the same time as you need to reference the /* bits */
types: /* wrap <dimension-name> */
,
/* unwrap <dimension-name> */
, and /* unwrapRaw <dimension-name> */
are replaced along with the
immediately following alphanumeric word (v
and bits
in the example above) with
Float.intBitsToFloat(<alphanumeric-arg>)
, Float.floatToIntBits(<alphanumeric-arg>)
, and
Float.floatToRawIntBits(<alphanumeric-arg>)
respectively, if the referenced dimension specializes
to float
in the current context, with the corresponding methods in Double
class if the
referenced dimension specializes to double
in the current context, or simply with
<alphanumeric-arg>
(in other words, it is left intact) if the referenced dimension specializes to
neither float
nor double
.
Template-level comments
As mentioned in the section about /* define */ blocks
, you can write something
that won't appear in the JPSG's output code using /* comment */
definition:
//comment// Explain something about the following generation structures /**/
/* if ... */
A/An: insert correct English indefinite articles
JPSG replaces //a//
and //an//
(as well as /*a*/
, //a*/
and /*a//
, and all equivalent
structures with "an", as usual for JPSG structures) with "a" or "an" depending on whether the next
word starts with a vowel letter: "a", "e", "i", "o" or "u". For example, JPSG translates the
following template:
/* with byte|char|int t */
/** Returns //a// byte value. */
byte getByte();
/* endwith */
/* with Apple|Orange|Banana fruit */
/** Returns //an// {@link Apple}. */
Apple getApple();
/* endwith */
into
/** Returns a byte value. */
byte getByte();
/** Returns a char value. */
char getChar();
/** Returns an int value. */
int getInt();
/** Returns an {@link Apple}. */
Apple getApple();
/** Returns an {@link Orange}. */
Orange getOrange();
/** Returns a {@link Banana}. */
Banana getBanana();
However, JPSG is not sophisticated enough to follow the actual English grammar rules that are based on sounds, not letters (consider "an hour", "a union").
As this structure is usually used in Javadoc comments specifically in expressions like "a char
variable", it recognizes Javadoc's @code
and @link
tags following the indefinite article and
specializes //a// {@code char} variable
and //an// {@link Apple}
(as you can see in the above
example) correctly as well.
Print the current option for a dimension
JPSG replaces structures /* print <dimension-name> */ ... /* endprint */
with the option for the
mentioned dimension in the current generation context. It doesn't make a lot of sense when the
dimension is defined using a /* with */
structure within the template file, because you can just
write the source option directly (that is, employ the main function of JPSG). However, /* print */
may be handy when the dimension is provided externally via a Gradle plugin (using
with()
method) or a Maven plugin configuration.
Gradle Plugin
The JPSG Gradle plugin is built after the Gradle's core ANTLR plugin and creates a similar set of tasks and dependencies.
Usage
The plugin depends on the Java Gradle plugin. To use the JPSG Gradle plugin, include the following in your build script:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'io.timeandspace:jpsg-gradle-plugin:1.4'
}
}
plugins {
id 'java'
}
apply plugin: 'jpsg'
This is Groovy syntax. For Kotlin syntax, see an example in Gradle docs.
You can find a full working example in jpsg-gradle-plugin-test
project.
Tasks
The JPSG Gradle plugin adds the following tasks to your project:
-
generateJavaSpecializations
-JpsgTask
Generates source code from production templates (stored in directorysrc/main/javaTemplates/
). -
generateResourceSpecializations
-JpsgTask
Generates resource files from production resource templates (stored in directorysrc/main/resourceTemplates/
). -
generateTestJavaSpecializations
-JpsgTask
Generates source code from test templates (stored in directorysrc/test/javaTemplates/
). -
generateTestResourceSpecializations
-JpsgTask
Generates resource files from test resource templates (stored in directorysrc/test/resourceTemplates/
). -
generateSourceSetJavaSpecializations
-JpsgTask
Generates source code from the templates for the given source set (stored in directorysrc/sourceSet/javaTemplates/
). -
generateSourceSetResourceSpecializations
-JpsgTask
Generates resource files from the resource templates for the given source set (stored in directorysrc/sourceSet/resourceTemplates/
).
The plugin adds the following dependencies to tasks added by the Java plugin:
Task name | Depends on |
---|---|
compileJava |
generateJavaSpecializations |
processResources |
generateResourceSpecializations |
compileTestJava |
generateTestJavaSpecializations |
processTestResources |
generateTestResourceSpecializations |
compileSourceSetJava |
generateSourceSetJavaSpecializations |
processSourceSetResources |
generateSourceSetResourceSpecializations |
Directory layout
src/sourceSet/javaTemplates/
(including main
and test
source sets) - JPSG
treat all files in this directory (including all files in subdirectories, recursively) as templates
for generation of source code in the given source set. Template files should be organized in
subdirectories repeating the package structure as well as for normal Java sources.
src/sourceSet/resourceTemplates/
- JPSG treats all files in this directory as
templates for generation of resource files in the given source set.
JpsgTask
configuration
The fully qualified class name of JPSG tasks is io.timeandspace.jpsg.JpsgTask
. So, to configure
all tasks added by the JPSG Gradle plugin, you can write:
tasks.withType(io.timeandspace.jpsg.JpsgTask) {
defaultTypes = "int|long|float|double"
exclude "float|double t int|long|float|double u"
}
Alternatively, you can configure specific JPSG tasks:
configure([generateJavaSpecializations, generateTestJavaSpecializations]) {
with "Enabled extraChecks"
}
Properties
String defaultTypes
For all template files without /* with */
blocks in the beginning JPSG attempts to deduce
dimensions from the name of the template file, taking possible options from this defaultTypes
configuration. See more details in the section about /* with */
blocks.
Format: <javaTypeOptions>
.
Default value: byte|char|short|int|long|float|double
.
File source
The source directory which JPSG traverses and considers all files in it as template files.
Default value: src/sourceSet/javaTemplates/
or
src/sourceSet/resourceTemplates/
, see the directory layout.
File target
The target directory where JPSG puts specialized sources.
Default value: ${project.buildDir}/generated-src/jpsg/${sourceSet.name}
.
Methods
never(String... options)
For all dimensions defined in the beginnings of template files in /* with */
blocks, or deduced
automatically by JPSG (see defaultTypes
property above), or defined inside template files at any
level of nesting, JPSG will skip generating code for the specified options. never()
may be called
several times, adding more options to skip.
options
parameter format: <options>
.
Example:
never("byte|short", "char") // Never generate code for byte, short, char types
never "Assert" // Additionally, don't generate code for non-Java type `Assert` option
exclude(String... conditions)
JPSG doesn't generate specialization files for dimension contexts (either determined by /* with */
blocks in the beginnings of the template files, or deduced using defaultTypes
) that match any of
the conditions passed to exclude()
. In other words, each exclusion condition is virtually added to
an /* if */
block in the beginning of each template file like the
following:
/* with <some explicitly defined dimensions (may be absent)> */
/* if !(exclusionCondition1) && ... && !(exclusionConditionN) */
/* if <some explicit, file-specific condition (may be absent) > */
... The rest of the template file
While conditions in explicit /* if */
blocks can only reference the existing (defined or deduced)
dimensions, exclusion conditions specified via exclude()
may reference some dimensions that are
not present in every template file. Such exclusion conditions don't match and therefore don't make
JPSG to skip any context specialization for the template files which don't define some dimensions
from these exclusion conditions.
Semantics of conditions are described in detain in the section about /* if */
blocks
.
Exclusion conditions are useful when you want to suppress generation for some combinations of
dimension options consistently in all template files. exclude()
allows to not write the same
/* if */
block in the beginning of each template file.
conditions
parameter format: <simple-condition>
.
Example:
exclude("object key byte|short|char|object value", "byte key short|char value")
exclude "Disabled extraChecks Enabled advancedStatistics"
with(String... dimensions)
JPSG adds the provided dimensions to the generation contexts in each template file. Each dimension must have a single option.
Note that JPSG adds the provided dimensions to the context of a template file after applying the
exclusion conditions (specified via exclude()
) and before filtering specializations with the
explicit /* if */
block in the beginning of the template file (if present). So the beginning of
each template file virtually looks like the following:
/* with <some explicitly defined dimensions (may be absent)> */
/* if !(exclusionCondition1) && ... && !(exclusionConditionN) */
/* with providedDimension1 ... providedDimensionM */
/* if <some explicit, file-specific condition (may be absent) > */
... The rest of the template file
with()
allows to enrich the generation contexts in all template files without adding them in to
the /* with */
block in the beginning of each template file.
with()
dimension may also be used in conjunction with /* print */
structures
to "macro print" something in template files during
generation.
dimensions
parameter format: <dimensions>
. Every dimension must have only a
single option.
Example:
with("Enabled extraChecks Disabled advancedStatistics", "Assert extraCheckStyle")
with "java8 minSupportedJavaVersion"