diff --git a/README.md b/README.md
index aba4603..7ed64bc 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,7 @@ _Details:_
- [Generation Via Includes](#generation-via-includes)
- [Usage](#usage)
- [Customizing](customizing.md) (e.g. add immutable collections, etc.)
+- **_NEW!!_** [RecordBuilder Enhancer](record-builder-enhancer/README.md) - Inject verification, defensive copying, null checks or custom code into your Java Record constructors during compilation!
## RecordBuilder Example
@@ -322,6 +323,11 @@ in the listed packages.
The target package for generation is the same as the package that contains the "Include"
annotation. Use `packagePattern` to change this (see Javadoc for details).
+## Enhancer
+
+[RecordBuilder Enhancer](record-builder-enhancer/README.md) - Inject verification, defensive copying, null
+checks or custom code into your Java Record constructors during compilation.
+
## Usage
### Maven
diff --git a/pom.xml b/pom.xml
index 1371eff..c36758d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,6 +12,9 @@
record-builder-processor
record-builder-test
record-builder-validator
+ record-builder-enhancer
+ record-builder-test-custom-enhancer
+ record-builder-enhancer-core
@@ -29,7 +32,7 @@
1.6
3.1.1
3.1.0
- 3.2.1
+ 3.3.0
2.5.3
3.2.0
3.0.0-M5
@@ -40,10 +43,13 @@
1.12.1
5.5.2
- 7.2
+ 9.3
2.0.1.Final
6.0.20.Final
3.0.1-b09
+ 1.12.10
+ 31.1-jre
+ 4.6.3
Record Builder
@@ -103,6 +109,18 @@
+
+ net.bytebuddy
+ byte-buddy
+ ${byte-buddy.version}
+
+
+
+ net.bytebuddy
+ byte-buddy-agent
+ ${byte-buddy.version}
+
+
com.squareup
javapoet
@@ -115,12 +133,30 @@
${project.version}
+
+ io.soabase.record-builder
+ record-builder-enhancer
+ ${project.version}
+
+
+
+ io.soabase.record-builder
+ record-builder-enhancer-core
+ ${project.version}
+
+
io.soabase.record-builder
record-builder-processor
${project.version}
+
+ io.soabase.record-builder
+ record-builder-test-custom-enhancer
+ ${project.version}
+
+
io.soabase.record-builder
record-builder-validator
@@ -150,6 +186,30 @@
javax.el
${javax-el-version}
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+
+
+
+ org.ow2.asm
+ asm
+ ${asm-version}
+
+
+
+ org.ow2.asm
+ asm-tree
+ ${asm-version}
+
+
+
+ info.picocli
+ picocli
+ ${picocli.version}
+
@@ -222,6 +282,7 @@
**/io/soabase/com/google/**
**/com/company/**
**/META-INF/services/**
+ **/safe/**
**/jvm.config
**/.java-version
**/.travis.yml
@@ -267,44 +328,6 @@
org.apache.maven.plugins
maven-shade-plugin
${maven-shade-plugin-version}
-
- true
- ${project.build.outputDirectory}/META-INF/reduced-pom.xml
-
-
-
- META-INF/*.SF
- META-INF/*.DSA
- META-INF/*.RSA
-
-
-
-
-
-
- package
-
- shade
-
-
-
-
-
-
-
-
-
-
- org.ow2.asm
- asm
- ${asm-version}
-
-
- org.ow2.asm
- asm-commons
- ${asm-version}
-
-
diff --git a/record-builder-enhancer-core/pom.xml b/record-builder-enhancer-core/pom.xml
new file mode 100644
index 0000000..1a86a5c
--- /dev/null
+++ b/record-builder-enhancer-core/pom.xml
@@ -0,0 +1,69 @@
+
+
+ 4.0.0
+
+ io.soabase.record-builder
+ record-builder
+ 34-SNAPSHOT
+
+
+ record-builder-enhancer-core
+
+
+ ${project.parent.basedir}/src/etc/header.txt
+
+
+
+
+ org.ow2.asm
+ asm
+
+
+
+ org.ow2.asm
+ asm-tree
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+ package
+
+ shade
+
+
+
+
+ true
+ ${project.build.directory}/dependency-reduced-pom.xml
+ true
+
+
+ org.objectweb
+ recordbuilder.org.objectweb
+
+
+
+
+
+
+
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
diff --git a/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/RecordBuilderEnhance.java b/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/RecordBuilderEnhance.java
new file mode 100644
index 0000000..2bf93c1
--- /dev/null
+++ b/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/RecordBuilderEnhance.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer;
+
+import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
+
+import java.lang.annotation.*;
+
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.TYPE)
+@Inherited
+public @interface RecordBuilderEnhance {
+ Class extends RecordBuilderEnhancer>[] enhancers();
+
+ RecordBuilderEnhanceArguments[] arguments() default {};
+
+ @Retention(RetentionPolicy.CLASS)
+ @Target(ElementType.ANNOTATION_TYPE)
+ @Inherited
+ @interface Template {
+ Class extends RecordBuilderEnhancer>[] enhancers();
+
+ RecordBuilderEnhanceArguments[] arguments() default {};
+ }
+}
diff --git a/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/RecordBuilderEnhanceArguments.java b/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/RecordBuilderEnhanceArguments.java
new file mode 100644
index 0000000..7ab19ad
--- /dev/null
+++ b/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/RecordBuilderEnhanceArguments.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer;
+
+import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
+
+import java.lang.annotation.*;
+
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.TYPE)
+@Inherited
+public @interface RecordBuilderEnhanceArguments {
+ Class extends RecordBuilderEnhancer> enhancer();
+
+ String[] arguments();
+}
diff --git a/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/spi/Entry.java b/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/spi/Entry.java
new file mode 100644
index 0000000..220213d
--- /dev/null
+++ b/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/spi/Entry.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.spi;
+
+import javax.lang.model.element.RecordComponentElement;
+import javax.lang.model.type.TypeMirror;
+
+public record Entry(int parameterIndex, RecordComponentElement element, TypeMirror erasedType) {
+}
diff --git a/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/spi/Processor.java b/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/spi/Processor.java
new file mode 100644
index 0000000..32b8572
--- /dev/null
+++ b/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/spi/Processor.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.spi;
+
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+import java.util.List;
+
+public interface Processor {
+ Elements elements();
+
+ Types types();
+
+ boolean hasEnhancer(Class extends RecordBuilderEnhancer> enhancer);
+
+ boolean verboseRequested();
+
+ void logInfo(CharSequence msg);
+
+ void logWarning(CharSequence msg);
+
+ void logError(CharSequence msg);
+
+ List asEntries(TypeElement element);
+}
diff --git a/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/spi/RecordBuilderEnhancer.java b/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/spi/RecordBuilderEnhancer.java
new file mode 100644
index 0000000..53a4bc1
--- /dev/null
+++ b/record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/spi/RecordBuilderEnhancer.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.spi;
+
+import org.objectweb.asm.tree.InsnList;
+
+import javax.lang.model.element.TypeElement;
+import java.util.List;
+
+@FunctionalInterface
+public interface RecordBuilderEnhancer {
+ InsnList enhance(Processor processor, TypeElement element, List arguments);
+}
diff --git a/record-builder-enhancer/README.md b/record-builder-enhancer/README.md
new file mode 100644
index 0000000..3b762d1
--- /dev/null
+++ b/record-builder-enhancer/README.md
@@ -0,0 +1,454 @@
+# RecordBuilder Enhancer
+
+## What is RecordBuilder Enhancer
+
+Inject verification, defensive copying, null checks or custom code into your Java Record constructors during compilation.
+
+#### Features:
+
+- [Builtin enhancers](#builtin-enhancers) to help with null checks and defensive copying
+- [SPI](#write-your-own-custom-enhancer) for writing your own [custom enhancers](#write-your-own-custom-enhancer)
+- [Create a custom annotation](#create-a-custom-annotation) that specifies a custom set of enhancers
+
+#### Is it safe? Does it use undocumented features of Java?
+
+- The Enhancer modifies your Java class files. There are some inherent safety concerns with this. However:
+ - The industry standard [ASM](https://asm.ow2.io) library is used to do the modifications. ASM is even used in the JDK itself.
+ - The Enhancer only inserts code in the default constructor of Java records. The code is inserted just after the
+ call to `super()` and before any existing code in the constructor.
+ - It's implemented as a standard [javac plugin](https://docs.oracle.com/en/java/javase/16/docs/api/jdk.compiler/com/sun/source/util/Plugin.html) and uses no undocumented features
+- If you don't like the builtin enhancers you can [write your own](#write-your-own-custom-enhancer)
+
+#### How does it relate to [RecordBuilder](../README.md)?
+
+They aren't directly related but they share some code and both work on java records.
+
+#### Why RecordBuilder Enhancer?
+
+Java Records are fantastic data carriers and solve much of the pain of the lack of these types of classes
+in older versions of Java. [RecordBuilder](../README.md) adds builders and withers to records. However,
+records are still missing simple `null` checks and a few other niceties. This means boilerplate
+for every record. RecordBuilder Enhancer is targeted at being able to write Java records without having to add
+any additional code.
+
+## How to Use RecordBuilder Enhancer
+
+First, configure your build environment: see [details below](#javac-plugin). Then, use the Enhancer's
+annotations to specify Java records that you want to be enhanced.
+
+```java
+@RecordBuilderEnhance(enhancers = RequireNonNull.class)
+public record MyRecord(String s, List l) {
+}
+```
+
+Enhancer inserts code into the default constructor as if you wrote this:
+
+```java
+public record MyRecord(String s, List l) {
+ public MyRecord {
+ Objects.requireNonNull(s, "s is null");
+ Objects.requireNonNull(l, "l is null");
+ }
+}
+```
+
+The class file will be updated like this:
+
+
+
+Enhancers are applied in the order listed in the annotation. E.g.
+
+```java
+// will apply RequireNonNull and then CopyCollection
+@RecordBuilderEnhance(enhancers = {RequireNonNull.class, CopyCollection.class})
+public record MyRecord(List l) {}
+```
+
+_becomes_
+
+```java
+public record MyRecord(List l) {
+ Objects.requireNonNull(l);
+ l = List.copyOf(l);
+}
+```
+
+#### Arguments
+
+Enhancers can optional receive arguments (the builtin [NotNullAnnotations](#notnullannotations) does). Use `@RecordBuilderEnhanceArguments`
+to pass arguments. E.g.
+
+```java
+@RecordBuilderEnhance(enhancers = NotNullAnnotations.class,
+ arguments = @RecordBuilderEnhanceArguments(enhancer = NotNullAnnotations.class, arguments = "altnonnull"))
+public record MyRecord(@AltNonNull List l) {}
+```
+
+
+## Builtin Enhancers
+
+| ID | Arguments | Description |
+|-----------------------------------------------------------------------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [EmptyNullOptional](#emptynulloptional) | - | Use `empty()` for null Optional/OptionalInt/OptionalLong/OptionalDouble record components |
+| [EmptyNullString](#emptynullstring) | - | Use `""` for null String record components |
+| [CopyCollection](#copycollection) | - | Make defensive copies of Collection, List and Map record components |
+| [CopyCollectionNullableEmpty](#copycollectionnullableempty) | - | Make defensive copies of Collection, List and Map record components or empty collections when `null` |
+| [GuavaCopyCollection](#guavacopycollection) | - | Same as CopyCollection but uses Google Guava collections |
+| [GuavaCopyCollectionNullableEmpty](#guavacopycollectionnullableempty) | - | Same as CopyCollectionNullableEmpty but uses Google Guava collections |
+| [RequireNonNull](#requirenonnull) | - | Call `requireNonNull()` on all non-primitive record components - note: checks other enhancers and doesn't apply to Strings if EmptyNullString is being used, etc. |
+| [NotNullAnnotations](#notnullannotations) | expression (optional) | Any parameter with an annotation whose name matches this enhancer's regular expression argument will be passed to `requireNonNull()`. The argument is optional. If not supplied then _(notnull)|(nonnull)_ is used. Matching is always case insensitive. |
+
+### Examples
+
+#### EmptyNullOptional
+
+```java
+@RecordBuilderEnhance(enhancers = EmptyNullOptional.class)
+public record MyRecord(Optional s, OptionalInt i) {}
+```
+
+_becomes_
+
+```java
+public record MyRecord(Optional s, OptionalInt i) {
+ public MyRecord {
+ s = (s != null) ? s : Optional.empty();
+ i = (i != null) ? i : OptionalInt.empty();
+ }
+}
+```
+
+-------
+
+#### EmptyNullString
+
+```java
+@RecordBuilderEnhance(enhancers = EmptyNullString.class)
+public record MyRecord(String s) {}
+```
+
+_becomes_
+
+```java
+public record MyRecord(String s) {
+ public MyRecord {
+ s = (s != null) ? s : "";
+ }
+}
+```
+
+-------
+
+#### CopyCollection
+
+```java
+@RecordBuilderEnhance(enhancers = CopyCollection.class)
+public record MyRecord(Collection c, Set s, List l, Map m) {}
+```
+
+_becomes_
+
+```java
+public record MyRecord(String s) {
+ public MyRecord {
+ c = Set.copyOf(c);
+ s = Set.copyOf(s);
+ l = List.copyOf(l);
+ m = Map.copyOf(m);
+ }
+}
+```
+
+-------
+
+#### CopyCollectionNullableEmpty
+
+```java
+@RecordBuilderEnhance(enhancers = CopyCollectionNullableEmpty.class)
+public record MyRecord(Collection c, Set s, List l, Map m) {}
+```
+
+_becomes_
+
+```java
+public record MyRecord(String s) {
+ public MyRecord {
+ c = (c != null) ? Set.copyOf(c) : Set.of();
+ s = (s != null) ? Set.copyOf(s) : Set.of();
+ l = (l != null) ? List.copyOf(l) : List.of();
+ m = (m != null) ? Map.copyOf(m) : Map.of();
+ }
+}
+```
+
+-------
+
+#### GuavaCopyCollection
+
+```java
+@RecordBuilderEnhance(enhancers = GuavaCopyCollection.class)
+public record MyRecord(Collection c, Set s, List l, Map m) {}
+```
+
+_becomes_
+
+```java
+public record MyRecord(String s) {
+ public MyRecord {
+ c = ImmutableSet.copyOf(c);
+ s = ImmutableSet.copyOf(s);
+ l = ImmutableList.copyOf(l);
+ m = Map.copyOf(m);
+ }
+}
+```
+
+-------
+
+#### GuavaCopyCollectionNullableEmpty
+
+```java
+@RecordBuilderEnhance(enhancers = GuavaCopyCollectionNullableEmpty.class)
+public record MyRecord(Collection c, Set s, List l, Map m) {}
+```
+
+_becomes_
+
+```java
+public record MyRecord(String s) {
+ public MyRecord {
+ c = (c != null) ? ImmutableSet.copyOf(c) : ImmutableSet.of();
+ s = (s != null) ? ImmutableSet.copyOf(s) : ImmutableSet.of();
+ l = (l != null) ? ImmutableList.copyOf(l) : ImmutableList.of();
+ m = (m != null) ? ImmutableMap.copyOf(m) : ImmutableMap.of();
+ }
+}
+```
+
+-------
+
+#### RequireNonNull
+
+```java
+@RecordBuilderEnhance(enhancers = RequireNonNull.class)
+public record MyRecord(String s, Instant t) {}
+```
+
+_becomes_
+
+```java
+public record MyRecord(String s, Instant t) {
+ public MyRecord {
+ Objects.requireNonNull(s, "s is null");
+ Objects.requireNonNull(t, "t is null");
+ }
+}
+```
+
+-------
+
+#### NotNullAnnotations
+
+```java
+@RecordBuilderEnhance(enhancers = NotNullAnnotations.class,
+ arguments = @RecordBuilderEnhanceArguments(enhancer = NotNullAnnotations.class, arguments = "(notnull)|(mynil)"))
+public record MyRecord(String s, @NotNull Instant t, @MyNil Thing thing) {}
+```
+
+_becomes_
+
+```java
+public record MyRecord(String s, @NotNull Instant t, @MyNil Thing thing) {
+ public MyRecord {
+ Objects.requireNonNull(t, "t is null");
+ Objects.requireNonNull(thing, "thing is null");
+ }
+}
+```
+
+## Create A Custom Annotation
+
+Using `@RecordBuilderEnhance.Template` you can create your own RecordBuilderEnhance annotation that always uses the set of enhancers that you want.
+
+```java
+@RecordBuilderEnhance.Template(enhancers = {RequireNonNull.class, NotNullAnnotations.class},
+ arguments = @RecordBuilderEnhanceArguments(enhancer = NotNullAnnotations.class, arguments = "altnonnull.*"))
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.TYPE)
+@Inherited
+public @interface MyCoEnhance {
+}
+```
+
+Now, you can use `@MyCoEnhance` instead of `@RecordBuilderEnhance` and the record will be enhanced with the enhancers specified.
+
+## Javac Plugin
+
+Add a dependency that contains the discoverable javac plugin to your build tool (see below for [Maven](#maven) or [Gradle](#gradle)). javac will
+auto discover the Enhancer plugin. By default the enhancer assumes the standard directory layout used by
+most Java build systems. i.e. if a Java source file is at `/foo/bar/myproject/src/main/java/my/package/MyClass.java`
+the Enhancer will assume that the compiled class file for that source file will be found at
+`/foo/bar/myproject/target/classes/my/package/MyClass.class`. If your build system does not use this method then
+the Enhancer will need additional configuration ([see below](#options)). You can also
+configure some of the behavior of the Enhancer ([see below](#options)).
+
+### Maven
+
+```xml
+
+
+ io.soabase.record-builder
+ record-builder-enhancer
+ ${record.builder.version}
+ provided
+
+
+
+
+ io.soabase.record-builder
+ record-builder-enhancer-core
+ ${record.builder.version}
+
+```
+
+### Gradle
+
+```groovy
+dependencies {
+ compileOnly 'io.soabase.record-builder:record-builder-enhancer:$version-goes-here'
+ api 'io.soabase.record-builder:record-enhancer-core:$version-goes-here'
+}
+```
+
+### Options
+
+For normal usage you won't need to set any options for the Enhancer. The following options are available if you need them:
+
+```text
+[-hv] [--disable] [--dryRun] [--outputDirectory=] [DIRECTORY]
+
+ [DIRECTORY] The build's output directory - i.e. where javac writes generated classes.
+ The value can be a full path or a relative path. If not provided the Enhancer
+ plugin will attempt to use standard directories.
+ --disable Deactivate/disable the plugin
+ --dryRun Dry run only - doesn't modify any classes. You should enable verbose as well via: -v
+ -h, --help Outputs this help
+ --outputDirectory=
+ Optional alternate output directory for enhanced class files
+ -v, --verbose Verbose output during compilation
+```
+
+javac plugin options are specifed on the javac command line or as part of your build tool. On the command line:
+
+```shell
+javac -Xplugin:"recordbuilderenhancer ...arguments..."
+```
+
+In Maven:
+
+```xml
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ -Xplugin:recordbuilderenhancer ...arguments...
+
+
+```
+
+## Write Your Own Custom Enhancer
+
+Notes on writing your own enhancer:
+
+- Custom Enhancers must be built as separate modules from the code base you want to enhance. This is because
+the enhancers must be available during compilation.
+- You should somewhat be familiar with Java's AST classes though they are not hard to understand for newcomers
+- Enhancers use the [ASM library](https://asm.ow2.io) to produce a list of statements to be inserted into Java record constructors
+- You will need some knowledge of Java bytecode specifics and the Java Language specification. However, it's pretty simple
+to use javac and javap to show you what bytecodes you need to specify ([see below](#how-to-get-the-bytecodes-for-your-enhancer))
+- **IMPORTANT** - the ASM library has been shaded into the RecordBuilder Enhancer JAR. Make sure your custom enhancer uses the ASM classes
+that are in the package `recordbuilder.org.objectweb.asm.*`. Many libraries use ASM and you may see the same classes in multiple packages.
+
+For reference look at the implementation of the builtin enhancers to see exactly how to write one. [EmptyNullString](src/main/java/io/soabase/recordbuilder/enhancer/enhancers/EmptyNullString.java)
+is a simple one to use as an example.
+
+#### Add the SPI to your build
+
+The RecordBuilder Enhancer SPI must be added to the module for your custom enhancer. It's artifact ID is `record-builder-enhancer-core`.
+Your enhancer module can contain as many enhancers as you want. Each enhancer must implement the following interface:
+
+```java
+public interface RecordBuilderEnhancer {
+ InsnList enhance(Processor processor, TypeElement element, List arguments);
+}
+```
+
+Your custom enhancer is called for Java records that are annotated to list your enhancer class.
+Your enhancer is created once during the start of the build process and will be called multiple times for each Java record
+that is annotated with your enhancer. Your enhancer must return a list of instructions to insert or an empty list.
+The `element` parameter refers to the Java record being currently compiled. `arguments` are any arguments specified in the annotation
+for the Java record. `Processor` holds utilities useful during enhancing. Look at the builtin enhancers to see details on how to write
+them - [EmptyNullString](src/main/java/io/soabase/recordbuilder/enhancer/enhancers/EmptyNullString.java) is a good one to start with.
+
+Build and install your enhancer JAR and you can then use your new custom enhancer as a dependency in other projects and it will be available as an enhancer.
+Alternatively, if you have a multi-module build your enhancer can be a module and used to enhance the other modules in the
+project. The [record-builder-test-custom-enhancer](../record-builder-test-custom-enhancer) does this.
+
+#### How to get the bytecodes for your enhancer
+
+The trick for getting the bytecodes for your enhancer is to write a simple Java source file that does what you want and then
+use the java tools to get the bytecodes. For example, let's say you want a custom enhancer that outputs the current date and time to standard
+out.
+
+Create a text file called "Temp.java" ala:
+
+```java
+import java.time.Instant;
+
+public class Temp {
+ public Temp() {
+ System.out.println(Instant.now());
+ }
+}
+```
+
+From a terminal compile the class:
+
+```shell
+javac Temp.java
+```
+
+Then dump the java bytecodes from the compiled class:
+
+```shell
+javap -c Temp.class
+```
+
+You will see:
+
+```text
+Compiled from "Temp.java"
+public class Temp {
+ public Temp();
+ Code:
+ 0: aload_0
+ 1: invokespecial #1 // Method java/lang/Object."":()V
+ 4: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
+ 7: invokestatic #13 // Method java/time/Instant.now:()Ljava/time/Instant;
+ 10: invokevirtual #19 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
+ 13: return
+}
+```
+
+The first two lines are the call to `super()`. The lines labeld `4`, `7`, and `10` are the bytecodes that you want for your enhancer.
+
+Your enhance() implementation would look like this:
+
+```java
+InsnList insnList = new InsnList();
+insnList.add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
+insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/time/Instant", "now", "()Ljava/time/Instant;"));
+insnList.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V"));
+return insnList;
+```
diff --git a/record-builder-enhancer/disassembly1.png b/record-builder-enhancer/disassembly1.png
new file mode 100644
index 0000000..f052243
Binary files /dev/null and b/record-builder-enhancer/disassembly1.png differ
diff --git a/record-builder-enhancer/pom.xml b/record-builder-enhancer/pom.xml
new file mode 100644
index 0000000..75f748d
--- /dev/null
+++ b/record-builder-enhancer/pom.xml
@@ -0,0 +1,86 @@
+
+
+ 4.0.0
+
+ io.soabase.record-builder
+ record-builder
+ 34-SNAPSHOT
+
+
+ record-builder-enhancer
+
+
+ ${project.parent.basedir}/src/etc/header.txt
+
+
+
+
+ io.soabase.record-builder
+ record-builder-enhancer-core
+
+
+
+ info.picocli
+ picocli
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+ package
+
+ shade
+
+
+
+
+ true
+ ${project.build.directory}/dependency-reduced-pom.xml
+ true
+
+
+ picocli
+ recordbuilder.picocli
+
+
+ safe
+ META-INF.services
+
+
+
+
+
+ io.soabase.recordbuilder.enhancer.Main
+
+
+
+
+
+
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/EnhancersController.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/EnhancersController.java
new file mode 100644
index 0000000..7cf9ce8
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/EnhancersController.java
@@ -0,0 +1,131 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer;
+
+import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
+
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static io.soabase.recordbuilder.enhancer.RecordBuilderEnhancerPlugin.adjustedClassName;
+
+class EnhancersController {
+ private final Map> enhancers = new ConcurrentHashMap<>();
+
+ record EnhancerAndArgs(RecordBuilderEnhancer enhancer, List arguments) {}
+
+ List getEnhancers(ProcessorImpl processor, TypeElement typeElement) {
+ return internalGetEnhancers(processor, typeElement).flatMap(spec -> toEnhancer(processor, spec)).toList();
+ }
+
+ private Stream internalGetEnhancers(ProcessorImpl processor, TypeElement typeElement) {
+ Optional extends AnnotationMirror> recordBuilderEnhance = getAnnotationMirror(processor, typeElement);
+ Optional extends AnnotationMirror> recordBuilderEnhanceTemplate = getTemplateAnnotationMirror(processor, typeElement);
+ if (recordBuilderEnhance.isPresent() && recordBuilderEnhanceTemplate.isPresent()) {
+ processor.logError("RecordBuilderEnhance and RecordBuilderEnhance.Template cannot be combined.");
+ return Stream.of();
+ }
+ return Stream.concat(recordBuilderEnhance.stream().flatMap(this::getEnhancersAnnotationValue), recordBuilderEnhanceTemplate.stream().flatMap(this::getEnhancersAnnotationValue));
+ }
+
+ private Map getAnnotationValueMap(AnnotationMirror annotationMirror)
+ {
+ return annotationMirror.getElementValues().entrySet().stream()
+ .collect(Collectors.toMap(entry -> entry.getKey().getSimpleName().toString(), entry -> entry.getValue().getValue()));
+ }
+
+ private record EnhancerSpec(String enhancerClass, List arguments) {}
+
+ @SuppressWarnings("unchecked")
+ private Stream getEnhancersAnnotationValue(AnnotationMirror annotationMirror)
+ {
+ Map annotationValueMap = getAnnotationValueMap(annotationMirror);
+
+ List extends AnnotationValue> argumentsValue = (List extends AnnotationValue>) annotationValueMap.getOrDefault("arguments", List.of()); // list of RecordBuilderEnhanceArguments mirrors
+ Map> argumentsMap = argumentsValue.stream()
+ .flatMap(argumentMirror -> {
+ Map argumentMap = getAnnotationValueMap((AnnotationMirror) argumentMirror.getValue());
+ Object enhancer = argumentMap.get("enhancer");
+ Object arguments = argumentMap.get("arguments");
+ if ((enhancer != null) && (arguments != null)) {
+ List argumentList = ((List extends AnnotationValue>) arguments).stream().map(value -> value.getValue().toString()).toList();
+ return Stream.of(Map.entry(enhancer.toString(), argumentList));
+ }
+ return Stream.of();
+ })
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+ List extends AnnotationValue> enhancers = (List extends AnnotationValue>) annotationValueMap.get("enhancers");
+ if (enhancers != null) {
+ return enhancers.stream().map(annotationValue -> {
+ TypeMirror typeMirror = (TypeMirror) annotationValue.getValue();
+ return new EnhancerSpec(typeMirror.toString(), argumentsMap.getOrDefault(typeMirror.toString(), List.of()));
+ });
+ }
+ return Stream.of();
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map getArgumentsAnnotations(AnnotationMirror annotationMirror)
+ {
+ return annotationMirror.getElementValues().entrySet().stream()
+ .filter(entry -> entry.getKey().getSimpleName().contentEquals("arguments"))
+ .flatMap(entry -> ((List extends AnnotationValue>) entry.getValue().getValue()).stream())
+ .flatMap(annotationValue -> ((AnnotationMirror) annotationValue.getValue()).getElementValues().entrySet().stream()) // now as RecordBuilderEnhanceArguments
+
+ .collect(Collectors.toMap(entry -> entry.getKey().getSimpleName().toString(), Map.Entry::getValue));
+ }
+
+ private Optional extends AnnotationMirror> getAnnotationMirror(ProcessorImpl processor, TypeElement typeElement) {
+ return processor.elements().getAllAnnotationMirrors(typeElement).stream()
+ .filter(annotationMirror -> annotationMirror.getAnnotationType().toString().equals(adjustedClassName(RecordBuilderEnhance.class)))
+ .findFirst();
+ }
+
+ private Optional extends AnnotationMirror> getTemplateAnnotationMirror(ProcessorImpl processor, TypeElement typeElement) {
+ return processor.elements().getAllAnnotationMirrors(typeElement).stream()
+ .flatMap(annotationMirror -> processor.elements().getAllAnnotationMirrors(annotationMirror.getAnnotationType().asElement()).stream())
+ .filter(annotationMirror -> annotationMirror.getAnnotationType().toString().equals(adjustedClassName(RecordBuilderEnhance.Template.class)))
+ .findFirst();
+ }
+
+ private Stream toEnhancer(ProcessorImpl processor, EnhancerSpec spec)
+ {
+ return enhancers.computeIfAbsent(spec.enhancerClass(), __ -> newEnhancer(processor, spec.enhancerClass()))
+ .stream()
+ .map(enhancer -> new EnhancerAndArgs(enhancer, spec.arguments()));
+ }
+
+ private Optional newEnhancer(ProcessorImpl processor, String enhancerClass)
+ {
+ try {
+ Class> clazz = Class.forName(enhancerClass, true, RecordBuilderEnhancerPlugin.class.getClassLoader());
+ Object enhancer = clazz.getConstructor().newInstance();
+ return Optional.of((RecordBuilderEnhancer) enhancer);
+ } catch (Exception e) {
+ processor.logError("Could not create enhancer instance. type=%s exception=%s message=%s".formatted(enhancerClass, e.getClass().getSimpleName(), e.getMessage()));
+ return Optional.empty();
+ }
+ }
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/Main.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/Main.java
new file mode 100644
index 0000000..12d46f5
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/Main.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer;
+
+import picocli.CommandLine;
+
+public class Main {
+ public static void main(String[] args) {
+ CommandLine commandLine = new CommandLine(new PluginOptions());
+ commandLine.usage(System.out);
+ }
+
+ private Main() {
+ }
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/PluginOptions.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/PluginOptions.java
new file mode 100644
index 0000000..b8cb84d
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/PluginOptions.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer;
+
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.Parameters;
+
+import java.io.File;
+
+@Command(name = "RecordBuilder Enhancer",
+ description = "Enhances Java record class files with validations, preconditions, etc. See https://github.com/Randgalt/record-builder for details",
+ usageHelpAutoWidth = true)
+class PluginOptions {
+ @Parameters(paramLabel = "DIRECTORY", arity = "0..1", description = "The build's output directory - i.e. where javac writes generated classes. The value can be a full path or a relative path. If not provided the Enhancer plugin will attempt to use standard directories.")
+ String directory = "";
+
+ @Option(names = {"-h", "--help"}, description = "Outputs this help")
+ boolean helpRequested = false;
+
+ @Option(names = {"-v", "--verbose"}, description = "Verbose output during compilation")
+ boolean verbose = false;
+
+ @Option(names = {"--disable"}, description = "Deactivate/disable the plugin")
+ boolean disable = false;
+
+ @Option(names = {"--dryRun"}, description = "Dry run only - doesn't modify any classes. You should enable verbose as well via: -v")
+ boolean dryRun = false;
+
+ @Option(names = {"--outputDirectory"}, description = "Optional alternate output directory for enhanced class files")
+ File outputTo = null;
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/ProcessorImpl.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/ProcessorImpl.java
new file mode 100644
index 0000000..0c647ab
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/ProcessorImpl.java
@@ -0,0 +1,110 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer;
+
+import com.sun.source.tree.CompilationUnitTree;
+import com.sun.source.util.Trees;
+import io.soabase.recordbuilder.enhancer.EnhancersController.EnhancerAndArgs;
+import io.soabase.recordbuilder.enhancer.spi.Entry;
+import io.soabase.recordbuilder.enhancer.spi.Processor;
+import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
+
+import javax.lang.model.element.RecordComponentElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+import javax.tools.Diagnostic;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.IntStream;
+
+class ProcessorImpl
+ implements Processor {
+ private final Collection extends Class extends RecordBuilderEnhancer>> enhancers;
+ private final Elements elements;
+ private final Types types;
+ private final Trees trees;
+ private final CompilationUnitTree compilationUnit;
+ private final boolean verboseRequested;
+
+ ProcessorImpl(Elements elements, Types types, Trees trees, CompilationUnitTree compilationUnit, boolean verboseRequested) {
+ this(Set.of(), elements, types, trees, compilationUnit, verboseRequested);
+ }
+
+ private ProcessorImpl(Collection extends Class extends RecordBuilderEnhancer>> enhancers, Elements elements, Types types, Trees trees, CompilationUnitTree compilationUnit, boolean verboseRequested) {
+ this.enhancers = enhancers;
+ this.elements = elements;
+ this.types = types;
+ this.trees = trees;
+ this.compilationUnit = compilationUnit;
+ this.verboseRequested = verboseRequested;
+ }
+
+ ProcessorImpl withEnhancers(List enhancers)
+ {
+ Collection extends Class extends RecordBuilderEnhancer>> enhancersList = enhancers.stream().map(enhancerAndArgs -> enhancerAndArgs.enhancer().getClass()).toList();
+ return new ProcessorImpl(enhancersList, elements, types, trees, compilationUnit, verboseRequested);
+ }
+
+ @Override
+ public boolean verboseRequested() {
+ return verboseRequested;
+ }
+
+ @Override
+ public List asEntries(TypeElement element) {
+ List extends RecordComponentElement> recordComponents = element.getRecordComponents();
+ return IntStream.range(0, recordComponents.size())
+ .mapToObj(index -> new Entry(index + 1, recordComponents.get(index), types().erasure(recordComponents.get(index).asType())))
+ .toList();
+ }
+
+ @Override
+ public boolean hasEnhancer(Class extends RecordBuilderEnhancer> enhancer) {
+ return enhancers.contains(enhancer);
+ }
+
+ @Override
+ public void logInfo(CharSequence msg) {
+ printMessage(Diagnostic.Kind.NOTE, msg);
+ }
+
+ @Override
+ public void logWarning(CharSequence msg) {
+ printMessage(Diagnostic.Kind.MANDATORY_WARNING, msg);
+ }
+
+ @Override
+ public void logError(CharSequence msg) {
+ msg += " - Use -h for help in your -Xplugin arguments";
+ printMessage(Diagnostic.Kind.ERROR, msg);
+ }
+
+ @Override
+ public Elements elements() {
+ return elements;
+ }
+
+ @Override
+ public Types types() {
+ return types;
+ }
+
+ private void printMessage(Diagnostic.Kind kind, CharSequence msg) {
+ trees.printMessage(kind, "[RecordBuilder Enhancer] " + msg, compilationUnit, compilationUnit);
+ }
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/RecordBuilderEnhancerPlugin.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/RecordBuilderEnhancerPlugin.java
new file mode 100644
index 0000000..b8cdaea
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/RecordBuilderEnhancerPlugin.java
@@ -0,0 +1,139 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer;
+
+import com.sun.source.util.JavacTask;
+import com.sun.source.util.Plugin;
+import com.sun.source.util.TaskEvent;
+import com.sun.source.util.TaskListener;
+import io.soabase.recordbuilder.enhancer.EnhancersController.EnhancerAndArgs;
+import io.soabase.recordbuilder.enhancer.Session.FileStreams;
+import picocli.CommandLine;
+import recordbuilder.org.objectweb.asm.ClassReader;
+import recordbuilder.org.objectweb.asm.ClassWriter;
+import recordbuilder.org.objectweb.asm.Opcodes;
+import recordbuilder.org.objectweb.asm.tree.*;
+
+import javax.lang.model.element.TypeElement;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public class RecordBuilderEnhancerPlugin
+ implements Plugin, TaskListener {
+ private volatile Session session;
+
+ @Override
+ public String getName() {
+ return "recordbuilderenhancer";
+ }
+
+ @Override
+ public boolean autoStart() {
+ return true;
+ }
+
+ @Override
+ public void init(JavacTask task, String... args) {
+ PluginOptions pluginOptions = new PluginOptions();
+ CommandLine commandLine = new CommandLine(pluginOptions);
+ commandLine.parseArgs(args);
+ if (!pluginOptions.disable) {
+ session = new Session(task, this, pluginOptions, commandLine);
+ }
+ }
+
+ @Override
+ public void finished(TaskEvent taskEvent) {
+ if (taskEvent.getKind() == TaskEvent.Kind.GENERATE) {
+ TypeElement typeElement = taskEvent.getTypeElement();
+ ProcessorImpl processor = session.newProcessor(taskEvent);
+ session.checkPrintHelp(processor);
+ List enhancers = session.enhancersController().getEnhancers(processor, typeElement);
+ if (!enhancers.isEmpty()) {
+ if (typeElement.getRecordComponents().isEmpty()) {
+ processor.logError(typeElement.getQualifiedName() + " is not a record");
+ } else {
+ if (processor.verboseRequested()) {
+ processor.logWarning("Enhancing %s with %s".formatted(typeElement.getSimpleName(), enhancers.stream().map(enhancer -> enhancer.getClass().getName()).collect(Collectors.joining(","))));
+ }
+ session.getFileStreams(processor, typeElement, taskEvent.getCompilationUnit().getSourceFile().toUri()).ifPresent(fileStreams -> enhance(typeElement, processor.withEnhancers(enhancers), enhancers, fileStreams));
+ }
+ }
+ }
+ }
+
+ private boolean removeAndSaveSuperCall(MethodNode constructor, InsnList insnList) {
+ ListIterator iterator = constructor.instructions.iterator();
+ while (iterator.hasNext()) {
+ AbstractInsnNode node = iterator.next();
+ iterator.remove();
+ insnList.add(node);
+ if ((node.getOpcode() == Opcodes.INVOKESPECIAL) && ((MethodInsnNode) node).owner.equals("java/lang/Record") && ((MethodInsnNode) node).name.equals("")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void enhance(TypeElement typeElement, ProcessorImpl processor, List specs, FileStreams fileStreams) {
+ try {
+ ClassNode classNode = new ClassNode();
+ try (InputStream in = fileStreams.openInputStream()) {
+ ClassReader classReader = new ClassReader(in);
+ classReader.accept(classNode, 0);
+ }
+ InsnList insnList = new InsnList();
+ MethodNode constructor = findConstructor(classNode).orElseThrow(() -> new IllegalStateException("Could not find default constructor"));
+ if (!removeAndSaveSuperCall(constructor, insnList)) {
+ processor.logError("Unrecognized constructor - missing super() call.");
+ return;
+ }
+ specs.stream()
+ .map(spec -> spec.enhancer().enhance(processor, typeElement, spec.arguments()))
+ .forEach(insnList::add);
+ constructor.instructions.insert(insnList);
+
+ ClassWriter classWriter = new ClassWriter(Opcodes.ASM9 | ClassWriter.COMPUTE_FRAMES);
+ classNode.accept(classWriter);
+
+ if (!session.isDryRun()) {
+ try (OutputStream out = fileStreams.openOutputStream()) {
+ out.write(classWriter.toByteArray());
+ }
+ }
+ } catch (IOException e) {
+ processor.logError("Could not process " + typeElement.getQualifiedName() + " - " + e.getMessage());
+ }
+ }
+
+ static String adjustedClassName(Class> clazz) {
+ return clazz.getName().replace('$', '.');
+ }
+
+ private static Optional findConstructor(ClassNode classNode) {
+ String defaultConstructorDescription = classNode.recordComponents.stream()
+ .map(recordComponentNode -> recordComponentNode.descriptor)
+ .collect(Collectors.joining("", "(", ")V"));
+ return classNode.methods.stream()
+ .filter(methodNode -> methodNode.name.equals("") && methodNode.desc.equals(defaultConstructorDescription))
+ .findFirst();
+ }
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/Session.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/Session.java
new file mode 100644
index 0000000..daae13b
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/Session.java
@@ -0,0 +1,186 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer;
+
+import com.sun.source.util.JavacTask;
+import com.sun.source.util.TaskEvent;
+import com.sun.source.util.TaskListener;
+import com.sun.source.util.Trees;
+import io.soabase.recordbuilder.enhancer.spi.Processor;
+import picocli.CommandLine;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+import javax.tools.*;
+import java.io.*;
+import java.net.URI;
+import java.util.List;
+import java.util.Optional;
+
+public class Session implements SessionFlag {
+ private final JavacTask task;
+ private final StandardJavaFileManager fileManager;
+ private final Trees trees;
+ private final EnhancersController enhancersController;
+ private final PluginOptions pluginOptions;
+ private final CommandLine commandLine;
+
+ public Session(JavacTask task, TaskListener taskListener, PluginOptions pluginOptions, CommandLine commandLine) {
+ this.task = task;
+ this.pluginOptions = pluginOptions;
+ this.commandLine = commandLine;
+ task.addTaskListener(taskListener);
+ enhancersController = new EnhancersController();
+ //PluginOptions.setupCommandLine(this.commandLine, enhancersController.enhancers(), true);
+ fileManager = ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, null, null);
+ try {
+ fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(new File(this.pluginOptions.directory).getAbsoluteFile()));
+ } catch (IOException e) {
+ throw new RuntimeException("Could not set the class output path", e);
+ }
+
+ trees = Trees.instance(task);
+
+ if (pluginOptions.outputTo != null) {
+ if (!pluginOptions.outputTo.isDirectory() && !pluginOptions.outputTo.exists() && !pluginOptions.outputTo.mkdirs()) {
+ throw new RuntimeException("Could not create directory: " + pluginOptions.outputTo);
+ }
+ }
+ }
+
+ public boolean enabled()
+ {
+ return !pluginOptions.disable;
+ }
+
+ public boolean isDryRun()
+ {
+ return pluginOptions.dryRun;
+ }
+
+ public JavacTask task() {
+ return task;
+ }
+
+ public interface FileStreams
+ {
+ InputStream openInputStream() throws IOException;
+
+ OutputStream openOutputStream() throws IOException;
+ }
+
+ public Optional getFileStreams(Processor processor, TypeElement element, URI fileUri) {
+ String directory = pluginOptions.directory;
+ if (directory.isEmpty()) {
+ String pathToFile = fileUri.getPath();
+ int srcMainJavaIndex = pathToFile.indexOf("src/main/java");
+ if (srcMainJavaIndex >= 0) {
+ directory = pathToFile.substring(0, srcMainJavaIndex) + "target/classes";
+ }
+ else {
+ directory = ".";
+ }
+ }
+ String className = getClassName(element);
+ File absoluteDirectory = new File(directory).getAbsoluteFile();
+ if (pluginOptions.verbose) {
+ processor.logWarning("Using classPath %s for class %s".formatted(absoluteDirectory, className));
+ }
+ try {
+ fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(absoluteDirectory));
+ JavaFileObject inputJavaFile = fileManager.getJavaFileForOutput(StandardLocation.CLASS_OUTPUT, className, JavaFileObject.Kind.CLASS, null);
+ JavaFileObject outputJavaFile;
+ if (pluginOptions.outputTo != null) {
+ fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(pluginOptions.outputTo));
+ outputJavaFile = fileManager.getJavaFileForOutput(StandardLocation.CLASS_OUTPUT, className, JavaFileObject.Kind.CLASS, null);
+ }
+ else {
+ outputJavaFile = inputJavaFile;
+ }
+ FileStreams fileStreams = new FileStreams() {
+ @Override
+ public InputStream openInputStream() throws IOException {
+ return inputJavaFile.openInputStream();
+ }
+
+ @Override
+ public OutputStream openOutputStream() throws IOException {
+ return outputJavaFile.openOutputStream();
+ }
+ };
+ return Optional.of(fileStreams);
+ } catch (IOException e) {
+ processor.logError("Could not set classpath to %s for class %s: %s".formatted(absoluteDirectory, className, e.getMessage()));
+ return Optional.empty();
+ }
+ }
+
+ public EnhancersController enhancersController() {
+ return enhancersController;
+ }
+
+ public ProcessorImpl newProcessor(TaskEvent taskEvent)
+ {
+ return new ProcessorImpl(task.getElements(), task.getTypes(), trees, taskEvent.getCompilationUnit(), pluginOptions.verbose);
+ }
+
+ public void checkPrintHelp(Processor processor)
+ {
+ if (pluginOptions.helpRequested && getAndSetFlag("show-help")) {
+ StringWriter help = new StringWriter();
+ help.write('\n');
+ commandLine.usage(new PrintWriter(help));
+ processor.logError(help.toString());
+ }
+ }
+
+ @Override
+ public boolean getAndSetFlag(String name)
+ {
+ boolean wasSet = isSet(name);
+ if (!wasSet) {
+ set(name);
+ return true;
+ }
+ return false;
+ }
+
+ private String getClassName(TypeElement typeElement) {
+ return task.getElements().getPackageOf(typeElement).getQualifiedName() + "." + getClassName(typeElement, "");
+ }
+
+ private static String getClassName(Element element, String separator) {
+ // prefix enclosing class names if nested in a class
+ if (element instanceof TypeElement) {
+ return getClassName(element.getEnclosingElement(), "$") + element.getSimpleName().toString() + separator;
+ }
+ return "";
+ }
+
+ private boolean isSet(String name)
+ {
+ return Boolean.parseBoolean(System.getProperty(toKey(name)));
+ }
+
+ private void set(String name)
+ {
+ System.setProperty(toKey(name), "true");
+ }
+
+ private String toKey(String name) {
+ return Session.class.getName() + ":" + name;
+ }
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/SessionFlag.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/SessionFlag.java
new file mode 100644
index 0000000..82ecb9e
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/SessionFlag.java
@@ -0,0 +1,21 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer;
+
+@FunctionalInterface
+public interface SessionFlag {
+ boolean getAndSetFlag(String name);
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollection.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollection.java
new file mode 100644
index 0000000..9c08f4b
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollection.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.enhancers;
+
+public class CopyCollection extends CopyCollectionBase {
+ @Override
+ protected boolean isInterface() {
+ return true;
+ }
+
+ @Override
+ protected String mapMethod() {
+ return "(Ljava/util/Map;)Ljava/util/Map;";
+ }
+
+ @Override
+ protected String setMethod() {
+ return "(Ljava/util/Collection;)Ljava/util/Set;";
+ }
+
+ @Override
+ protected String listMethod() {
+ return "(Ljava/util/Collection;)Ljava/util/List;";
+ }
+
+ @Override
+ protected String map() {
+ return "java/util/Map";
+ }
+
+ @Override
+ protected String set() {
+ return "java/util/Set";
+ }
+
+ @Override
+ protected String list() {
+ return "java/util/List";
+ }
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollectionBase.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollectionBase.java
new file mode 100644
index 0000000..b4e0d08
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollectionBase.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.enhancers;
+
+import io.soabase.recordbuilder.enhancer.spi.Processor;
+import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
+import recordbuilder.org.objectweb.asm.Opcodes;
+import recordbuilder.org.objectweb.asm.tree.InsnList;
+import recordbuilder.org.objectweb.asm.tree.MethodInsnNode;
+import recordbuilder.org.objectweb.asm.tree.VarInsnNode;
+
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+import java.util.List;
+
+public abstract class CopyCollectionBase
+ implements RecordBuilderEnhancer {
+ @Override
+ public InsnList enhance(Processor processor, TypeElement element, List arguments) {
+ TypeMirror collectionType = processor.elements().getTypeElement("java.util.Collection").asType();
+ TypeMirror listType = processor.elements().getTypeElement("java.util.List").asType();
+ TypeMirror mapType = processor.elements().getTypeElement("java.util.Map").asType();
+
+ InsnList insnList = new InsnList();
+ processor.asEntries(element).stream()
+ .filter(entry -> processor.types().isAssignable(entry.erasedType(), collectionType) || processor.types().isAssignable(entry.erasedType(), mapType))
+ .forEach(entry -> {
+ // aload_1
+ // invokestatic #7 // InterfaceMethod java/util/List.copyOf:(Ljava/util/Collection;)Ljava/util/Set;
+ // astore_1
+ insnList.add(new VarInsnNode(Opcodes.ALOAD, entry.parameterIndex()));
+ if (processor.types().isAssignable(entry.erasedType(), listType)) {
+ insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, list(), "copyOf", listMethod(), isInterface()));
+ } else if (processor.types().isAssignable(entry.erasedType(), collectionType)) {
+ insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, set(), "copyOf", setMethod(), isInterface()));
+ } else {
+ insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, map(), "copyOf", mapMethod(), isInterface()));
+ }
+ insnList.add(new VarInsnNode(Opcodes.ASTORE, entry.parameterIndex()));
+ });
+ return insnList;
+ }
+
+ abstract protected boolean isInterface();
+
+ abstract protected String mapMethod();
+
+ abstract protected String setMethod();
+
+ abstract protected String listMethod();
+
+ abstract protected String map();
+
+ abstract protected String set();
+
+ abstract protected String list();
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollectionNullableEmpty.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollectionNullableEmpty.java
new file mode 100644
index 0000000..b38aa6a
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollectionNullableEmpty.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.enhancers;
+
+public class CopyCollectionNullableEmpty extends CopyCollectionNullableEmptyBase {
+ @Override
+ protected boolean isInterface() {
+ return true;
+ }
+
+ @Override
+ protected String mapEmptyMethod() {
+ return "()Ljava/util/Map;";
+ }
+
+ @Override
+ protected String setEmptyMethod() {
+ return "()Ljava/util/Set;";
+ }
+
+ @Override
+ protected String listEmptyMethod() {
+ return "()Ljava/util/List;";
+ }
+
+ @Override
+ protected String mapCopyMethod() {
+ return "(Ljava/util/Map;)Ljava/util/Map;";
+ }
+
+ @Override
+ protected String setCopyMethod() {
+ return "(Ljava/util/Collection;)Ljava/util/Set;";
+ }
+
+ @Override
+ protected String listCopyMethod() {
+ return "(Ljava/util/Collection;)Ljava/util/List;";
+ }
+
+ @Override
+ protected String map() {
+ return "java/util/Map";
+ }
+
+ @Override
+ protected String set() {
+ return "java/util/Set";
+ }
+
+ @Override
+ protected String list() {
+ return "java/util/List";
+ }
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollectionNullableEmptyBase.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollectionNullableEmptyBase.java
new file mode 100644
index 0000000..f962746
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollectionNullableEmptyBase.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.enhancers;
+
+import io.soabase.recordbuilder.enhancer.spi.Processor;
+import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
+import recordbuilder.org.objectweb.asm.Label;
+import recordbuilder.org.objectweb.asm.Opcodes;
+import recordbuilder.org.objectweb.asm.tree.*;
+
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+import java.util.List;
+
+public abstract class CopyCollectionNullableEmptyBase
+ implements RecordBuilderEnhancer {
+ @Override
+ public InsnList enhance(Processor processor, TypeElement element, List arguments) {
+ TypeMirror collectionType = processor.elements().getTypeElement("java.util.Collection").asType();
+ TypeMirror listType = processor.elements().getTypeElement("java.util.List").asType();
+ TypeMirror mapType = processor.elements().getTypeElement("java.util.Map").asType();
+
+ InsnList insnList = new InsnList();
+ processor.asEntries(element).stream()
+ .filter(entry -> processor.types().isAssignable(entry.erasedType(), collectionType) || processor.types().isAssignable(entry.erasedType(), mapType))
+ .forEach(entry -> {
+ /*
+ 4: aload_1
+ 5: ifnull 15
+ 8: aload_1
+ 9: invokestatic #7 // InterfaceMethod java/util/Set.copyOf:(Ljava/util/Collection;)Ljava/util/Set;
+ 12: goto 18
+ 15: invokestatic #13 // InterfaceMethod java/util/Set.of:()Ljava/util/Set;
+ 18: astore_1
+ */
+ LabelNode doEmptylabel = new LabelNode(new Label());
+ LabelNode doCopylabel = new LabelNode(new Label());
+ LabelNode doAssignlabel = new LabelNode(new Label());
+ insnList.add(new VarInsnNode(Opcodes.ALOAD, entry.parameterIndex()));
+ insnList.add(new JumpInsnNode(Opcodes.IFNULL, doEmptylabel));
+ insnList.add(new VarInsnNode(Opcodes.ALOAD, entry.parameterIndex()));
+ if (processor.types().isAssignable(entry.erasedType(), listType)) {
+ insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, list(), "copyOf", listCopyMethod(), isInterface()));
+ } else if (processor.types().isAssignable(entry.erasedType(), collectionType)) {
+ insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, set(), "copyOf", setCopyMethod(), isInterface()));
+ } else {
+ insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, map(), "copyOf", mapCopyMethod(), isInterface()));
+ }
+ insnList.add(new JumpInsnNode(Opcodes.GOTO, doAssignlabel));
+ insnList.add(doEmptylabel);
+ if (processor.types().isAssignable(entry.erasedType(), listType)) {
+ insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, list(), "of", listEmptyMethod(), isInterface()));
+ } else if (processor.types().isAssignable(entry.erasedType(), collectionType)) {
+ insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, set(), "of", setEmptyMethod(), isInterface()));
+ } else {
+ insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, map(), "of", mapEmptyMethod(), isInterface()));
+ }
+ insnList.add(doAssignlabel);
+ insnList.add(new VarInsnNode(Opcodes.ASTORE, entry.parameterIndex()));
+ });
+ return insnList;
+ }
+
+ abstract protected boolean isInterface();
+
+ abstract protected String mapEmptyMethod();
+
+ abstract protected String setEmptyMethod();
+
+ abstract protected String listEmptyMethod();
+
+ abstract protected String mapCopyMethod();
+
+ abstract protected String setCopyMethod();
+
+ abstract protected String listCopyMethod();
+
+ abstract protected String map();
+
+ abstract protected String set();
+
+ abstract protected String list();
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/EmptyNullOptional.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/EmptyNullOptional.java
new file mode 100644
index 0000000..0dba318
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/EmptyNullOptional.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.enhancers;
+
+import io.soabase.recordbuilder.enhancer.spi.Processor;
+import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
+import recordbuilder.org.objectweb.asm.Label;
+import recordbuilder.org.objectweb.asm.Opcodes;
+import recordbuilder.org.objectweb.asm.tree.*;
+
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+import java.util.List;
+
+public class EmptyNullOptional
+ implements RecordBuilderEnhancer {
+ @Override
+ public InsnList enhance(Processor processor, TypeElement element, List arguments) {
+ TypeMirror optionalType = processor.elements().getTypeElement("java.util.Optional").asType();
+ TypeMirror optionalIntType = processor.elements().getTypeElement("java.util.OptionalInt").asType();
+ TypeMirror optionalLongType = processor.elements().getTypeElement("java.util.OptionalLong").asType();
+ TypeMirror optionalDouble = processor.elements().getTypeElement("java.util.OptionalDouble").asType();
+
+ InsnList insnList = new InsnList();
+ processor.asEntries(element)
+ .stream()
+ .filter(entry -> processor.types().isAssignable(entry.erasedType(), optionalType) || processor.types().isAssignable(entry.erasedType(), optionalIntType) || processor.types().isAssignable(entry.erasedType(), optionalLongType) || processor.types().isAssignable(entry.erasedType(), optionalDouble))
+ .forEach(entry -> {
+ /*
+ 4: aload 5
+ 6: ifnull 14
+ 9: aload 5
+ 11: goto 17
+ 14: invokestatic #7 // Method java/util/Optional.empty:()Ljava/util/Optional;
+ 17: astore 5
+ */
+ LabelNode doEmptylabel = new LabelNode(new Label());
+ LabelNode doAssignlabel = new LabelNode(new Label());
+ insnList.add(new VarInsnNode(Opcodes.ALOAD, entry.parameterIndex()));
+ insnList.add(new JumpInsnNode(Opcodes.IFNULL, doEmptylabel));
+ insnList.add(new VarInsnNode(Opcodes.ALOAD, entry.parameterIndex()));
+ insnList.add(new JumpInsnNode(Opcodes.GOTO, doAssignlabel));
+ insnList.add(doEmptylabel);
+ if (processor.types().isAssignable(entry.erasedType(), optionalIntType)) {
+ insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/util/OptionalInt", "empty", "()Ljava/util/OptionalInt;"));
+ }
+ else if (processor.types().isAssignable(entry.erasedType(), optionalLongType)) {
+ insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/util/OptionalLong", "empty", "()Ljava/util/OptionalLong;"));
+ }
+ else if (processor.types().isAssignable(entry.erasedType(), optionalDouble)) {
+ insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/util/OptionalDouble", "empty", "()Ljava/util/OptionalDouble;"));
+ }
+ else {
+ insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/util/Optional", "empty", "()Ljava/util/Optional;"));
+ }
+ insnList.add(doAssignlabel);
+ insnList.add(new VarInsnNode(Opcodes.ASTORE, entry.parameterIndex()));
+ });
+ return insnList;
+ }
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/EmptyNullString.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/EmptyNullString.java
new file mode 100644
index 0000000..cc7a8c4
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/EmptyNullString.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.enhancers;
+
+import io.soabase.recordbuilder.enhancer.spi.Processor;
+import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
+import recordbuilder.org.objectweb.asm.Label;
+import recordbuilder.org.objectweb.asm.Opcodes;
+import recordbuilder.org.objectweb.asm.tree.*;
+
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+import java.util.List;
+
+public class EmptyNullString
+ implements RecordBuilderEnhancer {
+ @Override
+ public InsnList enhance(Processor processor, TypeElement element, List arguments) {
+ TypeMirror stringType = processor.elements().getTypeElement("java.lang.String").asType();
+ InsnList insnList = new InsnList();
+ processor.asEntries(element)
+ .stream()
+ .filter(entry -> processor.types().isAssignable(entry.erasedType(), stringType))
+ .forEach(entry -> {
+ /*
+ 4: aload_1
+ 5: ifnull 12
+ 8: aload_1
+ 9: goto 14
+ 12: ldc #7 // String
+ 14: astore_1
+ */
+ LabelNode doEmptylabel = new LabelNode(new Label());
+ LabelNode doAssignlabel = new LabelNode(new Label());
+ insnList.add(new VarInsnNode(Opcodes.ALOAD, entry.parameterIndex()));
+ insnList.add(new JumpInsnNode(Opcodes.IFNULL, doEmptylabel));
+ insnList.add(new VarInsnNode(Opcodes.ALOAD, entry.parameterIndex()));
+ insnList.add(new JumpInsnNode(Opcodes.GOTO, doAssignlabel));
+ insnList.add(doEmptylabel);
+ insnList.add(new LdcInsnNode(""));
+ insnList.add(doAssignlabel);
+ insnList.add(new VarInsnNode(Opcodes.ASTORE, entry.parameterIndex()));
+ });
+ return insnList;
+ }
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/GuavaCopyCollection.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/GuavaCopyCollection.java
new file mode 100644
index 0000000..e9afc34
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/GuavaCopyCollection.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.enhancers;
+
+public class GuavaCopyCollection extends CopyCollectionBase {
+ @Override
+ protected boolean isInterface() {
+ return false;
+ }
+
+ @Override
+ protected String mapMethod() {
+ return "(Ljava/util/Map;)Lcom/google/common/collect/ImmutableMap;";
+ }
+
+ @Override
+ protected String setMethod() {
+ return "(Ljava/util/Collection;)Lcom/google/common/collect/ImmutableSet;";
+ }
+
+ @Override
+ protected String listMethod() {
+ return "(Ljava/util/Collection;)Lcom/google/common/collect/ImmutableList;";
+ }
+
+ @Override
+ protected String map() {
+ return "com/google/common/collect/ImmutableMap";
+ }
+
+ @Override
+ protected String set() {
+ return "com/google/common/collect/ImmutableSet";
+ }
+
+ @Override
+ protected String list() {
+ return "com/google/common/collect/ImmutableList";
+ }
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/GuavaCopyCollectionNullableEmpty.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/GuavaCopyCollectionNullableEmpty.java
new file mode 100644
index 0000000..1c39d32
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/GuavaCopyCollectionNullableEmpty.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.enhancers;
+
+public class GuavaCopyCollectionNullableEmpty extends CopyCollectionNullableEmptyBase {
+ @Override
+ protected boolean isInterface() {
+ return false;
+ }
+
+ @Override
+ protected String mapEmptyMethod() {
+ return "()Lcom/google/common/collect/ImmutableMap;";
+ }
+
+ @Override
+ protected String setEmptyMethod() {
+ return "()Lcom/google/common/collect/ImmutableSet;";
+ }
+
+ @Override
+ protected String listEmptyMethod() {
+ return "()Lcom/google/common/collect/ImmutableList;";
+ }
+
+ @Override
+ protected String mapCopyMethod() {
+ return "(Ljava/util/Map;)Lcom/google/common/collect/ImmutableMap;";
+ }
+
+ @Override
+ protected String setCopyMethod() {
+ return "(Ljava/util/Collection;)Lcom/google/common/collect/ImmutableSet;";
+ }
+
+ @Override
+ protected String listCopyMethod() {
+ return "(Ljava/util/Collection;)Lcom/google/common/collect/ImmutableList;";
+ }
+
+ @Override
+ protected String map() {
+ return "com/google/common/collect/ImmutableMap";
+ }
+
+ @Override
+ protected String set() {
+ return "com/google/common/collect/ImmutableSet";
+ }
+
+ @Override
+ protected String list() {
+ return "com/google/common/collect/ImmutableList";
+ }
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/NotNullAnnotations.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/NotNullAnnotations.java
new file mode 100644
index 0000000..f5aadb7
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/NotNullAnnotations.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.enhancers;
+
+import io.soabase.recordbuilder.enhancer.spi.Entry;
+import io.soabase.recordbuilder.enhancer.spi.Processor;
+import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
+import recordbuilder.org.objectweb.asm.tree.InsnList;
+
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.TypeElement;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.util.stream.Stream;
+
+public class NotNullAnnotations
+ implements RecordBuilderEnhancer
+{
+ public static final String DEFAULT_EXPRESSION = "(notnull)|(nonnull)";
+
+ @Override
+ public InsnList enhance(Processor processor, TypeElement element, List arguments) {
+ InsnList insnList = new InsnList();
+ if (arguments.size() > 1) {
+ processor.logError("Too many arguments to NotNullAnnotations.");
+ } else {
+ String expression = arguments.isEmpty() ? DEFAULT_EXPRESSION : arguments.get(0);
+ try {
+ Pattern pattern = Pattern.compile(expression, Pattern.CASE_INSENSITIVE);
+ processor.asEntries(element)
+ .stream()
+ .filter(entry -> !entry.erasedType().getKind().isPrimitive())
+ .filter(entry -> hasMatchingAnnotation(entry, pattern))
+ .forEach(entry -> RequireNonNull.enhance(insnList, entry));
+ } catch (PatternSyntaxException e) {
+ processor.logError("Bad argument to NotNullAnnotations: " + e.getMessage());
+ }
+ }
+ return insnList;
+ }
+
+ private boolean hasMatchingAnnotation(Entry entry, Pattern pattern)
+ {
+ Stream extends AnnotationMirror> typeMirrors = entry.element().asType().getAnnotationMirrors().stream();
+ Stream extends AnnotationMirror> elementMirrors = entry.element().getAnnotationMirrors().stream();
+ return Stream.concat(typeMirrors, elementMirrors)
+ .anyMatch(mirror -> pattern.matcher(mirror.getAnnotationType().asElement().getSimpleName().toString()).matches());
+ }
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/ProcessorUtil.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/ProcessorUtil.java
new file mode 100644
index 0000000..c986126
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/ProcessorUtil.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.enhancers;
+
+import io.soabase.recordbuilder.enhancer.spi.Entry;
+import io.soabase.recordbuilder.enhancer.spi.Processor;
+import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
+
+import javax.lang.model.type.TypeMirror;
+import java.util.stream.Stream;
+
+class ProcessorUtil {
+ private ProcessorUtil() {
+ }
+
+ static boolean isNotHandledByOthers(Class extends RecordBuilderEnhancer> enhancer, Processor processor, Entry entry, TypeMirror... types)
+ {
+ if (!processor.hasEnhancer(enhancer)) {
+ return true;
+ }
+ if (types == null) {
+ return true;
+ }
+ return Stream.of(types).noneMatch(type -> processor.types().isAssignable(entry.erasedType(), type));
+ }
+}
diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/RequireNonNull.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/RequireNonNull.java
new file mode 100644
index 0000000..ba5c148
--- /dev/null
+++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/RequireNonNull.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.enhancers;
+
+import io.soabase.recordbuilder.enhancer.spi.Entry;
+import io.soabase.recordbuilder.enhancer.spi.Processor;
+import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
+import recordbuilder.org.objectweb.asm.Opcodes;
+import recordbuilder.org.objectweb.asm.tree.*;
+
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+import java.util.List;
+
+import static io.soabase.recordbuilder.enhancer.enhancers.ProcessorUtil.isNotHandledByOthers;
+
+public class RequireNonNull
+ implements RecordBuilderEnhancer {
+ @Override
+ public InsnList enhance(Processor processor, TypeElement element, List arguments) {
+ TypeMirror optionalType = processor.elements().getTypeElement("java.util.Optional").asType();
+ TypeMirror stringType = processor.elements().getTypeElement("java.lang.String").asType();
+ TypeMirror collectionType = processor.elements().getTypeElement("java.util.Collection").asType();
+ TypeMirror listType = processor.elements().getTypeElement("java.util.List").asType();
+ TypeMirror mapType = processor.elements().getTypeElement("java.util.Map").asType();
+
+ InsnList insnList = new InsnList();
+ processor.asEntries(element)
+ .stream()
+ .filter(entry -> !entry.erasedType().getKind().isPrimitive())
+ .filter(entry -> isNotHandledByOthers(EmptyNullOptional.class, processor, entry, optionalType))
+ .filter(entry -> isNotHandledByOthers(EmptyNullString.class, processor, entry, stringType))
+ .filter(entry -> isNotHandledByOthers(CopyCollectionNullableEmpty.class, processor, entry, collectionType, listType, mapType))
+ .filter(entry -> isNotHandledByOthers(CopyCollection.class, processor, entry, collectionType, listType, mapType))
+ .filter(entry -> isNotHandledByOthers(GuavaCopyCollectionNullableEmpty.class, processor, entry, collectionType, listType, mapType))
+ .filter(entry -> isNotHandledByOthers(GuavaCopyCollection.class, processor, entry, collectionType, listType, mapType))
+ .forEach(entry -> enhance(insnList, entry));
+ return insnList;
+ }
+
+ static void enhance(InsnList insnList, Entry entry) {
+ // java.util.Objects.requireNonNull(var1, " is null");
+ /*
+ ALOAD 1
+ LDC "X is null"
+ INVOKESTATIC java/util/Objects.requireNonNull (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;
+ POP
+ */
+ insnList.add(new VarInsnNode(Opcodes.ALOAD, entry.parameterIndex()));
+ insnList.add(new LdcInsnNode("%s is null".formatted(entry.element().getSimpleName())));
+ insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/util/Objects", "requireNonNull", "(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;"));
+ insnList.add(new InsnNode(Opcodes.POP));
+ }
+}
diff --git a/record-builder-enhancer/src/main/resources/safe/com.sun.source.util.Plugin b/record-builder-enhancer/src/main/resources/safe/com.sun.source.util.Plugin
new file mode 100644
index 0000000..bb4865e
--- /dev/null
+++ b/record-builder-enhancer/src/main/resources/safe/com.sun.source.util.Plugin
@@ -0,0 +1 @@
+io.soabase.recordbuilder.enhancer.RecordBuilderEnhancerPlugin
\ No newline at end of file
diff --git a/record-builder-test-custom-enhancer/pom.xml b/record-builder-test-custom-enhancer/pom.xml
new file mode 100644
index 0000000..834ea86
--- /dev/null
+++ b/record-builder-test-custom-enhancer/pom.xml
@@ -0,0 +1,34 @@
+
+
+ 4.0.0
+
+
+ io.soabase.record-builder
+ record-builder
+ 34-SNAPSHOT
+
+
+ record-builder-test-custom-enhancer
+
+
+
+ io.soabase.record-builder
+ record-builder-enhancer-core
+ ${project.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+
+ true
+
+
+
+
+
diff --git a/record-builder-test-custom-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/test/TestEnhancer.java b/record-builder-test-custom-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/test/TestEnhancer.java
new file mode 100644
index 0000000..7338d88
--- /dev/null
+++ b/record-builder-test-custom-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/test/TestEnhancer.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.test;
+
+import io.soabase.recordbuilder.enhancer.spi.Processor;
+import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
+import recordbuilder.org.objectweb.asm.Opcodes;
+import recordbuilder.org.objectweb.asm.tree.FieldInsnNode;
+import recordbuilder.org.objectweb.asm.tree.InsnList;
+import recordbuilder.org.objectweb.asm.tree.InsnNode;
+import recordbuilder.org.objectweb.asm.tree.MethodInsnNode;
+
+import javax.lang.model.element.TypeElement;
+import java.util.List;
+
+public class TestEnhancer
+ implements RecordBuilderEnhancer
+{
+ @Override
+ public InsnList enhance(Processor processor, TypeElement element, List arguments)
+ {
+ /*
+ 4: getstatic #7 // Field io/soabase/recordbuilder/enhancer/test/Counter.COUNTER:Ljava/util/concurrent/atomic/AtomicInteger;
+ 7: invokevirtual #13 // Method java/util/concurrent/atomic/AtomicInteger.incrementAndGet:()I
+ 10: pop
+ */
+ InsnList insnList = new InsnList();
+ insnList.add(new FieldInsnNode(Opcodes.GETSTATIC, "io/soabase/recordbuilder/enhancer/test/Counter", "COUNTER", "Ljava/util/concurrent/atomic/AtomicInteger;"));
+ insnList.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/util/concurrent/atomic/AtomicInteger", "incrementAndGet", "()I"));
+ insnList.add(new InsnNode(Opcodes.POP));
+ return insnList;
+ }
+
+ public String description() {
+ return "This is a test";
+ }
+}
diff --git a/record-builder-test/pom.xml b/record-builder-test/pom.xml
index c2d68d9..2c6b9b5 100644
--- a/record-builder-test/pom.xml
+++ b/record-builder-test/pom.xml
@@ -19,17 +19,43 @@
validation-api
+
+ io.soabase.record-builder
+ record-builder-core
+
+
+
+ io.soabase.record-builder
+ record-builder-enhancer-core
+
+
io.soabase.record-builder
record-builder-processor
provided
+
+ io.soabase.record-builder
+ record-builder-enhancer
+ provided
+
+
io.soabase.record-builder
record-builder-validator
+
+ io.soabase.record-builder
+ record-builder-test-custom-enhancer
+
+
+
+ com.google.guava
+ guava
+
+
org.hibernate.validator
hibernate-validator
diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/CopyCollectionNullableEmptyTest.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/CopyCollectionNullableEmptyTest.java
new file mode 100644
index 0000000..cc610dd
--- /dev/null
+++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/CopyCollectionNullableEmptyTest.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright 2019 Jordan Zimmerman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.soabase.recordbuilder.enhancer.test;
+
+import io.soabase.recordbuilder.enhancer.RecordBuilderEnhance;
+import io.soabase.recordbuilder.enhancer.enhancers.CopyCollectionNullableEmpty;
+
+import java.math.BigInteger;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@RecordBuilderEnhance(enhancers = CopyCollectionNullableEmpty.class)
+public record CopyCollectionNullableEmptyTest(Collection c, List l, Set s, Map