This module is used by Helidon to generate types with builders (Prototypes) to be used in API of modules from a blueprint interface.
There are two modules that are used:
helidon-builder-api- module required incompilescope, contains annotations and APIs needed to write blueprints, to compile the generated code, and at runtimehelidon-builder-codegen- module to be placed on annotation processor path, generates the sources
There is one module useful for internal development
helidon-builder-tests-common-types(located undertests/common-types) that contains blueprints for the types we use inhelidon-common-types. As the common types module is used by the processor, we would end up with a cyclic dependency, so this allows us to generate the next iteration of common types (requires manual copying of the generated types)
This document describes the main features and usage, there are further customization option. Kindly check usages of
Prototype.Blueprint in this repository, to see examples...
Table of contents:
- Goals - what we do
- Non-Goals - what we decided not to do
- Rules - what are the rules when using this module
- Use Cases - supported use cases
- Getting Started - set up your
pom.xmland use this module - API - more details on available annotations and interfaces
Generate all required types for Helidon APIs with builders, that follow the same style (method names, required validation etc.).
Support for builders that can read options from Helidon configuration (helidon-config).
- We MUST NOT change bytecode of user classes
- We MUST NOT use reflection (everything is code generated)
- Support inheritance of prototypes (and of blueprints if in the same module)
- The generated code is the public API (and must have javadoc generated)
- The annotated blueprint interface is used to generate configuration metadata, and configuration documentation ( see Config metadata)
- Support prototypes configured from Helidon configuration (as an optional feature)
- Support additional methods to be generated (factory methods, prototype methods, builder methods)
- Support the following collections:
List,Set, andMap - Support for default values, for the most commonly used types to be typed explicitly (String, int, long, boolean etc.)
- Support for
enumoptions
We are not building a general purpose solution, there are limitations that are known and will not be targeted:
- the solution expects that everything is single package - blueprints are required to be package local, which does not allow using built types across packages within a single module
- we only support interface based definition of blueprints (no classes)
- we only support non-nullable options, instead of nullable, use
Optionalgetters - implementation types of collections are fixed to
java.util.ArrayList,java.util.LinkedHashSetandjava.util.LinkedHashMap
There are a few rules we required and enforce:
- Blueprint MUST be an interface
- Blueprint interface MUST be package private
- Blueprint interface must have a name that ends with
Blueprint; the name beforeBlueprintwill be the name of the prototype - In case we use the blueprint -> prototype -> runtime type use case (see below):
- The blueprint must extend
Prototype.Factory<RuntimeType>whereRuntimeTypeis the type of the runtime object - The runtime type must implement
RuntimeType.Api<Prototype> - The runtime type must have a
public static Prototype.Builder builder()method implemented by user - The runtime type must have a
public static RuntimeType create(Prototype)method implemented by user - The runtime type must have a
public static RuntimeType create(Consumer<Prototype.Builder>)method implemented by user
- The blueprint must extend
There are two use cases we cover:
- We need a type with a builder (we will use
Keysas an example) - We need a runtime object, with a prototype with a builder (we will use
Retryas an example)
For both use cases, we need to understand how to create instances, obtain builders etc.
For this simple approach, the user facing API will look as:
Keys keys = Keys.builder()
.name("name")
.build();Configuration based API:
// the location of config is arbitrary, the API expects in ono the Keys node
Keys keys = Keys.create(config.get("keys"));The "blueprint" of such type:
import io.helidon.config.metadata.Configured;
import io.helidon.config.metadata.ConfiguredOption;
@Prototype.Blueprint
@Prototype.Configured // support method config(Config) on the builder, and a static create(Config)
interface KeysBlueprint {
@Option.Configured
String name();
}This will generate:
Keys extends KeysBlueprintinterfaceKeys.BuilderBase implements Keysbase builder, to support extensibility ofKeysKeys.Builder extends Keys.BuilderBase, io.helidon.common.Builder<Builder, Keys>inner class - the fluent API builder forKeysKeys.BuilderBase.KeysImpl implements Keysimplementation ofKeys
For this approach, the user facing API will be similar to:
Retry retry = Retry.builder() // method builder is not generated, must be hand coded, and will return "RetryPrototype.builder()"
.build(); // generated, creates a Retry instance through a factory method defined on Retry or on RetryPrototypeBlueprint
RetryPrototype prototype = RetryPrototype.builder()
.buildPrototype(); // alternative build method to obtain the intermediate prototype object
Retry retryFromPrototype = prototype.build(); // to runtime typeThe "blueprint" of such type:
@Prototype.Blueprint
@Prototype.Configured // support method config(Config) on the builder, and a static create(Config) method if desired
intrerface RetryPrototypeBlueprint extends Prototype.Factory
<Retry> {
@Option.Configured
String name ();
}- Write your interface that you want to have a builder for
interface MyConfigBeanBlueprint {
String getName();
boolean isEnabled();
int getPort();
}- Annotate your interface definition with
@Blueprint, and optionally use@Prototype.Configuredand@Option.Configured,@Option.Singularetc. to customize the getter methods. Remember to review the annotation attributes javadoc for any customizations - Update your pom file to add annotation processor
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<!-- Codegen integration with Java annotation processing -->
<groupId>io.helidon.codegen</groupId>
<artifactId>helidon-codegen-apt</artifactId>
<version>${helidon.version}</version>
</path>
<path>
<groupId>io.helidon.builder</groupId>
<artifactId>helidon-builder-codegen</artifactId>
<version>${helidon.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
<!--
Only for Helidon developers:
the following section is to enable correct reactor ordering without adding processor
to module classpath/module path
this is ONLY needed when adding (any) Helidon processor to a Helidon module (within the same Maven project)
-->
<dependencies>
<dependency>
<groupId>io.helidon.builder</groupId>
<artifactId>helidon-builder-codegen</artifactId>
<version>${helidon.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
...Generated types will be available under ./target/generated-sources/annotations
- MyConfigBean (in the same package as MyConfigBeanBlueprint), with inner classes
BuilderBase(for inheritance),Builder, - Support for
toString(),hashCode(), andequals()are always included. - Support for
builder(MyConfigBean)to create a new builder from an existing instance - Support for
from(MyConfigBean)andfrom(MyConfigBean.BuilderBase<?, ?>)to update builder from an instance or builder - Support for validation of required and non-nullable options (required options are options that have
@Option.Requiredand are primitive), non-nullable option is any option that is not primitive, collection, and does not return anOptional) - Support for builder decorator (
@Bluprint(decorator = MyDecorator.class)),class MyDecorator implements BuilderDecorator
The API has to sections:
- Inner types of
Prototypeclass to configureBlueprints, and ofRuntimeTypeto configure runtime types - Inner types of
Optionclass to configure options
Annotations:
| Annotation | Required | Description |
|---|---|---|
Prototype.Blueprint |
true |
Annotation on the blueprint interface is required to trigger annotation processing |
Prototype.Implement |
false |
Add additional implemented types to the generated prototype |
Prototype.Annotated |
false |
Allows adding an annotation (or annotations) to the generated class or methods |
Prototype.PrototypeFactoryMethod |
false |
Annotates a method in a CustomMethods type to be added as a static method to the prototype |
Prototype.ConfigFactoryMethod |
false |
Annotates a method in a CustomMethods type that creates an option from Config on a configured type |
Prototype.RuntimeTypeFactoryMethod |
false |
Annotates a method in a CustomMethods type that creates an option runtime type from its prototype (the parameter must be another prototype |
Prototype.Singular |
false |
Used for lists, sets, and maps to add methods add*/put* in addition to the full collection setters |
Prototype.SameGeneric |
false |
Use for maps, where we want a setter method to use the same generic type for key and for value (such as Class<T> key, T valuel) |
Prototype.Redundant |
false |
A redundant option will not be part of generated toString, hashCode, and equals methods (allows finer grained control) |
Prototype.Confidential |
false |
A confidential option will not have value visible when toString is called, only if it is null or it has a value (****) |
Prototype.CustomMethods |
false |
reference a class that will contain declarations (all static) of custom methods to be added to the generated code, can add prototype, builder, and factory methods |
Prototype.BuilderMethod |
false |
Annotation to be placed on factory methods that are to be added to builder, first parameter is the BuilderBase<?, ?> of the prototype |
Prototype.PrototypeMethod |
false |
Annotation to be placed on factory methods that are to be added to prototype, first parameter is the prototype instance |
Interfaces:
| Interface | Generated | Description |
|---|---|---|
RuntimeType.Api |
false |
runtime type must implement this interface to mark which prototype is used to create it |
Prototype.Factory |
false |
if blueprint implements factory, it means the prototype is used to create a single runtime type and will have methods build and get both on builder an on prototype interface that create a new instance of the runtime object |
Prototype.BuilderDecorator |
false |
custom decorator to modify builder before validation is done in method build |
Prototype.Api |
true |
all prototypes implement this interface |
Prototype.Builder |
true |
all prototype builders implement this interface, defines method buildPrototype |
Prototype.ConfiguredBuilder |
true |
all prototype builders that support configuration implement this interface, defines method config(Config) |
| Annotation | Description |
|---|---|
@Option.Singular |
For collection based options. Adds setter for a single value (for List<String> algorithms(), there would be the following setters: algorithms(List<String>), addAlgorithms(List<String>), addAlgorithm(String)) |
@Option.Configured |
For options that are configured from config (must be explicitly marked, default is not-configured), also ignored unless @Prototype.Configured is specified on the blueprint interface |
@Option.Required |
We can recognize required options through signature in most cases (any option that does not return an Optional and does not have a default value); this option is useful for primitive types, where we need an explicit value set, rather than using the primitive's default value |
@Option.Provider |
Satisfied by a provider implementation, see javadoc for details |
@Option.AllowedValues |
Allowed values for the property, not required for enum, where we create this automatically, though we can configure description of each value (works automatically for enum defined in the same module); the description is used for generated documentation |
@Option.SameGeneric |
Advanced configuration of a Map, where the map accepts two typed values, and we must use the same generic on setters (such as Map<Class<Object>, Object> - <T> Builder put(Class<T>, T)) |
@Option.Redundant |
Marks an option that is not used by equals and hashCode methods |
@Option.Confidential |
Marks an option that will not be visible in toString() |
@Option.Deprecated |
Marks a deprecated option that has a replacement option in this builder, use Java's deprecation for other cases, they will be honored in the generated code |
@Option.Type |
Explicitly defined type of a property (may include generics), in case the type is code generated in the current module, and we cannot obtain the correct information from the annotation processing environment |
@Option.Decorator |
Support for field decoration (to do side-effects on setter call) |
To configure default value(s) of an option, one of the following annotations can be used (mutually exclusive!). Most defaults support an array, to provide default values for collections.
| Annotation | Description |
|---|---|
@Option.Default |
Default value(s) that are String or we support coercion to the correct type (enum, Duration) |
@Option.DefaultInt |
Default value(s) that are int |
@Option.DefaultLong |
Default value(s) that are long |
@Option.DefaultDouble |
Default value(s) that are double |
@Option.DefaultBoolean |
Default value(s) that are boolean |
@Option.DefaultMethod |
Static method to invoke to obtain a default value |
@Option.DefaultCode |
Source code to add to the generated assignment, single line only supported |