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 0000000..f052243 Binary files /dev/null and b/record-builder-enhancer/disassembly1.png differ diff --git a/record-builder-enhancer/pom.xml b/record-builder-enhancer/pom.xml new file mode 100644 index 0000000..75f748d --- /dev/null +++ b/record-builder-enhancer/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + io.soabase.record-builder + record-builder + 34-SNAPSHOT + + + record-builder-enhancer + + + ${project.parent.basedir}/src/etc/header.txt + + + + + io.soabase.record-builder + record-builder-enhancer-core + + + + info.picocli + picocli + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + + + true + ${project.build.directory}/dependency-reduced-pom.xml + true + + + picocli + recordbuilder.picocli + + + safe + META-INF.services + + + + + + io.soabase.recordbuilder.enhancer.Main + + + + + + + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + diff --git a/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/EnhancersController.java b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/EnhancersController.java new file mode 100644 index 0000000..7cf9ce8 --- /dev/null +++ b/record-builder-enhancer/src/main/java/io/soabase/recordbuilder/enhancer/EnhancersController.java @@ -0,0 +1,131 @@ +/** + * Copyright 2019 Jordan Zimmerman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.soabase.recordbuilder.enhancer; + +import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static io.soabase.recordbuilder.enhancer.RecordBuilderEnhancerPlugin.adjustedClassName; + +class EnhancersController { + private final Map> enhancers = new ConcurrentHashMap<>(); + + record EnhancerAndArgs(RecordBuilderEnhancer enhancer, List arguments) {} + + List getEnhancers(ProcessorImpl processor, TypeElement typeElement) { + return internalGetEnhancers(processor, typeElement).flatMap(spec -> toEnhancer(processor, spec)).toList(); + } + + private Stream internalGetEnhancers(ProcessorImpl processor, TypeElement typeElement) { + Optional 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); + } +}