Compare commits

..

66 Commits

Author SHA1 Message Date
Jordan Zimmerman
07e52035ee [maven-release-plugin] prepare release record-builder-1.18 2021-02-04 06:35:29 -05:00
Jordan Zimmerman
07fc606147 Update README.md 2021-02-04 06:33:50 -05:00
Jordan Zimmerman
677813e875 [maven-release-plugin] prepare for next development iteration 2021-02-04 06:31:51 -05:00
Jordan Zimmerman
7d877963fb [maven-release-plugin] prepare release record-builder-1.18-java15 2021-02-04 06:31:51 -05:00
Jordan Zimmerman
96bb6ef9f3 Abandon previous attempt to have Java15 specific modules. I can just manually do it from now on 2021-02-04 06:31:51 -05:00
Jordan Zimmerman
6f3046f507 Update README.md 2021-02-01 13:22:08 -05:00
Jordan Zimmerman
7564643556 [maven-release-plugin] prepare for next development iteration 2021-02-01 13:08:04 -05:00
Jordan Zimmerman
dfd76fc58b [maven-release-plugin] prepare release record-builder-1.17 2021-02-01 13:08:02 -05:00
Jordan Zimmerman
04e9135591 [maven-release-plugin] prepare for next development iteration 2021-02-01 13:06:44 -05:00
Jordan Zimmerman
b512a6e968 [maven-release-plugin] prepare release record-builder-1.17 2021-02-01 13:06:44 -05:00
Jordan Zimmerman
5a1cd35320 [maven-release-plugin] rollback the release of record-builder-1.17 2021-02-01 13:06:44 -05:00
Jordan Zimmerman
a615e3abb6 [maven-release-plugin] prepare release record-builder-1.17 2021-02-01 13:06:44 -05:00
Jordan Zimmerman
3f8bb47cbf Support alternate artifacts built with Java 15 2021-02-01 13:00:42 -05:00
Jordan Zimmerman
5a8e72f0e9 [maven-release-plugin] prepare for next development iteration 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
44ad4531b6 [maven-release-plugin] prepare release record-builder-1.16 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
7e78d32780 Added support for putting @RecordInterface on Java beans 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
0dc4aa7657 Prep for Java 16 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
b6d9a6202f Create FUNDING.yml 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
b21368f32f Have the consumer version of with() use the other with() to get the builder. This will ensure better testing and is more logical 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
501da86afd you're right - we only need provided/compileOnly. I've made the updates to Maven as well. 2021-02-01 12:26:07 -05:00
Marc Philipp
13d867e6e6 Use compileOnly instead of implementation
Since most users only need the annotations.
2021-02-01 12:26:07 -05:00
Jordan Zimmerman
a1206fa57f [maven-release-plugin] prepare for next development iteration 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
b89722ebfe [maven-release-plugin] prepare release record-builder-1.14.ea 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
75163f53ed Update README.md 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
9855e7b504 Switch to Github Actions 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
aa3bdedf28 Update maven.yml 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
6d5e15baa1 Create maven.yml 2021-02-01 12:26:07 -05:00
Mikaël Barbero
9c8e3626ba Stop relying on toString() to detect package name.
Fix for #15
2020-12-11 18:18:32 +00:00
Jordan Zimmerman
24b85e7ad5 [maven-release-plugin] prepare for next development iteration 2020-11-29 08:27:34 -05:00
Jordan Zimmerman
861e2e745a [maven-release-plugin] prepare release record-builder-1.13.ea 2020-11-29 08:27:25 -05:00
Jordan Zimmerman
1fc7c9a4b3 Make sure the downcast variable name doesn't collide with a record component name 2020-11-28 22:39:00 -05:00
Jordan Zimmerman
570514e077 buildPackageName() can't assume that the immediate enclosing element is the package. It may be a nested class, etc. 2020-11-28 22:38:48 -05:00
Jordan Zimmerman
9d8b9e65bc [maven-release-plugin] prepare for next development iteration 2020-11-28 08:44:33 -05:00
Jordan Zimmerman
93d6204b76 [maven-release-plugin] prepare release record-builder-1.12.ea 2020-11-28 08:44:25 -05:00
Jordan Zimmerman
15e5bfccc6 [maven-release-plugin] rollback the release of record-builder-1.12.ea 2020-11-28 08:42:15 -05:00
Jordan Zimmerman
67c54244c5 [maven-release-plugin] prepare release record-builder-1.12.ea 2020-11-28 08:41:59 -05:00
Jordan Zimmerman
870ac4a9d9 [maven-release-plugin] rollback the release of record-builder-1.12.ea 2020-11-28 08:29:46 -05:00
Jordan Zimmerman
9ee8b5912a [maven-release-plugin] prepare for next development iteration 2020-11-28 08:28:09 -05:00
Jordan Zimmerman
6d9bcf27da [maven-release-plugin] prepare release record-builder-1.12.ea 2020-11-28 08:27:49 -05:00
Jordan Zimmerman
44db5fdf17 Change to 1.12.ea-SNAPSHOT - last release must have missed this 2020-11-28 08:25:10 -05:00
Jordan Zimmerman
90a65235a9 Add support for static constructor
Add a static constructor/builder so it can statically imported. Instead of
calling "new Record(...)" you can call just "Record(...)".
2020-11-28 07:35:34 -05:00
Ted Cassirer
7e8ddbd700 Remove unused variable in RecordBuilder 2020-11-25 09:33:08 -05:00
Ted Cassirer
3a534fbea9 Don't add the allArgsConstructor to the RecordBuilder if record has no fields 2020-11-25 08:53:06 -05:00
Jordan Zimmerman
6d7ebe2545 Update README.md 2020-11-09 14:42:30 -05:00
Jordan Zimmerman
f1e47391c8 Update README.md 2020-11-04 11:34:41 -05:00
Jordan Zimmerman
c999d0ba06 Update README.md 2020-11-04 10:47:33 -05:00
Jordan Zimmerman
e0243c8b1c Fixed some typos in previous PR 2020-11-04 10:44:43 -05:00
Jordan Zimmerman
65bbbaea05 Update README.md 2020-11-04 10:38:53 -05:00
Jordan Zimmerman
5ae03a2c66 Update README.md 2020-11-04 10:37:38 -05:00
Jordan Zimmerman
437e314799 Better exception when Wither is set as implementor on non-builder class 2020-11-04 10:36:35 -05:00
Jordan Zimmerman
a870beee21 Update README.md 2020-11-02 23:41:19 -05:00
Jordan Zimmerman
5b879743ef Added simple module-info. I've never done this before, I hope it's right 2020-11-02 23:37:57 -05:00
Jordan Zimmerman
c7bdafb0b9 Add method to do downcasting 2020-11-02 23:37:57 -05:00
Jordan Zimmerman
d67c62ed3b Update README.md 2020-10-05 15:00:28 -05:00
Jordan Zimmerman
39cf2b0353 Update README.md 2020-10-05 15:00:16 -05:00
Jordan Zimmerman
6813b88f8d Update README.md 2020-10-05 14:59:28 -05:00
Jordan Zimmerman
54662d69c7 Support Include versions of the annotation 2020-10-05 14:45:34 -05:00
Jordan Zimmerman
7811ff8823 Update README.md 2020-10-05 11:30:04 -05:00
Jordan Zimmerman
4c5076690d Update README.md 2020-10-05 11:29:32 -05:00
Jordan Zimmerman
39a800f10e Update README.md 2020-10-05 11:29:00 -05:00
Jordan Zimmerman
610081b27e [maven-release-plugin] prepare for next development iteration 2020-10-05 11:22:40 -05:00
Jordan Zimmerman
1eb91d612e [maven-release-plugin] prepare release record-builder-1.10.ea 2020-10-05 11:22:31 -05:00
Jordan Zimmerman
7c84f26972 Generated record from interface should implement the wither 2020-10-05 11:20:43 -05:00
Jordan Zimmerman
82cc4f4cad Update README.md 2020-10-04 14:54:54 -05:00
Jordan Zimmerman
f16e1b1d0e Update README.md 2020-10-04 14:51:40 -05:00
Jordan Zimmerman
c92bf78ec5 [maven-release-plugin] prepare for next development iteration 2020-10-04 14:48:03 -05:00
32 changed files with 968 additions and 141 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: Randgalt

24
.github/workflows/maven.yml vendored Normal file
View 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
View 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

View File

@@ -1 +0,0 @@
--enable-preview

View File

@@ -1,3 +0,0 @@
language: java
jdk:
- openjdk15

209
README.md
View File

@@ -1,13 +1,13 @@
[![Build Status](https://travis-ci.org/Randgalt/record-builder.svg?branch=master)](https://travis-ci.org/Randgalt/record-builder)
[![Build Status](https://github.com/Randgalt/record-builder/workflows/Java%20CI%20with%20Maven/badge.svg)](https://github.com/Randgalt/record-builder/actions)
[![Maven Central](https://img.shields.io/maven-central/v/io.soabase.record-builder/record-builder.svg)](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,15 +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();
NameAndAge r4 = r3.with().age(101).name("baz").build();
// alternate method of accessing the builder (note: no need to call "build()")
var r5 = r3.with(b -> b.age(200).name("whatever"));
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._
@@ -84,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
*/
@@ -160,6 +176,18 @@ 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}
*/
@@ -168,15 +196,24 @@ public class NameAndAgeBuilder {
* Return a new record builder using the current values
*/
default NameAndAgeBuilder with() {
var r = (NameAndAge)(Object)this;
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) {
var r = (NameAndAge)(Object)this;
NameAndAge r = _downcast(this);
return new NameAndAge(name, r.age());
}
@@ -184,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) {
var r = (NameAndAge)(Object)this;
NameAndAge r = _downcast(this);
return new NameAndAge(r.name(), age);
}
}
@@ -205,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
@@ -219,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>
@@ -255,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:
@@ -277,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'
}
```
@@ -294,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:
@@ -325,3 +362,77 @@ Alternatively, you can provide values for each individual meta data (or combinat
- `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
View 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
View 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
View File

@@ -5,7 +5,7 @@
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<packaging>pom</packaging>
<version>1.9.ea</version>
<version>1.18</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.9.ea</tag>
<tag>record-builder-1.18</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>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>1.9.ea</version>
<version>1.18</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

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

View File

@@ -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
*

View File

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

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>1.9.ea</version>
<version>1.18</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

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

View File

@@ -33,6 +33,7 @@ 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;
@@ -52,15 +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)
@@ -68,7 +71,10 @@ class InternalRecordBuilderProcessor
.addTypeVariables(typeVariables);
addWithNestedClass();
addDefaultConstructor();
addAllArgsConstructor();
addStaticBuilder();
if (recordComponents.size() > 0) {
addAllArgsConstructor();
}
addStaticDefaultBuilderMethod();
addStaticCopyBuilderMethod();
addStaticComponentsMethod();
@@ -81,6 +87,7 @@ class InternalRecordBuilderProcessor
add1SetterMethod(component);
add1GetterMethod(component);
});
addStaticDowncastMethod();
builderType = builder.build();
}
@@ -127,15 +134,13 @@ class InternalRecordBuilderProcessor
Adds a method that returns a pre-filled copy builder similar to:
default MyRecord with(Consumer<MyRecordBuilder> consumer) {
MyRecord r = (MyRecord)(Object)this;
MyRecordBuilder builder MyRecordBuilder.builder(r);
MyRecordBuilder builder = with();
consumer.accept(builder);
return builder.build();
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
.add("$T builder = $L.$L(r);\n", builderClassType.typeName(), builderClassType.name(), metaData.copyMethodName())
.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());
@@ -157,13 +162,13 @@ class InternalRecordBuilderProcessor
Adds a method that returns a pre-filled copy builder similar to:
default MyRecordBuilder with() {
MyRecord r = (MyRecord)(Object)this;
MyRecord r = _downcast(this);
return MyRecordBuilder.builder(r);
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
.add("return $L.$L(r);", builderClassType.name(), metaData.copyMethodName());
.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")
@@ -174,19 +179,35 @@ class InternalRecordBuilderProcessor
classBuilder.addMethod(methodSpec);
}
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 = (MyRecord)(Object)this;
MyRecord r = _downcast(this);
return new MyRecord(name, r.age());
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
.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(", ");
@@ -196,7 +217,7 @@ class InternalRecordBuilderProcessor
codeBlockBuilder.add(parameterComponent.name());
}
else {
codeBlockBuilder.add("r.$L()", parameterComponent.name());
codeBlockBuilder.add("$L.$L()", uniqueVarName, parameterComponent.name());
}
});
codeBlockBuilder.add(");");
@@ -229,6 +250,27 @@ class InternalRecordBuilderProcessor
builder.addMethod(constructor);
}
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()
{
/*
@@ -324,14 +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);
codeBuilder.add("\n&& ($L == $L.$L)", name, uniqueVarName, name);
}
else {
codeBuilder.add("\n&& $T.equals($L, b.$L)", Objects.class, name, name);
codeBuilder.add("\n&& $T.equals($L, $L.$L)", Objects.class, name, uniqueVarName, name);
}
});
codeBuilder.add(")");
@@ -356,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) {
@@ -364,15 +422,7 @@ 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()
@@ -460,6 +510,38 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec);
}
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)
{
/*

View File

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

View File

@@ -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()
*/
@@ -80,6 +85,7 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
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;
@@ -93,6 +99,7 @@ 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());
@@ -126,6 +133,11 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
return buildMethodName;
}
@Override
public String downCastMethodName() {
return downCastMethodName;
}
@Override
public String componentsMethodName() {
return componentsMethodName;

View File

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

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>1.9.ea</version>
<version>1.18</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>

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.RecordInterface;
import java.time.Instant;
@RecordInterface
public interface BeanStyle {
String getName();
Instant getDate();
boolean isSomething();
}

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 io.soabase.recordbuilder.core.RecordInterface;
@RecordInterface.Include({
Thingy.class
})
@RecordBuilder.Include({
Nested.NestedRecord.class
})
public class Builder {
}

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.time.Instant;
public interface Customer {
String name();
String address();
Instant activeDate();
}

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.test;
import io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder
public record Empty() implements EmptyBuilder.With {
}

View File

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

View File

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

View File

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

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.test;
import io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder
public record RecordWithAnR(int r, String b) {}

View File

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

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.
*/
@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;

View File

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

View File

@@ -59,4 +59,11 @@ class TestWithers {
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));
}
}