AutoAnnotator
AutoAnnotator is a metaprogramming tool for automatically annotating types with validation constraints, JPA annotations and arbitrary other annotations. It is written in Kotlin but works for Java and probably all other JVM languages as well.
Purpose
The main purpose of AutoAnnotator is avoidance of boilerplate annotations when working with JPA POJOs.
Given this POJO
Kotlin:
import java.time.ZonedDateTime
import javax.persistence.Entity
@Entity
class AutoAnnotatedPojo(
val nonNullString: String,
val nullableString: String?,
val zonedDateTime: ZonedDateTime
)
Java:
import java.time.ZonedDateTime;
import javax.persistence.Entity;
@Entity
public class AutoAnnotatedPojo {
@javax.annotation.Nonnull
private String nonNullString = "";
private String nullableString;
@javax.annotation.Nonnull
private ZonedDateTime zonedDateTime = ZonedDateTime.now();
}
you can use AutoAnnotator to manipulate your bytecode as if you had written:
Kotlin:
import java.time.ZonedDateTime
import javax.persistence.Column
import javax.persistence.Entity
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Pattern
@Entity
class AutoAnnotatedPojo(
@field:[NotNull NotBlank]
val nonNullString: String,
@field:Pattern(regexp = """(?s).*\S.*""", message = "must be null or not blank")
val nullableString: String?,
@field:[NotNull Column(columnDefinition = "timestamp with time zone")]
val zonedDateTime: ZonedDateTime
)
Java:
import org.jetbrains.annotations.NotNull;
import java.time.ZonedDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
@Entity
public class AutoAnnotatedPojo {
@javax.annotation.Nonnull
@NotNull
@NotBlank
private String nonNullString = "";
@Pattern(regexp = "(?s).*\\S.*", message = "must be null or not blank")
private String nullableString;
@javax.annotation.Nonnull
@NotNull
@Column(columnDefinition = "timestamp with time zone")
private ZonedDateTime zonedDateTime = ZonedDateTime.now();
}
For more examples, please see the tests in the autoannotator-core
module.
@javax.validation.constraints.NotNull
can be useful even in Kotlin, because your JPA provider may be able to infer a NOT NULL
database constraint from it. E.g., Hibernate does this by default.
In Kotlin, you probably do not want blank strings because null
is the idiomatic way of representing a missing value.
Usage
1. Dependencies
Maven:
<dependency>
<groupId>com.marcoeckstein</groupId>
<artifactId>autoannotator-api</artifactId>
<version>${version}</version>
</dependency>
Gradle Kotlin DSL:
implementation("com.marcoeckstein:autoannotator-api:$version")
The autoannotator-api
module has no compile time dependencies to libraries with the annotations it may use (depending on the configuration). It is expected that your project's classpath contains libraries with these annotations. E.g., if you use javax.validation.constraints.NotNull
(by default you do), your project must have a dependency to javax.validation:validation-api
or a substitute. If you have a JPA project, you probably have all required dependencies.
2. Configuration
AutoAnnotator is configured inside your sources, allowing for type safety and autocompletion. Your project needs to provide a single parameterless function annotated with @AutoAnnotatorConfigSource
that returns an AutoAnnotatorConfig
.
Kotlin:
import com.marcoeckstein.autoannotator.api.AnnotationInfo
import com.marcoeckstein.autoannotator.api.AutoAnnotatorConfig
import com.marcoeckstein.autoannotator.api.AutoAnnotatorConfigSource
import com.marcoeckstein.autoannotator.api.ClassFilter
import com.marcoeckstein.autoannotator.api.ClassOptions
import java.time.ZonedDateTime
import javax.persistence.Column
@AutoAnnotatorConfigSource
fun get() = AutoAnnotatorConfig(
ClassFilter(packagePrefix = "mypackage.domain.model"),
ClassOptions(
annotationsByFieldType = mapOf(
ZonedDateTime::class.qualifiedName!! to setOf(
AnnotationInfo(
clazz = Column::class,
members = mapOf(Column::columnDefinition to "timestamp with time zone")
)
)
)
)
)
Java:
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.marcoeckstein.autoannotator.api.AnnotationInfo;
import com.marcoeckstein.autoannotator.api.AutoAnnotatorConfig;
import com.marcoeckstein.autoannotator.api.AutoAnnotatorConfigSource;
import com.marcoeckstein.autoannotator.api.ClassFilter;
import com.marcoeckstein.autoannotator.api.ClassOptions;
import java.time.ZonedDateTime;
import javax.persistence.Column;
public class Config {
@AutoAnnotatorConfigSource
public static AutoAnnotatorConfig get() {
return new AutoAnnotatorConfig(
new ClassFilter("mypackage.domain.model"),
new ClassOptions(
ImmutableMap.of(
ZonedDateTime.class.getName(),
ImmutableSet.of(
new AnnotationInfo(
javax.persistence.Column.class, // clazz
ImmutableMap.of("columnDefinition", "timestamp with time zone") // members
)
)
)
)
);
}
}
The configuration classes have rich documentation, so you can easily explore all options by using your IDE's autocompletion and integrated documentation features.
3. Build lifecycle
AutoAnnotator needs to run after your project's main sources have compiled. Depending on the configuration, it may need to run after your project's test sources have compiled as well.
Maven
The default phase is process-classes
, which comes directly after compile
in the default lifecycle, but compile
should also work.
<plugin>
<groupId>com.marcoeckstein</groupId>
<artifactId>autoannotator-maven-plugin</artifactId>
<version>${version}</version>
<executions>
<execution>
<phase>process-classes</phase><!-- Default -->
<goals>
<goal>annotate</goal><!-- The only goal -->
</goals>
</execution>
</executions>
</plugin>
If you want to auto-annotate classes in your test source directory, you have to choose the phase and edit the plugin configuration accordingly:
<plugin>
<groupId>com.marcoeckstein</groupId>
<artifactId>autoannotator-maven-plugin</artifactId>
<version>${version}</version>
<configuration>
<annotateTestClasses>true</annotateTestClasses>
</configuration>
<executions>
<execution>
<phase>process-test-classes</phase><!-- Anything from test-compile should work. -->
<goals>
<goal>annotate</goal>
</goals>
</execution>
</executions>
</plugin>
For details and other configuration parameters, see the plugin parameter descriptions.
You should also configure your IDE to run this goal after building. E.g., in IntelliJ IDEA's Maven tab, right-click the plugin's goal and check "Execute After Build".
CLI (Command-line interface)
If you do not use Maven, you can use the autoannotator-cli
module at "com.marcoeckstein:autoannotator-api:$version"
. The main class is com.marcoeckstein.autoannotator.cli.Main
. Logging is configured via org.slf4j.simpleLogger.*
properties. For options documentation, call the CLI with --help
.
Classpath
The Java process running the CLI needs a classpath that includes:
- The
autoannotator-cli
jar-with-dependencies (aka "fat JAR") - Your project's classpath, or at least:
- All classes you want to annotate
- All types of fields in those classes (even if not annotated)
- All annotations you want to add or update
- The class with the
@AutoAnnotatorConfigSource
function - All classes used in the
@AutoAnnotatorConfigSource
function
I.e., writing the string"mypackage.MyClass"
inside that function generally requires fewer classes than the expressionsmypackage.MyClass::class.qualifiedName!!
(Kotlin) ormypackage.MyClass.class.getName()
(Java).
If you wanted to use autoannotator-cli
in Maven, you could do this:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>...</version>
<executions>
<execution>
<configuration>
<target name="autoannotator">
<property name="maven.compile.classpath" refid="maven.compile.classpath"/>
<java taskname="autoannotator"
dir="${basedir}"
fork="true"
failonerror="true"
classpathref="maven.plugin.classpath"
classpath="${maven.compile.classpath}"
classname="com.marcoeckstein.autoannotator.cli.Main">
<sysproperty key="org.slf4j.simpleLogger.defaultLogLevel" value="info"/>
</java>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.marcoeckstein</groupId>
<artifactId>autoannotator-cli</artifactId>
<version>...</version>
<classifier>jar-with-dependencies</classifier>
</dependency>
</dependencies>
</plugin>
Development
Due to limitations of the Dokka plugin, there are some caveats:
- For development, JDK 8 is required. A higher JDK version cannot be used. If you use Windows and have a different JDK installed, you can set environment variable
JAVA8_HOME
and run the PowerShell scriptmvn-runner.ps1
. - The following warnings are to be expected:
dokka-maven-plugin:0.10.0:dokka
:- "Can't find node by signature `org.jetbrains.annotations.NotNull`, referenced at..."
dokka-maven-plugin:0.10.0:javadoc
:- "null
👎 -1: Tag @see cannot be used in inline documentation."
- "null
mvn-runner.ps1
This interactive PowerShell script - if used with goal deploy
- expects you to have the following data in your %UserProfile%\.m2\settings.xml
:
<settings>
<servers>
<server>
<id>Sonatype Staging</id>
<username>USERNAME</username>
<password>${sonatype.password}</password>
</server>
<server>
<id>Sonatype Snapshots</id>
<username>USERNAME</username>
<password>${sonatype.password}</password>
</server>
...
</servers>
...
</settings>