Compare commits

..

53 Commits

Author SHA1 Message Date
Jordan Zimmerman
1367b90edc [maven-release-plugin] prepare release record-builder-1.18-java15 2021-02-02 10:12:02 -05:00
Jordan Zimmerman
8581f16734 Abandon previous attempt to have Java15 specific modules. I can just manually do it from now on 2021-02-02 10:10:52 -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
25 changed files with 568 additions and 107 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

24
.github/workflows/maven_java15.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 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: 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

177
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 = r4.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,7 +196,7 @@ 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);
}
@@ -176,8 +204,7 @@ public class NameAndAgeBuilder {
* Return a new record built from the builder passed to the given consumer
*/
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
var r = (NameAndAge)(Object)this;
NameAndAgeBuilder builder = NameAndAgeBuilder.builder(r);
NameAndAgeBuilder builder = with();
consumer.accept(builder);
return builder.build();
}
@@ -186,7 +213,7 @@ public class NameAndAgeBuilder {
* 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());
}
@@ -194,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);
}
}
@@ -230,6 +257,8 @@ 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
@@ -260,18 +289,19 @@ annotation. Use `packagePattern` to change this (see Javadoc for details).
### 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>
@@ -291,21 +321,11 @@ annotation. Use `packagePattern` to change this (see Javadoc for details).
</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:
@@ -313,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'
}
```
@@ -330,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:
@@ -361,3 +362,79 @@ 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. The artifact IDs for these are:
- core: `record-builder-core-java15`
- processor: `record-builder-processor-java15`
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-java15</artifactId>
<version>set-version-here</version>
</dependency>
</dependencies>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>set-version-here</version>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-processor-java15</artifactId>
<version>set-version-here</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-java15:$version-goes-here'
compileOnly 'io.soabase.record-builder:record-builder-core-java15:$version-goes-here'
}
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.11.ea</version>
<version>1.18-java15</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.11.ea</tag>
<tag>record-builder-1.18-java15</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.11.ea</version>
<version>1.18-java15</version>
</parent>
<modelVersion>4.0.0</modelVersion>

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

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

View File

@@ -86,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

@@ -53,6 +53,7 @@ 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, Optional<String> packageNameOpt)
{
@@ -62,6 +63,7 @@ class InternalRecordBuilderProcessor
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)
@@ -69,7 +71,10 @@ class InternalRecordBuilderProcessor
.addTypeVariables(typeVariables);
addWithNestedClass();
addDefaultConstructor();
addAllArgsConstructor();
addStaticBuilder();
if (recordComponents.size() > 0) {
addAllArgsConstructor();
}
addStaticDefaultBuilderMethod();
addStaticCopyBuilderMethod();
addStaticComponentsMethod();
@@ -82,6 +87,7 @@ class InternalRecordBuilderProcessor
add1SetterMethod(component);
add1GetterMethod(component);
});
addStaticDowncastMethod();
builderType = builder.build();
}
@@ -128,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());
@@ -158,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")
@@ -175,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(", ");
@@ -197,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(");");
@@ -230,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()
{
/*
@@ -325,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(")");
@@ -357,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) {
@@ -365,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()
@@ -461,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

@@ -48,12 +48,17 @@ 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__";
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 = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(iface));
@@ -79,6 +84,8 @@ class InternalRecordInterfaceProcessor {
builder.addSuperinterface(builderClassType.typeName());
}
alternateMethods = buildAlternateMethods(recordComponents);
recordType = builder.build();
}
@@ -126,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()) {
@@ -153,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;
}
@@ -184,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

@@ -117,15 +117,18 @@ public class RecordBuilderProcessor extends AbstractProcessor {
else
{
var packageName = buildPackageName(packagePattern, element, typeElement);
if ( annotationClass.equals(RECORD_INTERFACE_INCLUDE) )
if (packageName != null)
{
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));
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));
}
}
}
}
@@ -134,13 +137,28 @@ public class RecordBuilderProcessor extends AbstractProcessor {
}
private String buildPackageName(String packagePattern, Element builderElement, TypeElement includedClass) {
String replaced = packagePattern.replace("*", ((PackageElement)includedClass.getEnclosingElement()).getQualifiedName().toString());
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() )
{

View File

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

@@ -15,10 +15,14 @@
*/
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,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,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

@@ -20,6 +20,9 @@ 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
@@ -32,4 +35,17 @@ public class TestRecordInterface
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));
}
}