From 5b25be2cf556bd7d75d29c7eca0edb42189d0b7e Mon Sep 17 00:00:00 2001 From: Jordan Zimmerman Date: Thu, 16 Jun 2022 13:47:24 +0100 Subject: [PATCH] RecordBuilder Enhancer New module to Inject verification, defensive copying, null checks or custom code into your Java Record constructors during compilation. --- README.md | 6 + pom.xml | 103 ++-- record-builder-enhancer-core/pom.xml | 69 +++ .../enhancer/RecordBuilderEnhance.java | 38 ++ .../RecordBuilderEnhanceArguments.java | 29 ++ .../recordbuilder/enhancer/spi/Entry.java | 22 + .../recordbuilder/enhancer/spi/Processor.java | 39 ++ .../enhancer/spi/RecordBuilderEnhancer.java | 26 + record-builder-enhancer/README.md | 454 ++++++++++++++++++ record-builder-enhancer/disassembly1.png | Bin 0 -> 86424 bytes record-builder-enhancer/pom.xml | 86 ++++ .../enhancer/EnhancersController.java | 131 +++++ .../soabase/recordbuilder/enhancer/Main.java | 28 ++ .../recordbuilder/enhancer/PluginOptions.java | 45 ++ .../recordbuilder/enhancer/ProcessorImpl.java | 110 +++++ .../enhancer/RecordBuilderEnhancerPlugin.java | 139 ++++++ .../recordbuilder/enhancer/Session.java | 186 +++++++ .../recordbuilder/enhancer/SessionFlag.java | 21 + .../enhancer/enhancers/CopyCollection.java | 53 ++ .../enhancers/CopyCollectionBase.java | 70 +++ .../CopyCollectionNullableEmpty.java | 68 +++ .../CopyCollectionNullableEmptyBase.java | 96 ++++ .../enhancer/enhancers/EmptyNullOptional.java | 74 +++ .../enhancer/enhancers/EmptyNullString.java | 59 +++ .../enhancers/GuavaCopyCollection.java | 53 ++ .../GuavaCopyCollectionNullableEmpty.java | 68 +++ .../enhancers/NotNullAnnotations.java | 63 +++ .../enhancer/enhancers/ProcessorUtil.java | 39 ++ .../enhancer/enhancers/RequireNonNull.java | 67 +++ .../resources/safe/com.sun.source.util.Plugin | 1 + record-builder-test-custom-enhancer/pom.xml | 34 ++ .../enhancer/test/TestEnhancer.java | 50 ++ record-builder-test/pom.xml | 26 + .../test/CopyCollectionNullableEmptyTest.java | 31 ++ .../recordbuilder/enhancer/test/Counter.java | 25 + .../enhancer/test/CustomEnhance.java | 31 ++ .../enhancer/test/CustomEnhanced.java | 28 ++ .../GuavaCopyCollectionNullableEmptyTest.java | 31 ++ .../test/GuavaCopyCollectionTest.java | 31 ++ .../enhancer/test/MadeUpNotNullable.java | 27 ++ .../enhancer/test/NotNullAnnotation.java | 30 ++ .../test/NotNullableCopyCollectionTest.java | 29 ++ .../enhancer/test/OptionalTest.java | 28 ++ .../enhancer/test/PluginTest.java | 22 + .../enhancer/test/StringTest.java | 23 + .../enhancer/test/TestEnhanced.java | 99 ++++ 46 files changed, 2748 insertions(+), 40 deletions(-) create mode 100644 record-builder-enhancer-core/pom.xml create mode 100644 record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/RecordBuilderEnhance.java create mode 100644 record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/RecordBuilderEnhanceArguments.java create mode 100644 record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/spi/Entry.java create mode 100644 record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/spi/Processor.java create mode 100644 record-builder-enhancer-core/src/main/java/io/soabase/recordbuilder/enhancer/spi/RecordBuilderEnhancer.java create mode 100644 record-builder-enhancer/README.md create mode 100644 record-builder-enhancer/disassembly1.png create mode 100644 record-builder-enhancer/pom.xml create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/EnhancersController.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/Main.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/PluginOptions.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/ProcessorImpl.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/RecordBuilderEnhancerPlugin.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/Session.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/SessionFlag.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollection.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollectionBase.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollectionNullableEmpty.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/CopyCollectionNullableEmptyBase.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/EmptyNullOptional.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/EmptyNullString.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/GuavaCopyCollection.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/GuavaCopyCollectionNullableEmpty.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/NotNullAnnotations.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/ProcessorUtil.java create mode 100644 record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/enhancers/RequireNonNull.java create mode 100644 record-builder-enhancer/src/main/resources/safe/com.sun.source.util.Plugin create mode 100644 record-builder-test-custom-enhancer/pom.xml create mode 100644 record-builder-test-custom-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/test/TestEnhancer.java create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/CopyCollectionNullableEmptyTest.java create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/Counter.java create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/CustomEnhance.java create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/CustomEnhanced.java create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/GuavaCopyCollectionNullableEmptyTest.java create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/GuavaCopyCollectionTest.java create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/MadeUpNotNullable.java create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/NotNullAnnotation.java create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/NotNullableCopyCollectionTest.java create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/OptionalTest.java create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/PluginTest.java create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/StringTest.java create mode 100644 record-builder-test/src/test/java/io/soabase/recordbuilder/enhancer/test/TestEnhanced.java 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[] enhancers(); + + RecordBuilderEnhanceArguments[] arguments() default {}; + + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.ANNOTATION_TYPE) + @Inherited + @interface Template { + Class[] 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 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 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: + +![](disassembly1.png) + +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 0000000000000000000000000000000000000000..f05224345c0a09b9ea5f5ae6d74698e8a7d355c8 GIT binary patch literal 86424 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
  • *v$Gvx*&@n4*jI#}1QY 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

    Ny%h&%|tWlz}JelLEhSQs<11pC+4pGAm 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 + + 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 recordBuilderEnhance = getAnnotationMirror(processor, typeElement); + Optional 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 argumentsValue = (List) 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) 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 enhancers = (List) 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) 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 getAnnotationMirror(ProcessorImpl processor, TypeElement typeElement) { + return processor.elements().getAllAnnotationMirrors(typeElement).stream() + .filter(annotationMirror -> annotationMirror.getAnnotationType().toString().equals(adjustedClassName(RecordBuilderEnhance.class))) + .findFirst(); + } + + private Optional 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> 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> 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> 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 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 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 typeMirrors = entry.element().asType().getAnnotationMirrors().stream(); + Stream 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 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, AtomicBoolean> m) { +} diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/Counter.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/Counter.java new file mode 100644 index 0000000..4f7ac45 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/Counter.java @@ -0,0 +1,25 @@ +/** + * 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 java.util.concurrent.atomic.AtomicInteger; + +public class Counter { + public static final AtomicInteger COUNTER = new AtomicInteger(); + + private Counter() { + } +} diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/CustomEnhance.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/CustomEnhance.java new file mode 100644 index 0000000..bd23bbf --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/CustomEnhance.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 io.soabase.recordbuilder.enhancer.enhancers.EmptyNullOptional; +import io.soabase.recordbuilder.enhancer.enhancers.EmptyNullString; +import io.soabase.recordbuilder.enhancer.enhancers.RequireNonNull; + +import java.lang.annotation.*; + +@RecordBuilderEnhance.Template(enhancers = {CopyCollectionNullableEmpty.class, EmptyNullString.class, EmptyNullOptional.class, RequireNonNull.class}) +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +@Inherited +public @interface CustomEnhance { +} diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/CustomEnhanced.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/CustomEnhanced.java new file mode 100644 index 0000000..3a84d98 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/CustomEnhanced.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.test; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +@CustomEnhance +public record CustomEnhanced(String s, Optional o, Instant i, List l) { + public CustomEnhanced { + Counter.COUNTER.decrementAndGet(); + } +} diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/GuavaCopyCollectionNullableEmptyTest.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/GuavaCopyCollectionNullableEmptyTest.java new file mode 100644 index 0000000..46db802 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/GuavaCopyCollectionNullableEmptyTest.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.GuavaCopyCollectionNullableEmpty; + +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 = GuavaCopyCollectionNullableEmpty.class) +public record GuavaCopyCollectionNullableEmptyTest(Collection c, List l, Set s, Map, AtomicBoolean> m) { +} diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/GuavaCopyCollectionTest.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/GuavaCopyCollectionTest.java new file mode 100644 index 0000000..f6c0376 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/GuavaCopyCollectionTest.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.GuavaCopyCollection; + +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 = GuavaCopyCollection.class) +public record GuavaCopyCollectionTest(Collection c, List l, Set s, Map, AtomicBoolean> m) { +} diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/MadeUpNotNullable.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/MadeUpNotNullable.java new file mode 100644 index 0000000..dcc3251 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/MadeUpNotNullable.java @@ -0,0 +1,27 @@ +/** + * 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 java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.RECORD_COMPONENT; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +@Target(RECORD_COMPONENT) +@Retention(SOURCE) +public @interface MadeUpNotNullable { +} diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/NotNullAnnotation.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/NotNullAnnotation.java new file mode 100644 index 0000000..3c869f2 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/NotNullAnnotation.java @@ -0,0 +1,30 @@ +/** + * 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.RecordBuilderEnhanceArguments; +import io.soabase.recordbuilder.enhancer.enhancers.NotNullAnnotations; + +import javax.validation.constraints.NotNull; + +@RecordBuilderEnhance(enhancers = NotNullAnnotations.class, + arguments = @RecordBuilderEnhanceArguments(enhancer = NotNullAnnotations.class, arguments = ".*notnull.*")) +public record NotNullAnnotation(String s, @NotNull Integer i, @MadeUpNotNullable Double d) { + public NotNullAnnotation() { + this(null, null, null); + } +} diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/NotNullableCopyCollectionTest.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/NotNullableCopyCollectionTest.java new file mode 100644 index 0000000..407ff29 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/NotNullableCopyCollectionTest.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.test; + +import io.soabase.recordbuilder.enhancer.RecordBuilderEnhance; +import io.soabase.recordbuilder.enhancer.enhancers.CopyCollection; +import io.soabase.recordbuilder.enhancer.enhancers.RequireNonNull; + +import java.math.BigInteger; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +@RecordBuilderEnhance(enhancers = {RequireNonNull.class, CopyCollection.class}) +public record NotNullableCopyCollectionTest(Collection c, List l, Set s, Map, AtomicBoolean> m) { +} diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/OptionalTest.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/OptionalTest.java new file mode 100644 index 0000000..3b08bc9 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/OptionalTest.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.test; + +import io.soabase.recordbuilder.enhancer.RecordBuilderEnhance; +import io.soabase.recordbuilder.enhancer.enhancers.EmptyNullOptional; + +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; + +@RecordBuilderEnhance(enhancers = EmptyNullOptional.class) +public record OptionalTest(OptionalInt i, OptionalLong l, OptionalDouble d, Optional o) { +} diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/PluginTest.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/PluginTest.java new file mode 100644 index 0000000..286a0ee --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/PluginTest.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.test; + +import io.soabase.recordbuilder.enhancer.RecordBuilderEnhance; + +@RecordBuilderEnhance(enhancers = TestEnhancer.class) +public record PluginTest(int i) { +} diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/StringTest.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/StringTest.java new file mode 100644 index 0000000..b94ac84 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/enhancer/test/StringTest.java @@ -0,0 +1,23 @@ +/** + * 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.EmptyNullString; + +@RecordBuilderEnhance(enhancers = EmptyNullString.class) +public record StringTest(String s) { +} diff --git a/record-builder-test/src/test/java/io/soabase/recordbuilder/enhancer/test/TestEnhanced.java b/record-builder-test/src/test/java/io/soabase/recordbuilder/enhancer/test/TestEnhanced.java new file mode 100644 index 0000000..9e8756d --- /dev/null +++ b/record-builder-test/src/test/java/io/soabase/recordbuilder/enhancer/test/TestEnhanced.java @@ -0,0 +1,99 @@ +/** + * 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 com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +class TestEnhanced { + @Test + void testOptional() + { + OptionalTest optionalTest = new OptionalTest(null, null, null, null); + Assertions.assertTrue(optionalTest.d().isEmpty()); + Assertions.assertTrue(optionalTest.i().isEmpty()); + Assertions.assertTrue(optionalTest.l().isEmpty()); + Assertions.assertTrue(optionalTest.o().isEmpty()); + } + + @Test + void testString() + { + StringTest stringTest = new StringTest(null); + Assertions.assertNotNull(stringTest.s()); + } + + @Test + void testCopyCollectionNullableEmptyTest() + { + CopyCollectionNullableEmptyTest test = new CopyCollectionNullableEmptyTest(null, null, null, null); + Assertions.assertTrue(test.c().isEmpty()); + Assertions.assertTrue(test.l().isEmpty()); + Assertions.assertTrue(test.s().isEmpty()); + Assertions.assertTrue(test.m().isEmpty()); + } + + @Test + void testCustomEnhanced() + { + Instant now = Instant.now(); + int current = Counter.COUNTER.get(); + CustomEnhanced customEnhanced = new CustomEnhanced(null, null, now, null); + Assertions.assertTrue(customEnhanced.o().isEmpty()); + Assertions.assertEquals(customEnhanced.s(), ""); + Assertions.assertTrue(customEnhanced.l().isEmpty()); + Assertions.assertEquals(customEnhanced.i(), now); + Assertions.assertEquals(current - 1, Counter.COUNTER.get()); + } + + @Test + void testGuavaCopyCollectionNullableEmptyTest() + { + GuavaCopyCollectionNullableEmptyTest test = new GuavaCopyCollectionNullableEmptyTest(null, null, null, null); + Assertions.assertTrue(test.l() instanceof ImmutableList); + Assertions.assertTrue(test.s() instanceof ImmutableSet); + Assertions.assertTrue(test.c() instanceof ImmutableSet); + Assertions.assertTrue(test.m() instanceof ImmutableMap, AtomicBoolean>); + } + + @Test + void testPlugin() + { + int previous = Counter.COUNTER.get(); + new PluginTest(0); + Assertions.assertEquals(previous + 1, Counter.COUNTER.get()); + } + + @Test + void testNotNullAnnotations() + { + NotNullAnnotation notNullAnnotation = new NotNullAnnotation(null, 10, 10.0); + Assertions.assertNull(notNullAnnotation.s()); + Assertions.assertNotNull(notNullAnnotation.i()); + Assertions.assertNotNull(notNullAnnotation.d()); + Assertions.assertThrows(NullPointerException.class, () -> new NotNullAnnotation("s", null, 10.0)); + Assertions.assertThrows(NullPointerException.class, () -> new NotNullAnnotation("s", 100, null)); + Assertions.assertThrows(NullPointerException.class, NotNullAnnotation::new); + } +}