Compare commits

...

26 Commits

Author SHA1 Message Date
Jordan Zimmerman
5b25be2cf5 RecordBuilder Enhancer
New module to Inject verification, defensive copying, null checks or
custom code into your Java Record constructors during compilation.
2022-06-20 10:35:23 +01:00
Jordan Zimmerman
b435b5d3fd Revert "TYPE_USE annotations were being ignored (#115)"
This reverts commit d3c1bb36f3.

A bug was found - the PR needs more work
2022-06-13 08:37:24 +01:00
Jordan Zimmerman
d3c1bb36f3 TYPE_USE annotations were being ignored (#115)
Java's DAG for annotations processors doesn't contain `TYPE_USE` annotations
on the Element for some reason. However, they are on the type. So, use the
type instead.

Note due to limitations of JavaPoet this doesn't fix `TYPE_USE` annotations on
parameterized types or array components. If we want to address those we will need
changes in JavaPoet which has been dormant for a very long time.

Fixes #113
Relates to #111
2022-06-12 08:56:32 +01:00
Lovro Pandžić
c3719326c9 Use Optional.ofNullable() to check for null values
Fixes #107
2022-05-10 16:06:12 +01:00
Jordan Zimmerman
661d0818c0 _FromWith class was missing @Generated on methods/ctor 2022-04-08 09:15:59 +01:00
Jordan Zimmerman
79bc8396f2 Update README.md 2022-04-08 09:03:30 +01:00
Jordan Zimmerman
b2149622e4 Update README.md 2022-04-08 09:01:07 +01:00
Jordan Zimmerman
0718e37f76 [maven-release-plugin] prepare for next development iteration 2022-04-08 08:35:12 +01:00
Jordan Zimmerman
d112a1b352 [maven-release-plugin] prepare release record-builder-33 2022-04-08 08:35:07 +01:00
Jordan Zimmerman
86093b6bad Make the functional static builder optional (#105)
Closes #100
2022-04-08 08:33:46 +01:00
Jordan Zimmerman
7b6ad4d7ba Generated equals() and the default with() method were missing generic angle brackets causing compiler warnings (#102)
Closes #101
2022-04-08 08:29:55 +01:00
Jordan Zimmerman
cd059f1207 Options/changes so that Jacoco checks don't fail (#104)
- Added new optional Annotation `@RecordBuilderGenerated` - Jacoco ignores
classes with any annotation names "*Generated*" but it needs to be class retained.
For backward compatibility this annotation is not added by default (though it's been
added to `@RecordBuilderFull`). There is a new option to enable it.
- The from with method now uses an internal static class instead of an anonymous
inner class so that the annotation can be on this as well. This new class's name
is configurable in the options.

Thanks to user @madisparn for initial PR and issue report.

Fixes #87
2022-04-07 12:21:50 +01:00
Jordan Zimmerman
642dd01421 Don't include @Valid on base interfaces (#99)
The validation API doesn't accept `@Valid` on base interfaces. Filter
them out.

Fixes #97
2022-03-21 10:06:02 +00:00
Mads Baardsgaard
efd1a6b0d4 Add flag to add concrete setters for optionals (#94)
* Add flag to add concrete setters for optionals

* Add another test with concrete optionals disabled
2022-03-21 09:08:00 +00:00
Mads Baardsgaard
b525eddc76 Make withers and getters optional features (#95)
* Make withers and getters optional features

* Add example record with wither and getter disabled
2022-03-21 08:51:42 +00:00
Jordan Zimmerman
d3828eda74 Fix NPE for uninitialized non-null collection (#98)
Closes #91

Co-authored-by: Clément MATHIEU <clement@unportant.info>
2022-03-20 10:46:39 +00:00
Jordan Zimmerman
fef69af183 [maven-release-plugin] prepare for next development iteration 2022-02-04 11:45:10 +00:00
Jordan Zimmerman
99f9639b82 [maven-release-plugin] prepare release record-builder-32 2022-02-04 11:45:05 +00:00
Mads Baardsgaard
43bc65e258 Add configurable method name prefixes to builders (#86)
Add configurable Bean interface to add prefixed getters to record
2022-02-02 17:44:57 +00:00
Jordan Zimmerman
bae1b771b7 [maven-release-plugin] prepare for next development iteration 2022-01-25 11:06:05 +00:00
Jordan Zimmerman
1dd00b2c65 [maven-release-plugin] prepare release record-builder-31 2022-01-25 11:06:01 +00:00
Jordan Zimmerman
3b34b5dee3 [maven-release-plugin] prepare for next development iteration 2022-01-25 10:59:50 +00:00
Jordan Zimmerman
7248bad2bd [maven-release-plugin] prepare release record-builder-30 2022-01-25 10:59:45 +00:00
Jordan Zimmerman
7e494d8753 Optional functional methods for With
When enabled, some functional methods are added to the `With` nested
class.

E.g.

```java
@RecordBuilder
record MyRecord<T>(String name, T value, int qty) implements MyRecordBuilder.With<T> {}

...

MyRecord<Thing> r = ...

var other = r.map((name, value, qty) -> new Other(...));
```
2022-01-22 09:17:15 +00:00
Madis Pärn
3954499d4b Allow components stream method to work with null fields (#85)
Co-authored-by: Madis Parn <madis.parn@topia.com>
2022-01-22 07:56:39 +00:00
Jordan Zimmerman
13959dee2a [maven-release-plugin] prepare for next development iteration 2021-11-03 08:51:54 +00:00
73 changed files with 3787 additions and 283 deletions

270
README.md
View File

@@ -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
@@ -89,168 +90,176 @@ _Hat tip to [Benji Weber](https://benjiweber.co.uk/blog/2020/09/19/fun-with-java
## Builder Class Definition
(Note: you can see a builder class built using `@RecordBuilderFull` here: [SingleItemsBuilder.java](https://gist.github.com/Randgalt/8aa487a847ea2acdd76d702f7cf17d6a))
(Note: you can see a builder class built using `@RecordBuilderFull` here: [FullRecordBuilder.java](https://gist.github.com/Randgalt/8aa487a847ea2acdd76d702f7cf17d6a))
The full builder class is defined as:
```java
public class NameAndAgeBuilder {
private String name;
private String name;
private int age;
private int age;
private NameAndAgeBuilder() {
}
private NameAndAgeBuilder() {
}
private NameAndAgeBuilder(String name, int age) {
this.name = name;
this.age = age;
}
private NameAndAgeBuilder(String name, int age) {
this.name = name;
this.age = age;
}
/**
* Static constructor/builder. Can be used instead of new NameAndAge(...)
*/
public static NameAndAge NameAndAge(String name, int age) {
return new NameAndAge(name, age);
}
/**
* Static constructor/builder. Can be used instead of new NameAndAge(...)
*/
public static NameAndAge NameAndAge(String name, int age) {
return new NameAndAge(name, age);
}
/**
* Return a new builder with all fields set to default Java values
*/
public static NameAndAgeBuilder builder() {
return new NameAndAgeBuilder();
}
/**
* Return a new builder with all fields set to default Java values
*/
public static NameAndAgeBuilder builder() {
return new NameAndAgeBuilder();
}
/**
* Return a new builder with all fields set to the values taken from the given record instance
*/
public static NameAndAgeBuilder builder(NameAndAge from) {
return new NameAndAgeBuilder(from.name(), from.age());
}
/**
* Return a new builder with all fields set to the values taken from the given record instance
*/
public static NameAndAgeBuilder builder(NameAndAge from) {
return new NameAndAgeBuilder(from.name(), from.age());
}
/**
* Return a "with"er for an existing record instance
*/
public static NameAndAgeBuilder.With from(NameAndAge from) {
return new NameAndAgeBuilder.With() {
@Override
public String name() {
return from.name();
}
/**
* Return a "with"er for an existing record instance
*/
public static NameAndAgeBuilder.With from(NameAndAge from) {
return new _FromWith(from);
}
@Override
public int age() {
return from.age();
}
};
}
/**
* Return a stream of the record components as map entries keyed with the component name and the value as the component value
*/
public static Stream<Map.Entry<String, Object>> stream(NameAndAge record) {
return Stream.of(new AbstractMap.SimpleImmutableEntry<>("name", record.name()),
new AbstractMap.SimpleImmutableEntry<>("age", record.age()));
}
/**
* Return a stream of the record components as map entries keyed with the component name and the value as the component value
*/
public static Stream<Map.Entry<String, Object>> stream(NameAndAge record) {
return Stream.of(Map.entry("name", record.name()),
Map.entry("age", record.age()));
}
/**
* Return a new record instance with all fields set to the current values in this builder
*/
public NameAndAge build() {
return new NameAndAge(name, age);
}
/**
* Return a new record instance with all fields set to the current values in this builder
*/
public NameAndAge build() {
return new NameAndAge(name, age);
}
@Override
public String toString() {
return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
}
@Override
public String toString() {
return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public boolean equals(Object o) {
return (this == o) || ((o instanceof NameAndAgeBuilder r)
&& Objects.equals(name, r.name)
&& (age == r.age));
}
@Override
public boolean equals(Object o) {
return (this == o) || ((o instanceof NameAndAgeBuilder r)
&& Objects.equals(name, r.name)
&& (age == r.age));
}
/**
* Set a new value for the {@code name} record component in the builder
*/
public NameAndAgeBuilder name(String name) {
this.name = name;
return this;
}
/**
* Set a new value for the {@code name} record component in the builder
*/
public NameAndAgeBuilder name(String name) {
this.name = name;
return this;
}
/**
* Return the current value for the {@code name} record component in the builder
*/
public String name() {
return name;
}
/**
* Set a new value for the {@code age} record component in the builder
*/
public NameAndAgeBuilder age(int age) {
this.age = age;
return this;
}
/**
* Return the current value for the {@code age} record component in the builder
*/
public int age() {
return age;
}
/**
* Add withers to {@code NameAndAge}
*/
public interface With {
/**
* Return the current value for the {@code name} record component in the builder
*/
String name();
public String name() {
return name;
}
/**
* Set a new value for the {@code age} record component in the builder
*/
public NameAndAgeBuilder age(int age) {
this.age = age;
return this;
}
/**
* Return the current value for the {@code age} record component in the builder
*/
int age();
/**
* Return a new record builder using the current values
*/
default NameAndAgeBuilder with() {
return new NameAndAgeBuilder(name(), age());
public int age() {
return age;
}
/**
* Return a new record built from the builder passed to the given consumer
* Add withers to {@code NameAndAge}
*/
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
NameAndAgeBuilder builder = with();
consumer.accept(builder);
return builder.build();
public interface With {
/**
* Return the current value for the {@code name} record component in the builder
*/
String name();
/**
* Return the current value for the {@code age} record component in the builder
*/
int age();
/**
* Return a new record builder using the current values
*/
default NameAndAgeBuilder with() {
return new NameAndAgeBuilder(name(), age());
}
/**
* Return a new record built from the builder passed to the given consumer
*/
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
NameAndAgeBuilder builder = with();
consumer.accept(builder);
return builder.build();
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
*/
default NameAndAge withName(String name) {
return new NameAndAge(name, age());
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
*/
default NameAndAge withAge(int age) {
return new NameAndAge(name(), age);
}
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
*/
default NameAndAge withName(String name) {
return new NameAndAge(name, age());
}
private static final class _FromWith implements NameAndAgeBuilder.With {
private final NameAndAge from;
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
*/
default NameAndAge withAge(int age) {
return new NameAndAge(name(), age);
private _FromWith(NameAndAge from) {
this.from = from;
}
@Override
public String name() {
return from.name();
}
@Override
public int age() {
return from.age();
}
}
}
}
```
@@ -314,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

127
pom.xml
View File

@@ -5,13 +5,16 @@
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<packaging>pom</packaging>
<version>29</version>
<version>34-SNAPSHOT</version>
<modules>
<module>record-builder-core</module>
<module>record-builder-processor</module>
<module>record-builder-test</module>
<module>record-builder-validator</module>
<module>record-builder-enhancer</module>
<module>record-builder-test-custom-enhancer</module> <!-- must be set to ignore in IntelliJ -->
<module>record-builder-enhancer-core</module> <!-- must be set to ignore in IntelliJ -->
</modules>
<properties>
@@ -29,18 +32,24 @@
<maven-gpg-plugin-version>1.6</maven-gpg-plugin-version>
<maven-javadoc-plugin-version>3.1.1</maven-javadoc-plugin-version>
<maven-clean-plugin-version>3.1.0</maven-clean-plugin-version>
<maven-shade-plugin-version>3.2.1</maven-shade-plugin-version>
<maven-shade-plugin-version>3.3.0</maven-shade-plugin-version>
<maven-release-plugin-version>2.5.3</maven-release-plugin-version>
<maven-jar-plugin-version>3.2.0</maven-jar-plugin-version>
<maven-surefire-plugin-version>3.0.0-M5</maven-surefire-plugin-version>
<jacoco-maven-plugin-version>0.8.7</jacoco-maven-plugin-version>
<license-file-path>src/etc/header.txt</license-file-path>
<javapoet-version>1.12.1</javapoet-version>
<junit-jupiter-version>5.5.2</junit-jupiter-version>
<asm-version>7.2</asm-version>
<asm-version>9.3</asm-version>
<validation-api-version>2.0.1.Final</validation-api-version>
<hibernate-validator-version>6.0.20.Final</hibernate-validator-version>
<javax-el-version>3.0.1-b09</javax-el-version>
<byte-buddy.version>1.12.10</byte-buddy.version>
<guava.version>31.1-jre</guava.version>
<picocli.version>4.6.3</picocli.version>
</properties>
<name>Record Builder</name>
@@ -77,7 +86,7 @@
<url>https://github.com/randgalt/record-builder</url>
<connection>scm:git:https://github.com/randgalt/record-builder.git</connection>
<developerConnection>scm:git:git@github.com:randgalt/record-builder.git</developerConnection>
<tag>record-builder-29</tag>
<tag>record-builder-1.16</tag>
</scm>
<issueManagement>
@@ -100,6 +109,18 @@
<dependencyManagement>
<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>${byte-buddy.version}</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>${byte-buddy.version}</version>
</dependency>
<dependency>
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
@@ -112,12 +133,30 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-enhancer</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-enhancer-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-processor</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-test-custom-enhancer</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-validator</artifactId>
@@ -147,12 +186,42 @@
<artifactId>javax.el</artifactId>
<version>${javax-el-version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm-version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-tree</artifactId>
<version>${asm-version}</version>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>${picocli.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin-version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
@@ -213,6 +282,7 @@
<exclude>**/io/soabase/com/google/**</exclude>
<exclude>**/com/company/**</exclude>
<exclude>**/META-INF/services/**</exclude>
<exclude>**/safe/**</exclude>
<exclude>**/jvm.config</exclude>
<exclude>**/.java-version</exclude>
<exclude>**/.travis.yml</exclude>
@@ -258,44 +328,6 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>${maven-shade-plugin-version}</version>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<dependencyReducedPomLocation>${project.build.outputDirectory}/META-INF/reduced-pom.xml</dependencyReducedPomLocation>
<filters>
<filter>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
</transformers>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm-version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>${asm-version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
@@ -319,6 +351,12 @@
<artifactId>maven-gpg-plugin</artifactId>
<version>${maven-gpg-plugin-version}</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco-maven-plugin-version}</version>
</plugin>
</plugins>
</pluginManagement>
@@ -352,6 +390,11 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>29</version>
<version>34-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -99,6 +99,11 @@ public @interface RecordBuilder {
*/
String componentsMethodName() default "stream";
/**
* If true, a "With" interface is generated and an associated static factory
*/
boolean enableWither() default true;
/**
* The name to use for the nested With class
*/
@@ -139,6 +144,11 @@ public @interface RecordBuilder {
*/
boolean emptyDefaultForOptional() default true;
/**
* Add non-optional setter methods for optional record components.
*/
boolean addConcreteSettersForOptional() default false;
/**
* Add not-null checks for record components annotated with any annotation named either "NotNull",
* "NoNull", or "NonNull" (see {@link #interpretNotNullsPattern()} for the actual regex matching pattern).
@@ -182,6 +192,66 @@ public @interface RecordBuilder {
* The prefix for adder methods when {@link #addSingleItemCollectionBuilders()} is enabled
*/
String singleItemBuilderPrefix() default "add";
/**
* When enabled, adds functional methods to the nested "With" class (such as {@code map()} and {@code accept()}).
*/
boolean addFunctionalMethodsToWith() default false;
/**
* If set, all builder setter methods will be prefixed with this string. Camel-casing will
* still be enforced, so if this option is set to "set" a field named "myField" will get
* a corresponding setter named "setMyField".
*/
String setterPrefix() default "";
/**
* If true, getters will be generated for the Builder class.
*/
boolean enableGetters() default true;
/**
* If set, all builder getter methods will be prefixed with this string. Camel-casing will
* still be enforced, so if this option is set to "get", a field named "myField" will get
* a corresponding getter named "getMyField".
*/
String getterPrefix() default "";
/**
* If set, all boolean builder getter methods will be prefixed with this string.
* Camel-casing will still be enforced, so if this option is set to "is", a field named
* "myField" will get a corresponding getter named "isMyField".
*/
String booleanPrefix() default "";
/**
* If set, the Builder will contain an internal interface with this name. This interface
* contains getters for all the fields in the Record prefixed with the value supplied in
* {@link this.getterPrefix} and {@link this.booleanPrefix}. This interface can be
* implemented by the original Record to have proper bean-style prefixed getters.
*
* Please note that unless either of the aforementioned prefixes are set,
* this option does nothing.
*/
String beanClassName() default "";
/**
* If true, generated classes are annotated with {@code RecordBuilderGenerated} which has a retention
* policy of {@code CLASS}. This ensures that analyzers such as Jacoco will ignore the generated class.
*/
boolean addClassRetainedGenerated() default false;
/**
* The {@link #fromMethodName} method instantiates an internal private class. This is the
* name of that class.
*/
String fromWithClassName() default "_FromWith";
/**
* If true, a functional-style builder is added so that record instances can be instantiated
* without {@code new}.
*/
boolean addStaticBuilder() default true;
}
@Retention(RetentionPolicy.CLASS)

View File

@@ -20,7 +20,9 @@ import java.lang.annotation.*;
@RecordBuilder.Template(options = @RecordBuilder.Options(
interpretNotNulls = true,
useImmutableCollections = true,
addSingleItemCollectionBuilders = true
addSingleItemCollectionBuilders = true,
addFunctionalMethodsToWith = true,
addClassRetainedGenerated = true
))
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)

View File

@@ -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.core;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
/**
* Jacoco ignores classes and methods annotated with `*Generated`
*/
@Target({PACKAGE, TYPE, METHOD, CONSTRUCTOR, FIELD, LOCAL_VARIABLE, PARAMETER})
@Retention(RetentionPolicy.CLASS)
public @interface RecordBuilderGenerated {
}

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>34-SNAPSHOT</version>
</parent>
<artifactId>record-builder-enhancer-core</artifactId>
<properties>
<license-file-path>${project.parent.basedir}/src/etc/header.txt</license-file-path>
</properties>
<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-tree</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xml</dependencyReducedPomLocation>
<useDependencyReducedPomInJar>true</useDependencyReducedPomInJar>
<relocations>
<relocation>
<pattern>org.objectweb</pattern>
<shadedPattern>recordbuilder.org.objectweb</shadedPattern>
</relocation>
</relocations>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
</transformers>
<filters>
<filter>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,38 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.enhancer;
import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
import java.lang.annotation.*;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
public @interface RecordBuilderEnhance {
Class<? extends RecordBuilderEnhancer>[] enhancers();
RecordBuilderEnhanceArguments[] arguments() default {};
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.ANNOTATION_TYPE)
@Inherited
@interface Template {
Class<? extends RecordBuilderEnhancer>[] enhancers();
RecordBuilderEnhanceArguments[] arguments() default {};
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.enhancer;
import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
import java.lang.annotation.*;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
public @interface RecordBuilderEnhanceArguments {
Class<? extends RecordBuilderEnhancer> enhancer();
String[] arguments();
}

View File

@@ -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) {
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.enhancer.spi;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import java.util.List;
public interface Processor {
Elements elements();
Types types();
boolean hasEnhancer(Class<? extends RecordBuilderEnhancer> enhancer);
boolean verboseRequested();
void logInfo(CharSequence msg);
void logWarning(CharSequence msg);
void logError(CharSequence msg);
List<Entry> asEntries(TypeElement element);
}

View File

@@ -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<String> arguments);
}

View File

@@ -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<String> l) {
}
```
Enhancer inserts code into the default constructor as if you wrote this:
```java
public record MyRecord(String s, List<String> 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<String> l) {}
```
_becomes_
```java
public record MyRecord(List<String> 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<String> 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)&#124;(nonnull)_ is used. Matching is always case insensitive. |
### Examples
#### EmptyNullOptional
```java
@RecordBuilderEnhance(enhancers = EmptyNullOptional.class)
public record MyRecord(Optional<String> s, OptionalInt i) {}
```
_becomes_
```java
public record MyRecord(Optional<String> 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<String> c, Set<String> s, List<String> l, Map<String, String> 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<String> c, Set<String> s, List<String> l, Map<String, String> 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<String> c, Set<String> s, List<String> l, Map<String, String> 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<String> c, Set<String> s, List<String> l, Map<String, String> 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
<!-- only needed during compilation -->
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-enhancer</artifactId>
<version>${record.builder.version}</version>
<scope>provided</scope>
</dependency>
<!-- contains the annotations -->
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-enhancer-core</artifactId>
<version>${record.builder.version}</version>
</dependency>
```
### 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=<outputTo>] [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=<outputTo>
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
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgument>-Xplugin:recordbuilderenhancer ...arguments...</compilerArgument>
</configuration>
</plugin>
```
## 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<String> 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."<init>":()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;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>34-SNAPSHOT</version>
</parent>
<artifactId>record-builder-enhancer</artifactId>
<properties>
<license-file-path>${project.parent.basedir}/src/etc/header.txt</license-file-path>
</properties>
<dependencies>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-enhancer-core</artifactId>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xml</dependencyReducedPomLocation>
<useDependencyReducedPomInJar>true</useDependencyReducedPomInJar>
<relocations>
<relocation>
<pattern>picocli</pattern>
<shadedPattern>recordbuilder.picocli</shadedPattern>
</relocation>
<relocation>
<pattern>safe</pattern>
<shadedPattern>META-INF.services</shadedPattern>
</relocation>
</relocations>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>io.soabase.recordbuilder.enhancer.Main</Main-Class>
</manifestEntries>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
</transformers>
<filters>
<filter>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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<String, Optional<RecordBuilderEnhancer>> enhancers = new ConcurrentHashMap<>();
record EnhancerAndArgs(RecordBuilderEnhancer enhancer, List<String> arguments) {}
List<EnhancerAndArgs> getEnhancers(ProcessorImpl processor, TypeElement typeElement) {
return internalGetEnhancers(processor, typeElement).flatMap(spec -> toEnhancer(processor, spec)).toList();
}
private Stream<EnhancerSpec> internalGetEnhancers(ProcessorImpl processor, TypeElement typeElement) {
Optional<? extends AnnotationMirror> recordBuilderEnhance = getAnnotationMirror(processor, typeElement);
Optional<? extends AnnotationMirror> recordBuilderEnhanceTemplate = getTemplateAnnotationMirror(processor, typeElement);
if (recordBuilderEnhance.isPresent() && recordBuilderEnhanceTemplate.isPresent()) {
processor.logError("RecordBuilderEnhance and RecordBuilderEnhance.Template cannot be combined.");
return Stream.of();
}
return Stream.concat(recordBuilderEnhance.stream().flatMap(this::getEnhancersAnnotationValue), recordBuilderEnhanceTemplate.stream().flatMap(this::getEnhancersAnnotationValue));
}
private Map<String, Object> 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<String> arguments) {}
@SuppressWarnings("unchecked")
private Stream<EnhancerSpec> getEnhancersAnnotationValue(AnnotationMirror annotationMirror)
{
Map<String, Object> annotationValueMap = getAnnotationValueMap(annotationMirror);
List<? extends AnnotationValue> argumentsValue = (List<? extends AnnotationValue>) annotationValueMap.getOrDefault("arguments", List.of()); // list of RecordBuilderEnhanceArguments mirrors
Map<String, List<String>> argumentsMap = argumentsValue.stream()
.flatMap(argumentMirror -> {
Map<String, Object> argumentMap = getAnnotationValueMap((AnnotationMirror) argumentMirror.getValue());
Object enhancer = argumentMap.get("enhancer");
Object arguments = argumentMap.get("arguments");
if ((enhancer != null) && (arguments != null)) {
List<String> argumentList = ((List<? extends AnnotationValue>) arguments).stream().map(value -> value.getValue().toString()).toList();
return Stream.of(Map.entry(enhancer.toString(), argumentList));
}
return Stream.of();
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
List<? extends AnnotationValue> enhancers = (List<? extends AnnotationValue>) annotationValueMap.get("enhancers");
if (enhancers != null) {
return enhancers.stream().map(annotationValue -> {
TypeMirror typeMirror = (TypeMirror) annotationValue.getValue();
return new EnhancerSpec(typeMirror.toString(), argumentsMap.getOrDefault(typeMirror.toString(), List.of()));
});
}
return Stream.of();
}
@SuppressWarnings("unchecked")
private Map<String, ? extends AnnotationValue> getArgumentsAnnotations(AnnotationMirror annotationMirror)
{
return annotationMirror.getElementValues().entrySet().stream()
.filter(entry -> entry.getKey().getSimpleName().contentEquals("arguments"))
.flatMap(entry -> ((List<? extends AnnotationValue>) entry.getValue().getValue()).stream())
.flatMap(annotationValue -> ((AnnotationMirror) annotationValue.getValue()).getElementValues().entrySet().stream()) // now as RecordBuilderEnhanceArguments
.collect(Collectors.toMap(entry -> entry.getKey().getSimpleName().toString(), Map.Entry::getValue));
}
private Optional<? extends AnnotationMirror> getAnnotationMirror(ProcessorImpl processor, TypeElement typeElement) {
return processor.elements().getAllAnnotationMirrors(typeElement).stream()
.filter(annotationMirror -> annotationMirror.getAnnotationType().toString().equals(adjustedClassName(RecordBuilderEnhance.class)))
.findFirst();
}
private Optional<? extends AnnotationMirror> getTemplateAnnotationMirror(ProcessorImpl processor, TypeElement typeElement) {
return processor.elements().getAllAnnotationMirrors(typeElement).stream()
.flatMap(annotationMirror -> processor.elements().getAllAnnotationMirrors(annotationMirror.getAnnotationType().asElement()).stream())
.filter(annotationMirror -> annotationMirror.getAnnotationType().toString().equals(adjustedClassName(RecordBuilderEnhance.Template.class)))
.findFirst();
}
private Stream<EnhancerAndArgs> toEnhancer(ProcessorImpl processor, EnhancerSpec spec)
{
return enhancers.computeIfAbsent(spec.enhancerClass(), __ -> newEnhancer(processor, spec.enhancerClass()))
.stream()
.map(enhancer -> new EnhancerAndArgs(enhancer, spec.arguments()));
}
private Optional<RecordBuilderEnhancer> 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();
}
}
}

View File

@@ -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() {
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,110 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.enhancer;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.Trees;
import io.soabase.recordbuilder.enhancer.EnhancersController.EnhancerAndArgs;
import io.soabase.recordbuilder.enhancer.spi.Entry;
import io.soabase.recordbuilder.enhancer.spi.Processor;
import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
import javax.lang.model.element.RecordComponentElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.IntStream;
class ProcessorImpl
implements Processor {
private final Collection<? extends Class<? extends RecordBuilderEnhancer>> enhancers;
private final Elements elements;
private final Types types;
private final Trees trees;
private final CompilationUnitTree compilationUnit;
private final boolean verboseRequested;
ProcessorImpl(Elements elements, Types types, Trees trees, CompilationUnitTree compilationUnit, boolean verboseRequested) {
this(Set.of(), elements, types, trees, compilationUnit, verboseRequested);
}
private ProcessorImpl(Collection<? extends Class<? extends RecordBuilderEnhancer>> enhancers, Elements elements, Types types, Trees trees, CompilationUnitTree compilationUnit, boolean verboseRequested) {
this.enhancers = enhancers;
this.elements = elements;
this.types = types;
this.trees = trees;
this.compilationUnit = compilationUnit;
this.verboseRequested = verboseRequested;
}
ProcessorImpl withEnhancers(List<EnhancerAndArgs> enhancers)
{
Collection<? extends Class<? extends RecordBuilderEnhancer>> enhancersList = enhancers.stream().map(enhancerAndArgs -> enhancerAndArgs.enhancer().getClass()).toList();
return new ProcessorImpl(enhancersList, elements, types, trees, compilationUnit, verboseRequested);
}
@Override
public boolean verboseRequested() {
return verboseRequested;
}
@Override
public List<Entry> asEntries(TypeElement element) {
List<? extends RecordComponentElement> recordComponents = element.getRecordComponents();
return IntStream.range(0, recordComponents.size())
.mapToObj(index -> new Entry(index + 1, recordComponents.get(index), types().erasure(recordComponents.get(index).asType())))
.toList();
}
@Override
public boolean hasEnhancer(Class<? extends RecordBuilderEnhancer> enhancer) {
return enhancers.contains(enhancer);
}
@Override
public void logInfo(CharSequence msg) {
printMessage(Diagnostic.Kind.NOTE, msg);
}
@Override
public void logWarning(CharSequence msg) {
printMessage(Diagnostic.Kind.MANDATORY_WARNING, msg);
}
@Override
public void logError(CharSequence msg) {
msg += " - Use -h for help in your -Xplugin arguments";
printMessage(Diagnostic.Kind.ERROR, msg);
}
@Override
public Elements elements() {
return elements;
}
@Override
public Types types() {
return types;
}
private void printMessage(Diagnostic.Kind kind, CharSequence msg) {
trees.printMessage(kind, "[RecordBuilder Enhancer] " + msg, compilationUnit, compilationUnit);
}
}

View File

@@ -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<EnhancerAndArgs> 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<AbstractInsnNode> 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("<init>")) {
return true;
}
}
return false;
}
private void enhance(TypeElement typeElement, ProcessorImpl processor, List<EnhancerAndArgs> 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<MethodNode> 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("<init>") && methodNode.desc.equals(defaultConstructorDescription))
.findFirst();
}
}

View File

@@ -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<FileStreams> 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;
}
}

View File

@@ -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);
}

View File

@@ -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";
}
}

View File

@@ -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<String> 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();
}

View File

@@ -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";
}
}

View File

@@ -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<String> 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();
}

View File

@@ -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<String> 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;
}
}

View File

@@ -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<String> 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;
}
}

View File

@@ -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";
}
}

View File

@@ -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";
}
}

View File

@@ -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<String> arguments) {
InsnList insnList = new InsnList();
if (arguments.size() > 1) {
processor.logError("Too many arguments to NotNullAnnotations.");
} else {
String expression = arguments.isEmpty() ? DEFAULT_EXPRESSION : arguments.get(0);
try {
Pattern pattern = Pattern.compile(expression, Pattern.CASE_INSENSITIVE);
processor.asEntries(element)
.stream()
.filter(entry -> !entry.erasedType().getKind().isPrimitive())
.filter(entry -> hasMatchingAnnotation(entry, pattern))
.forEach(entry -> RequireNonNull.enhance(insnList, entry));
} catch (PatternSyntaxException e) {
processor.logError("Bad argument to NotNullAnnotations: " + e.getMessage());
}
}
return insnList;
}
private boolean hasMatchingAnnotation(Entry entry, Pattern pattern)
{
Stream<? extends AnnotationMirror> typeMirrors = entry.element().asType().getAnnotationMirrors().stream();
Stream<? extends AnnotationMirror> elementMirrors = entry.element().getAnnotationMirrors().stream();
return Stream.concat(typeMirrors, elementMirrors)
.anyMatch(mirror -> pattern.matcher(mirror.getAnnotationType().asElement().getSimpleName().toString()).matches());
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.enhancer.enhancers;
import io.soabase.recordbuilder.enhancer.spi.Entry;
import io.soabase.recordbuilder.enhancer.spi.Processor;
import io.soabase.recordbuilder.enhancer.spi.RecordBuilderEnhancer;
import javax.lang.model.type.TypeMirror;
import java.util.stream.Stream;
class ProcessorUtil {
private ProcessorUtil() {
}
static boolean isNotHandledByOthers(Class<? extends RecordBuilderEnhancer> enhancer, Processor processor, Entry entry, TypeMirror... types)
{
if (!processor.hasEnhancer(enhancer)) {
return true;
}
if (types == null) {
return true;
}
return Stream.of(types).noneMatch(type -> processor.types().isAssignable(entry.erasedType(), type));
}
}

View File

@@ -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<String> 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, "<name> 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));
}
}

View File

@@ -0,0 +1 @@
io.soabase.recordbuilder.enhancer.RecordBuilderEnhancerPlugin

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>29</version>
<version>34-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -109,6 +109,10 @@ class CollectionBuilderUtils {
return Optional.empty();
}
boolean isImmutableCollection(RecordClassType component) {
return useImmutableCollections && (isList(component) || isMap(component) || isSet(component) || component.rawTypeName().equals(collectionType));
}
boolean isList(RecordClassType component) {
return component.rawTypeName().equals(listType);
}

View File

@@ -15,22 +15,27 @@
*/
package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.*;
import io.soabase.recordbuilder.core.RecordBuilder;
import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.EXCLUDE_WILDCARD_TYPES;
import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.STANDARD;
import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.STANDARD_FOR_SETTER;
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.recordBuilderGeneratedAnnotation;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.*;
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
import com.squareup.javapoet.*;
import io.soabase.recordbuilder.core.RecordBuilder;
class InternalRecordBuilderProcessor {
private final RecordBuilder.Options metaData;
@@ -46,11 +51,9 @@ class InternalRecordBuilderProcessor {
private final CollectionBuilderUtils collectionBuilderUtils;
private static final TypeName overrideType = TypeName.get(Override.class);
private static final TypeName optionalType = TypeName.get(Optional.class);
private static final TypeName optionalIntType = TypeName.get(OptionalInt.class);
private static final TypeName optionalLongType = TypeName.get(OptionalLong.class);
private static final TypeName optionalDoubleType = TypeName.get(OptionalDouble.class);
private static final TypeName validType = ClassName.get("javax.validation", "Valid");
private static final TypeName validatorTypeName = ClassName.get("io.soabase.recordbuilder.validator", "RecordBuilderValidator");
private static final TypeVariableName rType = TypeVariableName.get("R");
private final ProcessingEnvironment processingEnv;
InternalRecordBuilderProcessor(ProcessingEnvironment processingEnv, TypeElement record, RecordBuilder.Options metaData, Optional<String> packageNameOpt) {
@@ -69,16 +72,28 @@ class InternalRecordBuilderProcessor {
builder = TypeSpec.classBuilder(builderClassType.name())
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables);
if (metaData.addClassRetainedGenerated()) {
builder.addAnnotation(recordBuilderGeneratedAnnotation);
}
addVisibility(recordActualPackage.equals(packageName), record.getModifiers());
addWithNestedClass();
if (metaData.enableWither()) {
addWithNestedClass();
}
if (!metaData.beanClassName().isEmpty()) {
addBeanNestedClass();
}
addDefaultConstructor();
addStaticBuilder();
if (metaData.addStaticBuilder()) {
addStaticBuilder();
}
if (recordComponents.size() > 0) {
addAllArgsConstructor();
}
addStaticDefaultBuilderMethod();
addStaticCopyBuilderMethod();
addStaticFromWithMethod();
if (metaData.enableWither()) {
addStaticFromWithMethod();
}
addStaticComponentsMethod();
addBuildMethod();
addToStringMethod();
@@ -87,7 +102,12 @@ class InternalRecordBuilderProcessor {
recordComponents.forEach(component -> {
add1Field(component);
add1SetterMethod(component);
add1GetterMethod(component);
if (metaData.enableGetters()) {
add1GetterMethod(component);
}
if (metaData.addConcreteSettersForOptional()) {
add1ConcreteOptionalSetterMethod(component);
}
var collectionMetaData = collectionBuilderUtils.singleItemsMetaData(component, EXCLUDE_WILDCARD_TYPES);
collectionMetaData.ifPresent(meta -> add1CollectionBuilders(meta, component));
});
@@ -146,10 +166,44 @@ class InternalRecordBuilderProcessor {
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PUBLIC)
.addTypeVariables(typeVariables);
recordComponents.forEach(component -> addWithGetterMethod(classBuilder, component));
if (metaData.addClassRetainedGenerated()) {
classBuilder.addAnnotation(recordBuilderGeneratedAnnotation);
}
recordComponents.forEach(component -> addNestedGetterMethod(classBuilder, component, prefixedName(component, true)));
addWithBuilderMethod(classBuilder);
addWithSuppliedBuilderMethod(classBuilder);
IntStream.range(0, recordComponents.size()).forEach(index -> add1WithMethod(classBuilder, recordComponents.get(index), index));
if (metaData.addFunctionalMethodsToWith()) {
classBuilder.addType(buildFunctionalInterface("Function", true))
.addType(buildFunctionalInterface("Consumer", false))
.addMethod(buildFunctionalHandler("Function", "map", true))
.addMethod(buildFunctionalHandler("Consumer", "accept", false));
}
builder.addType(classBuilder.build());
}
private void addBeanNestedClass() {
/*
Adds a nested interface that adds getters similar to:
public class MyRecordBuilder {
public interface Bean {
// getter methods
}
}
*/
var classBuilder = TypeSpec.interfaceBuilder(metaData.beanClassName())
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Add getters to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PUBLIC)
.addTypeVariables(typeVariables);
recordComponents.forEach(component -> {
if (prefixedName(component, true).equals(component.name())) {
return;
}
addNestedGetterMethod(classBuilder, component, component.name());
add1PrefixedGetterMethod(classBuilder, component);
});
builder.addType(classBuilder.build());
}
@@ -189,7 +243,7 @@ class InternalRecordBuilderProcessor {
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("return new $L(", builderClassType.name());
.add("return new $L$L(", builderClassType.name(), typeVariables.isEmpty() ? "" : "<>");
addComponentCallsAsArguments(-1, codeBlockBuilder);
codeBlockBuilder.add(");");
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
@@ -237,7 +291,6 @@ class InternalRecordBuilderProcessor {
codeBlockBuilder.add(";$]");
var methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
var singleItemsMetaData = collectionBuilderUtils.singleItemsMetaData(component, STANDARD_FOR_SETTER);
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
addConstructorAnnotations(component, parameterSpecBuilder);
var methodSpec = MethodSpec.methodBuilder(methodName)
@@ -251,6 +304,28 @@ class InternalRecordBuilderProcessor {
classBuilder.addMethod(methodSpec);
}
private void add1PrefixedGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component) {
/*
Adds a get method for the component similar to:
default MyRecord getName() {
return name();
}
*/
var codeBlockBuilder = CodeBlock.builder();
codeBlockBuilder.add("$[return $L()$];", component.name());
var methodName = prefixedName(component, true);
var methodSpec = MethodSpec.methodBuilder(methodName)
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Returns the value of {@code $L}\n", component.name())
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addCode(codeBlockBuilder.build())
.returns(component.typeName())
.build();
classBuilder.addMethod(methodSpec);
}
private void addComponentCallsAsArguments(int index, CodeBlock.Builder codeBlockBuilder) {
IntStream.range(0, recordComponents.size()).forEach(parameterIndex -> {
if (parameterIndex > 0) {
@@ -260,7 +335,7 @@ class InternalRecordBuilderProcessor {
if (parameterIndex == index) {
collectionBuilderUtils.add(codeBlockBuilder, parameterComponent);
} else {
codeBlockBuilder.add("$L()", parameterComponent.name());
codeBlockBuilder.add("$L()", prefixedName(parameterComponent, true));
}
});
}
@@ -314,8 +389,10 @@ class InternalRecordBuilderProcessor {
private void addNullCheckCodeBlock(CodeBlock.Builder builder, int index) {
if (metaData.interpretNotNulls()) {
var component = recordComponents.get(index);
if (!component.typeName().isPrimitive() && isNullAnnotated(component)) {
builder.addStatement("$T.requireNonNull($L, $S)", Objects.class, component.name(), component.name() + " is required");
if (!collectionBuilderUtils.isImmutableCollection(component)) {
if (!component.typeName().isPrimitive() && isNullAnnotated(component)) {
builder.addStatement("$T.requireNonNull($L, $S)", Objects.class, component.name(), component.name() + " is required");
}
}
}
}
@@ -417,7 +494,12 @@ class InternalRecordBuilderProcessor {
*/
var codeBuilder = CodeBlock.builder();
codeBuilder.add("return (this == o) || (");
codeBuilder.add("(o instanceof $L $L)", builderClassType.name(), uniqueVarName);
if (typeVariables.isEmpty()) {
codeBuilder.add("(o instanceof $L $L)", builderClassType.name(), uniqueVarName);
} else {
String wildcardList = typeVariables.stream().map(__ -> "?").collect(Collectors.joining(","));
codeBuilder.add("(o instanceof $L<$L> $L)", builderClassType.name(), wildcardList, uniqueVarName);
}
recordComponents.forEach(recordComponent -> {
String name = recordComponent.name();
if (recordComponent.typeName().isPrimitive()) {
@@ -464,6 +546,16 @@ class InternalRecordBuilderProcessor {
*/
var codeBuilder = CodeBlock.builder();
IntStream.range(0, recordComponents.size()).forEach(index -> {
var recordComponent = recordComponents.get(index);
if (collectionBuilderUtils.isImmutableCollection(recordComponent)) {
codeBuilder.add("$[$L = ", recordComponent.name());
collectionBuilderUtils.add(codeBuilder, recordComponents.get(index));
codeBuilder.add(";\n$]");
}
});
addNullCheckCodeBlock(codeBuilder);
codeBuilder.add("$[return ");
if (metaData.useValidationApi()) {
@@ -474,7 +566,7 @@ class InternalRecordBuilderProcessor {
if (index > 0) {
codeBuilder.add(", ");
}
collectionBuilderUtils.add(codeBuilder, recordComponents.get(index));
codeBuilder.add("$L", recordComponents.get(index).name());
});
codeBuilder.add(")");
if (metaData.useValidationApi()) {
@@ -484,63 +576,85 @@ class InternalRecordBuilderProcessor {
return codeBuilder.build();
}
private TypeName buildWithTypeName()
{
ClassName rawTypeName = ClassName.get(packageName, builderClassType.name() + "." + metaData.withClassName());
if (typeVariables.isEmpty()) {
return rawTypeName;
}
return ParameterizedTypeName.get(rawTypeName, typeVariables.toArray(new TypeName[]{}));
}
private void addFromWithClass() {
/*
Adds static private class that implements/proxies the Wither
private static final class _FromWith implements MyRecordBuilder.With {
private final MyRecord from;
@Override
public String p1() {
return from.p1();
}
@Override
public String p2() {
return from.p2();
}
}
*/
var fromWithClassBuilder = TypeSpec.classBuilder(metaData.fromWithClassName())
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables)
.addSuperinterface(buildWithTypeName());
if (metaData.addClassRetainedGenerated()) {
fromWithClassBuilder.addAnnotation(recordBuilderGeneratedAnnotation);
}
fromWithClassBuilder.addField(recordClassType.typeName(), "from", Modifier.PRIVATE, Modifier.FINAL);
MethodSpec constructorSpec = MethodSpec.constructorBuilder()
.addParameter(recordClassType.typeName(), "from")
.addStatement("this.from = from")
.addModifiers(Modifier.PRIVATE)
.addAnnotation(generatedRecordBuilderAnnotation)
.build();
fromWithClassBuilder.addMethod(constructorSpec);
IntStream.range(0, recordComponents.size()).forEach(index -> {
var component = recordComponents.get(index);
MethodSpec methodSpec = MethodSpec.methodBuilder(prefixedName(component, true))
.returns(component.typeName())
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addStatement("return from.$L()", component.name())
.addAnnotation(generatedRecordBuilderAnnotation)
.build();
fromWithClassBuilder.addMethod(methodSpec);
});
this.builder.addType(fromWithClassBuilder.build());
}
private void addStaticFromWithMethod() {
/*
Adds static method that returns a "with"er view of an existing record.
public static With from(MyRecord from) {
return new MyRecordBuilder.With() {
@Override
public String p1() {
return from.p1();
}
@Override
public String p2() {
return from.p2();
}
};
return new _FromWith(from);
}
*/
var witherClassNameBuilder = CodeBlock.builder()
.add("$L.$L", builderClassType.name(), metaData.withClassName());
if (!typeVariables.isEmpty()) {
witherClassNameBuilder.add("<");
IntStream.range(0, typeVariables.size()).forEach(index -> {
if (index > 0) {
witherClassNameBuilder.add(", ");
}
witherClassNameBuilder.add(typeVariables.get(index).name);
});
witherClassNameBuilder.add(">");
}
var witherClassName = witherClassNameBuilder.build().toString();
var codeBuilder = CodeBlock.builder()
.add("return new $L", witherClassName)
.add("() {\n").indent();
IntStream.range(0, recordComponents.size()).forEach(index -> {
var component = recordComponents.get(index);
if (index > 0) {
codeBuilder.add("\n");
}
codeBuilder.add("@Override\n")
.add("public $T $L() {\n", component.typeName(), component.name())
.indent()
.addStatement("return from.$L()", component.name())
.unindent()
.add("}\n");
});
codeBuilder.unindent().addStatement("}");
var withType = ClassName.get("", witherClassName);
var methodSpec = MethodSpec.methodBuilder("from")//metaData.copyMethodName())
addFromWithClass();
var methodSpec = MethodSpec.methodBuilder(metaData.fromMethodName())
.addJavadoc("Return a \"with\"er for an existing record instance\n")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables)
.addParameter(recordClassType.typeName(), metaData.fromMethodName())
.returns(withType)
.addCode(codeBuilder.build())
.returns(buildWithTypeName())
.addStatement("return new $L$L(from)", metaData.fromWithClassName(), typeVariables.isEmpty() ? "" : "<>")
.build();
builder.addMethod(methodSpec);
}
@@ -598,8 +712,8 @@ class InternalRecordBuilderProcessor {
Adds a static method that converts a record instance into a stream of its component parts
public static Stream<Map.Entry<String, Object>> stream(MyRecord record) {
return Stream.of(Map.entry("p1", record.p1()),
Map.entry("p2", record.p2()));
return Stream.of(new AbstractMap.SimpleImmutableEntry<>("p1", record.p1()),
new AbstractMap.SimpleImmutableEntry<>("p2", record.p2()));
}
*/
var codeBuilder = CodeBlock.builder().add("return $T.of(", Stream.class);
@@ -608,7 +722,7 @@ class InternalRecordBuilderProcessor {
codeBuilder.add(",\n ");
}
var name = recordComponents.get(index).name();
codeBuilder.add("$T.entry($S, record.$L())", Map.class, name, name);
codeBuilder.add("new $T<>($S, record.$L())", AbstractMap.SimpleImmutableEntry.class, name, name);
});
codeBuilder.add(")");
var mapEntryTypeVariables = ParameterizedTypeName.get(Map.Entry.class, String.class, Object.class);
@@ -633,43 +747,29 @@ class InternalRecordBuilderProcessor {
*/
var fieldSpecBuilder = FieldSpec.builder(component.typeName(), component.name(), Modifier.PRIVATE);
if (metaData.emptyDefaultForOptional()) {
TypeName thisOptionalType = null;
if (isOptional(component)) {
thisOptionalType = optionalType;
} else if (component.typeName().equals(optionalIntType)) {
thisOptionalType = optionalIntType;
} else if (component.typeName().equals(optionalLongType)) {
thisOptionalType = optionalLongType;
} else if (component.typeName().equals(optionalDoubleType)) {
thisOptionalType = optionalDoubleType;
}
if (thisOptionalType != null) {
var codeBlock = CodeBlock.builder().add("$T.empty()", thisOptionalType).build();
Optional<OptionalType> thisOptionalType = OptionalType.fromClassType(component);
if (thisOptionalType.isPresent()) {
var codeBlock = CodeBlock.builder()
.add("$T.empty()", thisOptionalType.get().typeName())
.build();
fieldSpecBuilder.initializer(codeBlock);
}
}
builder.addField(fieldSpecBuilder.build());
}
private boolean isOptional(ClassType component) {
if (component.typeName().equals(optionalType)) {
return true;
}
return (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName) && parameterizedTypeName.rawType.equals(optionalType);
}
private void addWithGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component) {
private void addNestedGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component, String methodName) {
/*
For a single record component, add a getter similar to:
T p();
*/
var methodSpecBuilder = MethodSpec.methodBuilder(component.name())
var methodSpecBuilder = MethodSpec.methodBuilder(methodName)
.addJavadoc("Return the current value for the {@code $L} record component in the builder\n", component.name())
.addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(component.typeName());
addAccessorAnnotations(component, methodSpecBuilder);
addAccessorAnnotations(component, methodSpecBuilder, this::filterOutValid);
classBuilder.addMethod(methodSpecBuilder.build());
}
@@ -677,6 +777,10 @@ class InternalRecordBuilderProcessor {
return !annotationSpec.type.equals(overrideType);
}
private boolean filterOutValid(AnnotationSpec annotationSpec) {
return !annotationSpec.type.equals(validType);
}
private void addConstructorAnnotations(RecordClassType component, ParameterSpec.Builder parameterSpecBuilder) {
if (metaData.inheritComponentAnnotations()) {
component.getCanonicalConstructorAnnotations()
@@ -687,12 +791,13 @@ class InternalRecordBuilderProcessor {
}
}
private void addAccessorAnnotations(RecordClassType component, MethodSpec.Builder methodSpecBuilder) {
private void addAccessorAnnotations(RecordClassType component, MethodSpec.Builder methodSpecBuilder, Predicate<AnnotationSpec> additionalFilter) {
if (metaData.inheritComponentAnnotations()) {
component.getAccessorAnnotations()
.stream()
.map(AnnotationSpec::get)
.filter(this::filterOutOverride)
.filter(additionalFilter)
.forEach(methodSpecBuilder::addAnnotation);
}
}
@@ -828,13 +933,13 @@ class InternalRecordBuilderProcessor {
return p;
}
*/
var methodSpecBuilder = MethodSpec.methodBuilder(component.name())
var methodSpecBuilder = MethodSpec.methodBuilder(prefixedName(component, true))
.addJavadoc("Return the current value for the {@code $L} record component in the builder\n", component.name())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(component.typeName())
.addStatement("return $L", component.name());
addAccessorAnnotations(component, methodSpecBuilder);
addAccessorAnnotations(component, methodSpecBuilder, __ -> true);
builder.addMethod(methodSpecBuilder.build());
}
@@ -847,8 +952,7 @@ class InternalRecordBuilderProcessor {
return this;
}
*/
var methodSpec = MethodSpec.methodBuilder(component.name())
var methodSpec = MethodSpec.methodBuilder(prefixedName(component, false))
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(builderClassType.typeName());
@@ -869,5 +973,119 @@ class InternalRecordBuilderProcessor {
methodSpec.addStatement("return this").addParameter(parameterSpecBuilder.build());
builder.addMethod(methodSpec.build());
}
private void add1ConcreteOptionalSetterMethod(RecordClassType component) {
/*
For a single optional record component, add a concrete setter similar to:
public MyRecordBuilder p(T p) {
this.p = p;
return this;
}
*/
var optionalType = OptionalType.fromClassType(component);
if (optionalType.isEmpty()) {
return;
}
var type = optionalType.get();
var methodSpec = MethodSpec.methodBuilder(prefixedName(component, false))
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(builderClassType.typeName());
var parameterSpecBuilder = ParameterSpec.builder(type.valueType(), component.name());
methodSpec.addJavadoc("Set a new value for the {@code $L} record component in the builder\n", component.name())
.addStatement(getOptionalStatement(type), component.name(), type.typeName(), component.name());
addConstructorAnnotations(component, parameterSpecBuilder);
methodSpec.addStatement("return this").addParameter(parameterSpecBuilder.build());
builder.addMethod(methodSpec.build());
}
private String getOptionalStatement(OptionalType type) {
if(type.isOptional()) {
return "this.$L = $T.ofNullable($L)";
}
return "this.$L = $T.of($L)";
}
private List<TypeVariableName> typeVariablesWithReturn() {
var variables = new ArrayList<TypeVariableName>();
variables.add(rType);
variables.addAll(typeVariables);
return variables;
}
private MethodSpec buildFunctionalHandler(String className, String methodName, boolean isMap) {
/*
Build a Functional handler ala:
default <R> R map(Function<R, T> proc) {
return proc.apply(p());
}
*/
var localTypeVariables = isMap ? typeVariablesWithReturn() : typeVariables;
var typeName = localTypeVariables.isEmpty() ? ClassName.get("", className) : ParameterizedTypeName.get(ClassName.get("", className), localTypeVariables.toArray(TypeName[]::new));
var methodBuilder = MethodSpec.methodBuilder(methodName)
.addAnnotation(generatedRecordBuilderAnnotation)
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addParameter(typeName, "proc");
var codeBlockBuilder = CodeBlock.builder();
if (isMap) {
methodBuilder.addJavadoc("Map record components into a new object");
methodBuilder.addTypeVariable(rType);
methodBuilder.returns(rType);
codeBlockBuilder.add("return ");
} else {
methodBuilder.addJavadoc("Perform an operation on record components");
}
codeBlockBuilder.add("proc.apply(");
addComponentCallsAsArguments(-1, codeBlockBuilder);
codeBlockBuilder.add(");");
methodBuilder.addCode(codeBlockBuilder.build());
return methodBuilder.build();
}
private TypeSpec buildFunctionalInterface(String className, boolean isMap) {
/*
Build a Functional interface ala:
@FunctionalInterface
interface Function<R, T> {
R apply(T a);
}
*/
var localTypeVariables = isMap ? typeVariablesWithReturn() : typeVariables;
var methodBuilder = MethodSpec.methodBuilder("apply").addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT);
recordComponents.forEach(component -> {
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
addConstructorAnnotations(component, parameterSpecBuilder);
methodBuilder.addParameter(parameterSpecBuilder.build());
});
if (isMap) {
methodBuilder.returns(rType);
}
return TypeSpec.interfaceBuilder(className)
.addAnnotation(generatedRecordBuilderAnnotation)
.addAnnotation(FunctionalInterface.class)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addTypeVariables(localTypeVariables)
.addMethod(methodBuilder.build())
.build();
}
private String prefixedName(RecordClassType component, boolean isGetter) {
BiFunction<String, String, String> prefixer = (p, s) -> p.isEmpty()
? s : p + Character.toUpperCase(s.charAt(0)) + s.substring(1);
boolean isBool = component.typeName().toString().toLowerCase(Locale.ROOT).equals("boolean");
if (isGetter) {
if (isBool) {
return prefixer.apply(metaData.booleanPrefix(), component.name());
}
return prefixer.apply(metaData.getterPrefix(), component.name());
}
return prefixer.apply(metaData.setterPrefix(), component.name());
}
}

View File

@@ -33,6 +33,7 @@ import java.util.stream.Collectors;
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordInterfaceAnnotation;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.recordBuilderGeneratedAnnotation;
class InternalRecordInterfaceProcessor {
private final ProcessingEnvironment processingEnv;
@@ -68,6 +69,9 @@ class InternalRecordInterfaceProcessor {
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordInterfaceAnnotation)
.addTypeVariables(typeVariables);
if (metaData.addClassRetainedGenerated()) {
builder.addAnnotation(recordBuilderGeneratedAnnotation);
}
if (addRecordBuilder) {
ClassType builderClassType = ElementUtils.getClassType(packageName, getBuilderName(iface, metaData, recordClassType, metaData.suffix()) + "." + metaData.withClassName(), iface.getTypeParameters());

View File

@@ -0,0 +1,66 @@
/**
* 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.processor;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
public record OptionalType(TypeName typeName, TypeName valueType) {
private static final TypeName optionalType = TypeName.get(Optional.class);
private static final TypeName optionalIntType = TypeName.get(OptionalInt.class);
private static final TypeName optionalLongType = TypeName.get(OptionalLong.class);
private static final TypeName optionalDoubleType = TypeName.get(OptionalDouble.class);
private static boolean isOptional(ClassType component) {
if (component.typeName().equals(optionalType)) {
return true;
}
return (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName)
&& parameterizedTypeName.rawType.equals(optionalType);
}
static Optional<OptionalType> fromClassType(final ClassType component) {
if (isOptional(component)) {
if (!(component.typeName() instanceof ParameterizedTypeName parameterizedType)) {
return Optional.of(new OptionalType(optionalType, TypeName.get(Object.class)));
}
final TypeName containingType = parameterizedType.typeArguments.isEmpty()
? TypeName.get(Object.class)
: parameterizedType.typeArguments.get(0);
return Optional.of(new OptionalType(optionalType, containingType));
}
if (component.typeName().equals(optionalIntType)) {
return Optional.of(new OptionalType(optionalIntType, TypeName.get(int.class)));
}
if (component.typeName().equals(optionalLongType)) {
return Optional.of(new OptionalType(optionalLongType, TypeName.get(long.class)));
}
if (component.typeName().equals(optionalDoubleType)) {
return Optional.of(new OptionalType(optionalDoubleType, TypeName.get(double.class)));
}
return Optional.empty();
}
public boolean isOptional() {
return typeName.equals(optionalType);
}
}

View File

@@ -19,6 +19,7 @@ import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.TypeSpec;
import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordBuilderGenerated;
import io.soabase.recordbuilder.core.RecordInterface;
import javax.annotation.processing.AbstractProcessor;
@@ -46,6 +47,7 @@ public class RecordBuilderProcessor
static final AnnotationSpec generatedRecordBuilderAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RecordBuilder.class.getName()).build();
static final AnnotationSpec generatedRecordInterfaceAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RecordInterface.class.getName()).build();
static final AnnotationSpec recordBuilderGeneratedAnnotation = AnnotationSpec.builder(RecordBuilderGenerated.class).build();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>34-SNAPSHOT</version>
</parent>
<artifactId>record-builder-test-custom-enhancer</artifactId>
<dependencies>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-enhancer-core</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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<String> 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";
}
}

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>29</version>
<version>34-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -19,17 +19,43 @@
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-core</artifactId>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-enhancer-core</artifactId>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-enhancer</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-validator</artifactId>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-test-custom-enhancer</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
@@ -66,6 +92,48 @@
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>default-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>default-report</id>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>default-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<includes>
<include>io/soabase/recordbuilder/test/jacoco/*</include>
</includes>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>COMPLEXITY</counter>
<value>COVEREDRATIO</value>
<minimum>0.60</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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<Instant> c, List<BigInteger> l, Set<Boolean> s, Map<Map<String, Short>, AtomicBoolean> m) {
}

View File

@@ -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() {
}
}

View File

@@ -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 {
}

View File

@@ -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<String> o, Instant i, List<BigDecimal> l) {
public CustomEnhanced {
Counter.COUNTER.decrementAndGet();
}
}

View File

@@ -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<Instant> c, List<BigInteger> l, Set<Boolean> s, Map<Map<String, Short>, AtomicBoolean> m) {
}

View File

@@ -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<Instant> c, List<BigInteger> l, Set<Boolean> s, Map<Map<String, Short>, AtomicBoolean> m) {
}

View File

@@ -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 {
}

View File

@@ -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);
}
}

View File

@@ -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<Instant> c, List<BigInteger> l, Set<Boolean> s, Map<Map<String, Short>, AtomicBoolean> m) {
}

View File

@@ -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<String> o) {
}

View File

@@ -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) {
}

View File

@@ -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) {
}

View File

@@ -17,12 +17,20 @@ package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RecordBuilder
@RecordBuilder.Options(useImmutableCollections = true)
public record CollectionRecord<T, X extends Point>(List<T> l, Set<T> s, Map<T, X> m, Collection<X> c) implements CollectionRecordBuilder.With<T, X> {
@RecordBuilder.Options(useImmutableCollections = true, addFunctionalMethodsToWith = true)
public record CollectionRecord<T, X extends Point>(List<T> l, Set<T> s, Map<T, X> m,
Collection<X> c) implements CollectionRecordBuilder.With<T, X> {
public static void main(String[] args) {
var r = new CollectionRecord<>(List.of("hey"), Set.of("there"), Map.of("one", new Point(10, 20)), Set.of(new Point(30, 40)));
Instant now = r.map((l1, s1, m1, c1) -> Instant.now());
r.accept((l1, s1, m1, c1) -> {
});
}
}

View File

@@ -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.test;
import java.util.List;
import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.test.CustomMethodNamesBuilder.Bean;
@RecordBuilder
@RecordBuilder.Options(
setterPrefix = "set", getterPrefix = "get", booleanPrefix = "is", beanClassName = "Bean")
public record CustomMethodNames(
int theValue,
List<Integer> theList,
boolean theBoolean) implements Bean {
}

View File

@@ -22,5 +22,5 @@ import java.util.List;
import java.util.Map;
@RecordBuilderFull
public record FullRecord(@NotNull List<Number> numbers, @NotNull Map<Number, FullRecord> fullRecords) {
public record FullRecord(@NotNull List<Number> numbers, @NotNull Map<Number, FullRecord> fullRecords, @NotNull String justAString) {
}

View File

@@ -0,0 +1,23 @@
package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
/**
* 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.
*/
@RecordBuilder.Options(addStaticBuilder = false)
@RecordBuilder
public record NoStaticBuilder(String foo) {
}

View File

@@ -15,6 +15,8 @@
*/
package io.soabase.recordbuilder.test;
import javax.validation.constraints.NotNull;
import io.soabase.recordbuilder.core.RecordBuilder;
import java.util.Optional;
@@ -22,6 +24,6 @@ import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
@RecordBuilder.Options(emptyDefaultForOptional = true)
@RecordBuilder.Options(emptyDefaultForOptional = true, addConcreteSettersForOptional = true)
@RecordBuilder
public record RecordWithOptional(Optional<String> value, Optional raw, OptionalInt i, OptionalLong l, OptionalDouble d) {}
public record RecordWithOptional(@NotNull Optional<String> value, Optional raw, OptionalInt i, OptionalLong l, OptionalDouble d) {}

View File

@@ -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.test;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder.Options(emptyDefaultForOptional = true)
@RecordBuilder
public record RecordWithOptional2(Optional<String> value, Optional raw, OptionalInt i, OptionalLong l, OptionalDouble d) {}

View File

@@ -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.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@RecordBuilder
@RecordBuilder.Options(useValidationApi = true)
public record RequestWithValid(@NotNull @Valid Part part) implements RequestWithValidBuilder.With {
public record Part(@NotBlank String name) {}
}

View File

@@ -18,8 +18,9 @@ package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.validation.constraints.NotNull;
import java.util.List;
@RecordBuilder.Options(interpretNotNulls = true)
@RecordBuilder
public record RequiredRecord(@NotNull String hey, @NotNull int i) implements RequiredRecordBuilder.With {
public record RequiredRecord(@NotNull String hey, @NotNull int i, @NotNull List<String> l) implements RequiredRecordBuilder.With {
}

View File

@@ -27,7 +27,8 @@ import java.util.Set;
@RecordBuilder.Options(
addSingleItemCollectionBuilders = true,
singleItemBuilderPrefix = "add1",
useImmutableCollections = true
useImmutableCollections = true,
addFunctionalMethodsToWith = true
)
public record SingleItems<T>(List<String> strings, Set<List<T>> sets, Map<Instant, T> map, Collection<T> collection) implements SingleItemsBuilder.With<T> {
}

View File

@@ -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.test;
import io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder
@RecordBuilder.Options(
enableGetters = false,
enableWither = false
)
public record StrippedFeaturesRecord(int aField) {}

View File

@@ -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.test.jacoco;
import io.soabase.recordbuilder.core.RecordBuilderFull;
import io.soabase.recordbuilder.core.RecordBuilderGenerated;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;
@RecordBuilderFull
@RecordBuilderGenerated
public record FullRecordForJacoco(@NotNull List<Number> numbers, @NotNull Map<Number, FullRecordForJacoco> fullRecords, @NotNull String justAString) {
}

View File

@@ -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<BigInteger>);
Assertions.assertTrue(test.s() instanceof ImmutableSet<Boolean>);
Assertions.assertTrue(test.c() instanceof ImmutableSet<Instant>);
Assertions.assertTrue(test.m() instanceof ImmutableMap<Map<String, Short>, 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);
}
}

View File

@@ -15,14 +15,14 @@
*/
package io.soabase.recordbuilder.test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class TestOptional {
@Test
void testDefaultEmpty() {
@@ -33,4 +33,51 @@ class TestOptional {
Assertions.assertEquals(OptionalLong.empty(), record.l());
Assertions.assertEquals(OptionalDouble.empty(), record.d());
}
@Test
void testRawSetters() {
var record = RecordWithOptionalBuilder.builder()
.value("value")
.raw("rawValue")
.i(42)
.l(424242L)
.d(42.42)
.build();
Assertions.assertEquals(Optional.of("value"), record.value());
Assertions.assertEquals(Optional.of("rawValue"), record.raw());
Assertions.assertEquals(OptionalInt.of(42), record.i());
Assertions.assertEquals(OptionalLong.of(424242L), record.l());
Assertions.assertEquals(OptionalDouble.of(42.42), record.d());
}
@Test
void testOptionalSetters() {
var record = RecordWithOptional2Builder.builder()
.value(Optional.of("value"))
.raw(Optional.of("rawValue"))
.i(OptionalInt.of(42))
.l(OptionalLong.of(424242L))
.d(OptionalDouble.of(42.42))
.build();
Assertions.assertEquals(Optional.of("value"), record.value());
Assertions.assertEquals(Optional.of("rawValue"), record.raw());
Assertions.assertEquals(OptionalInt.of(42), record.i());
Assertions.assertEquals(OptionalLong.of(424242L), record.l());
Assertions.assertEquals(OptionalDouble.of(42.42), record.d());
}
@Test
void shouldAcceptNullForOptionalRawSetter() {
// given
String value = null;
// when
var record = RecordWithOptionalBuilder.builder()
.value(value)
.build();
// then
Assertions.assertEquals(Optional.empty(), record.value());
}
}

View File

@@ -20,11 +20,15 @@ import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class TestRecordBuilderFull {
@Test
void testNonNull() {
Assertions.assertThrows(NullPointerException.class, () -> FullRecordBuilder.builder().build());
var record = FullRecordBuilder.builder().justAString("").build();
Assertions.assertEquals(List.of(), record.numbers());
Assertions.assertEquals(Map.of(), record.fullRecords());
}
@Test
@@ -32,6 +36,7 @@ class TestRecordBuilderFull {
var record = FullRecordBuilder.builder()
.fullRecords(new HashMap<>())
.numbers(new ArrayList<>())
.justAString("")
.build();
Assertions.assertThrows(UnsupportedOperationException.class, () -> record.fullRecords().put(1, record));
Assertions.assertThrows(UnsupportedOperationException.class, () -> record.numbers().add(1));

View File

@@ -15,6 +15,9 @@
*/
package io.soabase.recordbuilder.test;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -48,4 +51,28 @@ public class TestRecordInterface
Assertions.assertEquals(generic.i(), 101);
Assertions.assertEquals(generic.s(), now);
}
@Test
public void testBuilderStreamWithValues()
{
var stream = SimpleRecordBuilder.stream(SimpleRecordBuilder.builder()
.i(19)
.s("value")
.build())
.toList();
Assertions.assertEquals(stream, List.of(
Map.entry("i", 19),
Map.entry("s", "value")));
}
@Test
public void testBuilderStreamWithNulls()
{
var stream = SimpleRecordBuilder.stream(SimpleRecordBuilder.builder()
.build())
.toList();
Assertions.assertEquals(stream, List.of(
new SimpleImmutableEntry<>("i", 0),
new SimpleImmutableEntry<>("s", null)));
}
}

View File

@@ -19,6 +19,7 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import javax.validation.ValidationException;
import java.util.List;
class TestValidation {
@Test
@@ -33,7 +34,7 @@ class TestValidation {
@Test
void testNotNullsWithNewProperty() {
var valid = RequiredRecordBuilder.builder().hey("hey").i(1).build();
var valid = RequiredRecordBuilder.builder().hey("hey").i(1).l(List.of()).build();
Assertions.assertThrows(NullPointerException.class, () -> valid.withHey(null));
}
@@ -42,4 +43,14 @@ class TestValidation {
var valid = RequiredRecord2Builder.builder().hey("hey").i(1).build();
Assertions.assertThrows(ValidationException.class, () -> valid.withHey(null));
}
@Test
void testRequestWithValid() {
Assertions.assertDoesNotThrow(() -> RequestWithValidBuilder.builder()
.part(new RequestWithValid.Part("jsfjsf"))
.build());
Assertions.assertThrows(ValidationException.class, () -> RequestWithValidBuilder.builder()
.part(new RequestWithValid.Part(""))
.build());
}
}

View File

@@ -0,0 +1,69 @@
/**
* 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.test;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
public class TestVariousOptions {
@Test
public void builderGetsCustomSetterAndGetterNames() {
var obj = CustomMethodNamesBuilder.builder()
.setTheValue(1)
.setTheList(List.of(2))
.setTheBoolean(true);
assertEquals(1, obj.getTheValue());
assertEquals(List.of(2), obj.getTheList());
assertTrue(obj.isTheBoolean());
assertEquals(new CustomMethodNames(1, List.of(2), true), obj.build());
}
@Test
public void withBuilderGetsCustomSetterAndGetterNames() {
var obj = CustomMethodNamesBuilder.from(CustomMethodNamesBuilder.builder()
.setTheValue(1)
.setTheList(List.of(2))
.setTheBoolean(true)
.build());
assertEquals(1, obj.getTheValue());
assertEquals(List.of(2), obj.getTheList());
assertTrue(obj.isTheBoolean());
}
@Test
public void recordHasPrefixedGetters() {
var obj = new CustomMethodNames(1, List.of(2), true);
assertEquals(1, obj.getTheValue());
assertEquals(List.of(2), obj.getTheList());
assertTrue(obj.isTheBoolean());
}
@Test
public void noStaticBuilder() {
boolean hasStaticBuilder = Stream.of(NoStaticBuilderBuilder.class.getDeclaredMethods())
.anyMatch(method -> method.getName().equals("NoStaticBuilder"));
assertFalse(hasStaticBuilder);
hasStaticBuilder = Stream.of(SimpleRecordBuilder.class.getDeclaredMethods())
.anyMatch(method -> method.getName().equals("SimpleRecord"));
assertTrue(hasStaticBuilder);
}
}

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>29</version>
<version>34-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>