Compare commits
76 Commits
record-bui
...
record-bui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24edc5e70c | ||
|
|
3832cb3881 | ||
|
|
2beafc4803 | ||
|
|
82b3925618 | ||
|
|
802dd1f880 | ||
|
|
ba90e6cdca | ||
|
|
07e52035ee | ||
|
|
07fc606147 | ||
|
|
677813e875 | ||
|
|
7d877963fb | ||
|
|
96bb6ef9f3 | ||
|
|
6f3046f507 | ||
|
|
7564643556 | ||
|
|
dfd76fc58b | ||
|
|
04e9135591 | ||
|
|
b512a6e968 | ||
|
|
5a1cd35320 | ||
|
|
a615e3abb6 | ||
|
|
3f8bb47cbf | ||
|
|
5a8e72f0e9 | ||
|
|
44ad4531b6 | ||
|
|
7e78d32780 | ||
|
|
0dc4aa7657 | ||
|
|
b6d9a6202f | ||
|
|
b21368f32f | ||
|
|
501da86afd | ||
|
|
13d867e6e6 | ||
|
|
a1206fa57f | ||
|
|
b89722ebfe | ||
|
|
75163f53ed | ||
|
|
9855e7b504 | ||
|
|
aa3bdedf28 | ||
|
|
6d5e15baa1 | ||
|
|
9c8e3626ba | ||
|
|
24b85e7ad5 | ||
|
|
861e2e745a | ||
|
|
1fc7c9a4b3 | ||
|
|
570514e077 | ||
|
|
9d8b9e65bc | ||
|
|
93d6204b76 | ||
|
|
15e5bfccc6 | ||
|
|
67c54244c5 | ||
|
|
870ac4a9d9 | ||
|
|
9ee8b5912a | ||
|
|
6d9bcf27da | ||
|
|
44db5fdf17 | ||
|
|
90a65235a9 | ||
|
|
7e8ddbd700 | ||
|
|
3a534fbea9 | ||
|
|
6d7ebe2545 | ||
|
|
f1e47391c8 | ||
|
|
c999d0ba06 | ||
|
|
e0243c8b1c | ||
|
|
65bbbaea05 | ||
|
|
5ae03a2c66 | ||
|
|
437e314799 | ||
|
|
a870beee21 | ||
|
|
5b879743ef | ||
|
|
c7bdafb0b9 | ||
|
|
d67c62ed3b | ||
|
|
39cf2b0353 | ||
|
|
6813b88f8d | ||
|
|
54662d69c7 | ||
|
|
7811ff8823 | ||
|
|
4c5076690d | ||
|
|
39a800f10e | ||
|
|
610081b27e | ||
|
|
1eb91d612e | ||
|
|
7c84f26972 | ||
|
|
82cc4f4cad | ||
|
|
f16e1b1d0e | ||
|
|
c92bf78ec5 | ||
|
|
4c4baa015f | ||
|
|
81b7b93a5b | ||
|
|
c39983e342 | ||
|
|
400caa2943 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: Randgalt
|
||||
24
.github/workflows/maven.yml
vendored
Normal file
24
.github/workflows/maven.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# This workflow will build a Java project with Maven
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
|
||||
|
||||
name: Maven Build - Java 16
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 16.0.0-ea
|
||||
- name: Build with Maven
|
||||
run: mvn -B package --file pom.xml
|
||||
28
.github/workflows/maven_java15.yml
vendored
Normal file
28
.github/workflows/maven_java15.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# This workflow will build a Java project with Maven
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
|
||||
|
||||
name: Maven Build - Java 15
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 15
|
||||
- name: Create Maven Directory
|
||||
run: mkdir -p .mvn/
|
||||
- name: Create Maven JVM file
|
||||
run: echo "--enable-preview" > .mvn/jvm.config
|
||||
- name: Build with Maven
|
||||
run: mvn -P java15 -B package --file pom.xml
|
||||
@@ -1 +0,0 @@
|
||||
--enable-preview
|
||||
@@ -1,3 +0,0 @@
|
||||
language: java
|
||||
jdk:
|
||||
- openjdk15
|
||||
223
README.md
223
README.md
@@ -1,13 +1,13 @@
|
||||
[](https://travis-ci.org/Randgalt/record-builder)
|
||||
[](https://search.maven.org/search?q=g:io.soabase.record-builder%20a:record-builder)
|
||||
[](https://github.com/Randgalt/record-builder/actions)
|
||||
[](https://search.maven.org/search?q=g:io.soabase.record-builder%20a:record-builder)
|
||||
|
||||
# RecordBuilder - Early Access
|
||||
# RecordBuilder
|
||||
|
||||
## What is RecordBuilder
|
||||
|
||||
Java 15 introduced [Records](https://cr.openjdk.java.net/~briangoetz/amber/datum.html) as a preview feature. Since Java 9,
|
||||
features in Java are being released in stages. While the Java 15 version of records is fantastic, it's currently missing important features
|
||||
for data classes: a builder and "with"ers. This project is an annotation processor that creates:
|
||||
Java 16 introduces [Records](https://openjdk.java.net/jeps/395). While this version of records is fantastic,
|
||||
it's currently missing some important features normally found in data classes: a builder
|
||||
and "with"ers. This project is an annotation processor that creates:
|
||||
|
||||
- a companion builder class for Java records
|
||||
- an interface that adds "with" copy methods
|
||||
@@ -19,6 +19,10 @@ _Details:_
|
||||
- [Wither Details](#Wither-Example)
|
||||
- [RecordBuilder Full Definition](#Builder-Class-Definition)
|
||||
- [Record From Interface Details](#RecordInterface-Example)
|
||||
- [Generation Via Includes](#generation-via-includes)
|
||||
- [Usage](#usage)
|
||||
- [Customizing](#customizing)
|
||||
- [Java 15 Versions](#java-15-versions)
|
||||
|
||||
## RecordBuilder Example
|
||||
|
||||
@@ -31,16 +35,21 @@ This will generate a builder class that can be used ala:
|
||||
|
||||
```java
|
||||
// build from components
|
||||
var n1 = NameAndAgeBuilder.builder().name(aName).age(anAge).build();
|
||||
NameAndAge n1 = NameAndAgeBuilder.builder().name(aName).age(anAge).build();
|
||||
|
||||
// generate a copy with a changed value
|
||||
var n2 = NameAndAgeBuilder.builder(n1).age(newAge).build(); // name is the same as the name in n1
|
||||
NameAndAge n2 = NameAndAgeBuilder.builder(n1).age(newAge).build(); // name is the same as the name in n1
|
||||
|
||||
// pass to other methods to set components
|
||||
var builder = new NameAndAgeBuilder();
|
||||
setName(builder);
|
||||
setAge(builder);
|
||||
var n3 = builder.build();
|
||||
NameAndAge n3 = builder.build();
|
||||
|
||||
// use the generated static constructor/builder
|
||||
import static NameAndAgeBuilder.NameAndAge;
|
||||
...
|
||||
var n4 = NameAndAge("hey", 42);
|
||||
```
|
||||
|
||||
## Wither Example
|
||||
@@ -53,12 +62,15 @@ public record NameAndAge(String name, int age) implements NameAndAgeBuilder.With
|
||||
In addition to creating a builder, your record is enhanced by "wither" methods ala:
|
||||
|
||||
```java
|
||||
var r1 = new NameAndAge("foo", 123);
|
||||
var r2 = r1.withName("bar");
|
||||
var r3 = r2.withAge(456);
|
||||
NameAndAge r1 = new NameAndAge("foo", 123);
|
||||
NameAndAge r2 = r1.withName("bar");
|
||||
NameAndAge r3 = r2.withAge(456);
|
||||
|
||||
// access the builder as well:
|
||||
var r4 = r3.with().age(101).name("baz").build();
|
||||
// access the builder as well
|
||||
NameAndAge r4 = r3.with().age(101).name("baz").build();
|
||||
|
||||
// alternate method of accessing the builder (note: no need to call "build()")
|
||||
NameAndAge r5 = r4.with(b -> b.age(200).name("whatever"));
|
||||
```
|
||||
|
||||
_Hat tip to [Benji Weber](https://benjiweber.co.uk/blog/2020/09/19/fun-with-java-records/) for the Withers idea._
|
||||
@@ -81,6 +93,13 @@ public class NameAndAgeBuilder {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new builder with all fields set to default Java values
|
||||
*/
|
||||
@@ -157,31 +176,44 @@ public class NameAndAgeBuilder {
|
||||
&& (age == b.age));
|
||||
}
|
||||
|
||||
/**
|
||||
* Downcast to {@code NameAndAge}
|
||||
*/
|
||||
private static NameAndAge _downcast(Object obj) {
|
||||
try {
|
||||
return (NameAndAge)obj;
|
||||
}
|
||||
catch (ClassCastException dummy) {
|
||||
throw new RuntimeException("NameAndAgeBuilder.With can only be implemented for NameAndAge");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add withers to {@code NameAndAge}
|
||||
*/
|
||||
public interface With {
|
||||
/**
|
||||
* Cast this to the record type
|
||||
*/
|
||||
default NameAndAge internalGetThis() {
|
||||
Object obj = this;
|
||||
return (PersonRecord)obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new record builder using the current values
|
||||
*/
|
||||
default NameAndAgeBuilder with() {
|
||||
NameAndAge r = internalGetThis();
|
||||
NameAndAge r = _downcast(this);
|
||||
return NameAndAgeBuilder.builder(r);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
NameAndAge r = internalGetThis();
|
||||
NameAndAge r = _downcast(this);
|
||||
return new NameAndAge(name, r.age());
|
||||
}
|
||||
|
||||
@@ -189,7 +221,7 @@ public class NameAndAgeBuilder {
|
||||
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
|
||||
*/
|
||||
default NameAndAge withAge(int age) {
|
||||
NameAndAge r = internalGetThis();
|
||||
NameAndAge r = _downcast(this);
|
||||
return new NameAndAge(r.name(), age);
|
||||
}
|
||||
}
|
||||
@@ -210,7 +242,8 @@ This will generate a record ala:
|
||||
|
||||
```java
|
||||
@RecordBuilder
|
||||
public record NameAndAgeRecord(String name, int age) implements NameAndAge {}
|
||||
public record NameAndAgeRecord(String name, int age) implements
|
||||
NameAndAge, NameAndAgeRecordBuilder.With {}
|
||||
```
|
||||
|
||||
Note that the generated record is annotated with `@RecordBuilder` so a record
|
||||
@@ -224,23 +257,51 @@ Notes:
|
||||
- ...cannot have type parameters
|
||||
- Methods with default implementations are used in the generation unless they are annotated with `@IgnoreDefaultMethod`
|
||||
- If you do not want a record builder generated, annotate your interface as `@RecordInterface(addRecordBuilder = false)`
|
||||
- If your interface is a JavaBean (e.g. `getThing()`, `isThing()`) the "get" and "is" prefixes are
|
||||
stripped and forwarding methods are added.
|
||||
|
||||
## Generation Via Includes
|
||||
|
||||
An alternate method of generation is to use the Include variants of the annotations. These variants
|
||||
act on lists of specified classes. This allows the source classes to be pristine or even come from
|
||||
libraries where you are not able to annotate the source.
|
||||
|
||||
E.g.
|
||||
|
||||
```
|
||||
import some.library.code.ImportedRecord
|
||||
import some.library.code.ImportedInterface
|
||||
|
||||
@RecordBuilder.Include({
|
||||
ImportedRecord.class // generates a record builder for ImportedRecord
|
||||
})
|
||||
@RecordInterface.Include({
|
||||
ImportedInterface.class // generates a record interface for ImportedInterface
|
||||
})
|
||||
public void Placeholder {
|
||||
}
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
## Usage
|
||||
|
||||
### Maven
|
||||
|
||||
1\. Add the dependency that contains the `@RecordBuilder` annotation.
|
||||
1) Add the dependency that contains the `@RecordBuilder` annotation.
|
||||
|
||||
```
|
||||
<dependency>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-core</artifactId>
|
||||
<version>set-version-here</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
```
|
||||
|
||||
2\. Enable the annotation processing for the Maven Compiler Plugin:
|
||||
2) Enable the annotation processing for the Maven Compiler Plugin:
|
||||
|
||||
```
|
||||
<plugin>
|
||||
@@ -260,21 +321,11 @@ Notes:
|
||||
</annotationProcessors>
|
||||
|
||||
|
||||
<!-- "release" and "enable-preview" are required while records are preview features -->
|
||||
<release>15</release>
|
||||
<compilerArgs>
|
||||
<arg>--enable-preview</arg>
|
||||
</compilerArgs>
|
||||
|
||||
... any other options here ...
|
||||
</configuration>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
3\. Enable Preview for Maven
|
||||
|
||||
Create a file in your project's root named `.mvn/jvm.config`. The file should have 1 line with the value: `--enable-preview`. (see: https://stackoverflow.com/questions/58023240)
|
||||
|
||||
### Gradle
|
||||
|
||||
Add the following to your build.gradle file:
|
||||
@@ -282,16 +333,7 @@ Add the following to your build.gradle file:
|
||||
```
|
||||
dependencies {
|
||||
annotationProcessor 'io.soabase.record-builder:record-builder-processor:$version-goes-here'
|
||||
implementation 'io.soabase.record-builder:record-builder-core:$version-goes-here'
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.fork = true
|
||||
options.forkOptions.jvmArgs += '--enable-preview'
|
||||
options.compilerArgs += '--enable-preview'
|
||||
}
|
||||
tasks.withType(Test) {
|
||||
jvmArgs += "--enable-preview"
|
||||
compileOnly 'io.soabase.record-builder:record-builder-core:$version-goes-here'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -299,16 +341,6 @@ tasks.withType(Test) {
|
||||
|
||||
Depending on your IDE you are likely to need to enable Annotation Processing in your IDE settings.
|
||||
|
||||
## Enable Preview
|
||||
|
||||
Note: records are a preview feature only. You'll need take a number of steps in order to try RecordBuilder:
|
||||
|
||||
- Install and make active Java 15 or later
|
||||
- Make sure your development tool is using Java 15 or later and is configured to enable preview features (for Maven I've documented how to do this here: [https://stackoverflow.com/a/59363152/2048051](https://stackoverflow.com/a/59363152/2048051))
|
||||
- Bear in mind that this is not yet meant for production and there are numerous bugs in the tools and JDKs.
|
||||
|
||||
Note: I've seen some very odd compilation bugs with the current Java 15 and Maven. If you get internal Javac errors I suggest rebuilding with `mvn clean package` and/or `mvn clean install`.
|
||||
|
||||
## Customizing
|
||||
|
||||
The names of the generated methods, etc. are determined by [RecordBuilderMetaData](https://github.com/Randgalt/record-builder/blob/master/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderMetaData.java). If you want to use your own meta data instance:
|
||||
@@ -327,7 +359,80 @@ Alternatively, you can provide values for each individual meta data (or combinat
|
||||
- `javac ... -AcomponentsMethodName=foo`
|
||||
- `javac ... -AwithClassName=foo`
|
||||
- `javac ... -AwithClassMethodPrefix=foo`
|
||||
- `javac ... -AwithClassGetThisMethodName=foo`
|
||||
- `javac ... -AfileComment=foo`
|
||||
- `javac ... -AfileIndent=foo`
|
||||
- `javac ... -AprefixEnclosingClassNames=foo`
|
||||
|
||||
## Java 15 Versions
|
||||
|
||||
Artifacts compiled wth Java 15 are available. They are the same versions
|
||||
as the Java 16 versions with `-java15` appended.
|
||||
|
||||
Note: records are a preview feature only in Java 15. You'll need take a number of steps in order to try RecordBuilder:
|
||||
|
||||
- Install and make active Java 15 or later
|
||||
- Make sure your development tool is using Java 15 or later and is configured to enable preview features (for Maven I've documented how to do this here: [https://stackoverflow.com/a/59363152/2048051](https://stackoverflow.com/a/59363152/2048051))
|
||||
- Bear in mind that this is not yet meant for production and there are numerous bugs in the tools and JDKs.
|
||||
|
||||
Note: I've seen some very odd compilation bugs with the current Java 15 and Maven. If you get internal Javac errors I suggest rebuilding with `mvn clean package` and/or `mvn clean install`.
|
||||
|
||||
You will need to enable preview in your build tools:
|
||||
|
||||
### Maven
|
||||
|
||||
```
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-core</artifactId>
|
||||
<version>record-builder-version-java15</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>maven-compiler-version</version>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<annotationProcessorPath>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-processor</artifactId>
|
||||
<version>record-builder-version-java15</version>
|
||||
</annotationProcessorPath>
|
||||
</annotationProcessorPaths>
|
||||
<annotationProcessors>
|
||||
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
|
||||
</annotationProcessors>
|
||||
|
||||
|
||||
<!-- "release" and "enable-preview" are required while records are preview features -->
|
||||
<release>15</release>
|
||||
<compilerArgs>
|
||||
<arg>--enable-preview</arg>
|
||||
</compilerArgs>
|
||||
|
||||
... any other options here ...
|
||||
</configuration>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
Create a file in your project's root named `.mvn/jvm.config`. The file should have 1 line with the value: `--enable-preview`. (see: https://stackoverflow.com/questions/58023240)
|
||||
|
||||
### Gradle
|
||||
|
||||
```
|
||||
dependencies {
|
||||
annotationProcessor 'io.soabase.record-builder:record-builder-processor:$record-builder-version-java15'
|
||||
compileOnly 'io.soabase.record-builder:record-builder-core:$record-builder-version-java15'
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.fork = true
|
||||
options.forkOptions.jvmArgs += '--enable-preview'
|
||||
options.compilerArgs += '--enable-preview'
|
||||
}
|
||||
tasks.withType(Test) {
|
||||
jvmArgs += "--enable-preview"
|
||||
}
|
||||
```
|
||||
|
||||
22
java15.sh
Executable file
22
java15.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
jenv local 15
|
||||
javahome
|
||||
mkdir -p .mvn/
|
||||
echo "--enable-preview" > .mvn/jvm.config
|
||||
21
java16.sh
Executable file
21
java16.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
jenv local 16-ea
|
||||
javahome
|
||||
rm -fr .mvn
|
||||
20
pom.xml
20
pom.xml
@@ -5,7 +5,7 @@
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>1.8.ea</version>
|
||||
<version>1.19</version>
|
||||
|
||||
<modules>
|
||||
<module>record-builder-core</module>
|
||||
@@ -18,7 +18,9 @@
|
||||
<project.build.resourceEncoding>UTF-8</project.build.resourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
|
||||
<jdk-version>15</jdk-version>
|
||||
<enable-preview />
|
||||
|
||||
<jdk-version>16</jdk-version>
|
||||
|
||||
<maven-compiler-plugin-version>3.8.1</maven-compiler-plugin-version>
|
||||
<maven-source-plugin-version>3.2.0</maven-source-plugin-version>
|
||||
@@ -71,7 +73,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-1.8.ea</tag>
|
||||
<tag>record-builder-1.19</tag>
|
||||
</scm>
|
||||
|
||||
<issueManagement>
|
||||
@@ -124,7 +126,7 @@
|
||||
<configuration>
|
||||
<release>${jdk-version}</release>
|
||||
<compilerArgs>
|
||||
<arg>--enable-preview</arg>
|
||||
<arg>${enable-preview}</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
@@ -278,7 +280,7 @@
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin-version}</version>
|
||||
<configuration>
|
||||
<argLine>--enable-preview</argLine>
|
||||
<argLine>${enable-preview}</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
@@ -342,5 +344,13 @@
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>java15</id>
|
||||
<properties>
|
||||
<jdk-version>15</jdk-version>
|
||||
<enable-preview>--enable-preview</enable-preview>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.8.ea</version>
|
||||
<version>1.19</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -23,4 +23,19 @@ import java.lang.annotation.Target;
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface RecordBuilder {
|
||||
@Target({ElementType.TYPE, ElementType.PACKAGE})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface Include {
|
||||
Class<?>[] value();
|
||||
|
||||
/**
|
||||
* Pattern used to generate the package for the generated class. The value
|
||||
* is the literal package name however two replacement values can be used. '@'
|
||||
* is replaced with the package of the Include annotation. '*' is replaced with
|
||||
* the package of the included class.
|
||||
*
|
||||
* @return package pattern
|
||||
*/
|
||||
String packagePattern() default "@";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,15 @@ public interface RecordBuilderMetaData {
|
||||
return "build";
|
||||
}
|
||||
|
||||
/**
|
||||
* The name to use for the downcast method
|
||||
*
|
||||
* @return downcast method
|
||||
*/
|
||||
default String downCastMethodName() {
|
||||
return "_downcast";
|
||||
}
|
||||
|
||||
/**
|
||||
* The name to use for the method that returns the record components as a stream
|
||||
*
|
||||
@@ -105,15 +114,6 @@ public interface RecordBuilderMetaData {
|
||||
return "with";
|
||||
}
|
||||
|
||||
/**
|
||||
* The name to use for the method that returns "this" cast to the record type
|
||||
*
|
||||
* @return method name
|
||||
*/
|
||||
default String withClassGetThisMethodName() {
|
||||
return "internalGetThis";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the comment to place at the top of generated files. Return null or an empty string for no comment.
|
||||
*
|
||||
|
||||
@@ -24,4 +24,22 @@ import java.lang.annotation.Target;
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface RecordInterface {
|
||||
boolean addRecordBuilder() default true;
|
||||
|
||||
@Target({ElementType.TYPE, ElementType.PACKAGE})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface Include {
|
||||
Class<?>[] value();
|
||||
|
||||
boolean addRecordBuilder() default true;
|
||||
|
||||
/**
|
||||
* Pattern used to generate the package for the generated class. The value
|
||||
* is the literal package name however two replacement values can be used. '@'
|
||||
* is replaced with the package of the Include annotation. '*' is replaced with
|
||||
* the package of the included class.
|
||||
*
|
||||
* @return package pattern
|
||||
*/
|
||||
String packagePattern() default "@";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.8.ea</version>
|
||||
<version>1.19</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -20,14 +20,63 @@ import com.squareup.javapoet.ParameterizedTypeName;
|
||||
import com.squareup.javapoet.TypeName;
|
||||
import com.squareup.javapoet.TypeVariableName;
|
||||
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.AnnotationMirror;
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.RecordComponentElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.TypeParameterElement;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ElementUtils {
|
||||
public static Optional<? extends AnnotationMirror> findAnnotationMirror(ProcessingEnvironment processingEnv, Element element, String annotationClass) {
|
||||
return processingEnv.getElementUtils().getAllAnnotationMirrors(element).stream()
|
||||
.filter(e -> e.getAnnotationType().toString().equals(annotationClass))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
public static Optional<? extends AnnotationValue> getAnnotationValue(Map<? extends ExecutableElement, ? extends AnnotationValue> values, String name) {
|
||||
return values.entrySet()
|
||||
.stream()
|
||||
.filter(e -> e.getKey().getSimpleName().toString().equals(name))
|
||||
.map(Map.Entry::getValue)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static List<TypeMirror> getClassesAttribute(AnnotationValue attribute)
|
||||
{
|
||||
List<? extends AnnotationValue> values = (attribute != null) ? (List<? extends AnnotationValue>)attribute.getValue() : Collections.emptyList();
|
||||
return values.stream().map(v -> (TypeMirror)v.getValue()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static boolean getBooleanAttribute(AnnotationValue attribute)
|
||||
{
|
||||
Object value = (attribute != null) ? attribute.getValue() : null;
|
||||
if ( value != null )
|
||||
{
|
||||
return Boolean.parseBoolean(String.valueOf(value));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String getStringAttribute(AnnotationValue attribute, String defaultValue)
|
||||
{
|
||||
Object value = (attribute != null) ? attribute.getValue() : null;
|
||||
if ( value != null )
|
||||
{
|
||||
return String.valueOf(value);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static String getPackageName(TypeElement typeElement) {
|
||||
while (typeElement.getNestingKind().isNested()) {
|
||||
Element enclosingElement = typeElement.getEnclosingElement();
|
||||
@@ -37,8 +86,9 @@ public class ElementUtils {
|
||||
break;
|
||||
}
|
||||
}
|
||||
String name = typeElement.getEnclosingElement().toString();
|
||||
return !name.equals("unnamed package") ? name : "";
|
||||
String name = typeElement.getQualifiedName().toString();
|
||||
int index = name.lastIndexOf(".");
|
||||
return (index > -1) ? name.substring(0, index) : "";
|
||||
}
|
||||
|
||||
public static ClassType getClassType(String packageName, String simpleName, List<? extends TypeParameterElement> typeParameters) {
|
||||
|
||||
@@ -15,15 +15,26 @@
|
||||
*/
|
||||
package io.soabase.recordbuilder.processor;
|
||||
|
||||
import com.squareup.javapoet.*;
|
||||
import com.squareup.javapoet.ClassName;
|
||||
import com.squareup.javapoet.CodeBlock;
|
||||
import com.squareup.javapoet.FieldSpec;
|
||||
import com.squareup.javapoet.MethodSpec;
|
||||
import com.squareup.javapoet.ParameterSpec;
|
||||
import com.squareup.javapoet.ParameterizedTypeName;
|
||||
import com.squareup.javapoet.TypeName;
|
||||
import com.squareup.javapoet.TypeSpec;
|
||||
import com.squareup.javapoet.TypeVariableName;
|
||||
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
|
||||
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
@@ -32,7 +43,8 @@ import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
|
||||
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
|
||||
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
|
||||
|
||||
class InternalRecordBuilderProcessor {
|
||||
class InternalRecordBuilderProcessor
|
||||
{
|
||||
private final RecordBuilderMetaData metaData;
|
||||
private final ClassType recordClassType;
|
||||
private final String packageName;
|
||||
@@ -41,14 +53,17 @@ class InternalRecordBuilderProcessor {
|
||||
private final List<ClassType> recordComponents;
|
||||
private final TypeSpec builderType;
|
||||
private final TypeSpec.Builder builder;
|
||||
private final String uniqueVarName;
|
||||
|
||||
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData) {
|
||||
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData, Optional<String> packageNameOpt)
|
||||
{
|
||||
this.metaData = metaData;
|
||||
recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
|
||||
packageName = ElementUtils.getPackageName(record);
|
||||
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(record));
|
||||
builderClassType = ElementUtils.getClassType(packageName, getBuilderName(record, metaData, recordClassType, metaData.suffix()), record.getTypeParameters());
|
||||
typeVariables = record.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList());
|
||||
recordComponents = record.getRecordComponents().stream().map(ElementUtils::getClassType).collect(Collectors.toList());
|
||||
uniqueVarName = getUniqueVarName();
|
||||
|
||||
builder = TypeSpec.classBuilder(builderClassType.name())
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
@@ -56,7 +71,10 @@ class InternalRecordBuilderProcessor {
|
||||
.addTypeVariables(typeVariables);
|
||||
addWithNestedClass();
|
||||
addDefaultConstructor();
|
||||
addAllArgsConstructor();
|
||||
addStaticBuilder();
|
||||
if (recordComponents.size() > 0) {
|
||||
addAllArgsConstructor();
|
||||
}
|
||||
addStaticDefaultBuilderMethod();
|
||||
addStaticCopyBuilderMethod();
|
||||
addStaticComponentsMethod();
|
||||
@@ -69,22 +87,27 @@ class InternalRecordBuilderProcessor {
|
||||
add1SetterMethod(component);
|
||||
add1GetterMethod(component);
|
||||
});
|
||||
addStaticDowncastMethod();
|
||||
builderType = builder.build();
|
||||
}
|
||||
|
||||
String packageName() {
|
||||
String packageName()
|
||||
{
|
||||
return packageName;
|
||||
}
|
||||
|
||||
ClassType builderClassType() {
|
||||
ClassType builderClassType()
|
||||
{
|
||||
return builderClassType;
|
||||
}
|
||||
|
||||
TypeSpec builderType() {
|
||||
TypeSpec builderType()
|
||||
{
|
||||
return builderType;
|
||||
}
|
||||
|
||||
private void addWithNestedClass() {
|
||||
private void addWithNestedClass()
|
||||
{
|
||||
/*
|
||||
Adds a nested interface that adds withers similar to:
|
||||
|
||||
@@ -94,45 +117,59 @@ class InternalRecordBuilderProcessor {
|
||||
}
|
||||
}
|
||||
*/
|
||||
TypeSpec.Builder classBuilder = TypeSpec.interfaceBuilder(metaData.withClassName())
|
||||
var classBuilder = TypeSpec.interfaceBuilder(metaData.withClassName())
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addTypeVariables(typeVariables);
|
||||
addWithGetThisMethod(classBuilder);
|
||||
addWithBuilderMethod(classBuilder);
|
||||
addWithSuppliedBuilderMethod(classBuilder);
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> add1WithMethod(classBuilder, recordComponents.get(index), index));
|
||||
builder.addType(classBuilder.build());
|
||||
}
|
||||
|
||||
private void addWithGetThisMethod(TypeSpec.Builder classBuilder) {
|
||||
private void addWithSuppliedBuilderMethod(TypeSpec.Builder classBuilder)
|
||||
{
|
||||
/*
|
||||
Adds a method that returns "this" cast to the record similar to:
|
||||
Adds a method that returns a pre-filled copy builder similar to:
|
||||
|
||||
default MyRecord internalGetThis() {
|
||||
Object obj = this;
|
||||
return (MyRecord)obj;
|
||||
default MyRecord with(Consumer<MyRecordBuilder> consumer) {
|
||||
MyRecordBuilder builder = with();
|
||||
consumer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
*/
|
||||
CodeBlock codeBlock = CodeBlock.builder()
|
||||
.add("Object obj = this;\n")
|
||||
.add("return ($T)obj;", recordClassType.typeName())
|
||||
.build();
|
||||
MethodSpec methodSpec = MethodSpec.methodBuilder(metaData.withClassGetThisMethodName())
|
||||
var codeBlockBuilder = CodeBlock.builder()
|
||||
.add("$T builder = with();\n", builderClassType.typeName())
|
||||
.add("consumer.accept(builder);\n")
|
||||
.add("return builder.build();\n");
|
||||
var consumerType = ParameterizedTypeName.get(ClassName.get(Consumer.class), builderClassType.typeName());
|
||||
var parameter = ParameterSpec.builder(consumerType, "consumer").build();
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Cast this to the record type")
|
||||
.addJavadoc("Return a new record built from the builder passed to the given consumer")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
|
||||
.addParameter(parameter)
|
||||
.returns(recordClassType.typeName())
|
||||
.addCode(codeBlock)
|
||||
.addCode(codeBlockBuilder.build())
|
||||
.build();
|
||||
classBuilder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addWithBuilderMethod(TypeSpec.Builder classBuilder) {
|
||||
CodeBlock.Builder codeBlockBuilder = CodeBlock.builder()
|
||||
.add("$T r = $L();\n", recordClassType.typeName(), metaData.withClassGetThisMethodName())
|
||||
.add("return $L.$L(r);", builderClassType.name(), metaData.copyMethodName());
|
||||
MethodSpec methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
|
||||
private void addWithBuilderMethod(TypeSpec.Builder classBuilder)
|
||||
{
|
||||
/*
|
||||
Adds a method that returns a pre-filled copy builder similar to:
|
||||
|
||||
default MyRecordBuilder with() {
|
||||
MyRecord r = _downcast(this);
|
||||
return MyRecordBuilder.builder(r);
|
||||
}
|
||||
*/
|
||||
var codeBlockBuilder = CodeBlock.builder()
|
||||
.add("$T $L = $L(this);\n", recordClassType.typeName(), uniqueVarName, metaData.downCastMethodName())
|
||||
.add("return $L.$L($L);", builderClassType.name(), metaData.copyMethodName(), uniqueVarName);
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Return a new record builder using the current values")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
|
||||
@@ -142,18 +179,35 @@ class InternalRecordBuilderProcessor {
|
||||
classBuilder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void add1WithMethod(TypeSpec.Builder classBuilder, ClassType component, int index) {
|
||||
private String getUniqueVarName()
|
||||
{
|
||||
return getUniqueVarName("");
|
||||
}
|
||||
|
||||
private String getUniqueVarName(String prefix)
|
||||
{
|
||||
var name = prefix + "r";
|
||||
var alreadyExists = recordComponents.stream()
|
||||
.map(ClassType::name)
|
||||
.anyMatch(n -> n.equals(name));
|
||||
return alreadyExists ? getUniqueVarName(prefix + "_") : name;
|
||||
}
|
||||
|
||||
private void add1WithMethod(TypeSpec.Builder classBuilder, ClassType component, int index)
|
||||
{
|
||||
/*
|
||||
Adds a with method for the component similar to:
|
||||
|
||||
default MyRecord withName(String name) {
|
||||
MyRecord r = internalGetThis();
|
||||
MyRecord r = _downcast(this);
|
||||
return new MyRecord(name, r.age());
|
||||
}
|
||||
*/
|
||||
CodeBlock.Builder codeBlockBuilder = CodeBlock.builder()
|
||||
.add("$T r = $L();\n", recordClassType.typeName(), metaData.withClassGetThisMethodName())
|
||||
.add("return new $T(", recordClassType.typeName());
|
||||
var codeBlockBuilder = CodeBlock.builder();
|
||||
if (recordComponents.size() > 1) {
|
||||
codeBlockBuilder.add("$T $L = $L(this);\n", recordClassType.typeName(), uniqueVarName, metaData.downCastMethodName());
|
||||
}
|
||||
codeBlockBuilder.add("return new $T(", recordClassType.typeName());
|
||||
IntStream.range(0, recordComponents.size()).forEach(parameterIndex -> {
|
||||
if (parameterIndex > 0) {
|
||||
codeBlockBuilder.add(", ");
|
||||
@@ -161,15 +215,16 @@ class InternalRecordBuilderProcessor {
|
||||
ClassType parameterComponent = recordComponents.get(parameterIndex);
|
||||
if (parameterIndex == index) {
|
||||
codeBlockBuilder.add(parameterComponent.name());
|
||||
} else {
|
||||
codeBlockBuilder.add("r.$L()", parameterComponent.name());
|
||||
}
|
||||
else {
|
||||
codeBlockBuilder.add("$L.$L()", uniqueVarName, parameterComponent.name());
|
||||
}
|
||||
});
|
||||
codeBlockBuilder.add(");");
|
||||
|
||||
String methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
|
||||
var methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
|
||||
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
|
||||
MethodSpec methodSpec = MethodSpec.methodBuilder(methodName)
|
||||
var methodSpec = MethodSpec.methodBuilder(methodName)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Return a new instance of {@code $L} with a new value for {@code $L}\n", recordClassType.name(), component.name())
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
|
||||
@@ -180,7 +235,8 @@ class InternalRecordBuilderProcessor {
|
||||
classBuilder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addDefaultConstructor() {
|
||||
private void addDefaultConstructor()
|
||||
{
|
||||
/*
|
||||
Adds a default constructor similar to:
|
||||
|
||||
@@ -194,7 +250,29 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(constructor);
|
||||
}
|
||||
|
||||
private void addAllArgsConstructor() {
|
||||
private void addStaticBuilder()
|
||||
{
|
||||
/*
|
||||
Adds an static builder similar to:
|
||||
|
||||
public static MyRecord(int p1, T p2, ...) {
|
||||
return new MyRecord(p1, p2, ...);
|
||||
}
|
||||
*/
|
||||
CodeBlock codeBlock = buildCodeBlock();
|
||||
var builder = MethodSpec.methodBuilder(recordClassType.name())
|
||||
.addJavadoc("Static constructor/builder. Can be used instead of new $L(...)\n", recordClassType.name())
|
||||
.addTypeVariables(typeVariables)
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.returns(recordClassType.typeName())
|
||||
.addStatement(codeBlock);
|
||||
recordComponents.forEach(component -> builder.addParameter(component.typeName(), component.name()));
|
||||
this.builder.addMethod(builder.build());
|
||||
}
|
||||
|
||||
private void addAllArgsConstructor()
|
||||
{
|
||||
/*
|
||||
Adds an all-args constructor similar to:
|
||||
|
||||
@@ -215,7 +293,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(constructorBuilder.build());
|
||||
}
|
||||
|
||||
private void addToStringMethod() {
|
||||
private void addToStringMethod()
|
||||
{
|
||||
/*
|
||||
add a toString() method similar to:
|
||||
|
||||
@@ -244,7 +323,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addHashCodeMethod() {
|
||||
private void addHashCodeMethod()
|
||||
{
|
||||
/*
|
||||
add a hashCode() method similar to:
|
||||
|
||||
@@ -272,7 +352,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addEqualsMethod() {
|
||||
private void addEqualsMethod()
|
||||
{
|
||||
/*
|
||||
add an equals() method similar to:
|
||||
|
||||
@@ -285,13 +366,14 @@ class InternalRecordBuilderProcessor {
|
||||
*/
|
||||
var codeBuilder = CodeBlock.builder();
|
||||
codeBuilder.add("return (this == o) || (");
|
||||
codeBuilder.add("(o instanceof $L b)", builderClassType.name());
|
||||
codeBuilder.add("(o instanceof $L $L)", builderClassType.name(), uniqueVarName);
|
||||
recordComponents.forEach(recordComponent -> {
|
||||
String name = recordComponent.name();
|
||||
if (recordComponent.typeName().isPrimitive()) {
|
||||
codeBuilder.add("\n&& ($L == b.$L)", name, name);
|
||||
} else {
|
||||
codeBuilder.add("\n&& $T.equals($L, b.$L)", Objects.class, name, name);
|
||||
codeBuilder.add("\n&& ($L == $L.$L)", name, uniqueVarName, name);
|
||||
}
|
||||
else {
|
||||
codeBuilder.add("\n&& $T.equals($L, $L.$L)", Objects.class, name, uniqueVarName, name);
|
||||
}
|
||||
});
|
||||
codeBuilder.add(")");
|
||||
@@ -307,7 +389,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addBuildMethod() {
|
||||
private void addBuildMethod()
|
||||
{
|
||||
/*
|
||||
Adds the build method that generates the record similar to:
|
||||
|
||||
@@ -315,6 +398,22 @@ class InternalRecordBuilderProcessor {
|
||||
return new MyRecord(p1, p2, ...);
|
||||
}
|
||||
*/
|
||||
CodeBlock codeBlock = buildCodeBlock();
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.buildMethodName())
|
||||
.addJavadoc("Return a new record instance with all fields set to the current values in this builder\n")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.returns(recordClassType.typeName())
|
||||
.addStatement(codeBlock)
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private CodeBlock buildCodeBlock() {
|
||||
/*
|
||||
Builds the code block for allocating the record from its parts
|
||||
*/
|
||||
|
||||
var codeBuilder = CodeBlock.builder().add("return new $T(", recordClassType.typeName());
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> {
|
||||
if (index > 0) {
|
||||
@@ -323,18 +422,11 @@ class InternalRecordBuilderProcessor {
|
||||
codeBuilder.add("$L", recordComponents.get(index).name());
|
||||
});
|
||||
codeBuilder.add(")");
|
||||
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.buildMethodName())
|
||||
.addJavadoc("Return a new record instance with all fields set to the current values in this builder\n")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.returns(recordClassType.typeName())
|
||||
.addStatement(codeBuilder.build())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
return codeBuilder.build();
|
||||
}
|
||||
|
||||
private void addStaticCopyBuilderMethod() {
|
||||
private void addStaticCopyBuilderMethod()
|
||||
{
|
||||
/*
|
||||
Adds a copy builder method that pre-fills the builder with existing values similar to:
|
||||
|
||||
@@ -363,7 +455,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addStaticDefaultBuilderMethod() {
|
||||
private void addStaticDefaultBuilderMethod()
|
||||
{
|
||||
/*
|
||||
Adds a the default builder method similar to:
|
||||
|
||||
@@ -382,7 +475,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addStaticComponentsMethod() {
|
||||
private void addStaticComponentsMethod()
|
||||
{
|
||||
/*
|
||||
Adds a static method that converts a record instance into a stream of its component parts
|
||||
|
||||
@@ -416,7 +510,40 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void add1Field(ClassType component) {
|
||||
private void addStaticDowncastMethod()
|
||||
{
|
||||
/*
|
||||
Adds a method that downcasts to the record type
|
||||
|
||||
private static MyRecord _downcast(Object this) {
|
||||
return (MyRecord)this;
|
||||
}
|
||||
*/
|
||||
var codeBlockBuilder = CodeBlock.builder()
|
||||
.add("try {\n")
|
||||
.indent()
|
||||
.add("return ($T)obj;\n", recordClassType.typeName())
|
||||
.unindent()
|
||||
.add("}\n")
|
||||
.add("catch (ClassCastException dummy) {\n")
|
||||
.indent()
|
||||
.add("throw new RuntimeException($S);\n", builderClassType.name() + "." + metaData.withClassName() + " can only be implemented by " + recordClassType.name())
|
||||
.unindent()
|
||||
.add("}");
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.downCastMethodName())
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Downcast to {@code $L}\n", recordClassType.name())
|
||||
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
|
||||
.addParameter(Object.class, "obj")
|
||||
.addTypeVariables(typeVariables)
|
||||
.returns(recordClassType.typeName())
|
||||
.addCode(codeBlockBuilder.build())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void add1Field(ClassType component)
|
||||
{
|
||||
/*
|
||||
For a single record component, add a field similar to:
|
||||
|
||||
@@ -426,7 +553,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addField(fieldSpec);
|
||||
}
|
||||
|
||||
private void add1GetterMethod(ClassType component) {
|
||||
private void add1GetterMethod(ClassType component)
|
||||
{
|
||||
/*
|
||||
For a single record component, add a getter similar to:
|
||||
|
||||
@@ -444,7 +572,8 @@ class InternalRecordBuilderProcessor {
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void add1SetterMethod(ClassType component) {
|
||||
private void add1SetterMethod(ClassType component)
|
||||
{
|
||||
/*
|
||||
For a single record component, add a setter similar to:
|
||||
|
||||
|
||||
@@ -15,12 +15,14 @@
|
||||
*/
|
||||
package io.soabase.recordbuilder.processor;
|
||||
|
||||
import com.squareup.javapoet.*;
|
||||
import com.squareup.javapoet.ClassName;
|
||||
import com.squareup.javapoet.MethodSpec;
|
||||
import com.squareup.javapoet.ParameterSpec;
|
||||
import com.squareup.javapoet.TypeSpec;
|
||||
import com.squareup.javapoet.TypeVariableName;
|
||||
import io.soabase.recordbuilder.core.IgnoreDefaultMethod;
|
||||
import io.soabase.recordbuilder.core.RecordBuilder;
|
||||
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
|
||||
import io.soabase.recordbuilder.core.RecordInterface;
|
||||
import io.soabase.recordbuilder.core.IgnoreDefaultMethod;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.ElementKind;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
@@ -28,7 +30,13 @@ import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.type.TypeKind;
|
||||
import javax.tools.Diagnostic;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -40,15 +48,20 @@ class InternalRecordInterfaceProcessor {
|
||||
private final ProcessingEnvironment processingEnv;
|
||||
private final String packageName;
|
||||
private final TypeSpec recordType;
|
||||
private final List<ExecutableElement> recordComponents;
|
||||
private final List<Component> recordComponents;
|
||||
private final TypeElement iface;
|
||||
private final ClassType recordClassType;
|
||||
private final List<String> alternateMethods;
|
||||
|
||||
private static final String FAKE_METHOD_NAME = "__FAKE__";
|
||||
|
||||
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, RecordInterface recordInterface, RecordBuilderMetaData metaData) {
|
||||
private static final Set<String> javaBeanPrefixes = Set.of("get", "is");
|
||||
|
||||
private record Component(ExecutableElement element, Optional<String> alternateName){}
|
||||
|
||||
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageNameOpt) {
|
||||
this.processingEnv = processingEnv;
|
||||
packageName = ElementUtils.getPackageName(iface);
|
||||
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(iface));
|
||||
recordComponents = getRecordComponents(iface);
|
||||
this.iface = iface;
|
||||
|
||||
@@ -65,10 +78,14 @@ class InternalRecordInterfaceProcessor {
|
||||
.addAnnotation(generatedRecordInterfaceAnnotation)
|
||||
.addTypeVariables(typeVariables);
|
||||
|
||||
if (recordInterface.addRecordBuilder()) {
|
||||
if (addRecordBuilder) {
|
||||
ClassType builderClassType = ElementUtils.getClassType(packageName, getBuilderName(iface, metaData, recordClassType, metaData.suffix()) + "." + metaData.withClassName(), iface.getTypeParameters());
|
||||
builder.addAnnotation(RecordBuilder.class);
|
||||
builder.addSuperinterface(builderClassType.typeName());
|
||||
}
|
||||
|
||||
alternateMethods = buildAlternateMethods(recordComponents);
|
||||
|
||||
recordType = builder.build();
|
||||
}
|
||||
|
||||
@@ -116,22 +133,43 @@ class InternalRecordInterfaceProcessor {
|
||||
String declaration = matcher.group(1).trim().replace("class", "record");
|
||||
String implementsSection = matcher.group(2).trim();
|
||||
String argumentList = matcher.group(5).trim();
|
||||
return declaration + argumentList + " " + implementsSection + " {}";
|
||||
|
||||
StringBuilder fixedRecord = new StringBuilder(declaration).append(argumentList).append(' ').append(implementsSection).append(" {");
|
||||
alternateMethods.forEach(method -> fixedRecord.append('\n').append(method));
|
||||
fixedRecord.append('}');
|
||||
return fixedRecord.toString();
|
||||
}
|
||||
|
||||
private MethodSpec generateArgumentList()
|
||||
{
|
||||
MethodSpec.Builder builder = MethodSpec.methodBuilder(FAKE_METHOD_NAME);
|
||||
recordComponents.forEach(element -> {
|
||||
ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(element.getReturnType()), element.getSimpleName().toString()).build();
|
||||
builder.addTypeVariables(element.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList()));
|
||||
recordComponents.forEach(component -> {
|
||||
String name = component.alternateName.orElseGet(() -> component.element.getSimpleName().toString());
|
||||
ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(component.element.getReturnType()), name).build();
|
||||
builder.addTypeVariables(component.element.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList()));
|
||||
builder.addParameter(parameterSpec);
|
||||
});
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private List<ExecutableElement> getRecordComponents(TypeElement iface) {
|
||||
List<ExecutableElement> components = new ArrayList<>();
|
||||
private List<String> buildAlternateMethods(List<Component> recordComponents) {
|
||||
return recordComponents.stream()
|
||||
.filter(component -> component.alternateName.isPresent())
|
||||
.map(component -> {
|
||||
var method = MethodSpec.methodBuilder(component.element.getSimpleName().toString())
|
||||
.addAnnotation(Override.class)
|
||||
.addAnnotation(generatedRecordInterfaceAnnotation)
|
||||
.returns(ClassName.get(component.element.getReturnType()))
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addCode("return $L();", component.alternateName.get())
|
||||
.build();
|
||||
return method.toString();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<Component> getRecordComponents(TypeElement iface) {
|
||||
List<Component> components = new ArrayList<>();
|
||||
try {
|
||||
getRecordComponents(iface, components, new HashSet<>(), new HashSet<>());
|
||||
if (components.isEmpty()) {
|
||||
@@ -143,15 +181,15 @@ class InternalRecordInterfaceProcessor {
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
private static class IllegalInterface extends RuntimeException
|
||||
{
|
||||
public IllegalInterface(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void getRecordComponents(TypeElement iface, Collection<? super ExecutableElement> components, Set<String> visitedSet, Set<String> usedNames) {
|
||||
private void getRecordComponents(TypeElement iface, Collection<Component> components, Set<String> visitedSet, Set<String> usedNames) {
|
||||
if (!visitedSet.add(iface.getQualifiedName().toString())) {
|
||||
return;
|
||||
}
|
||||
@@ -174,10 +212,22 @@ class InternalRecordInterfaceProcessor {
|
||||
}
|
||||
})
|
||||
.filter(element -> usedNames.add(element.getSimpleName().toString()))
|
||||
.map(element -> new Component(element, stripBeanPrefix(element.getSimpleName().toString())))
|
||||
.collect(Collectors.toCollection(() -> components));
|
||||
iface.getInterfaces().forEach(parentIface -> {
|
||||
TypeElement parentIfaceElement = (TypeElement) processingEnv.getTypeUtils().asElement(parentIface);
|
||||
getRecordComponents(parentIfaceElement, components, visitedSet, usedNames);
|
||||
});
|
||||
}
|
||||
|
||||
private Optional<String> stripBeanPrefix(String name)
|
||||
{
|
||||
return javaBeanPrefixes.stream()
|
||||
.filter(prefix -> name.startsWith(prefix) && (name.length() > prefix.length()))
|
||||
.findFirst()
|
||||
.map(prefix -> {
|
||||
var stripped = name.substring(prefix.length());
|
||||
return Character.toLowerCase(stripped.charAt(0)) + stripped.substring(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,11 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
|
||||
*/
|
||||
public static final String OPTION_BUILD_METHOD_NAME = "buildMethodName";
|
||||
|
||||
/**
|
||||
* @see #downCastMethodName()
|
||||
*/
|
||||
public static final String OPTION_DOWN_CAST_METHOD_NAME = "downCastMethodName";
|
||||
|
||||
/**
|
||||
* @see #componentsMethodName()
|
||||
*/
|
||||
@@ -75,20 +80,15 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
|
||||
*/
|
||||
public static final String OPTION_WITH_CLASS_METHOD_PREFIX = "withClassMethodPrefix";
|
||||
|
||||
/**
|
||||
* @see #withClassGetThisMethodName()
|
||||
*/
|
||||
public static final String OPTION_WITH_CLASS_GET_THIS_METHOD_NAME = "withClassGetThisMethodName";
|
||||
|
||||
private final String suffix;
|
||||
private final String interfaceSuffix;
|
||||
private final String copyMethodName;
|
||||
private final String builderMethodName;
|
||||
private final String buildMethodName;
|
||||
private final String downCastMethodName;
|
||||
private final String componentsMethodName;
|
||||
private final String withClassName;
|
||||
private final String withClassMethodPrefix;
|
||||
private final String withClassGetThisMethodName;
|
||||
private final String fileComment;
|
||||
private final String fileIndent;
|
||||
private final boolean prefixEnclosingClassNames;
|
||||
@@ -99,10 +99,10 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
|
||||
builderMethodName = options.getOrDefault(OPTION_BUILDER_METHOD_NAME, DEFAULT.builderMethodName());
|
||||
copyMethodName = options.getOrDefault(OPTION_COPY_METHOD_NAME, DEFAULT.copyMethodName());
|
||||
buildMethodName = options.getOrDefault(OPTION_BUILD_METHOD_NAME, DEFAULT.buildMethodName());
|
||||
downCastMethodName = options.getOrDefault(OPTION_DOWN_CAST_METHOD_NAME, DEFAULT.downCastMethodName());
|
||||
componentsMethodName = options.getOrDefault(OPTION_COMPONENTS_METHOD_NAME, DEFAULT.componentsMethodName());
|
||||
withClassName = options.getOrDefault(OPTION_WITH_CLASS_NAME, DEFAULT.withClassName());
|
||||
withClassMethodPrefix = options.getOrDefault(OPTION_WITH_CLASS_METHOD_PREFIX, DEFAULT.withClassMethodPrefix());
|
||||
withClassGetThisMethodName = options.getOrDefault(OPTION_WITH_CLASS_GET_THIS_METHOD_NAME, DEFAULT.withClassGetThisMethodName());
|
||||
fileComment = options.getOrDefault(OPTION_FILE_COMMENT, DEFAULT.fileComment());
|
||||
fileIndent = options.getOrDefault(OPTION_FILE_INDENT, DEFAULT.fileIndent());
|
||||
String prefixenclosingclassnamesopt = options.getOrDefault(OPTION_PREFIX_ENCLOSING_CLASS_NAMES, String.valueOf(DEFAULT.prefixEnclosingClassNames()));
|
||||
@@ -133,6 +133,11 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
|
||||
return buildMethodName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String downCastMethodName() {
|
||||
return downCastMethodName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String componentsMethodName() {
|
||||
return componentsMethodName;
|
||||
@@ -148,11 +153,6 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
|
||||
return withClassMethodPrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String withClassGetThisMethodName() {
|
||||
return withClassGetThisMethodName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fileComment() {
|
||||
return fileComment;
|
||||
|
||||
@@ -18,39 +18,46 @@ package io.soabase.recordbuilder.processor;
|
||||
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.RecordBuilderMetaData;
|
||||
import io.soabase.recordbuilder.core.RecordInterface;
|
||||
|
||||
import javax.annotation.processing.*;
|
||||
import javax.annotation.processing.AbstractProcessor;
|
||||
import javax.annotation.processing.Filer;
|
||||
import javax.annotation.processing.Generated;
|
||||
import javax.annotation.processing.RoundEnvironment;
|
||||
import javax.lang.model.SourceVersion;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.PackageElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
import javax.tools.Diagnostic;
|
||||
import javax.tools.JavaFileObject;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.RECORD_BUILDER;
|
||||
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.RECORD_INTERFACE;
|
||||
|
||||
@SupportedAnnotationTypes({RECORD_BUILDER, RECORD_INTERFACE})
|
||||
public class RecordBuilderProcessor extends AbstractProcessor {
|
||||
public static final String RECORD_BUILDER = "io.soabase.recordbuilder.core.RecordBuilder";
|
||||
public static final String RECORD_INTERFACE = "io.soabase.recordbuilder.core.RecordInterface";
|
||||
private static final String RECORD_BUILDER = RecordBuilder.class.getName();
|
||||
private static final String RECORD_BUILDER_INCLUDE = RecordBuilder.Include.class.getName().replace('$', '.');
|
||||
private static final String RECORD_INTERFACE = RecordInterface.class.getName();
|
||||
private static final String RECORD_INTERFACE_INCLUDE = RecordInterface.Include.class.getName().replace('$', '.');
|
||||
|
||||
static final AnnotationSpec generatedRecordBuilderAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RECORD_BUILDER).build();
|
||||
static final AnnotationSpec generatedRecordInterfaceAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RECORD_INTERFACE).build();
|
||||
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();
|
||||
|
||||
@Override
|
||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
||||
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation)
|
||||
.forEach(element -> process(annotation, element))
|
||||
);
|
||||
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> process(annotation, element)));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Set<String> getSupportedAnnotationTypes() {
|
||||
return Set.of(RECORD_BUILDER, RECORD_BUILDER_INCLUDE, RECORD_INTERFACE, RECORD_INTERFACE_INCLUDE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceVersion getSupportedSourceVersion() {
|
||||
// we don't directly return RELEASE_14 as that may
|
||||
@@ -63,37 +70,120 @@ public class RecordBuilderProcessor extends AbstractProcessor {
|
||||
private void process(TypeElement annotation, Element element) {
|
||||
var metaData = new RecordBuilderMetaDataLoader(processingEnv, s -> processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, s)).getMetaData();
|
||||
|
||||
if (annotation.getQualifiedName().toString().equals(RECORD_BUILDER)) {
|
||||
processRecordBuilder((TypeElement) element, metaData);
|
||||
} else if (annotation.getQualifiedName().toString().equals(RECORD_INTERFACE)) {
|
||||
processRecordInterface((TypeElement) element, element.getAnnotation(RecordInterface.class), metaData);
|
||||
} else {
|
||||
String annotationClass = annotation.getQualifiedName().toString();
|
||||
if ( annotationClass.equals(RECORD_BUILDER) )
|
||||
{
|
||||
processRecordBuilder((TypeElement)element, metaData, Optional.empty());
|
||||
}
|
||||
else if ( annotationClass.equals(RECORD_INTERFACE) )
|
||||
{
|
||||
processRecordInterface((TypeElement)element, element.getAnnotation(RecordInterface.class).addRecordBuilder(), metaData, Optional.empty());
|
||||
}
|
||||
else if ( annotationClass.equals(RECORD_BUILDER_INCLUDE) || annotationClass.equals(RECORD_INTERFACE_INCLUDE) )
|
||||
{
|
||||
processIncludes(element, metaData, annotationClass);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new RuntimeException("Unknown annotation: " + annotation);
|
||||
}
|
||||
}
|
||||
|
||||
private void processRecordInterface(TypeElement element, RecordInterface recordInterface, RecordBuilderMetaData metaData) {
|
||||
if (!element.getKind().isInterface()) {
|
||||
private void processIncludes(Element element, RecordBuilderMetaData metaData, String annotationClass) {
|
||||
var annotationMirrorOpt = ElementUtils.findAnnotationMirror(processingEnv, element, annotationClass);
|
||||
if ( annotationMirrorOpt.isEmpty() )
|
||||
{
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get annotation mirror for: " + annotationClass, element);
|
||||
}
|
||||
else
|
||||
{
|
||||
var values = processingEnv.getElementUtils().getElementValuesWithDefaults(annotationMirrorOpt.get());
|
||||
var classes = ElementUtils.getAnnotationValue(values, "value");
|
||||
if ( classes.isEmpty() )
|
||||
{
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get annotation value for: " + annotationClass, element);
|
||||
}
|
||||
else
|
||||
{
|
||||
var packagePattern = ElementUtils.getStringAttribute(ElementUtils.getAnnotationValue(values, "packagePattern").orElse(null), "*");
|
||||
var classesMirrors = ElementUtils.getClassesAttribute(classes.get());
|
||||
for ( TypeMirror mirror : classesMirrors )
|
||||
{
|
||||
TypeElement typeElement = (TypeElement)processingEnv.getTypeUtils().asElement(mirror);
|
||||
if ( typeElement == null )
|
||||
{
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get element for: " + mirror, element);
|
||||
}
|
||||
else
|
||||
{
|
||||
var packageName = buildPackageName(packagePattern, element, typeElement);
|
||||
if (packageName != null)
|
||||
{
|
||||
if ( annotationClass.equals(RECORD_INTERFACE_INCLUDE) )
|
||||
{
|
||||
var addRecordBuilderOpt = ElementUtils.getAnnotationValue(values, "addRecordBuilder");
|
||||
var addRecordBuilder = addRecordBuilderOpt.map(ElementUtils::getBooleanAttribute).orElse(true);
|
||||
processRecordInterface(typeElement, addRecordBuilder, metaData, Optional.of(packageName));
|
||||
}
|
||||
else
|
||||
{
|
||||
processRecordBuilder(typeElement, metaData, Optional.of(packageName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String buildPackageName(String packagePattern, Element builderElement, TypeElement includedClass) {
|
||||
PackageElement includedClassPackage = findPackageElement(includedClass, includedClass);
|
||||
if (includedClassPackage == null) {
|
||||
return null;
|
||||
}
|
||||
String replaced = packagePattern.replace("*", includedClassPackage.getQualifiedName().toString());
|
||||
if (builderElement instanceof PackageElement) {
|
||||
return replaced.replace("@", ((PackageElement)builderElement).getQualifiedName().toString());
|
||||
}
|
||||
return replaced.replace("@", ((PackageElement)builderElement.getEnclosingElement()).getQualifiedName().toString());
|
||||
}
|
||||
|
||||
private PackageElement findPackageElement(Element actualElement, Element includedClass) {
|
||||
if (includedClass == null) {
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Element has not package", actualElement);
|
||||
return null;
|
||||
}
|
||||
if (includedClass.getEnclosingElement() instanceof PackageElement) {
|
||||
return (PackageElement)includedClass.getEnclosingElement();
|
||||
}
|
||||
return findPackageElement(actualElement, includedClass.getEnclosingElement());
|
||||
}
|
||||
|
||||
private void processRecordInterface(TypeElement element, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageName) {
|
||||
if ( !element.getKind().isInterface() )
|
||||
{
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordInterface only valid for interfaces.", element);
|
||||
return;
|
||||
}
|
||||
var internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, recordInterface, metaData);
|
||||
if (!internalProcessor.isValid()) {
|
||||
var internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, addRecordBuilder, metaData, packageName);
|
||||
if ( !internalProcessor.isValid() )
|
||||
{
|
||||
return;
|
||||
}
|
||||
writeRecordInterfaceJavaFile(element, internalProcessor.packageName(), internalProcessor.recordClassType(), internalProcessor.recordType(), metaData, internalProcessor::toRecord);
|
||||
}
|
||||
|
||||
private void processRecordBuilder(TypeElement record, RecordBuilderMetaData metaData) {
|
||||
private void processRecordBuilder(TypeElement record, RecordBuilderMetaData metaData, Optional<String> packageName) {
|
||||
// we use string based name comparison for the element kind,
|
||||
// as the ElementKind.RECORD enum doesn't exist on JRE releases
|
||||
// older than Java 14, and we don't want to throw unexpected
|
||||
// NoSuchFieldErrors
|
||||
if (!"RECORD".equals(record.getKind().name())) {
|
||||
if ( !"RECORD".equals(record.getKind().name()) )
|
||||
{
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordBuilder only valid for records.", record);
|
||||
return;
|
||||
}
|
||||
var internalProcessor = new InternalRecordBuilderProcessor(record, metaData);
|
||||
var internalProcessor = new InternalRecordBuilderProcessor(record, metaData, packageName);
|
||||
writeRecordBuilderJavaFile(record, internalProcessor.packageName(), internalProcessor.builderClassType(), internalProcessor.builderType(), metaData);
|
||||
}
|
||||
|
||||
@@ -105,7 +195,7 @@ public class RecordBuilderProcessor extends AbstractProcessor {
|
||||
{
|
||||
String fullyQualifiedName = packageName.isEmpty() ? builderClassType.name() : (packageName + "." + builderClassType.name());
|
||||
JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName);
|
||||
try ( Writer writer = sourceFile.openWriter() )
|
||||
try (Writer writer = sourceFile.openWriter())
|
||||
{
|
||||
javaFile.writeTo(writer);
|
||||
}
|
||||
@@ -128,7 +218,7 @@ public class RecordBuilderProcessor extends AbstractProcessor {
|
||||
{
|
||||
String fullyQualifiedName = packageName.isEmpty() ? classType.name() : (packageName + "." + classType.name());
|
||||
JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName);
|
||||
try ( Writer writer = sourceFile.openWriter() )
|
||||
try (Writer writer = sourceFile.openWriter())
|
||||
{
|
||||
writer.write(recordSourceCode);
|
||||
}
|
||||
@@ -140,11 +230,10 @@ public class RecordBuilderProcessor extends AbstractProcessor {
|
||||
}
|
||||
|
||||
private JavaFile javaFileBuilder(String packageName, TypeSpec type, RecordBuilderMetaData metaData) {
|
||||
var javaFileBuilder = JavaFile.builder(packageName, type)
|
||||
.skipJavaLangImports(true)
|
||||
.indent(metaData.fileIndent());
|
||||
var javaFileBuilder = JavaFile.builder(packageName, type).skipJavaLangImports(true).indent(metaData.fileIndent());
|
||||
var comment = metaData.fileComment();
|
||||
if ((comment != null) && !comment.isEmpty()) {
|
||||
if ( (comment != null) && !comment.isEmpty() )
|
||||
{
|
||||
javaFileBuilder.addFileComment(comment);
|
||||
}
|
||||
return javaFileBuilder.build();
|
||||
@@ -152,7 +241,8 @@ public class RecordBuilderProcessor extends AbstractProcessor {
|
||||
|
||||
private void handleWriteError(TypeElement element, IOException e) {
|
||||
String message = "Could not create source file";
|
||||
if (e.getMessage() != null) {
|
||||
if ( e.getMessage() != null )
|
||||
{
|
||||
message = message + ": " + e.getMessage();
|
||||
}
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, element);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.8.ea</version>
|
||||
<version>1.19</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<dependency>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,6 +43,10 @@
|
||||
<annotationProcessors>
|
||||
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
|
||||
</annotationProcessors>
|
||||
<release>${jdk-version}</release>
|
||||
<compilerArgs>
|
||||
<arg>${enable-preview}</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
|
||||
@@ -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.RecordInterface;
|
||||
import java.time.Instant;
|
||||
|
||||
@RecordInterface
|
||||
public interface BeanStyle {
|
||||
String getName();
|
||||
|
||||
Instant getDate();
|
||||
|
||||
boolean isSomething();
|
||||
}
|
||||
@@ -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 io.soabase.recordbuilder.core.RecordInterface;
|
||||
|
||||
@RecordInterface.Include({
|
||||
Thingy.class
|
||||
})
|
||||
@RecordBuilder.Include({
|
||||
Nested.NestedRecord.class
|
||||
})
|
||||
public class Builder {
|
||||
}
|
||||
@@ -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.time.Instant;
|
||||
|
||||
public interface Customer {
|
||||
String name();
|
||||
|
||||
String address();
|
||||
|
||||
Instant activeDate();
|
||||
}
|
||||
@@ -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.test;
|
||||
|
||||
import io.soabase.recordbuilder.core.RecordBuilder;
|
||||
|
||||
@RecordBuilder
|
||||
public record Empty() implements EmptyBuilder.With {
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public class Nested {
|
||||
record NestedRecord(int x, int y){}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public record Pair<T, U>(T t, U u) {}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public record Point(int x, int y) {}
|
||||
@@ -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.test;
|
||||
|
||||
import io.soabase.recordbuilder.core.RecordBuilder;
|
||||
|
||||
@RecordBuilder
|
||||
public record RecordWithAnR(int r, String b) {}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public interface Thingy<T> {
|
||||
T getIt();
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@RecordBuilder.Include(value = {Point.class, Pair.class}, packagePattern = "*.foo")
|
||||
@RecordInterface.Include(value = Customer.class, addRecordBuilder = false, packagePattern = "*.bar")
|
||||
package io.soabase.recordbuilder.test;
|
||||
|
||||
import io.soabase.recordbuilder.core.RecordBuilder;
|
||||
import io.soabase.recordbuilder.core.RecordInterface;
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static io.soabase.recordbuilder.test.SimpleGenericRecordBuilder.SimpleGenericRecord;
|
||||
import static io.soabase.recordbuilder.test.SimpleRecordBuilder.SimpleRecord;
|
||||
|
||||
public class TestRecordInterface
|
||||
{
|
||||
@Test
|
||||
public void testHasDefaults()
|
||||
{
|
||||
var r1 = new HasDefaultsRecord(Instant.MIN, Instant.MAX);
|
||||
var r2 = r1.with(b -> b.tomorrow(Instant.MIN));
|
||||
Assertions.assertEquals(Instant.MIN, r1.time());
|
||||
Assertions.assertEquals(Instant.MAX, r1.tomorrow());
|
||||
Assertions.assertEquals(Instant.MIN, r2.time());
|
||||
Assertions.assertEquals(Instant.MIN, r2.tomorrow());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStaticConstructor()
|
||||
{
|
||||
var simple = SimpleRecord(10,"hey");
|
||||
Assertions.assertEquals(simple.i(), 10);
|
||||
Assertions.assertEquals(simple.s(), "hey");
|
||||
|
||||
var now = Instant.now();
|
||||
var generic = SimpleGenericRecord(101, now);
|
||||
Assertions.assertEquals(generic.i(), 101);
|
||||
Assertions.assertEquals(generic.s(), now);
|
||||
}
|
||||
}
|
||||
@@ -46,4 +46,24 @@ class TestWithers {
|
||||
Assertions.assertEquals(20, r3.i());
|
||||
Assertions.assertEquals("changed", r3.s());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWitherBuilderConsumer() {
|
||||
var r1 = new SimpleGenericRecord<>(10, "ten");
|
||||
var r2 = r1.with(r -> r.i(15));
|
||||
var r3 = r1.with(r -> r.s("twenty").i(20));
|
||||
Assertions.assertEquals(10, r1.i());
|
||||
Assertions.assertEquals("ten", r1.s());
|
||||
Assertions.assertEquals(15, r2.i());
|
||||
Assertions.assertEquals("ten", r2.s());
|
||||
Assertions.assertEquals(20, r3.i());
|
||||
Assertions.assertEquals("twenty", r3.s());
|
||||
}
|
||||
|
||||
private static class BadSubclass implements PersonRecordBuilder.With {}
|
||||
|
||||
@Test
|
||||
void testBadWithSubclass() {
|
||||
Assertions.assertThrows(RuntimeException.class, () -> new BadSubclass().withAge(10));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user