Compare commits

..

9 Commits

Author SHA1 Message Date
Jordan Zimmerman
378d19390c [maven-release-plugin] prepare release record-builder-1.17 2021-02-01 13:03:17 -05:00
Jordan Zimmerman
fe4eaec702 [maven-release-plugin] rollback the release of record-builder-1.17 2021-02-01 13:02:40 -05:00
Jordan Zimmerman
3f9ab38fb1 [maven-release-plugin] prepare release record-builder-1.17 2021-02-01 13:02:24 -05:00
Jordan Zimmerman
75d684f260 Merge branch 'master' of github.com:Randgalt/record-builder 2021-02-01 13:01:33 -05:00
Jordan Zimmerman
bdedc6e15a [maven-release-plugin] prepare for next development iteration 2021-02-01 12:28:04 -05:00
Jordan Zimmerman
3ddcecea08 [maven-release-plugin] prepare release record-builder-1.17 2021-02-01 12:27:56 -05:00
Jordan Zimmerman
56df5c1107 [maven-release-plugin] rollback the release of record-builder-1.17 2021-02-01 12:27:28 -05:00
Jordan Zimmerman
3dc1d87a3f [maven-release-plugin] prepare release record-builder-1.17 2021-02-01 12:27:15 -05:00
Jordan Zimmerman
1f02dcf4fe Support alternate artifacts built with Java 15 2021-02-01 12:26:07 -05:00
66 changed files with 1092 additions and 3000 deletions

View File

@@ -19,6 +19,6 @@ jobs:
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: 16
java-version: 16.0.0-ea
- name: Build with Maven
run: mvn -B package --file pom.xml

View File

@@ -1,7 +1,7 @@
# 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 17
name: Maven Build - Java 15
on:
push:
@@ -19,6 +19,6 @@ jobs:
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: 17
java-version: 15
- name: Build with Maven
run: mvn -B package --file pom.xml
run: mvn -P java15 -B package --file pom.xml

401
README.md
View File

@@ -1,5 +1,5 @@
[![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?sort=date)](https://search.maven.org/search?q=g:io.soabase.record-builder%20a:record-builder)
[![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
@@ -21,7 +21,8 @@ _Details:_
- [Record From Interface Details](#RecordInterface-Example)
- [Generation Via Includes](#generation-via-includes)
- [Usage](#usage)
- [Customizing](customizing.md) (e.g. add immutable collections, etc.)
- [Customizing](#customizing)
- [Java 15 Versions](#java-15-versions)
## RecordBuilder Example
@@ -70,187 +71,160 @@ NameAndAge r4 = r3.with().age(101).name("baz").build();
// alternate method of accessing the builder (note: no need to call "build()")
NameAndAge r5 = r4.with(b -> b.age(200).name("whatever"));
// perform some logic in addition to changing values
NameAndAge r5 = r4.with(b -> {
if (b.age() > 13) {
b.name("Teen " + b.name());
} else {
b.name("whatever"));
}
});
// or, if you cannot add the "With" interface to your record...
NameAndAge r6 = NameAndAgeBuilder.from(r5).with(b -> b.age(200).name("whatever"));
NameAndAge r7 = NameAndAgeBuilder.from(r5).withName("boop");
```
_Hat tip to [Benji Weber](https://benjiweber.co.uk/blog/2020/09/19/fun-with-java-records/) for the Withers idea._
## Builder Class Definition
(Note: you can see a builder class built using `@RecordBuilderFull` here: [SingleItemsBuilder.java](https://gist.github.com/Randgalt/8aa487a847ea2acdd76d702f7cf17d6a))
The full builder class is defined as:
```java
public class NameAndAgeBuilder {
private String name;
private String name;
private int age;
private int age;
private NameAndAgeBuilder() {
}
private NameAndAgeBuilder() {
}
private NameAndAgeBuilder(String name, int age) {
this.name = name;
this.age = age;
}
private NameAndAgeBuilder(String name, int age) {
this.name = name;
this.age = age;
}
/**
* Static constructor/builder. Can be used instead of new NameAndAge(...)
*/
public static NameAndAge NameAndAge(String name, int age) {
return new NameAndAge(name, age);
}
/**
* Static constructor/builder. Can be used instead of new NameAndAge(...)
*/
public static NameAndAge NameAndAge(String name, int age) {
return new NameAndAge(name, age);
}
/**
* Return a new builder with all fields set to default Java values
*/
public static NameAndAgeBuilder builder() {
return new NameAndAgeBuilder();
}
/**
* Return a new builder with all fields set to default Java values
*/
public static NameAndAgeBuilder builder() {
return new NameAndAgeBuilder();
}
/**
* Return a new builder with all fields set to the values taken from the given record instance
*/
public static NameAndAgeBuilder builder(NameAndAge from) {
return new NameAndAgeBuilder(from.name(), from.age());
}
/**
* Return a new builder with all fields set to the values taken from the given record instance
*/
public static NameAndAgeBuilder builder(NameAndAge from) {
return new NameAndAgeBuilder(from.name(), from.age());
}
/**
* Return a "with"er for an existing record instance
*/
public static NameAndAgeBuilder.With from(NameAndAge from) {
return new NameAndAgeBuilder.With() {
@Override
public String name() {
return from.name();
}
/**
* Return a new record instance with all fields set to the current values in this builder
*/
public NameAndAge build() {
return new NameAndAge(name, age);
}
@Override
public int age() {
return from.age();
}
};
}
/**
* Set a new value for the {@code name} record component in the builder
*/
public NameAndAgeBuilder name(String name) {
this.name = name;
return this;
}
/**
* Return a stream of the record components as map entries keyed with the component name and the value as the component value
*/
public static Stream<Map.Entry<String, Object>> stream(NameAndAge record) {
return Stream.of(Map.entry("name", record.name()),
Map.entry("age", record.age()));
}
/**
* Return a new record instance with all fields set to the current values in this builder
*/
public NameAndAge build() {
return new NameAndAge(name, age);
}
@Override
public String toString() {
return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public boolean equals(Object o) {
return (this == o) || ((o instanceof NameAndAgeBuilder r)
&& Objects.equals(name, r.name)
&& (age == r.age));
}
/**
* Set a new value for the {@code name} record component in the builder
*/
public NameAndAgeBuilder name(String name) {
this.name = name;
return this;
}
/**
* Return the current value for the {@code name} record component in the builder
*/
public String name() {
return name;
}
/**
* Set a new value for the {@code age} record component in the builder
*/
public NameAndAgeBuilder age(int age) {
this.age = age;
return this;
}
/**
* Return the current value for the {@code age} record component in the builder
*/
public int age() {
return age;
}
/**
* Add withers to {@code NameAndAge}
*/
public interface With {
/**
* Return the current value for the {@code name} record component in the builder
*/
String name();
public String name() {
return name;
}
/**
* Set a new value for the {@code age} record component in the builder
*/
public NameAndAgeBuilder age(int age) {
this.age = age;
return this;
}
/**
* Return the current value for the {@code age} record component in the builder
*/
int age();
/**
* Return a new record builder using the current values
*/
default NameAndAgeBuilder with() {
return new NameAndAgeBuilder(name(), age());
public int age() {
return age;
}
/**
* Return a new record built from the builder passed to the given consumer
* Return a stream of the record components as map entries keyed with the component name and the value as the component value
*/
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
NameAndAgeBuilder builder = with();
consumer.accept(builder);
return builder.build();
public static Stream<Map.Entry<String, Object>> stream(NameAndAge record) {
return Stream.of(new AbstractMap.SimpleEntry<>("name", record.name()),
new AbstractMap.SimpleEntry<>("age", record.age()));
}
@Override
public String toString() {
return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public boolean equals(Object o) {
return (this == o) || ((o instanceof NameAndAgeBuilder b)
&& Objects.equals(name, b.name)
&& (age == b.age));
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
* Downcast to {@code NameAndAge}
*/
default NameAndAge withName(String name) {
return new NameAndAge(name, age());
private static NameAndAge _downcast(Object obj) {
try {
return (NameAndAge)obj;
}
catch (ClassCastException dummy) {
throw new RuntimeException("NameAndAgeBuilder.With can only be implemented for NameAndAge");
}
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
* Add withers to {@code NameAndAge}
*/
default NameAndAge withAge(int age) {
return new NameAndAge(name(), age);
public interface With {
/**
* Return a new record builder using the current values
*/
default NameAndAgeBuilder with() {
NameAndAge r = _downcast(this);
return NameAndAgeBuilder.builder(r);
}
/**
* Return a new record built from the builder passed to the given consumer
*/
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
NameAndAgeBuilder builder = with();
consumer.accept(builder);
return builder.build();
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
*/
default NameAndAge withName(String name) {
NameAndAge r = _downcast(this);
return new NameAndAge(name, r.age());
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
*/
default NameAndAge withAge(int age) {
NameAndAge r = _downcast(this);
return new NameAndAge(r.name(), age);
}
}
}
}
```
@@ -294,7 +268,7 @@ libraries where you are not able to annotate the source.
E.g.
```java
```
import some.library.code.ImportedRecord
import some.library.code.ImportedInterface
@@ -308,9 +282,6 @@ public void Placeholder {
}
```
`@RecordBuilder.Include` also supports a `packages` attribute that includes all records
in the listed packages.
The target package for generation is the same as the package that contains the "Include"
annotation. Use `packagePattern` to change this (see Javadoc for details).
@@ -318,22 +289,48 @@ annotation. Use `packagePattern` to change this (see Javadoc for details).
### Maven
Add a dependency that contains the discoverable annotation processor:
1) Add the dependency that contains the `@RecordBuilder` annotation.
```xml
```
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-processor</artifactId>
<version>${record.builder.version}</version>
<artifactId>record-builder-core</artifactId>
<version>set-version-here</version>
<scope>provided</scope>
</dependency>
```
2) Enable the annotation processing for the Maven Compiler Plugin:
```
<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</artifactId>
<version>set-version-here</version>
</annotationProcessorPath>
</annotationProcessorPaths>
<annotationProcessors>
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
</annotationProcessors>
... any other options here ...
</configuration>
</plugin>
```
### Gradle
Add the following to your build.gradle file:
```groovy
```
dependencies {
annotationProcessor 'io.soabase.record-builder:record-builder-processor:$version-goes-here'
compileOnly 'io.soabase.record-builder:record-builder-core:$version-goes-here'
@@ -346,6 +343,98 @@ Depending on your IDE you are likely to need to enable Annotation Processing in
## Customizing
RecordBuilder can be customized to your needs and you can even create your
own custom RecordBuilder annotations. See [Customizing RecordBuilder](customizing.md)
for details.
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:
- Create a class that implements RecordBuilderMetaData
- When compiling, make sure that the compiled class is in the processor path
- Add a "metaDataClass" compiler option with the class name. E.g. `javac ... -AmetaDataClass=foo.bar.MyMetaData`
Alternatively, you can provide values for each individual meta data (or combinations):
- `javac ... -Asuffix=foo`
- `javac ... -AinterfaceSuffix=foo`
- `javac ... -AcopyMethodName=foo`
- `javac ... -AbuilderMethodName=foo`
- `javac ... -AbuildMethodName=foo`
- `javac ... -AcomponentsMethodName=foo`
- `javac ... -AwithClassName=foo`
- `javac ... -AwithClassMethodPrefix=foo`
- `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"
}
```

View File

@@ -1,95 +0,0 @@
[◀︎ RecordBuilder](README.md) • Customizing RecordBuilder
# RecordBuilderFull
Note: `@RecordBuilderFull` has most optional features enabled. It's an alternate
form of `@RecordBuilder` that uses the [templating mechanism](#create-a-custom-annotation) to
enable optional features.
# Customizing RecordBuilder
RecordBuilder can be customized in a number of ways. The types of customizations will change over time. See
[@RecordBuilder.Options](record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java)
for the current set of customizations and their default values. For example, the `useImmutableCollections` option
adds special handling for record components of type `java.util.List`, `java.util.Set`, `java.util.Map` and `java.util.Collection`. When the record is built, any components of these types are passed through an added shim method that uses the corresponding immutable collection (e.g. `List.copyOf(o)`) or an empty immutable collection if the component is `null`.
You can:
- [Customize an entire build](#customize-an-entire-build) - all uses of `@RecordBuilder` in your project
- [Customize a single record](#customize-a-single-record) annotated with `@RecordBuilder`
- [Create a custom annotation](#create-a-custom-annotation) that specifies your options and use that instead of `@RecordBuilder`
## Customize an entire build
To customize an entire build, use javac's annotation processor options via `-A` on the command line.
The options available are the same as the attributes in [@RecordBuilder.Options](record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java).
i.e. to disable "prefixing enclosing class names", compile with:
```shell
javac -AprefixEnclosingClassNames=false ...
```
_Note: use a separate `-A` for each option._
#### Maven
If you are using Maven, specify the options in the compiler plugin:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin-version}</version>
<configuration>
<compilerArgs>
<arg>-AprefixEnclosingClassNames=false</arg>
<arg>-AfileComment="something different"</arg>
</compilerArgs>
</configuration>
</plugin>
```
#### Gradle
For Gradle, specify the options:
```groovy
compilerArgs.addAll(['-AprefixEnclosingClassNames=false', '-AfileComment="something different"'])
```
## Customize a single record
To customize a single record, add `@RecordBuilder.Options` in addition to
`@RecordBuilder`.
E.g.
```java
@RecordBuilder.Options(withClassName = "Wither")
@RecordBuilder
public record MyRecord(String s){}
```
## Create a custom annotation
Using `@RecordBuilder.Template` you can create your own RecordBuilder annotation
that uses the set of options you want. E.g. to create a custom annotation that
uses an alternate file comment and an alternate With classname:
```java
@RecordBuilder.Template(options = @RecordBuilder.Options(
fileComment = "MyCo license",
withClassName = "Wither"
))
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
public @interface MyCoRecordBuilder {
}
```
Now, you can use `@MyCoRecordBuilder` instead of `@RecordBuilder` and the record
will be built with options as specified.
Note: the template mechanism also supports `@RecordInterface` templates via the `asRecordInterface` attribute.
When it is set a `@RecordInterface` template is created instead.

20
java15.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/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

20
java16.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/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

85
pom.xml
View File

@@ -5,14 +5,7 @@
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<packaging>pom</packaging>
<version>31</version>
<modules>
<module>record-builder-core</module>
<module>record-builder-processor</module>
<module>record-builder-test</module>
<module>record-builder-validator</module>
</modules>
<version>1.17</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -31,16 +24,11 @@
<maven-clean-plugin-version>3.1.0</maven-clean-plugin-version>
<maven-shade-plugin-version>3.2.1</maven-shade-plugin-version>
<maven-release-plugin-version>2.5.3</maven-release-plugin-version>
<maven-jar-plugin-version>3.2.0</maven-jar-plugin-version>
<license-file-path>src/etc/header.txt</license-file-path>
<maven-surefire-plugin-version>3.0.0-M5</maven-surefire-plugin-version>
<javapoet-version>1.12.1</javapoet-version>
<junit-jupiter-version>5.5.2</junit-jupiter-version>
<asm-version>7.2</asm-version>
<validation-api-version>2.0.1.Final</validation-api-version>
<hibernate-validator-version>6.0.20.Final</hibernate-validator-version>
<javax-el-version>3.0.1-b09</javax-el-version>
</properties>
<name>Record Builder</name>
@@ -77,7 +65,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-31</tag>
<tag>record-builder-1.17</tag>
</scm>
<issueManagement>
@@ -114,13 +102,7 @@
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-processor</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-validator</artifactId>
<artifactId>record-builder-core-java15</artifactId>
<version>${project.version}</version>
</dependency>
@@ -129,24 +111,6 @@
<artifactId>junit-jupiter</artifactId>
<version>${junit-jupiter-version}</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>${validation-api-version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator-version}</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>${javax-el-version}</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -190,7 +154,7 @@
<artifactId>maven-license-plugin</artifactId>
<version>${maven-license-plugin-version}</version>
<configuration>
<header>${license-file-path}</header>
<header>src/etc/header.txt</header>
<excludes>
<exclude>**/*.apt</exclude>
<exclude>**/*.md</exclude>
@@ -216,8 +180,6 @@
<exclude>**/jvm.config</exclude>
<exclude>**/.java-version</exclude>
<exclude>**/.travis.yml</exclude>
<exclude>**/gradlew</exclude>
<exclude>**/.github/**</exclude>
</excludes>
<strictCheck>true</strictCheck>
</configuration>
@@ -310,14 +272,8 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>${maven-jar-plugin-version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>${maven-gpg-plugin-version}</version>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin-version}</version>
</plugin>
</plugins>
</pluginManagement>
@@ -356,12 +312,26 @@
</build>
<profiles>
<profile>
<id>main</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<modules>
<module>record-builder-core</module>
<module>record-builder-processor</module>
<module>record-builder-test</module>
</modules>
</profile>
<profile>
<id>oss</id>
<build>
<plugins>
<plugin>
<artifactId>maven-gpg-plugin</artifactId>
<version>${maven-gpg-plugin-version}</version>
<configuration>
<passphrase>${gpg.passphrase}</passphrase>
<useAgent>true</useAgent>
@@ -379,5 +349,18 @@
</plugins>
</build>
</profile>
<profile>
<id>java15</id>
<properties>
<jdk-version>15</jdk-version>
</properties>
<modules>
<module>record-builder-core-java15</module>
<module>record-builder-processor-java15</module>
</modules>
</profile>
</profiles>
</project>

View File

@@ -3,27 +3,25 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>31</version>
<version>1.17</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>record-builder-validator</artifactId>
<properties>
<license-file-path>${project.parent.basedir}/src/etc/header.txt</license-file-path>
</properties>
<artifactId>record-builder-core-java15</artifactId>
<build>
<sourceDirectory>../record-builder-core/src/main/java</sourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>io.soabase.recordbuilder.validator</Automatic-Module-Name>
</manifestEntries>
</archive>
<proc>none</proc>
<release>${jdk-version}</release>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>

View File

@@ -3,29 +3,9 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>31</version>
<version>1.17-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>record-builder-core</artifactId>
<properties>
<license-file-path>${project.parent.basedir}/src/etc/header.txt</license-file-path>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>io.soabase.recordbuilder.core</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -15,186 +15,27 @@
*/
package io.soabase.recordbuilder.core;
import java.lang.annotation.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
public @interface RecordBuilder {
@Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE)
@Inherited
@interface Include {
/**
* @return list of classes to include
*/
Class<?>[] value() default {};
/**
* Synonym for {@code value()}. When using the other attributes it maybe more clear to
* use {@code classes()} instead of {@code value()}. Note: both attributes are applied
* (i.e. a union of classes from both attributes).
*
* @return list of classes
*/
Class<?>[] classes() default {};
/**
* Optional list of package names. All records in the packages will get processed as
* if they were listed as classes to include.
*
* @return list of package names
*/
String[] packages() default {};
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 {@code Include} annotation. '*' is replaced with
* is replaced with the package of the Include annotation. '*' is replaced with
* the package of the included class.
*
* @return package pattern
*/
String packagePattern() default "@";
}
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
@interface Options {
/**
* The builder class name will be the name of the record (prefixed with any enclosing class) plus this suffix. E.g.
* if the record name is "Foo", the builder will be named "FooBuilder".
*/
String suffix() default "Builder";
/**
* Used by {@code RecordInterface}. The generated record will have the same name as the annotated interface
* plus this suffix. E.g. if the interface name is "Foo", the record will be named "FooRecord".
*/
String interfaceSuffix() default "Record";
/**
* The name to use for the copy builder
*/
String copyMethodName() default "builder";
/**
* The name to use for the builder
*/
String builderMethodName() default "builder";
/**
* The name to use for the build method
*/
String buildMethodName() default "build";
/**
* The name to use for the from-to-wither method
*/
String fromMethodName() default "from";
/**
* The name to use for the method that returns the record components as a stream
*/
String componentsMethodName() default "stream";
/**
* The name to use for the nested With class
*/
String withClassName() default "With";
/**
* The prefix to use for the methods in the With class
*/
String withClassMethodPrefix() default "with";
/**
* Return the comment to place at the top of generated files. Return null or an empty string for no comment.
*/
String fileComment() default "Auto generated by io.soabase.recordbuilder.core.RecordBuilder: https://github.com/Randgalt/record-builder";
/**
* Return the file indent to use
*/
String fileIndent() default " ";
/**
* If the record is declared inside of another class, the outer class's name will
* be prefixed to the builder name if this returns true.
*/
boolean prefixEnclosingClassNames() default true;
/**
* If true, any annotations (if applicable) on record components are copied
* to the builder methods
*
* @return true/false
*/
boolean inheritComponentAnnotations() default true;
/**
* Set the default value of {@code Optional} record components to
* {@code Optional.empty()}
*/
boolean emptyDefaultForOptional() default true;
/**
* Add not-null checks for record components annotated with any annotation named either "NotNull",
* "NoNull", or "NonNull" (see {@link #interpretNotNullsPattern()} for the actual regex matching pattern).
*/
boolean interpretNotNulls() default false;
/**
* If {@link #interpretNotNulls()} is true, this is the regex pattern used to determine if an annotation name
* means "not null"
*/
String interpretNotNullsPattern() default "(?i)((notnull)|(nonnull)|(nonull))";
/**
* <p>Pass built records through the Java Validation API if it's available in the classpath.</p>
*
* <p>IMPORTANT:
* if this option is enabled you must include the {@code record-builder-validator} dependency in addition
* to {@code record-builder-core}. {@code record-builder-validator} is implemented completely via reflection and
* does not require other dependencies. Alternatively, you can define your own class with the package {@code package io.soabase.recordbuilder.validator;}
* named {@code RecordBuilderValidator} which has a public static method: {@code public static <T> T validate(T o)}.</p>
*/
boolean useValidationApi() default false;
/**
* Adds special handling for record components of type {@link java.util.List}, {@link java.util.Set},
* {@link java.util.Map} and {@link java.util.Collection}. When the record is built, any components
* of these types are passed through an added shim method that uses the corresponding immutable collection
* (e.g. {@code List.copyOf(o)}) or an empty immutable collection if the component is {@code null}.
*/
boolean useImmutableCollections() default false;
/**
* When enabled, collection types ({@code List}, {@code Set} and {@code Map}) are handled specially.
* The setters for these types now create an internal collection and items are added to that
* collection. Additionally, "adder" methods prefixed with {@link #singleItemBuilderPrefix()} are created
* to add single items to these collections.
*/
boolean addSingleItemCollectionBuilders() default false;
/**
* The prefix for adder methods when {@link #addSingleItemCollectionBuilders()} is enabled
*/
String singleItemBuilderPrefix() default "add";
/**
* When enabled, adds functional methods to the nested "With" class (such as {@code map()} and {@code accept()}).
*/
boolean addFunctionalMethodsToWith() default false;
}
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.ANNOTATION_TYPE)
@Inherited
@interface Template {
RecordBuilder.Options options();
boolean asRecordInterface() default false;
}
}

View File

@@ -1,34 +0,0 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.core;
import java.lang.annotation.*;
@RecordBuilder.Template(options = @RecordBuilder.Options(
interpretNotNulls = true,
useImmutableCollections = true,
addSingleItemCollectionBuilders = true,
addFunctionalMethodsToWith = true
))
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
/**
* An alternate form of {@code @RecordBuilder} that has most
* optional features turned on
*/
public @interface RecordBuilderFull {
}

View File

@@ -0,0 +1,144 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.core;
public interface RecordBuilderMetaData {
/**
* If you want to use your own meta data instance:
* <ul>
* <li>create a class that implements {@code RecordBuilderMetaData}</li>
* <li>When compiling, make sure that compiled class is in the processor path</li>
* <li>Add a "metaDataClass" compiler option with the class name. E.g. {@code javac ... -AmetaDataClass=foo.bar.MyMetaData}</li>
* </ul>
*/
String JAVAC_OPTION_NAME = "metaDataClass";
/**
* The default meta data instance
*/
RecordBuilderMetaData DEFAULT = new RecordBuilderMetaData() {};
/**
* The builder class name will be the name of the record (prefixed with any enclosing class) plus this suffix. E.g.
* if the record name is "Foo", the builder will be named "FooBuilder".
*
* @return suffix
*/
default String suffix() {
return "Builder";
}
/**
* Used by {@code RecordInterface}. The generated record will have the same name as the annotated interface
* plus this suffix. E.g. if the interface name is "Foo", the record will be named "FooRecord".
*
* @return suffix
*/
default String interfaceSuffix() {
return "Record";
}
/**
* The name to use for the copy builder
*
* @return copy builder name
*/
default String copyMethodName() {
return builderMethodName();
}
/**
* The name to use for the builder
*
* @return builder name
*/
default String builderMethodName() {
return "builder";
}
/**
* The name to use for the build method
*
* @return build method
*/
default String buildMethodName() {
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
*
* @return build method
*/
default String componentsMethodName() {
return "stream";
}
/**
* The name to use for the nested With class
*
* @return with class name
*/
default String withClassName() {
return "With";
}
/**
* The prefix to use for the methods in the With class
*
* @return prefix
*/
default String withClassMethodPrefix() {
return "with";
}
/**
* Return the comment to place at the top of generated files. Return null or an empty string for no comment.
*
* @return comment or empty
*/
default String fileComment() {
return "Auto generated by io.soabase.recordbuilder.core.RecordBuilder: https://github.com/Randgalt/record-builder";
}
/**
* Return the file indent to use
*
* @return file index
*/
default String fileIndent() {
return " ";
}
/**
* If the record is declared inside of another class, the outer class's name will
* be prefixed to the builder name if this returns true.
*
* @return true/false
*/
default boolean prefixEnclosingClassNames() {
return true;
}
}

View File

@@ -15,43 +15,27 @@
*/
package io.soabase.recordbuilder.core;
import java.lang.annotation.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
public @interface RecordInterface {
boolean addRecordBuilder() default true;
@Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE)
@Inherited
@interface Include {
/**
* @return list of classes to include
*/
Class<?>[] value() default {};
Class<?>[] value();
/**
* Synonym for {@code value()}. When using the other attributes it maybe more clear to
* use {@code classes()} instead of {@code value()}. Note: both attributes are applied
* (i.e. a union of classes from both attributes).
*
* @return list of classes
*/
Class<?>[] classes() default {};
/**
* If true the generated record is annotated with {@code @RecordBuilder}
*
* @return true/false
*/
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 {@code Include} annotation. '*' is replaced with
* is replaced with the package of the Include annotation. '*' is replaced with
* the package of the included class.
*
* @return package pattern

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>1.17</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>record-builder-processor-java15</artifactId>
<build>
<sourceDirectory>../record-builder-processor/src/main/java</sourceDirectory>
<resources>
<resource>
<directory>../record-builder-processor/src/resources/**</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<proc>none</proc>
<release>${jdk-version}</release>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-core-java15</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -3,16 +3,12 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>31</version>
<version>1.17-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>record-builder-processor</artifactId>
<properties>
<license-file-path>${project.parent.basedir}/src/etc/header.txt</license-file-path>
</properties>
<dependencies>
<dependency>
<groupId>com.squareup</groupId>

View File

@@ -1,224 +0,0 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.*;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.lang.model.element.Modifier;
import java.util.*;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
class CollectionBuilderUtils {
private final boolean useImmutableCollections;
private final boolean addSingleItemCollectionBuilders;
private final String listShimName;
private final String mapShimName;
private final String setShimName;
private final String collectionShimName;
private boolean needsListShim;
private boolean needsMapShim;
private boolean needsSetShim;
private boolean needsCollectionShim;
private static final TypeName listType = TypeName.get(List.class);
private static final TypeName mapType = TypeName.get(Map.class);
private static final TypeName setType = TypeName.get(Set.class);
private static final TypeName collectionType = TypeName.get(Collection.class);
private static final TypeVariableName tType = TypeVariableName.get("T");
private static final TypeVariableName kType = TypeVariableName.get("K");
private static final TypeVariableName vType = TypeVariableName.get("V");
private static final ParameterizedTypeName parameterizedListType = ParameterizedTypeName.get(ClassName.get(List.class), tType);
private static final ParameterizedTypeName parameterizedMapType = ParameterizedTypeName.get(ClassName.get(Map.class), kType, vType);
private static final ParameterizedTypeName parameterizedSetType = ParameterizedTypeName.get(ClassName.get(Set.class), tType);
private static final ParameterizedTypeName parameterizedCollectionType = ParameterizedTypeName.get(ClassName.get(Collection.class), tType);
CollectionBuilderUtils(List<RecordClassType> recordComponents, RecordBuilder.Options metaData) {
useImmutableCollections = metaData.useImmutableCollections();
addSingleItemCollectionBuilders = metaData.addSingleItemCollectionBuilders();
listShimName = adjustShimName(recordComponents, "__list", 0);
mapShimName = adjustShimName(recordComponents, "__map", 0);
setShimName = adjustShimName(recordComponents, "__set", 0);
collectionShimName = adjustShimName(recordComponents, "__collection", 0);
}
enum SingleItemsMetaDataMode {
STANDARD,
STANDARD_FOR_SETTER,
EXCLUDE_WILDCARD_TYPES
}
record SingleItemsMetaData(Class<?> singleItemCollectionClass, List<TypeName> typeArguments, TypeName wildType) {}
Optional<SingleItemsMetaData> singleItemsMetaData(RecordClassType component, SingleItemsMetaDataMode mode) {
if (addSingleItemCollectionBuilders && (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName)) {
Class<?> collectionClass = null;
ClassName wildcardClass = null;
int typeArgumentQty = 0;
if (isList(component)) {
collectionClass = ArrayList.class;
wildcardClass = ClassName.get(Collection.class);
typeArgumentQty = 1;
} else if (isSet(component)) {
collectionClass = HashSet.class;
wildcardClass = ClassName.get(Collection.class);
typeArgumentQty = 1;
} else if (isMap(component)) {
collectionClass = HashMap.class;
wildcardClass = (ClassName) component.rawTypeName();
typeArgumentQty = 2;
}
var hasWildcardTypeArguments = hasWildcardTypeArguments(parameterizedTypeName, typeArgumentQty);
if (collectionClass != null) {
return switch (mode) {
case STANDARD -> singleItemsMetaDataWithWildType(parameterizedTypeName, collectionClass, wildcardClass, typeArgumentQty);
case STANDARD_FOR_SETTER -> {
if (hasWildcardTypeArguments) {
yield Optional.of(new SingleItemsMetaData(collectionClass, parameterizedTypeName.typeArguments, component.typeName()));
}
yield singleItemsMetaDataWithWildType(parameterizedTypeName, collectionClass, wildcardClass, typeArgumentQty);
}
case EXCLUDE_WILDCARD_TYPES -> {
if (hasWildcardTypeArguments) {
yield Optional.empty();
}
yield singleItemsMetaDataWithWildType(parameterizedTypeName, collectionClass, wildcardClass, typeArgumentQty);
}
};
}
}
return Optional.empty();
}
boolean isList(RecordClassType component) {
return component.rawTypeName().equals(listType);
}
boolean isMap(RecordClassType component) {
return component.rawTypeName().equals(mapType);
}
boolean isSet(RecordClassType component) {
return component.rawTypeName().equals(setType);
}
void add(CodeBlock.Builder builder, RecordClassType component) {
if (useImmutableCollections) {
if (isList(component)) {
needsListShim = true;
builder.add("$L($L)", listShimName, component.name());
} else if (isMap(component)) {
needsMapShim = true;
builder.add("$L($L)", mapShimName, component.name());
} else if (isSet(component)) {
needsSetShim = true;
builder.add("$L($L)", setShimName, component.name());
} else if (component.rawTypeName().equals(collectionType)) {
needsCollectionShim = true;
builder.add("$L($L)", collectionShimName, component.name());
} else {
builder.add("$L", component.name());
}
} else {
builder.add("$L", component.name());
}
}
void addShims(TypeSpec.Builder builder) {
if (!useImmutableCollections) {
return;
}
if (needsListShim) {
builder.addMethod(buildMethod(listShimName, listType, parameterizedListType, tType));
}
if (needsSetShim) {
builder.addMethod(buildMethod(setShimName, setType, parameterizedSetType, tType));
}
if (needsMapShim) {
builder.addMethod(buildMethod(mapShimName, mapType, parameterizedMapType, kType, vType));
}
if (needsCollectionShim) {
builder.addMethod(buildCollectionsMethod());
}
}
private Optional<SingleItemsMetaData> singleItemsMetaDataWithWildType(ParameterizedTypeName parameterizedTypeName, Class<?> collectionClass, ClassName wildcardClass, int typeArgumentQty) {
TypeName wildType;
if (typeArgumentQty == 1) {
wildType = ParameterizedTypeName.get(wildcardClass, WildcardTypeName.subtypeOf(parameterizedTypeName.typeArguments.get(0)));
} else { // if (typeArgumentQty == 2)
wildType = ParameterizedTypeName.get(wildcardClass, WildcardTypeName.subtypeOf(parameterizedTypeName.typeArguments.get(0)), WildcardTypeName.subtypeOf(parameterizedTypeName.typeArguments.get(1)));
}
return Optional.of(new SingleItemsMetaData(collectionClass, parameterizedTypeName.typeArguments, wildType));
}
private boolean hasWildcardTypeArguments(ParameterizedTypeName parameterizedTypeName, int argumentCount) {
for (int i = 0; i < argumentCount; ++i) {
if (parameterizedTypeName.typeArguments.size() > i) {
if (parameterizedTypeName.typeArguments.get(i) instanceof WildcardTypeName) {
return true;
}
}
}
return false;
}
private String adjustShimName(List<RecordClassType> recordComponents, String baseName, int index) {
var name = (index == 0) ? baseName : (baseName + index);
if (recordComponents.stream().anyMatch(component -> component.name().equals(name))) {
return adjustShimName(recordComponents, baseName, index + 1);
}
return name;
}
private MethodSpec buildMethod(String name, TypeName mainType, ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) {
var code = CodeBlock.of("return (o != null) ? $T.copyOf(o) : $T.of()", mainType, mainType);
return MethodSpec.methodBuilder(name)
.addAnnotation(generatedRecordBuilderAnnotation)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.addTypeVariables(Arrays.asList(typeVariables))
.returns(parameterizedType)
.addParameter(parameterizedType, "o")
.addStatement(code)
.build();
}
private MethodSpec buildCollectionsMethod() {
var code = CodeBlock.builder()
.add("if (o instanceof Set) {\n")
.indent()
.addStatement("return $T.copyOf(o)", setType)
.unindent()
.addStatement("}")
.addStatement("return (o != null) ? $T.copyOf(o) : $T.of()", listType, listType)
.build();
return MethodSpec.methodBuilder(collectionShimName)
.addAnnotation(generatedRecordBuilderAnnotation)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.addTypeVariable(tType)
.returns(parameterizedCollectionType)
.addParameter(parameterizedCollectionType, "o")
.addCode(code)
.build();
}
}

View File

@@ -19,12 +19,11 @@ import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.RecordBuilder;
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.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.RecordComponentElement;
import javax.lang.model.element.TypeElement;
@@ -52,19 +51,12 @@ public class ElementUtils {
}
@SuppressWarnings("unchecked")
public static List<TypeMirror> getAttributeTypeMirrorList(AnnotationValue attribute)
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());
}
@SuppressWarnings("unchecked")
public static List<String> getAttributeStringList(AnnotationValue attribute)
{
List<? extends AnnotationValue> values = (attribute != null) ? (List<? extends AnnotationValue>)attribute.getValue() : Collections.emptyList();
return values.stream().map(v -> (String)v.getValue()).collect(Collectors.toList());
}
public static boolean getBooleanAttribute(AnnotationValue attribute)
{
Object value = (attribute != null) ? attribute.getValue() : null;
@@ -115,10 +107,8 @@ public class ElementUtils {
return new ClassType(ParameterizedTypeName.get(builderClassName, typeNames), builderClassName.simpleName());
}
public static RecordClassType getRecordClassType(ProcessingEnvironment processingEnv, RecordComponentElement recordComponent, List<? extends AnnotationMirror> accessorAnnotations, List<? extends AnnotationMirror> canonicalConstructorAnnotations) {
var typeName = TypeName.get(recordComponent.asType());
var rawTypeName = TypeName.get(processingEnv.getTypeUtils().erasure(recordComponent.asType()));
return new RecordClassType(typeName, rawTypeName, recordComponent.getSimpleName().toString(), accessorAnnotations, canonicalConstructorAnnotations);
public static ClassType getClassType(RecordComponentElement recordComponent) {
return new ClassType(TypeName.get(recordComponent.asType()), recordComponent.getSimpleName().toString());
}
public static String getWithMethodName(ClassType component, String prefix) {
@@ -129,29 +119,12 @@ public class ElementUtils {
return prefix + Character.toUpperCase(name.charAt(0)) + name.substring(1);
}
public static String getBuilderName(TypeElement element, RecordBuilder.Options metaData, ClassType classType, String suffix) {
public static String getBuilderName(TypeElement element, RecordBuilderMetaData metaData, ClassType classType, String suffix) {
// generate the class name
var baseName = classType.name() + suffix;
return metaData.prefixEnclosingClassNames() ? (getBuilderNamePrefix(element.getEnclosingElement()) + baseName) : baseName;
}
public static Optional<? extends Element> findCanonicalConstructor(TypeElement record) {
if ( record.getKind() != ElementKind.RECORD ) {
return Optional.empty();
}
// based on https://github.com/openjdk/jdk/pull/3556/files#diff-a6270f4b50989abe733607c69038b2036306d13f77276af005d023b7fc57f1a2R2368
var componentList = record.getRecordComponents().stream().map(e -> e.asType().toString()).collect(Collectors.toList());
return record.getEnclosedElements().stream()
.filter(element -> element.getKind() == ElementKind.CONSTRUCTOR)
.filter(element -> {
var parameters = ((ExecutableElement)element).getParameters();
var parametersList = parameters.stream().map(e -> e.asType().toString()).collect(Collectors.toList());
return componentList.equals(parametersList);
})
.findFirst();
}
private static String getBuilderNamePrefix(Element element) {
// prefix enclosing class names if nested in a class
if (element instanceof TypeElement) {

View File

@@ -1,96 +0,0 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
class IncludeHelper {
private final boolean isValid;
private final List<TypeElement> classTypeElements;
private final Map<? extends ExecutableElement, ? extends AnnotationValue> annotationValues;
IncludeHelper(ProcessingEnvironment processingEnv, Element element, AnnotationMirror annotationMirror, boolean packagesSupported) {
annotationValues = processingEnv.getElementUtils().getElementValuesWithDefaults(annotationMirror);
var value = ElementUtils.getAnnotationValue(annotationValues, "value");
var classes = ElementUtils.getAnnotationValue(annotationValues, "classes");
var packages = ElementUtils.getAnnotationValue(annotationValues, "packages");
var isValid = true;
var classTypeElements = new ArrayList<TypeElement>();
if (value.isPresent() || classes.isPresent() || packages.isPresent()) {
var valueList = value.map(ElementUtils::getAttributeTypeMirrorList).orElseGet(List::of);
var classesList = classes.map(ElementUtils::getAttributeTypeMirrorList).orElseGet(List::of);
var packagesList = packages.map(ElementUtils::getAttributeStringList).orElseGet(List::of);
if (valueList.isEmpty() && classesList.isEmpty() && packagesList.isEmpty()) {
if (packagesSupported) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "At least one of \"value\", \"classes\" or \"packages\" required", element);
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "At least one of \"value\" or \"classes\" required", element);
}
isValid = false;
}
isValid = processList(processingEnv, isValid, element, valueList, classTypeElements);
isValid = processList(processingEnv, isValid, element, classesList, classTypeElements);
packages.ifPresent(annotationValue -> processPackages(processingEnv, classTypeElements, packagesList));
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not read attribute for annotation", element);
isValid = false;
}
this.isValid = isValid;
this.classTypeElements = List.copyOf(classTypeElements);
}
Map<? extends ExecutableElement, ? extends AnnotationValue> getAnnotationValues() {
return annotationValues;
}
boolean isValid() {
return isValid;
}
List<TypeElement> getClassTypeElements() {
return classTypeElements;
}
private boolean processList(ProcessingEnvironment processingEnv, boolean isValid, Element element, List<TypeMirror> list, ArrayList<TypeElement> classTypeElements) {
for (var typeMirror : list) {
TypeElement typeElement = (TypeElement) processingEnv.getTypeUtils().asElement(typeMirror);
if (typeElement == null) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get element for: " + typeMirror, element);
isValid = false;
} else {
classTypeElements.add(typeElement);
}
}
return isValid;
}
private void processPackages(ProcessingEnvironment processingEnv, List<TypeElement> classTypeElements, List<String> packagesList) {
for (var packageName : packagesList) {
var packageElement = processingEnv.getElementUtils().getPackageElement(packageName);
for (var child : packageElement.getEnclosedElements()) {
if (child.getKind() == ElementKind.RECORD) {
classTypeElements.add((TypeElement) child);
}
}
}
}
}

View File

@@ -15,62 +15,60 @@
*/
package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.*;
import io.soabase.recordbuilder.core.RecordBuilder;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import java.util.*;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.*;
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
class InternalRecordBuilderProcessor {
private final RecordBuilder.Options metaData;
class InternalRecordBuilderProcessor
{
private final RecordBuilderMetaData metaData;
private final ClassType recordClassType;
private final String packageName;
private final ClassType builderClassType;
private final List<TypeVariableName> typeVariables;
private final List<RecordClassType> recordComponents;
private final List<ClassType> recordComponents;
private final TypeSpec builderType;
private final TypeSpec.Builder builder;
private final String uniqueVarName;
private final Pattern notNullPattern;
private final CollectionBuilderUtils collectionBuilderUtils;
private static final TypeName overrideType = TypeName.get(Override.class);
private static final TypeName optionalType = TypeName.get(Optional.class);
private static final TypeName optionalIntType = TypeName.get(OptionalInt.class);
private static final TypeName optionalLongType = TypeName.get(OptionalLong.class);
private static final TypeName optionalDoubleType = TypeName.get(OptionalDouble.class);
private static final TypeName validatorTypeName = ClassName.get("io.soabase.recordbuilder.validator", "RecordBuilderValidator");
private static final TypeVariableName rType = TypeVariableName.get("R");
private final ProcessingEnvironment processingEnv;
InternalRecordBuilderProcessor(ProcessingEnvironment processingEnv, TypeElement record, RecordBuilder.Options metaData, Optional<String> packageNameOpt) {
this.processingEnv = processingEnv;
var recordActualPackage = ElementUtils.getPackageName(record);
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData, Optional<String> packageNameOpt)
{
this.metaData = metaData;
recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
packageName = packageNameOpt.orElse(recordActualPackage);
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 = buildRecordComponents(record);
recordComponents = record.getRecordComponents().stream().map(ElementUtils::getClassType).collect(Collectors.toList());
uniqueVarName = getUniqueVarName();
notNullPattern = Pattern.compile(metaData.interpretNotNullsPattern());
collectionBuilderUtils = new CollectionBuilderUtils(recordComponents, this.metaData);
builder = TypeSpec.classBuilder(builderClassType.name())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables);
addVisibility(recordActualPackage.equals(packageName), record.getModifiers());
addWithNestedClass();
addDefaultConstructor();
addStaticBuilder();
@@ -79,7 +77,6 @@ class InternalRecordBuilderProcessor {
}
addStaticDefaultBuilderMethod();
addStaticCopyBuilderMethod();
addStaticFromWithMethod();
addStaticComponentsMethod();
addBuildMethod();
addToStringMethod();
@@ -89,50 +86,28 @@ class InternalRecordBuilderProcessor {
add1Field(component);
add1SetterMethod(component);
add1GetterMethod(component);
var collectionMetaData = collectionBuilderUtils.singleItemsMetaData(component, EXCLUDE_WILDCARD_TYPES);
collectionMetaData.ifPresent(meta -> add1CollectionBuilders(meta, component));
});
collectionBuilderUtils.addShims(builder);
addStaticDowncastMethod();
builderType = builder.build();
}
String packageName() {
String packageName()
{
return packageName;
}
ClassType builderClassType() {
ClassType builderClassType()
{
return builderClassType;
}
TypeSpec builderType() {
TypeSpec builderType()
{
return builderType;
}
private void addVisibility(boolean builderIsInRecordPackage, Set<Modifier> modifiers) {
if (builderIsInRecordPackage) {
if (modifiers.contains(Modifier.PUBLIC) || modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.PROTECTED)) {
builder.addModifiers(Modifier.PUBLIC); // builders are top level classes - can only be public or package-private
}
// is package-private
} else {
builder.addModifiers(Modifier.PUBLIC);
}
}
private List<RecordClassType> buildRecordComponents(TypeElement record) {
var accessorAnnotations = record.getRecordComponents().stream().map(e -> e.getAccessor().getAnnotationMirrors()).collect(Collectors.toList());
var canonicalConstructorAnnotations = ElementUtils.findCanonicalConstructor(record).map(constructor -> ((ExecutableElement) constructor).getParameters().stream().map(Element::getAnnotationMirrors).collect(Collectors.toList())).orElse(List.of());
var recordComponents = record.getRecordComponents();
return IntStream.range(0, recordComponents.size())
.mapToObj(index -> {
var thisAccessorAnnotations = (accessorAnnotations.size() > index) ? accessorAnnotations.get(index) : List.<AnnotationMirror>of();
var thisCanonicalConstructorAnnotations = (canonicalConstructorAnnotations.size() > index) ? canonicalConstructorAnnotations.get(index) : List.<AnnotationMirror>of();
return ElementUtils.getRecordClassType(processingEnv, recordComponents.get(index), thisAccessorAnnotations, thisCanonicalConstructorAnnotations);
})
.collect(Collectors.toList());
}
private void addWithNestedClass() {
private void addWithNestedClass()
{
/*
Adds a nested interface that adds withers similar to:
@@ -147,20 +122,14 @@ class InternalRecordBuilderProcessor {
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PUBLIC)
.addTypeVariables(typeVariables);
recordComponents.forEach(component -> addNestedGetterMethod(classBuilder, component));
addWithBuilderMethod(classBuilder);
addWithSuppliedBuilderMethod(classBuilder);
IntStream.range(0, recordComponents.size()).forEach(index -> add1WithMethod(classBuilder, recordComponents.get(index), index));
if (metaData.addFunctionalMethodsToWith()) {
classBuilder.addType(buildFunctionalInterface("Function", true))
.addType(buildFunctionalInterface("Consumer", false))
.addMethod(buildFunctionalHandler("Function", "map", true))
.addMethod(buildFunctionalHandler("Consumer", "accept", false));
}
builder.addType(classBuilder.build());
}
private void addWithSuppliedBuilderMethod(TypeSpec.Builder classBuilder) {
private void addWithSuppliedBuilderMethod(TypeSpec.Builder classBuilder)
{
/*
Adds a method that returns a pre-filled copy builder similar to:
@@ -187,18 +156,19 @@ class InternalRecordBuilderProcessor {
classBuilder.addMethod(methodSpec);
}
private void addWithBuilderMethod(TypeSpec.Builder classBuilder) {
private void addWithBuilderMethod(TypeSpec.Builder classBuilder)
{
/*
Adds a method that returns a pre-filled copy builder similar to:
default MyRecordBuilder with() {
MyRecord r = _downcast(this);
return MyRecordBuilder.builder(r);
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("return new $L(", builderClassType.name());
addComponentCallsAsArguments(-1, codeBlockBuilder);
codeBlockBuilder.add(");");
.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")
@@ -209,69 +179,64 @@ class InternalRecordBuilderProcessor {
classBuilder.addMethod(methodSpec);
}
private String getUniqueVarName() {
private String getUniqueVarName()
{
return getUniqueVarName("");
}
private String getUniqueVarName(String prefix) {
private String getUniqueVarName(String prefix)
{
var name = prefix + "r";
var alreadyExists = recordComponents.stream()
.map(ClassType::name)
.anyMatch(n -> n.equals(name));
.map(ClassType::name)
.anyMatch(n -> n.equals(name));
return alreadyExists ? getUniqueVarName(prefix + "_") : name;
}
private void add1WithMethod(TypeSpec.Builder classBuilder, RecordClassType component, int index) {
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 = _downcast(this);
return new MyRecord(name, r.age());
}
*/
var codeBlockBuilder = CodeBlock.builder();
addNullCheckCodeBlock(codeBlockBuilder, index);
codeBlockBuilder.add("$[return ");
if (metaData.useValidationApi()) {
codeBlockBuilder.add("$T.validate(", validatorTypeName);
if (recordComponents.size() > 1) {
codeBlockBuilder.add("$T $L = $L(this);\n", recordClassType.typeName(), uniqueVarName, metaData.downCastMethodName());
}
codeBlockBuilder.add("new $T(", recordClassType.typeName());
addComponentCallsAsArguments(index, codeBlockBuilder);
codeBlockBuilder.add(")");
if (metaData.useValidationApi()) {
codeBlockBuilder.add(")");
}
codeBlockBuilder.add(";$]");
codeBlockBuilder.add("return new $T(", recordClassType.typeName());
IntStream.range(0, recordComponents.size()).forEach(parameterIndex -> {
if (parameterIndex > 0) {
codeBlockBuilder.add(", ");
}
ClassType parameterComponent = recordComponents.get(parameterIndex);
if (parameterIndex == index) {
codeBlockBuilder.add(parameterComponent.name());
}
else {
codeBlockBuilder.add("$L.$L()", uniqueVarName, parameterComponent.name());
}
});
codeBlockBuilder.add(");");
var methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
addConstructorAnnotations(component, parameterSpecBuilder);
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
var methodSpec = MethodSpec.methodBuilder(methodName)
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Return a new instance of {@code $L} with a new value for {@code $L}\n", recordClassType.name(), component.name())
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addParameter(parameterSpecBuilder.build())
.addParameter(parameterSpec)
.addCode(codeBlockBuilder.build())
.returns(recordClassType.typeName())
.build();
classBuilder.addMethod(methodSpec);
}
private void addComponentCallsAsArguments(int index, CodeBlock.Builder codeBlockBuilder) {
IntStream.range(0, recordComponents.size()).forEach(parameterIndex -> {
if (parameterIndex > 0) {
codeBlockBuilder.add(", ");
}
RecordClassType parameterComponent = recordComponents.get(parameterIndex);
if (parameterIndex == index) {
collectionBuilderUtils.add(codeBlockBuilder, parameterComponent);
} else {
codeBlockBuilder.add("$L()", parameterComponent.name());
}
});
}
private void addDefaultConstructor() {
private void addDefaultConstructor()
{
/*
Adds a default constructor similar to:
@@ -285,7 +250,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(constructor);
}
private void addStaticBuilder() {
private void addStaticBuilder()
{
/*
Adds an static builder similar to:
@@ -300,38 +266,13 @@ class InternalRecordBuilderProcessor {
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(recordClassType.typeName())
.addCode(codeBlock);
recordComponents.forEach(component -> {
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
addConstructorAnnotations(component, parameterSpecBuilder);
builder.addParameter(parameterSpecBuilder.build());
});
.addStatement(codeBlock);
recordComponents.forEach(component -> builder.addParameter(component.typeName(), component.name()));
this.builder.addMethod(builder.build());
}
private void addNullCheckCodeBlock(CodeBlock.Builder builder) {
if (metaData.interpretNotNulls()) {
for (int i = 0; i < recordComponents.size(); ++i) {
addNullCheckCodeBlock(builder, i);
}
}
}
private void addNullCheckCodeBlock(CodeBlock.Builder builder, int index) {
if (metaData.interpretNotNulls()) {
var component = recordComponents.get(index);
if (!component.typeName().isPrimitive() && isNullAnnotated(component)) {
builder.addStatement("$T.requireNonNull($L, $S)", Objects.class, component.name(), component.name() + " is required");
}
}
}
private boolean isNullAnnotated(RecordClassType component) {
return component.getCanonicalConstructorAnnotations().stream()
.anyMatch(annotation -> notNullPattern.matcher(annotation.getAnnotationType().asElement().getSimpleName().toString()).matches());
}
private void addAllArgsConstructor() {
private void addAllArgsConstructor()
{
/*
Adds an all-args constructor similar to:
@@ -346,14 +287,14 @@ class InternalRecordBuilderProcessor {
.addAnnotation(generatedRecordBuilderAnnotation);
recordComponents.forEach(component -> {
constructorBuilder.addParameter(component.typeName(), component.name());
var collectionMetaData = collectionBuilderUtils.singleItemsMetaData(component, STANDARD);
collectionMetaData.ifPresentOrElse(meta -> constructorBuilder.addStatement("this.$L = new $T<>($L)", component.name(), meta.singleItemCollectionClass(), component.name()),
() -> constructorBuilder.addStatement("this.$L = $L", component.name(), component.name()));
var codeBuilder = CodeBlock.builder().add("this.$L = $L", component.name(), component.name());
constructorBuilder.addStatement(codeBuilder.build());
});
builder.addMethod(constructorBuilder.build());
}
private void addToStringMethod() {
private void addToStringMethod()
{
/*
add a toString() method similar to:
@@ -382,7 +323,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addHashCodeMethod() {
private void addHashCodeMethod()
{
/*
add a hashCode() method similar to:
@@ -410,7 +352,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addEqualsMethod() {
private void addEqualsMethod()
{
/*
add an equals() method similar to:
@@ -428,7 +371,8 @@ class InternalRecordBuilderProcessor {
String name = recordComponent.name();
if (recordComponent.typeName().isPrimitive()) {
codeBuilder.add("\n&& ($L == $L.$L)", name, uniqueVarName, name);
} else {
}
else {
codeBuilder.add("\n&& $T.equals($L, $L.$L)", Objects.class, name, uniqueVarName, name);
}
});
@@ -445,7 +389,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addBuildMethod() {
private void addBuildMethod()
{
/*
Adds the build method that generates the record similar to:
@@ -459,7 +404,7 @@ class InternalRecordBuilderProcessor {
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(recordClassType.typeName())
.addCode(codeBlock)
.addStatement(codeBlock)
.build();
builder.addMethod(methodSpec);
}
@@ -469,89 +414,19 @@ class InternalRecordBuilderProcessor {
Builds the code block for allocating the record from its parts
*/
var codeBuilder = CodeBlock.builder();
addNullCheckCodeBlock(codeBuilder);
codeBuilder.add("$[return ");
if (metaData.useValidationApi()) {
codeBuilder.add("$T.validate(", validatorTypeName);
}
codeBuilder.add("new $T(", recordClassType.typeName());
var codeBuilder = CodeBlock.builder().add("return new $T(", recordClassType.typeName());
IntStream.range(0, recordComponents.size()).forEach(index -> {
if (index > 0) {
codeBuilder.add(", ");
}
collectionBuilderUtils.add(codeBuilder, recordComponents.get(index));
codeBuilder.add("$L", recordComponents.get(index).name());
});
codeBuilder.add(")");
if (metaData.useValidationApi()) {
codeBuilder.add(")");
}
codeBuilder.add(";$]");
return codeBuilder.build();
}
private void addStaticFromWithMethod() {
/*
Adds static method that returns a "with"er view of an existing record.
public static With from(MyRecord from) {
return new MyRecordBuilder.With() {
@Override
public String p1() {
return from.p1();
}
@Override
public String p2() {
return from.p2();
}
};
}
*/
var witherClassNameBuilder = CodeBlock.builder()
.add("$L.$L", builderClassType.name(), metaData.withClassName());
if (!typeVariables.isEmpty()) {
witherClassNameBuilder.add("<");
IntStream.range(0, typeVariables.size()).forEach(index -> {
if (index > 0) {
witherClassNameBuilder.add(", ");
}
witherClassNameBuilder.add(typeVariables.get(index).name);
});
witherClassNameBuilder.add(">");
}
var witherClassName = witherClassNameBuilder.build().toString();
var codeBuilder = CodeBlock.builder()
.add("return new $L", witherClassName)
.add("() {\n").indent();
IntStream.range(0, recordComponents.size()).forEach(index -> {
var component = recordComponents.get(index);
if (index > 0) {
codeBuilder.add("\n");
}
codeBuilder.add("@Override\n")
.add("public $T $L() {\n", component.typeName(), component.name())
.indent()
.addStatement("return from.$L()", component.name())
.unindent()
.add("}\n");
});
codeBuilder.unindent().addStatement("}");
var withType = ClassName.get("", witherClassName);
var methodSpec = MethodSpec.methodBuilder("from")//metaData.copyMethodName())
.addJavadoc("Return a \"with\"er for an existing record instance\n")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables)
.addParameter(recordClassType.typeName(), metaData.fromMethodName())
.returns(withType)
.addCode(codeBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void addStaticCopyBuilderMethod() {
private void addStaticCopyBuilderMethod()
{
/*
Adds a copy builder method that pre-fills the builder with existing values similar to:
@@ -580,7 +455,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addStaticDefaultBuilderMethod() {
private void addStaticDefaultBuilderMethod()
{
/*
Adds a the default builder method similar to:
@@ -599,13 +475,16 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addStaticComponentsMethod() {
private void addStaticComponentsMethod()
{
/*
Adds a static method that converts a record instance into a stream of its component parts
public static Stream<Map.Entry<String, Object>> stream(MyRecord record) {
return Stream.of(new AbstractMap.SimpleImmutableEntry<>("p1", record.p1()),
new AbstractMap.SimpleImmutableEntry<>("p2", record.p2()));
return Stream.of(
new AbstractMap.SimpleEntry<>("p1", record.p1()),
new AbstractMap.SimpleEntry<>("p2", record.p2())
);
}
*/
var codeBuilder = CodeBlock.builder().add("return $T.of(", Stream.class);
@@ -614,7 +493,7 @@ class InternalRecordBuilderProcessor {
codeBuilder.add(",\n ");
}
var name = recordComponents.get(index).name();
codeBuilder.add("new $T<>($S, record.$L())", AbstractMap.SimpleImmutableEntry.class, name, name);
codeBuilder.add("new $T<>($S, record.$L())", AbstractMap.SimpleEntry.class, name, name);
});
codeBuilder.add(")");
var mapEntryTypeVariables = ParameterizedTypeName.get(Map.Entry.class, String.class, Object.class);
@@ -631,202 +510,51 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void add1Field(ClassType component) {
private void addStaticDowncastMethod()
{
/*
Adds a method that downcasts to the record type
private static MyRecord _downcast(Object this) {
return (MyRecord)this;
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("try {\n")
.indent()
.add("return ($T)obj;\n", recordClassType.typeName())
.unindent()
.add("}\n")
.add("catch (ClassCastException dummy) {\n")
.indent()
.add("throw new RuntimeException($S);\n", builderClassType.name() + "." + metaData.withClassName() + " can only be implemented by " + recordClassType.name())
.unindent()
.add("}");
var methodSpec = MethodSpec.methodBuilder(metaData.downCastMethodName())
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Downcast to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.addParameter(Object.class, "obj")
.addTypeVariables(typeVariables)
.returns(recordClassType.typeName())
.addCode(codeBlockBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void add1Field(ClassType component)
{
/*
For a single record component, add a field similar to:
private T p;
*/
var fieldSpecBuilder = FieldSpec.builder(component.typeName(), component.name(), Modifier.PRIVATE);
if (metaData.emptyDefaultForOptional()) {
TypeName thisOptionalType = null;
if (isOptional(component)) {
thisOptionalType = optionalType;
} else if (component.typeName().equals(optionalIntType)) {
thisOptionalType = optionalIntType;
} else if (component.typeName().equals(optionalLongType)) {
thisOptionalType = optionalLongType;
} else if (component.typeName().equals(optionalDoubleType)) {
thisOptionalType = optionalDoubleType;
}
if (thisOptionalType != null) {
var codeBlock = CodeBlock.builder().add("$T.empty()", thisOptionalType).build();
fieldSpecBuilder.initializer(codeBlock);
}
}
builder.addField(fieldSpecBuilder.build());
var fieldSpec = FieldSpec.builder(component.typeName(), component.name(), Modifier.PRIVATE).build();
builder.addField(fieldSpec);
}
private boolean isOptional(ClassType component) {
if (component.typeName().equals(optionalType)) {
return true;
}
return (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName) && parameterizedTypeName.rawType.equals(optionalType);
}
private void addNestedGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component) {
/*
For a single record component, add a getter similar to:
T p();
*/
var methodSpecBuilder = MethodSpec.methodBuilder(component.name())
.addJavadoc("Return the current value for the {@code $L} record component in the builder\n", component.name())
.addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(component.typeName());
addAccessorAnnotations(component, methodSpecBuilder);
classBuilder.addMethod(methodSpecBuilder.build());
}
private boolean filterOutOverride(AnnotationSpec annotationSpec) {
return !annotationSpec.type.equals(overrideType);
}
private void addConstructorAnnotations(RecordClassType component, ParameterSpec.Builder parameterSpecBuilder) {
if (metaData.inheritComponentAnnotations()) {
component.getCanonicalConstructorAnnotations()
.stream()
.map(AnnotationSpec::get)
.filter(this::filterOutOverride)
.forEach(parameterSpecBuilder::addAnnotation);
}
}
private void addAccessorAnnotations(RecordClassType component, MethodSpec.Builder methodSpecBuilder) {
if (metaData.inheritComponentAnnotations()) {
component.getAccessorAnnotations()
.stream()
.map(AnnotationSpec::get)
.filter(this::filterOutOverride)
.forEach(methodSpecBuilder::addAnnotation);
}
}
private String capitalize(String s) {
return (s.length() < 2) ? s.toUpperCase(Locale.ROOT) : (Character.toUpperCase(s.charAt(0)) + s.substring(1));
}
private void add1CollectionBuilders(CollectionBuilderUtils.SingleItemsMetaData meta, RecordClassType component) {
if (collectionBuilderUtils.isList(component) || collectionBuilderUtils.isSet(component)) {
add1ListBuilder(meta, component);
} else if (collectionBuilderUtils.isMap(component)) {
add1MapBuilder(meta, component);
}
}
private void add1MapBuilder(CollectionBuilderUtils.SingleItemsMetaData meta, RecordClassType component) {
/*
For a single map record component, add a methods similar to:
public T addP(K key, V value) {
if (this.p == null) {
this.p = new HashMap<>();
}
this.p.put(key, value);
return this;
}
public T addP(Stream<? extends Map.Entry<K, V> i) {
if (p == null) {
p = new HashMap<>();
}
i.forEach(this.p::put);
return this;
}
public T addP(Iterable<? extends Map.Entry<K, V> i) {
if (p == null) {
p = new HashMap<>();
}
i.forEach(this.p::put);
return this;
}
*/
for (var i = 0; i < 3; ++i) {
var codeBlockBuilder = CodeBlock.builder()
.beginControlFlow("if (this.$L == null)", component.name())
.addStatement("this.$L = new $T<>()", component.name(), HashMap.class)
.endControlFlow();
var methodSpecBuilder = MethodSpec.methodBuilder(metaData.singleItemBuilderPrefix() + capitalize(component.name()))
.addJavadoc("Add to the internally allocated {@code HashMap} for {@code $L}\n", component.name())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(builderClassType.typeName());
if (i == 0) {
methodSpecBuilder.addParameter(meta.typeArguments().get(0), "key");
methodSpecBuilder.addParameter(meta.typeArguments().get(1), "value");
codeBlockBuilder.addStatement("this.$L.put(key, value)", component.name());
} else {
var parameterClass = ClassName.get((i == 1) ? Stream.class : Iterable.class);
var entryType = ParameterizedTypeName.get(ClassName.get(Map.Entry.class), WildcardTypeName.subtypeOf(meta.typeArguments().get(0)), WildcardTypeName.subtypeOf(meta.typeArguments().get(1)));
ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(parameterClass, WildcardTypeName.subtypeOf(entryType));
methodSpecBuilder.addParameter(parameterizedTypeName, "i");
codeBlockBuilder.addStatement("i.forEach(entry -> this.$L.put(entry.getKey(), entry.getValue()))", component.name());
}
codeBlockBuilder.addStatement("return this");
methodSpecBuilder.addCode(codeBlockBuilder.build());
builder.addMethod(methodSpecBuilder.build());
}
}
private void add1ListBuilder(CollectionBuilderUtils.SingleItemsMetaData meta, RecordClassType component) {
/*
For a single list or set record component, add methods similar to:
public T addP(I i) {
if (this.p == null) {
this.p = new ArrayList<>();
}
this.p.add(i);
return this;
}
public T addP(Stream<? extends I> i) {
if (this.p == null) {
this.p = new ArrayList<>();
}
this.p.addAll(i);
return this;
}
public T addP(Iterable<? extends I> i) {
if (this.p == null) {
this.p = new ArrayList<>();
}
this.p.addAll(i);
return this;
}
*/
for (var i = 0; i < 3; ++i) {
var addClockBlock = CodeBlock.builder();
TypeName parameter;
if (i == 0) {
addClockBlock.addStatement("this.$L.add(i)", component.name());
parameter = meta.typeArguments().get(0);
} else {
addClockBlock.addStatement("i.forEach(this.$L::add)", component.name());
var parameterClass = ClassName.get((i == 1) ? Stream.class : Iterable.class);
parameter = ParameterizedTypeName.get(parameterClass, WildcardTypeName.subtypeOf(meta.typeArguments().get(0)));
}
var codeBlockBuilder = CodeBlock.builder()
.beginControlFlow("if (this.$L == null)", component.name())
.addStatement("this.$L = new $T<>()", component.name(), meta.singleItemCollectionClass())
.endControlFlow()
.add(addClockBlock.build())
.addStatement("return this");
var methodSpecBuilder = MethodSpec.methodBuilder(metaData.singleItemBuilderPrefix() + capitalize(component.name()))
.addJavadoc("Add to the internally allocated {@code $L} for {@code $L}\n", meta.singleItemCollectionClass().getSimpleName(), component.name())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(builderClassType.typeName())
.addParameter(parameter, "i")
.addCode(codeBlockBuilder.build());
builder.addMethod(methodSpecBuilder.build());
}
}
private void add1GetterMethod(RecordClassType component) {
private void add1GetterMethod(ClassType component)
{
/*
For a single record component, add a getter similar to:
@@ -834,17 +562,18 @@ class InternalRecordBuilderProcessor {
return p;
}
*/
var methodSpecBuilder = MethodSpec.methodBuilder(component.name())
var methodSpec = MethodSpec.methodBuilder(component.name())
.addJavadoc("Return the current value for the {@code $L} record component in the builder\n", component.name())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(component.typeName())
.addStatement("return $L", component.name());
addAccessorAnnotations(component, methodSpecBuilder);
builder.addMethod(methodSpecBuilder.build());
.addStatement("return $L", component.name())
.build();
builder.addMethod(methodSpec);
}
private void add1SetterMethod(RecordClassType component) {
private void add1SetterMethod(ClassType component)
{
/*
For a single record component, add a setter similar to:
@@ -853,92 +582,16 @@ class InternalRecordBuilderProcessor {
return this;
}
*/
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
var methodSpec = MethodSpec.methodBuilder(component.name())
.addJavadoc("Set a new value for the {@code $L} record component in the builder\n", component.name())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(builderClassType.typeName());
var collectionMetaData = collectionBuilderUtils.singleItemsMetaData(component, STANDARD_FOR_SETTER);
var parameterSpecBuilder = collectionMetaData.map(meta -> {
CodeBlock.Builder codeSpec = CodeBlock.builder();
codeSpec.addStatement("this.$L = ($L != null) ? new $T<>($L) : null", component.name(), component.name(), meta.singleItemCollectionClass(), component.name());
methodSpec.addJavadoc("Re-create the internally allocated {@code $L} for {@code $L} by copying the argument\n", meta.singleItemCollectionClass().getSimpleName(), component.name())
.addCode(codeSpec.build());
return ParameterSpec.builder(meta.wildType(), component.name());
}).orElseGet(() -> {
methodSpec.addJavadoc("Set a new value for the {@code $L} record component in the builder\n", component.name())
.addStatement("this.$L = $L", component.name(), component.name());
return ParameterSpec.builder(component.typeName(), component.name());
});
addConstructorAnnotations(component, parameterSpecBuilder);
methodSpec.addStatement("return this").addParameter(parameterSpecBuilder.build());
builder.addMethod(methodSpec.build());
}
private List<TypeVariableName> typeVariablesWithReturn() {
var variables = new ArrayList<TypeVariableName>();
variables.add(rType);
variables.addAll(typeVariables);
return variables;
}
private MethodSpec buildFunctionalHandler(String className, String methodName, boolean isMap) {
/*
Build a Functional handler ala:
default <R> R map(Function<R, T> proc) {
return proc.apply(p());
}
*/
var localTypeVariables = isMap ? typeVariablesWithReturn() : typeVariables;
var typeName = localTypeVariables.isEmpty() ? ClassName.get("", className) : ParameterizedTypeName.get(ClassName.get("", className), localTypeVariables.toArray(TypeName[]::new));
var methodBuilder = MethodSpec.methodBuilder(methodName)
.addAnnotation(generatedRecordBuilderAnnotation)
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addParameter(typeName, "proc");
var codeBlockBuilder = CodeBlock.builder();
if (isMap) {
methodBuilder.addJavadoc("Map record components into a new object");
methodBuilder.addTypeVariable(rType);
methodBuilder.returns(rType);
codeBlockBuilder.add("return ");
} else {
methodBuilder.addJavadoc("Perform an operation on record components");
}
codeBlockBuilder.add("proc.apply(");
addComponentCallsAsArguments(-1, codeBlockBuilder);
codeBlockBuilder.add(");");
methodBuilder.addCode(codeBlockBuilder.build());
return methodBuilder.build();
}
private TypeSpec buildFunctionalInterface(String className, boolean isMap) {
/*
Build a Functional interface ala:
@FunctionalInterface
interface Function<R, T> {
R apply(T a);
}
*/
var localTypeVariables = isMap ? typeVariablesWithReturn() : typeVariables;
var methodBuilder = MethodSpec.methodBuilder("apply").addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT);
recordComponents.forEach(component -> {
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
addConstructorAnnotations(component, parameterSpecBuilder);
methodBuilder.addParameter(parameterSpecBuilder.build());
});
if (isMap) {
methodBuilder.returns(rType);
}
return TypeSpec.interfaceBuilder(className)
.addAnnotation(generatedRecordBuilderAnnotation)
.addAnnotation(FunctionalInterface.class)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addTypeVariables(localTypeVariables)
.addMethod(methodBuilder.build())
.addParameter(parameterSpec)
.returns(builderClassType.typeName())
.addStatement("this.$L = $L", component.name(), component.name())
.addStatement("return this")
.build();
builder.addMethod(methodSpec);
}
}

View File

@@ -15,10 +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 javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
@@ -26,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;
@@ -47,10 +57,9 @@ class InternalRecordInterfaceProcessor {
private static final Set<String> javaBeanPrefixes = Set.of("get", "is");
private record Component(ExecutableElement element, Optional<String> alternateName) {
}
private record Component(ExecutableElement element, Optional<String> alternateName){}
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilder.Options metaData, Optional<String> packageNameOpt, boolean fromTemplate) {
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageNameOpt) {
this.processingEnv = processingEnv;
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(iface));
recordComponents = getRecordComponents(iface);
@@ -73,14 +82,6 @@ class InternalRecordInterfaceProcessor {
ClassType builderClassType = ElementUtils.getClassType(packageName, getBuilderName(iface, metaData, recordClassType, metaData.suffix()) + "." + metaData.withClassName(), iface.getTypeParameters());
builder.addAnnotation(RecordBuilder.class);
builder.addSuperinterface(builderClassType.typeName());
if (fromTemplate) {
builder.addAnnotation(AnnotationSpec.get(metaData));
} else {
var options = iface.getAnnotation(RecordBuilder.Options.class);
if (options != null) {
builder.addAnnotation(AnnotationSpec.get(options));
}
}
}
alternateMethods = buildAlternateMethods(recordComponents);
@@ -88,7 +89,8 @@ class InternalRecordInterfaceProcessor {
recordType = builder.build();
}
boolean isValid() {
boolean isValid()
{
return !recordComponents.isEmpty();
}
@@ -104,7 +106,8 @@ class InternalRecordInterfaceProcessor {
return recordClassType;
}
String toRecord(String classSource) {
String toRecord(String classSource)
{
// javapoet does yet support records - so a class was created and we can reshape it
// The class will look something like this:
/*
@@ -137,7 +140,8 @@ class InternalRecordInterfaceProcessor {
return fixedRecord.toString();
}
private MethodSpec generateArgumentList() {
private MethodSpec generateArgumentList()
{
MethodSpec.Builder builder = MethodSpec.methodBuilder(FAKE_METHOD_NAME);
recordComponents.forEach(component -> {
String name = component.alternateName.orElseGet(() -> component.element.getSimpleName().toString());
@@ -150,18 +154,18 @@ class InternalRecordInterfaceProcessor {
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());
.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) {
@@ -177,8 +181,8 @@ class InternalRecordInterfaceProcessor {
}
return components;
}
private static class IllegalInterface extends RuntimeException {
private static class IllegalInterface extends RuntimeException
{
public IllegalInterface(String message) {
super(message);
}
@@ -216,13 +220,14 @@ class InternalRecordInterfaceProcessor {
});
}
private Optional<String> stripBeanPrefix(String name) {
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);
});
.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

@@ -0,0 +1,175 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.processor;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import java.util.Map;
public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
/**
* @see #suffix()
*/
public static final String OPTION_SUFFIX = "suffix";
/**
* @see #interfaceSuffix()
*/
public static final String OPTION_INTERFACE_SUFFIX = "interfaceSuffix";
/**
* @see #copyMethodName()
*/
public static final String OPTION_COPY_METHOD_NAME = "copyMethodName";
/**
* @see #builderMethodName()
*/
public static final String OPTION_BUILDER_METHOD_NAME = "builderMethodName";
/**
* @see #buildMethodName()
*/
public static final String OPTION_BUILD_METHOD_NAME = "buildMethodName";
/**
* @see #downCastMethodName()
*/
public static final String OPTION_DOWN_CAST_METHOD_NAME = "downCastMethodName";
/**
* @see #componentsMethodName()
*/
public static final String OPTION_COMPONENTS_METHOD_NAME = "componentsMethodName";
/**
* @see #fileComment()
*/
public static final String OPTION_FILE_COMMENT = "fileComment";
/**
* @see #fileIndent()
*/
public static final String OPTION_FILE_INDENT = "fileIndent";
/**
* @see #prefixEnclosingClassNames()
*/
public static final String OPTION_PREFIX_ENCLOSING_CLASS_NAMES = "prefixEnclosingClassNames";
/**
* @see #withClassName()
*/
public static final String OPTION_WITH_CLASS_NAME = "withClassName";
/**
* @see #withClassMethodPrefix()
*/
public static final String OPTION_WITH_CLASS_METHOD_PREFIX = "withClassMethodPrefix";
private final String suffix;
private final String interfaceSuffix;
private final String copyMethodName;
private final String builderMethodName;
private final String buildMethodName;
private final String downCastMethodName;
private final String componentsMethodName;
private final String withClassName;
private final String withClassMethodPrefix;
private final String fileComment;
private final String fileIndent;
private final boolean prefixEnclosingClassNames;
public OptionBasedRecordBuilderMetaData(Map<String, String> options) {
suffix = options.getOrDefault(OPTION_SUFFIX, DEFAULT.suffix());
interfaceSuffix = options.getOrDefault(OPTION_INTERFACE_SUFFIX, DEFAULT.interfaceSuffix());
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());
fileComment = options.getOrDefault(OPTION_FILE_COMMENT, DEFAULT.fileComment());
fileIndent = options.getOrDefault(OPTION_FILE_INDENT, DEFAULT.fileIndent());
String prefixenclosingclassnamesopt = options.getOrDefault(OPTION_PREFIX_ENCLOSING_CLASS_NAMES, String.valueOf(DEFAULT.prefixEnclosingClassNames()));
if (prefixenclosingclassnamesopt == null) {
prefixEnclosingClassNames = true;
} else {
prefixEnclosingClassNames = Boolean.parseBoolean(prefixenclosingclassnamesopt);
}
}
@Override
public String suffix() {
return suffix;
}
@Override
public String copyMethodName() {
return copyMethodName;
}
@Override
public String builderMethodName() {
return builderMethodName;
}
@Override
public String buildMethodName() {
return buildMethodName;
}
@Override
public String downCastMethodName() {
return downCastMethodName;
}
@Override
public String componentsMethodName() {
return componentsMethodName;
}
@Override
public String withClassName() {
return withClassName;
}
@Override
public String withClassMethodPrefix() {
return withClassMethodPrefix;
}
@Override
public String fileComment() {
return fileComment;
}
@Override
public String fileIndent() {
return fileIndent;
}
@Override
public boolean prefixEnclosingClassNames() {
return prefixEnclosingClassNames;
}
@Override
public String interfaceSuffix() {
return interfaceSuffix;
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.processor;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.function.Consumer;
import javax.annotation.processing.ProcessingEnvironment;
class RecordBuilderMetaDataLoader {
private final RecordBuilderMetaData metaData;
RecordBuilderMetaDataLoader(ProcessingEnvironment processingEnv, Consumer<String> logger) {
Map<String, String> options = processingEnv.getOptions();
String metaDataClassName = options.get(RecordBuilderMetaData.JAVAC_OPTION_NAME);
if ((metaDataClassName != null) && !metaDataClassName.isEmpty()) {
RecordBuilderMetaData loadedMetaData = null;
try {
Class<?> clazz = Class.forName(metaDataClassName);
loadedMetaData = (RecordBuilderMetaData) clazz.getDeclaredConstructor().newInstance();
logger.accept("Found meta data: " + clazz);
} catch (InvocationTargetException e) {
// log the thrown exception instead of the invocation target exception
logger.accept("Could not load meta data: " + metaDataClassName + " - " + e.getCause());
} catch (Exception e) {
logger.accept("Could not load meta data: " + metaDataClassName + " - " + e);
}
metaData = (loadedMetaData != null) ? loadedMetaData : RecordBuilderMetaData.DEFAULT;
} else {
metaData = new OptionBasedRecordBuilderMetaData(options);
}
}
RecordBuilderMetaData getMetaData() {
return metaData;
}
}

View File

@@ -1,65 +0,0 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.processor;
import io.soabase.recordbuilder.core.RecordBuilder;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
class RecordBuilderOptions {
private static final Map<String, Object> defaultValues = buildDefaultValues();
static RecordBuilder.Options build(Map<String, String> options) {
return (RecordBuilder.Options)Proxy.newProxyInstance(RecordBuilderOptions.class.getClassLoader(), new Class[]{RecordBuilder.Options.class}, (proxy, method, args) -> {
var name = method.getName();
var defaultValue = defaultValues.get(name);
var option = options.get(name);
if (option != null) {
if (defaultValue instanceof String) {
return option;
}
if (defaultValue instanceof Boolean) {
return Boolean.parseBoolean(option);
}
if (defaultValue instanceof Integer) {
return Integer.parseInt(option);
}
if (defaultValue instanceof Long) {
return Long.parseLong(option);
}
if (defaultValue instanceof Double) {
return Double.parseDouble(option);
}
throw new IllegalArgumentException("Unhandled option type: " + defaultValue.getClass());
}
return defaultValue;
});
}
private static Map<String, Object> buildDefaultValues() {
var workMap = new HashMap<String, Object>();
for ( Method method : RecordBuilder.Options.class.getDeclaredMethods()) {
workMap.put(method.getName(), method.getDefaultValue());
}
workMap.put("toString", "Generated RecordBuilder.Options");
return Map.copyOf(workMap);
}
private RecordBuilderOptions() {
}
}

View File

@@ -19,8 +19,8 @@ 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.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Generated;
@@ -29,6 +29,7 @@ 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;
@@ -37,8 +38,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
public class RecordBuilderProcessor
extends AbstractProcessor {
public class RecordBuilderProcessor extends AbstractProcessor {
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();
@@ -50,12 +50,12 @@ public class RecordBuilderProcessor
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> process(annotation, element)));
return false;
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of("*");
return Set.of(RECORD_BUILDER, RECORD_BUILDER_INCLUDE, RECORD_INTERFACE, RECORD_INTERFACE_INCLUDE);
}
@Override
@@ -68,50 +68,67 @@ public class RecordBuilderProcessor
}
private void process(TypeElement annotation, Element element) {
var metaData = new RecordBuilderMetaDataLoader(processingEnv, s -> processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, s)).getMetaData();
String annotationClass = annotation.getQualifiedName().toString();
if (annotationClass.equals(RECORD_BUILDER)) {
var typeElement = (TypeElement) element;
processRecordBuilder(typeElement, getMetaData(typeElement), Optional.empty());
} else if (annotationClass.equals(RECORD_INTERFACE)) {
var typeElement = (TypeElement) element;
processRecordInterface(typeElement, element.getAnnotation(RecordInterface.class).addRecordBuilder(), getMetaData(typeElement), Optional.empty(), false);
} else if (annotationClass.equals(RECORD_BUILDER_INCLUDE) || annotationClass.equals(RECORD_INTERFACE_INCLUDE)) {
processIncludes(element, getMetaData(element), annotationClass);
} else {
var recordBuilderTemplate = annotation.getAnnotation(RecordBuilder.Template.class);
if (recordBuilderTemplate != null) {
if (recordBuilderTemplate.asRecordInterface()) {
processRecordInterface((TypeElement) element, true, recordBuilderTemplate.options(), Optional.empty(), true);
} else {
processRecordBuilder((TypeElement) element, recordBuilderTemplate.options(), Optional.empty());
}
}
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 RecordBuilder.Options getMetaData(Element element) {
var recordSpecificMetaData = element.getAnnotation(RecordBuilder.Options.class);
return (recordSpecificMetaData != null) ? recordSpecificMetaData : RecordBuilderOptions.build(processingEnv.getOptions());
}
private void processIncludes(Element element, RecordBuilder.Options metaData, String annotationClass) {
var isRecordBuilderInclude = annotationClass.equals(RECORD_BUILDER_INCLUDE);
private void processIncludes(Element element, RecordBuilderMetaData metaData, String annotationClass) {
var annotationMirrorOpt = ElementUtils.findAnnotationMirror(processingEnv, element, annotationClass);
if (annotationMirrorOpt.isEmpty()) {
if ( annotationMirrorOpt.isEmpty() )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get annotation mirror for: " + annotationClass, element);
} else {
var includeHelper = new IncludeHelper(processingEnv, element, annotationMirrorOpt.get(), isRecordBuilderInclude);
if (includeHelper.isValid()) {
var packagePattern = ElementUtils.getStringAttribute(ElementUtils.getAnnotationValue(includeHelper.getAnnotationValues(), "packagePattern").orElse(null), "*");
for (var typeElement : includeHelper.getClassTypeElements()) {
var packageName = buildPackageName(packagePattern, element, typeElement);
if (packageName != null) {
if (isRecordBuilderInclude) {
processRecordBuilder(typeElement, metaData, Optional.of(packageName));
} else {
var addRecordBuilderOpt = ElementUtils.getAnnotationValue(includeHelper.getAnnotationValues(), "addRecordBuilder");
var addRecordBuilder = addRecordBuilderOpt.map(ElementUtils::getBooleanAttribute).orElse(true);
processRecordInterface(typeElement, addRecordBuilder, metaData, Optional.of(packageName), false);
}
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));
}
}
}
}
@@ -126,9 +143,9 @@ public class RecordBuilderProcessor
}
String replaced = packagePattern.replace("*", includedClassPackage.getQualifiedName().toString());
if (builderElement instanceof PackageElement) {
return replaced.replace("@", ((PackageElement) builderElement).getQualifiedName().toString());
return replaced.replace("@", ((PackageElement)builderElement).getQualifiedName().toString());
}
return replaced.replace("@", ((PackageElement) builderElement.getEnclosingElement()).getQualifiedName().toString());
return replaced.replace("@", ((PackageElement)builderElement.getEnclosingElement()).getQualifiedName().toString());
}
private PackageElement findPackageElement(Element actualElement, Element includedClass) {
@@ -137,52 +154,59 @@ public class RecordBuilderProcessor
return null;
}
if (includedClass.getEnclosingElement() instanceof PackageElement) {
return (PackageElement) includedClass.getEnclosingElement();
return (PackageElement)includedClass.getEnclosingElement();
}
return findPackageElement(actualElement, includedClass.getEnclosingElement());
}
private void processRecordInterface(TypeElement element, boolean addRecordBuilder, RecordBuilder.Options metaData, Optional<String> packageName, boolean fromTemplate) {
if (!element.getKind().isInterface()) {
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, addRecordBuilder, metaData, packageName, fromTemplate);
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, RecordBuilder.Options metaData, Optional<String> packageName) {
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(processingEnv, record, metaData, packageName);
var internalProcessor = new InternalRecordBuilderProcessor(record, metaData, packageName);
writeRecordBuilderJavaFile(record, internalProcessor.packageName(), internalProcessor.builderClassType(), internalProcessor.builderType(), metaData);
}
private void writeRecordBuilderJavaFile(TypeElement record, String packageName, ClassType builderClassType, TypeSpec builderType, RecordBuilder.Options metaData) {
private void writeRecordBuilderJavaFile(TypeElement record, String packageName, ClassType builderClassType, TypeSpec builderType, RecordBuilderMetaData metaData) {
// produces the Java file
JavaFile javaFile = javaFileBuilder(packageName, builderType, metaData);
Filer filer = processingEnv.getFiler();
try {
try
{
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);
}
} catch (IOException e) {
}
catch ( IOException e )
{
handleWriteError(record, e);
}
}
private void writeRecordInterfaceJavaFile(TypeElement element, String packageName, ClassType classType, TypeSpec type, RecordBuilder.Options metaData, Function<String, String> toRecordProc) {
private void writeRecordInterfaceJavaFile(TypeElement element, String packageName, ClassType classType, TypeSpec type, RecordBuilderMetaData metaData, Function<String, String> toRecordProc) {
JavaFile javaFile = javaFileBuilder(packageName, type, metaData);
String classSourceCode = javaFile.toString();
@@ -190,21 +214,26 @@ public class RecordBuilderProcessor
String recordSourceCode = toRecordProc.apply(classSourceCode);
Filer filer = processingEnv.getFiler();
try {
try
{
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);
}
} catch (IOException e) {
}
catch ( IOException e )
{
handleWriteError(element, e);
}
}
private JavaFile javaFileBuilder(String packageName, TypeSpec type, RecordBuilder.Options metaData) {
private JavaFile javaFileBuilder(String packageName, TypeSpec type, RecordBuilderMetaData metaData) {
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();
@@ -212,7 +241,8 @@ public class RecordBuilderProcessor
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

@@ -1,46 +0,0 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.TypeName;
import javax.lang.model.element.AnnotationMirror;
import java.util.List;
public class RecordClassType extends ClassType {
private final TypeName rawTypeName;
private final List<? extends AnnotationMirror> accessorAnnotations;
private final List<? extends AnnotationMirror> canonicalConstructorAnnotations;
public RecordClassType(TypeName typeName, TypeName rawTypeName, String name, List<? extends AnnotationMirror> accessorAnnotations, List<? extends AnnotationMirror> canonicalConstructorAnnotations) {
super(typeName, name);
this.rawTypeName = rawTypeName;
this.accessorAnnotations = accessorAnnotations;
this.canonicalConstructorAnnotations = canonicalConstructorAnnotations;
}
public TypeName rawTypeName() {
return rawTypeName;
}
public List<? extends AnnotationMirror> getAccessorAnnotations() {
return accessorAnnotations;
}
public List<? extends AnnotationMirror> getCanonicalConstructorAnnotations() {
return canonicalConstructorAnnotations;
}
}

View File

@@ -3,42 +3,16 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>31</version>
<version>1.17-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>record-builder-test</artifactId>
<properties>
<license-file-path>${project.parent.basedir}/src/etc/header.txt</license-file-path>
</properties>
<dependencies>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-validator</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<artifactId>record-builder-core</artifactId>
<scope>provided</scope>
</dependency>
@@ -51,6 +25,27 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-processor</artifactId>
<version>${project.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
<annotationProcessors>
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>

View File

@@ -1,26 +0,0 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
@RecordBuilder
public record Annotated(@NotNull @Null String hey, @Min(10) @Max(100) int i, double d) {
}

View File

@@ -1,36 +0,0 @@
/**
* 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;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RecordInterface
@RecordBuilder.Options(useImmutableCollections = true)
public interface CollectionInterface<T, X extends Point> {
List<T> l();
Set<T> s();
Map<T, X> m();
Collection<X> c();
}

View File

@@ -1,36 +0,0 @@
/**
* 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 java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RecordBuilder
@RecordBuilder.Options(useImmutableCollections = true, addFunctionalMethodsToWith = true)
public record CollectionRecord<T, X extends Point>(List<T> l, Set<T> s, Map<T, X> m,
Collection<X> c) implements CollectionRecordBuilder.With<T, X> {
public static void main(String[] args) {
var r = new CollectionRecord<>(List.of("hey"), Set.of("there"), Map.of("one", new Point(10, 20)), Set.of(new Point(30, 40)));
Instant now = r.map((l1, s1, m1, c1) -> Instant.now());
r.accept((l1, s1, m1, c1) -> {
});
}
}

View File

@@ -1,28 +0,0 @@
/**
* 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 java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RecordBuilder
@RecordBuilder.Options(useImmutableCollections = true)
public record CollectionRecordConflicts(List<String> __list, Set<String> __set, Map<String, String> __map, Collection<String> __collection) implements CollectionRecordConflictsBuilder.With {
}

View File

@@ -1,35 +0,0 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.lang.model.type.ErrorType;
import java.util.List;
@RecordBuilder
public record ExceptionDetails(
String internalMessage, String endUserMessage, String httpStatus,
ErrorType errorType, List<String> jsonProblems, Throwable cause
) {
@Override
public List<String> jsonProblems() {
if (jsonProblems == null) {
return List.of();
}
return jsonProblems;
}
}

View File

@@ -1,26 +0,0 @@
/**
* 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.RecordBuilderFull;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;
@RecordBuilderFull
public record FullRecord(@NotNull List<Number> numbers, @NotNull Map<Number, FullRecord> fullRecords) {
}

View File

@@ -1,25 +0,0 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.validation.constraints.NotNull;
@RecordBuilder()
@RecordBuilder.Options(inheritComponentAnnotations = false)
public record IgnoreAnnotated(@NotNull String s) {
}

View File

@@ -1,24 +0,0 @@
/**
* 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.Options(prefixEnclosingClassNames = false)
@RecordBuilder.Include(IncludeWithOption.Hey.class)
public class IncludeWithOption {
public static record Hey(String s){}
}

View File

@@ -1,23 +0,0 @@
/**
* 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;
@MyInterfaceTemplate
public interface InterfaceTemplateTest {
String name();
int age();
}

View File

@@ -1,24 +0,0 @@
/**
* 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 java.util.List;
@RecordBuilder
public record MutableCollectionRecord(List<String> l) implements MutableCollectionRecordBuilder.With {
}

View File

@@ -1,26 +0,0 @@
/**
* 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.Template(options = @RecordBuilder.Options(
fileComment = "This is a test",
withClassName = "Com"),
asRecordInterface = true
)
public @interface MyInterfaceTemplate {
}

View File

@@ -1,26 +0,0 @@
/**
* 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.Template(options = @RecordBuilder.Options(
fileComment = "This is a test",
withClassName = "Com"
))
public @interface MyTemplate
{
}

View File

@@ -1,27 +0,0 @@
/**
* 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 java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
@RecordBuilder.Options(emptyDefaultForOptional = true)
@RecordBuilder
public record RecordWithOptional(Optional<String> value, Optional raw, OptionalInt i, OptionalLong l, OptionalDouble d) {}

View File

@@ -1,25 +0,0 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.validation.constraints.NotNull;
@RecordBuilder.Options(interpretNotNulls = true)
@RecordBuilder
public record RequiredRecord(@NotNull String hey, @NotNull int i) implements RequiredRecordBuilder.With {
}

View File

@@ -1,25 +0,0 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.validation.constraints.NotNull;
@RecordBuilder.Options(useValidationApi = true)
@RecordBuilder
public record RequiredRecord2(@NotNull String hey, @NotNull int i) implements RequiredRecord2Builder.With {
}

View File

@@ -18,6 +18,5 @@ package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder
@RecordBuilder.Options(prefixEnclosingClassNames = false)
public record SimpleRecord(int i, String s) {
}

View File

@@ -1,34 +0,0 @@
/**
* 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 java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RecordBuilder
@RecordBuilder.Options(
addSingleItemCollectionBuilders = true,
singleItemBuilderPrefix = "add1",
useImmutableCollections = true,
addFunctionalMethodsToWith = true
)
public record SingleItems<T>(List<String> strings, Set<List<T>> sets, Map<Instant, T> map, Collection<T> collection) implements SingleItemsBuilder.With<T> {
}

View File

@@ -1,23 +0,0 @@
/**
* 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;
@MyTemplate
public record TemplateTest(String text, Instant date) implements TemplateTestBuilder.Com
{
}

View File

@@ -1,32 +0,0 @@
/**
* 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 java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RecordBuilder
@RecordBuilder.Options(
addSingleItemCollectionBuilders = true,
useImmutableCollections = true
)
public record WildcardSingleItems<T>(List<? extends String> strings, Set<? extends List<? extends T>> sets, Map<? extends Instant, ? extends T> map, Collection<? extends T> collection) implements WildcardSingleItemsBuilder.With<T> {
}

View File

@@ -1,25 +0,0 @@
/**
* 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.includes;
import io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder.Include(
packages = "io.soabase.recordbuilder.test.includes.pack",
classes = JustATest.class
)
public class IncludeFactory {
}

View File

@@ -1,19 +0,0 @@
/**
* 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.includes;
public record JustATest(int i) {
}

View File

@@ -1,19 +0,0 @@
/**
* 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.includes.pack;
public interface AlsoIgnoreMe {
}

View File

@@ -1,19 +0,0 @@
/**
* 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.includes.pack;
public class IgnoreMe {
}

View File

@@ -1,19 +0,0 @@
/**
* 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.includes.pack;
public record PackRecord1(String name) {
}

View File

@@ -1,19 +0,0 @@
/**
* 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.includes.pack;
public record PackRecord2(String name, int age) {
}

View File

@@ -1,21 +0,0 @@
/**
* 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.includes.pack;
import java.time.Instant;
public record PackRecord3(Instant time, int age) {
}

View File

@@ -1,22 +0,0 @@
/**
* 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.visibility;
import io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder
record PackagePrivateRecord(int i) {
}

View File

@@ -1,28 +0,0 @@
/**
* 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.visibility;
import io.soabase.recordbuilder.core.RecordBuilder;
class Wrapper {
@RecordBuilder
protected record ProtectedRecord(int i) {
}
@RecordBuilder
record PackagePrivateRecord(int i) {
}
}

View File

@@ -1,107 +0,0 @@
/**
* 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 javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import java.lang.reflect.AnnotatedElement;
class TestAnnotated {
@Test
void testInheritComponentAnnotationsFalse() throws NoSuchMethodException {
var method = IgnoreAnnotatedBuilder.class.getMethod("s");
Assertions.assertNull(method.getAnnotation(NotNull.class));
}
@Test
void testStaticConstructor() throws NoSuchMethodException {
var method = AnnotatedBuilder.class.getMethod("Annotated", String.class, Integer.TYPE, Double.TYPE);
var parameters = method.getParameters();
Assertions.assertEquals(3, parameters.length);
assertHey(parameters[0]);
assertI(parameters[1]);
assertD(parameters[2]);
}
@Test
void testSetters() throws NoSuchMethodException {
var method = AnnotatedBuilder.class.getMethod("hey", String.class);
var parameters = method.getParameters();
Assertions.assertEquals(1, parameters.length);
assertHey(parameters[0]);
method = AnnotatedBuilder.class.getMethod("i", Integer.TYPE);
parameters = method.getParameters();
Assertions.assertEquals(1, parameters.length);
assertI(parameters[0]);
method = AnnotatedBuilder.class.getMethod("d", Double.TYPE);
parameters = method.getParameters();
Assertions.assertEquals(1, parameters.length);
assertD(parameters[0]);
}
@Test
void testGetters() throws NoSuchMethodException {
var method = AnnotatedBuilder.class.getMethod("hey");
assertHey(method);
method = AnnotatedBuilder.class.getMethod("i");
assertI(method);
method = AnnotatedBuilder.class.getMethod("d");
assertD(method);
}
@Test
void testWitherSetters() throws NoSuchMethodException {
var method = AnnotatedBuilder.With.class.getMethod("withHey", String.class);
var parameters = method.getParameters();
Assertions.assertEquals(1, parameters.length);
assertHey(parameters[0]);
method = AnnotatedBuilder.With.class.getMethod("withI", Integer.TYPE);
parameters = method.getParameters();
Assertions.assertEquals(1, parameters.length);
assertI(parameters[0]);
method = AnnotatedBuilder.With.class.getMethod("withD", Double.TYPE);
parameters = method.getParameters();
Assertions.assertEquals(1, parameters.length);
assertD(parameters[0]);
}
private void assertD(AnnotatedElement d) {
Assertions.assertEquals(0, d.getAnnotations().length);
}
private void assertI(AnnotatedElement i) {
Assertions.assertNotNull(i.getAnnotation(Min.class));
Assertions.assertEquals(i.getAnnotation(Min.class).value(), 10);
Assertions.assertNotNull(i.getAnnotation(Max.class));
Assertions.assertEquals(i.getAnnotation(Max.class).value(), 100);
}
private void assertHey(AnnotatedElement hey) {
Assertions.assertNotNull(hey.getAnnotation(NotNull.class));
Assertions.assertNotNull(hey.getAnnotation(Null.class));
}
}

View File

@@ -1,135 +0,0 @@
/**
* 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.util.*;
import static io.soabase.recordbuilder.test.foo.PointBuilder.Point;
import static org.junit.jupiter.api.Assertions.*;
class TestCollections {
@Test
void testRecordBuilderOptionsCopied() {
try {
assertNotNull(CollectionInterfaceRecordBuilder.class.getDeclaredMethod("__list", List.class));
} catch (NoSuchMethodException e) {
Assertions.fail(e);
}
}
@Test
void testCollectionRecordDefaultValues() {
var defaultValues = CollectionRecordBuilder.builder().build();
assertNotNull(defaultValues.l());
assertNotNull(defaultValues.s());
assertNotNull(defaultValues.m());
assertNotNull(defaultValues.c());
}
@Test
void testCollectionRecordImmutable() {
var list = new ArrayList<String>();
list.add("one");
var set = new HashSet<String>();
set.add("one");
var map = new HashMap<String, Point>();
map.put("one", Point(10, 20));
var collectionAsSet = new HashSet<Point>();
collectionAsSet.add(Point(30, 40));
var r = CollectionRecordBuilder.<String, Point>builder()
.l(list)
.s(set)
.m(map)
.c(collectionAsSet)
.build();
assertValues(r, list, set, map, collectionAsSet);
assertValueChanges(r, list, set, map, collectionAsSet);
assertImmutable(r);
var collectionAsList = new ArrayList<Point>();
var x = CollectionRecordBuilder.<String, Point>builder()
.l(list)
.s(set)
.m(map)
.c(collectionAsList)
.build();
assertTrue(x.c() instanceof List);
}
@Test
void testCollectionRecordImmutableWithers() {
var r = CollectionRecordBuilder.<String, Point>builder().build();
var list = new ArrayList<String>();
list.add("one");
var set = new HashSet<String>();
set.add("one");
var map = new HashMap<String, Point>();
map.put("one", Point(10, 20));
var collectionAsSet = new HashSet<Point>();
collectionAsSet.add(Point(30, 40));
var x = r.withL(list).withS(set).withM(map).withC(collectionAsSet);
assertValues(x, list, set, map, collectionAsSet);
assertValueChanges(x, list, set, map, collectionAsSet);
assertImmutable(x);
}
@Test
public void testMutableCollectionRecord() {
var r = MutableCollectionRecordBuilder.builder().build();
assertNull(r.l());
var list = new ArrayList<String>();
list.add("one");
r = MutableCollectionRecordBuilder.builder().l(list).build();
assertEquals(r.l(), list);
list.add("two");
assertEquals(r.l(), list);
}
private void assertImmutable(CollectionRecord<String, Point> r) {
assertThrows(UnsupportedOperationException.class, () -> r.l().add("hey"));
assertThrows(UnsupportedOperationException.class, () -> r.s().add("hey"));
assertThrows(UnsupportedOperationException.class, () -> r.m().put("hey", Point(1, 2)));
assertThrows(UnsupportedOperationException.class, () -> r.c().add(Point(1, 2)));
}
private void assertValueChanges(CollectionRecord<String, Point> r, ArrayList<String> list, HashSet<String> set, HashMap<String, Point> map, HashSet<Point> collectionAsSet) {
list.add("two");
set.add("two");
map.put("two", Point(50, 60));
collectionAsSet.add(Point(70, 80));
assertNotEquals(r.l(), list);
assertNotEquals(r.s(), set);
assertNotEquals(r.m(), map);
assertNotEquals(r.c(), collectionAsSet);
}
private void assertValues(CollectionRecord<String, Point> r, ArrayList<String> list, HashSet<String> set, HashMap<String, Point> map, HashSet<Point> collectionAsSet) {
assertEquals(r.l(), list);
assertEquals(r.s(), set);
assertEquals(r.m(), map);
assertEquals(r.c(), collectionAsSet);
assertTrue(r.c() instanceof Set);
}
}

View File

@@ -1,28 +0,0 @@
/**
* 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;
class TestIncludes {
@Test
void testOptionsOnInclude() {
// assert it's not prefixed with the enclosing class name
IncludeWithOption.Hey hey = io.soabase.recordbuilder.test.HeyBuilder.builder().s("this is s").build();
Assertions.assertEquals("this is s", hey.s());
}
}

View File

@@ -1,36 +0,0 @@
/**
* 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.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
class TestOptional {
@Test
void testDefaultEmpty() {
var record = RecordWithOptionalBuilder.builder();
Assertions.assertEquals(Optional.empty(), record.value());
Assertions.assertEquals(Optional.empty(), record.raw());
Assertions.assertEquals(OptionalInt.empty(), record.i());
Assertions.assertEquals(OptionalLong.empty(), record.l());
Assertions.assertEquals(OptionalDouble.empty(), record.d());
}
}

View File

@@ -1,39 +0,0 @@
/**
* 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.util.ArrayList;
import java.util.HashMap;
class TestRecordBuilderFull {
@Test
void testNonNull() {
Assertions.assertThrows(NullPointerException.class, () -> FullRecordBuilder.builder().build());
}
@Test
void testImmutable() {
var record = FullRecordBuilder.builder()
.fullRecords(new HashMap<>())
.numbers(new ArrayList<>())
.build();
Assertions.assertThrows(UnsupportedOperationException.class, () -> record.fullRecords().put(1, record));
Assertions.assertThrows(UnsupportedOperationException.class, () -> record.numbers().add(1));
}
}

View File

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

View File

@@ -1,76 +0,0 @@
/**
* 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 java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class TestSingleItems {
@Test
public void testInternalCollections()
{
var now = Instant.now();
var item = SingleItemsBuilder.<String>builder()
.add1Map(now, "now")
.add1Map(Instant.MIN, "before")
.add1Sets(Arrays.asList("1", "2"))
.add1Sets(List.of("3"))
.add1Strings("a")
.add1Strings("b")
.add1Strings("c")
.build();
Assertions.assertEquals(item.map(), Map.of(now, "now", Instant.MIN, "before"));
Assertions.assertEquals(item.sets(), Set.of(List.of("1", "2"), List.of("3")));
Assertions.assertEquals(item.strings(), List.of("a", "b", "c"));
var copy = item.with()
.add1Strings("new")
.add1Map(Instant.MAX, "after")
.add1Sets(List.of("10", "20", "30"))
.build();
Assertions.assertNotEquals(item, copy);
Assertions.assertEquals(copy.map(), Map.of(now, "now", Instant.MIN, "before", Instant.MAX, "after"));
Assertions.assertEquals(copy.sets(), Set.of(List.of("1", "2"), List.of("3"), List.of("10", "20", "30")));
Assertions.assertEquals(copy.strings(), List.of("a", "b", "c", "new"));
var stringsToAdd = Arrays.asList("x", "y", "z");
var listToAdd = Arrays.asList(List.of("aa", "bb"), List.of("cc"));
var mapToAdd = Map.of(now.plusMillis(1), "now+1", now.plusMillis(2), "now+2");
var streamed = SingleItemsBuilder.builder(item)
.add1Strings(stringsToAdd.stream())
.add1Sets(listToAdd.stream())
.add1Map(mapToAdd.entrySet().stream())
.build();
Assertions.assertEquals(streamed.map(), Map.of(now, "now", Instant.MIN, "before", now.plusMillis(1), "now+1", now.plusMillis(2), "now+2"));
Assertions.assertEquals(streamed.sets(), Set.of(List.of("1", "2"), List.of("3"), List.of("aa", "bb"), List.of("cc")));
Assertions.assertEquals(streamed.strings(), Arrays.asList("a", "b", "c", "x", "y", "z"));
var nulls = SingleItemsBuilder.builder(item)
.strings(null)
.sets(null)
.map(null)
.build();
Assertions.assertEquals(nulls.map(), Map.of());
Assertions.assertEquals(nulls.sets(), Set.of());
Assertions.assertEquals(nulls.strings(), List.of());
}
}

View File

@@ -1,31 +0,0 @@
/**
* 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;
class TestTemplate {
@Test
void testTemplate() {
var t = TemplateTestBuilder.TemplateTest("one", Instant.MIN);
var w = t.withText("other");
Assertions.assertEquals("one", t.text());
Assertions.assertEquals("other", w.text());
}
}

View File

@@ -1,45 +0,0 @@
/**
* 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 javax.validation.ValidationException;
class TestValidation {
@Test
void testNotNulls() {
Assertions.assertThrows(NullPointerException.class, () -> RequiredRecordBuilder.builder().build());
}
@Test
void testValidation() {
Assertions.assertThrows(ValidationException.class, () -> RequiredRecord2Builder.builder().build());
}
@Test
void testNotNullsWithNewProperty() {
var valid = RequiredRecordBuilder.builder().hey("hey").i(1).build();
Assertions.assertThrows(NullPointerException.class, () -> valid.withHey(null));
}
@Test
void testValidationWithNewProperty() {
var valid = RequiredRecord2Builder.builder().hey("hey").i(1).build();
Assertions.assertThrows(ValidationException.class, () -> valid.withHey(null));
}
}

View File

@@ -21,17 +21,6 @@ import org.junit.jupiter.api.Test;
import java.util.List;
class TestWithers {
@Test
void testFromWithers() {
var r1 = new SimpleGenericRecord<>(10, List.of("1", "2", "3"));
var r2 = SimpleGenericRecordBuilder.from(r1).withS(List.of("4", "5"));
var r3 = SimpleGenericRecordBuilder.from(r1).with(b -> b.i(20).s(List.of("6", "7")));
Assertions.assertEquals(List.of("1", "2", "3"), r1.s());
Assertions.assertEquals(List.of("4", "5"), r2.s());
Assertions.assertEquals(List.of("6", "7"), r3.s());
Assertions.assertEquals(20, r3.i());
}
@Test
void testWithers() {
var r1 = new SimpleGenericRecord<>(10, List.of("1", "2", "3"));
@@ -70,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));
}
}

View File

@@ -1,32 +0,0 @@
/**
* 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.visibility;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Modifier;
class TestVisibility {
@Test
void testMatches() {
Assertions.assertFalse(Modifier.isPublic(PackagePrivateRecordBuilder.class.getModifiers()));
Assertions.assertFalse(Modifier.isPrivate(PackagePrivateRecordBuilder.class.getModifiers()));
Assertions.assertFalse(Modifier.isProtected(PackagePrivateRecordBuilder.class.getModifiers()));
Assertions.assertTrue(Modifier.isPublic(WrapperProtectedRecordBuilder.class.getModifiers()));
}
}

View File

@@ -1,80 +0,0 @@
/**
* 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.validator;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Set;
// complete Java Validation via reflection to avoid dependencies
public class RecordBuilderValidator {
private static final Object validator;
private static final Method validationMethod;
private static final Constructor<?> constraintViolationExceptionCtor;
private static final Class<?>[] emptyGroups = new Class<?>[0];
private static final boolean PRINT_ERROR_STACKTRACE = Boolean.getBoolean("record_builder_validator_errors");
static {
Object localValidator = null;
Method localValidationMethod = null;
Constructor<?> localConstraintViolationExceptionCtor = null;
try {
var validationClass = Class.forName("javax.validation.Validation");
var factoryClass = validationClass.getDeclaredMethod("buildDefaultValidatorFactory");
var factory = factoryClass.invoke(null);
var getValidatorMethod = factory.getClass().getMethod("getValidator");
var constraintViolationExceptionClass = Class.forName("javax.validation.ConstraintViolationException");
localValidator = getValidatorMethod.invoke(factory);
localValidationMethod = localValidator.getClass().getMethod("validate", Object.class, Class[].class);
localConstraintViolationExceptionCtor = constraintViolationExceptionClass.getConstructor(Set.class);
} catch (Exception e) {
if (PRINT_ERROR_STACKTRACE) {
e.printStackTrace();
}
}
validator = localValidator;
validationMethod = localValidationMethod;
constraintViolationExceptionCtor = localConstraintViolationExceptionCtor;
}
public static <T> T validate(T o) {
if ((validator != null) && (validationMethod != null)) {
try {
var violations = validationMethod.invoke(validator, o, emptyGroups);
if (!((Collection<?>) violations).isEmpty()) {
throw (RuntimeException) constraintViolationExceptionCtor.newInstance(violations);
}
} catch (IllegalAccessException | InstantiationException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
if (e.getCause() != null) {
if (e.getCause() instanceof RuntimeException) {
throw (RuntimeException) e.getCause();
}
throw new RuntimeException(e.getCause());
}
throw new RuntimeException(e);
}
}
return o;
}
private RecordBuilderValidator() {
}
}