Compare commits

...

51 Commits

Author SHA1 Message Date
Jordan Zimmerman
0647b66bcd [maven-release-plugin] prepare release record-builder-21 2021-06-25 19:21:36 +01:00
Jordan Zimmerman
08a4471d15 [maven-release-plugin] prepare for next development iteration 2021-06-25 19:15:49 +01:00
Jordan Zimmerman
a1acfb2863 [maven-release-plugin] prepare release record-builder-21-java15 2021-06-25 19:15:42 +01:00
Jordan Zimmerman
82bc1c1625 Added more unit tests
Closes #36
2021-06-24 05:37:43 +01:00
Jordan Zimmerman
2d029a2786 Fix up some minor version/path issues in the POMs 2021-06-24 05:07:35 +01:00
dependabot[bot]
2625b3d849 Bump hibernate-validator from 6.0.16.Final to 6.0.20.Final
Bumps [hibernate-validator](https://github.com/hibernate/hibernate-validator) from 6.0.16.Final to 6.0.20.Final.
- [Release notes](https://github.com/hibernate/hibernate-validator/releases)
- [Changelog](https://github.com/hibernate/hibernate-validator/blob/6.0.20.Final/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-validator/compare/6.0.16.Final...6.0.20.Final)

---
updated-dependencies:
- dependency-name: org.hibernate.validator:hibernate-validator
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-24 04:51:54 +01:00
Jordan Zimmerman
cc5e189f94 Support Java Validation API
Option to pass created records through the Java Validation API if it's
available in the classpath. IMPORTANT: when enabled, the record-builder-validator
module must also be included. record-builder-validator is written totally
via reflection so if no validation framework is included it's a NOP.
2021-06-24 04:50:26 +01:00
Jordan Zimmerman
9125ba0660 Add support for NotNull-style annotations
When enabled, annotations matching the configured regex for NotNull
annotations cause `Object.requireNonNull()` to be added for annotated
components.
2021-06-24 04:39:06 +01:00
Jordan Zimmerman
664809fc69 Use manifest based automatic module instead of hand-coded version (#43)
This is much saner. It seems silly to maintain an extra file in the
project when this can all be done via Maven config.
2021-06-22 13:37:56 +01:00
Jordan Zimmerman
9d011b82aa Set optional fields to empty by default (#38)
Sets `Optional` fields to `empty()` by default. Adds an option
to control this.

Closes #34
2021-06-22 13:04:43 +01:00
Jordan Zimmerman
35125f550d Support copying component annotations to builder (#33)
- Enabled via new option `inheritComponentAnnotations` (true by default)
- Canonical constructor parameter annotations are copied to RecordBuilder setters and the static builder
- Record component accessor annotations are copied to RecordBuilder getters
2021-06-22 12:09:11 +01:00
Jordan Zimmerman
cb2bd68697 Rework how options are specified (#37)
- Remove `RecordBuilderMetaData`
- Unify how the javac options are handled
- Create `RecordBuilder.Options` to specify options
- Allow creation of custom annotations that bundle options
2021-06-22 05:06:44 +01:00
Thiago Henrique Hüpner
f40cfd48ee Enable syntax-highlighting in the README (#29) 2021-04-03 09:40:46 -05:00
Jordan Zimmerman
d9f2adc2f9 Update README.md 2021-03-23 15:43:40 -05:00
Jordan Zimmerman
99832d50ae Update README.md 2021-03-23 15:43:14 -05:00
Jordan Zimmerman
1203109108 Update README.md 2021-03-23 15:42:43 -05:00
Michał Górniewski
7e4675f7c0 Add support for Java Platform Module System (#28) 2021-03-20 09:29:23 -05:00
Jordan Zimmerman
8ab9d8bdca [maven-release-plugin] prepare for next development iteration 2021-03-16 09:50:46 -05:00
Jordan Zimmerman
24edc5e70c [maven-release-plugin] prepare release record-builder-1.19 2021-03-16 09:50:39 -05:00
Jordan Zimmerman
3832cb3881 Merge branch 'master' of github.com:Randgalt/record-builder 2021-03-16 09:50:12 -05:00
Jordan Zimmerman
2beafc4803 [maven-release-plugin] rollback the release of record-builder-1.19 2021-03-16 09:49:45 -05:00
Jordan Zimmerman
82b3925618 [maven-release-plugin] prepare release record-builder-1.19 2021-03-16 09:49:32 -05:00
Jordan Zimmerman
802dd1f880 Update README.md 2021-02-04 10:12:17 -05:00
Jordan Zimmerman
ba90e6cdca [maven-release-plugin] prepare for next development iteration 2021-02-04 06:35:37 -05:00
Jordan Zimmerman
07e52035ee [maven-release-plugin] prepare release record-builder-1.18 2021-02-04 06:35:29 -05:00
Jordan Zimmerman
07fc606147 Update README.md 2021-02-04 06:33:50 -05:00
Jordan Zimmerman
677813e875 [maven-release-plugin] prepare for next development iteration 2021-02-04 06:31:51 -05:00
Jordan Zimmerman
7d877963fb [maven-release-plugin] prepare release record-builder-1.18-java15 2021-02-04 06:31:51 -05:00
Jordan Zimmerman
96bb6ef9f3 Abandon previous attempt to have Java15 specific modules. I can just manually do it from now on 2021-02-04 06:31:51 -05:00
Jordan Zimmerman
6f3046f507 Update README.md 2021-02-01 13:22:08 -05:00
Jordan Zimmerman
7564643556 [maven-release-plugin] prepare for next development iteration 2021-02-01 13:08:04 -05:00
Jordan Zimmerman
dfd76fc58b [maven-release-plugin] prepare release record-builder-1.17 2021-02-01 13:08:02 -05:00
Jordan Zimmerman
04e9135591 [maven-release-plugin] prepare for next development iteration 2021-02-01 13:06:44 -05:00
Jordan Zimmerman
b512a6e968 [maven-release-plugin] prepare release record-builder-1.17 2021-02-01 13:06:44 -05:00
Jordan Zimmerman
5a1cd35320 [maven-release-plugin] rollback the release of record-builder-1.17 2021-02-01 13:06:44 -05:00
Jordan Zimmerman
a615e3abb6 [maven-release-plugin] prepare release record-builder-1.17 2021-02-01 13:06:44 -05:00
Jordan Zimmerman
3f8bb47cbf Support alternate artifacts built with Java 15 2021-02-01 13:00:42 -05:00
Jordan Zimmerman
5a8e72f0e9 [maven-release-plugin] prepare for next development iteration 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
44ad4531b6 [maven-release-plugin] prepare release record-builder-1.16 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
7e78d32780 Added support for putting @RecordInterface on Java beans 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
0dc4aa7657 Prep for Java 16 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
b6d9a6202f Create FUNDING.yml 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
b21368f32f Have the consumer version of with() use the other with() to get the builder. This will ensure better testing and is more logical 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
501da86afd you're right - we only need provided/compileOnly. I've made the updates to Maven as well. 2021-02-01 12:26:07 -05:00
Marc Philipp
13d867e6e6 Use compileOnly instead of implementation
Since most users only need the annotations.
2021-02-01 12:26:07 -05:00
Jordan Zimmerman
a1206fa57f [maven-release-plugin] prepare for next development iteration 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
b89722ebfe [maven-release-plugin] prepare release record-builder-1.14.ea 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
75163f53ed Update README.md 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
9855e7b504 Switch to Github Actions 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
aa3bdedf28 Update maven.yml 2021-02-01 12:26:07 -05:00
Jordan Zimmerman
6d5e15baa1 Create maven.yml 2021-02-01 12:26:07 -05:00
38 changed files with 1380 additions and 664 deletions

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

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

24
.github/workflows/maven.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Maven Build - Java 16
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: 16.0.0-ea
- name: Build with Maven
run: mvn -B package --file pom.xml

28
.github/workflows/maven_java15.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Maven Build - Java 15
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: 15
- name: Create Maven Directory
run: mkdir -p .mvn/
- name: Create Maven JVM file
run: echo "--enable-preview" > .mvn/jvm.config
- name: Build with Maven
run: mvn -P java15 -B package --file pom.xml

View File

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

View File

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

154
README.md
View File

@@ -1,13 +1,13 @@
[![Build Status](https://travis-ci.org/Randgalt/record-builder.svg?branch=master)](https://travis-ci.org/Randgalt/record-builder) [![Build Status](https://github.com/Randgalt/record-builder/workflows/Java%20CI%20with%20Maven/badge.svg)](https://github.com/Randgalt/record-builder/actions)
[![Maven Central](https://img.shields.io/maven-central/v/io.soabase.record-builder/record-builder.svg)](https://search.maven.org/search?q=g:io.soabase.record-builder%20a:record-builder) [![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)
# RecordBuilder - Early Access # RecordBuilder
## What is RecordBuilder ## What is RecordBuilder
Java 15 introduced [Records](https://cr.openjdk.java.net/~briangoetz/amber/datum.html) as a preview feature. Since Java 9, Java 16 introduces [Records](https://openjdk.java.net/jeps/395). While this version of records is fantastic,
features in Java are being released in stages. While the Java 15 version of records is fantastic, it's currently missing important features it's currently missing some important features normally found in data classes: a builder
for data classes: a builder and "with"ers. This project is an annotation processor that creates: and "with"ers. This project is an annotation processor that creates:
- a companion builder class for Java records - a companion builder class for Java records
- an interface that adds "with" copy methods - an interface that adds "with" copy methods
@@ -21,7 +21,8 @@ _Details:_
- [Record From Interface Details](#RecordInterface-Example) - [Record From Interface Details](#RecordInterface-Example)
- [Generation Via Includes](#generation-via-includes) - [Generation Via Includes](#generation-via-includes)
- [Usage](#usage) - [Usage](#usage)
- [Customizing](#customizing) - [Customizing](customizing.md)
- [Java 15 Versions](#java-15-versions)
## RecordBuilder Example ## RecordBuilder Example
@@ -70,6 +71,15 @@ NameAndAge r4 = r3.with().age(101).name("baz").build();
// alternate method of accessing the builder (note: no need to call "build()") // alternate method of accessing the builder (note: no need to call "build()")
NameAndAge r5 = r4.with(b -> b.age(200).name("whatever")); 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"));
}
});
``` ```
_Hat tip to [Benji Weber](https://benjiweber.co.uk/blog/2020/09/19/fun-with-java-records/) for the Withers idea._ _Hat tip to [Benji Weber](https://benjiweber.co.uk/blog/2020/09/19/fun-with-java-records/) for the Withers idea._
@@ -203,8 +213,7 @@ public class NameAndAgeBuilder {
* Return a new record built from the builder passed to the given consumer * Return a new record built from the builder passed to the given consumer
*/ */
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) { default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
NameAndAge r = _downcast(this); NameAndAgeBuilder builder = with();
NameAndAgeBuilder builder = NameAndAgeBuilder.builder(r);
consumer.accept(builder); consumer.accept(builder);
return builder.build(); return builder.build();
} }
@@ -257,6 +266,8 @@ Notes:
- ...cannot have type parameters - ...cannot have type parameters
- Methods with default implementations are used in the generation unless they are annotated with `@IgnoreDefaultMethod` - Methods with default implementations are used in the generation unless they are annotated with `@IgnoreDefaultMethod`
- If you do not want a record builder generated, annotate your interface as `@RecordInterface(addRecordBuilder = false)` - If you do not want a record builder generated, annotate your interface as `@RecordInterface(addRecordBuilder = false)`
- If your interface is a JavaBean (e.g. `getThing()`, `isThing()`) the "get" and "is" prefixes are
stripped and forwarding methods are added.
## Generation Via Includes ## Generation Via Includes
@@ -266,7 +277,7 @@ libraries where you are not able to annotate the source.
E.g. E.g.
``` ```java
import some.library.code.ImportedRecord import some.library.code.ImportedRecord
import some.library.code.ImportedInterface import some.library.code.ImportedInterface
@@ -287,20 +298,21 @@ annotation. Use `packagePattern` to change this (see Javadoc for details).
### Maven ### Maven
1\. Add the dependency that contains the `@RecordBuilder` annotation. 1) Add the dependency that contains the `@RecordBuilder` annotation.
``` ```xml
<dependency> <dependency>
<groupId>io.soabase.record-builder</groupId> <groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-core</artifactId> <artifactId>record-builder-core</artifactId>
<version>set-version-here</version> <version>set-version-here</version>
<scope>provided</scope>
</dependency> </dependency>
``` ```
2\. Enable the annotation processing for the Maven Compiler Plugin: 2) Enable the annotation processing for the Maven Compiler Plugin:
``` ```xml
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
@@ -318,6 +330,74 @@ annotation. Use `packagePattern` to change this (see Javadoc for details).
</annotationProcessors> </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'
}
```
### IDE
Depending on your IDE you are likely to need to enable Annotation Processing in your IDE settings.
## 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.
## Java 15 Versions
Artifacts compiled wth Java 15 are available. These versions have `-java15` appended.
Note: records are a preview feature only in Java 15. You'll need take a number of steps in order to try RecordBuilder:
- Install and make active Java 15 or later
- Make sure your development tool is using Java 15 or later and is configured to enable preview features (for Maven I've documented how to do this here: [https://stackoverflow.com/a/59363152/2048051](https://stackoverflow.com/a/59363152/2048051))
- Bear in mind that this is not yet meant for production and there are numerous bugs in the tools and JDKs.
Note: I've seen some very odd compilation bugs with the current Java 15 and Maven. If you get internal Javac errors I suggest rebuilding with `mvn clean package` and/or `mvn clean install`.
You will need to enable preview in your build tools:
### Maven
```xml
<dependencies>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-core</artifactId>
<version>record-builder-version-java15</version>
</dependency>
</dependencies>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>maven-compiler-version</version>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-processor</artifactId>
<version>record-builder-version-java15</version>
</annotationProcessorPath>
</annotationProcessorPaths>
<annotationProcessors>
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
</annotationProcessors>
<!-- "release" and "enable-preview" are required while records are preview features --> <!-- "release" and "enable-preview" are required while records are preview features -->
<release>15</release> <release>15</release>
<compilerArgs> <compilerArgs>
@@ -329,18 +409,14 @@ annotation. Use `packagePattern` to change this (see Javadoc for details).
</plugin> </plugin>
``` ```
3\. Enable Preview for Maven
Create a file in your project's root named `.mvn/jvm.config`. The file should have 1 line with the value: `--enable-preview`. (see: https://stackoverflow.com/questions/58023240) 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 ### Gradle
Add the following to your build.gradle file: ```groovy
```
dependencies { dependencies {
annotationProcessor 'io.soabase.record-builder:record-builder-processor:$version-goes-here' annotationProcessor 'io.soabase.record-builder:record-builder-processor:$record-builder-version-java15'
implementation 'io.soabase.record-builder:record-builder-core:$version-goes-here' compileOnly 'io.soabase.record-builder:record-builder-core:$record-builder-version-java15'
} }
tasks.withType(JavaCompile) { tasks.withType(JavaCompile) {
@@ -352,39 +428,3 @@ tasks.withType(Test) {
jvmArgs += "--enable-preview" jvmArgs += "--enable-preview"
} }
``` ```
### IDE
Depending on your IDE you are likely to need to enable Annotation Processing in your IDE settings.
## Enable Preview
Note: records are a preview feature only. You'll need take a number of steps in order to try RecordBuilder:
- Install and make active Java 15 or later
- Make sure your development tool is using Java 15 or later and is configured to enable preview features (for Maven I've documented how to do this here: [https://stackoverflow.com/a/59363152/2048051](https://stackoverflow.com/a/59363152/2048051))
- Bear in mind that this is not yet meant for production and there are numerous bugs in the tools and JDKs.
Note: I've seen some very odd compilation bugs with the current Java 15 and Maven. If you get internal Javac errors I suggest rebuilding with `mvn clean package` and/or `mvn clean install`.
## Customizing
The names of the generated methods, etc. are determined by [RecordBuilderMetaData](https://github.com/Randgalt/record-builder/blob/master/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderMetaData.java). If you want to use your own meta data instance:
- 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`

85
customizing.md Normal file
View File

@@ -0,0 +1,85 @@
[◀︎ RecordBuilder](README.md) • Customizing RecordBuilder
# 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.
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.

22
java15.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
#
# Copyright 2019 Jordan Zimmerman
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
jenv local 15
javahome
mkdir -p .mvn/
echo "--enable-preview" > .mvn/jvm.config

21
java16.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
#
# Copyright 2019 Jordan Zimmerman
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
jenv local 16
javahome
rm -fr .mvn

68
pom.xml
View File

@@ -5,12 +5,13 @@
<groupId>io.soabase.record-builder</groupId> <groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId> <artifactId>record-builder</artifactId>
<packaging>pom</packaging> <packaging>pom</packaging>
<version>1.14.ea-SNAPSHOT</version> <version>21</version>
<modules> <modules>
<module>record-builder-core</module> <module>record-builder-core</module>
<module>record-builder-processor</module> <module>record-builder-processor</module>
<module>record-builder-test</module> <module>record-builder-test</module>
<module>record-builder-validator</module>
</modules> </modules>
<properties> <properties>
@@ -18,7 +19,9 @@
<project.build.resourceEncoding>UTF-8</project.build.resourceEncoding> <project.build.resourceEncoding>UTF-8</project.build.resourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<jdk-version>15</jdk-version> <enable-preview />
<jdk-version>16</jdk-version>
<maven-compiler-plugin-version>3.8.1</maven-compiler-plugin-version> <maven-compiler-plugin-version>3.8.1</maven-compiler-plugin-version>
<maven-source-plugin-version>3.2.0</maven-source-plugin-version> <maven-source-plugin-version>3.2.0</maven-source-plugin-version>
@@ -31,10 +34,16 @@
<maven-shade-plugin-version>3.2.1</maven-shade-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-release-plugin-version>2.5.3</maven-release-plugin-version>
<maven-surefire-plugin-version>3.0.0-M5</maven-surefire-plugin-version> <maven-surefire-plugin-version>3.0.0-M5</maven-surefire-plugin-version>
<maven-jar-plugin-version>3.2.0</maven-jar-plugin-version>
<license-file-path>src/etc/header.txt</license-file-path>
<javapoet-version>1.12.1</javapoet-version> <javapoet-version>1.12.1</javapoet-version>
<junit-jupiter-version>5.5.2</junit-jupiter-version> <junit-jupiter-version>5.5.2</junit-jupiter-version>
<asm-version>7.2</asm-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> </properties>
<name>Record Builder</name> <name>Record Builder</name>
@@ -71,7 +80,7 @@
<url>https://github.com/randgalt/record-builder</url> <url>https://github.com/randgalt/record-builder</url>
<connection>scm:git:https://github.com/randgalt/record-builder.git</connection> <connection>scm:git:https://github.com/randgalt/record-builder.git</connection>
<developerConnection>scm:git:git@github.com:randgalt/record-builder.git</developerConnection> <developerConnection>scm:git:git@github.com:randgalt/record-builder.git</developerConnection>
<tag>HEAD</tag> <tag>record-builder-21</tag>
</scm> </scm>
<issueManagement> <issueManagement>
@@ -106,11 +115,35 @@
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-validator</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId> <artifactId>junit-jupiter</artifactId>
<version>${junit-jupiter-version}</version> <version>${junit-jupiter-version}</version>
</dependency> </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> </dependencies>
</dependencyManagement> </dependencyManagement>
@@ -124,7 +157,7 @@
<configuration> <configuration>
<release>${jdk-version}</release> <release>${jdk-version}</release>
<compilerArgs> <compilerArgs>
<arg>--enable-preview</arg> <arg>${enable-preview}</arg>
</compilerArgs> </compilerArgs>
</configuration> </configuration>
</plugin> </plugin>
@@ -157,7 +190,7 @@
<artifactId>maven-license-plugin</artifactId> <artifactId>maven-license-plugin</artifactId>
<version>${maven-license-plugin-version}</version> <version>${maven-license-plugin-version}</version>
<configuration> <configuration>
<header>src/etc/header.txt</header> <header>${license-file-path}</header>
<excludes> <excludes>
<exclude>**/*.apt</exclude> <exclude>**/*.apt</exclude>
<exclude>**/*.md</exclude> <exclude>**/*.md</exclude>
@@ -183,6 +216,8 @@
<exclude>**/jvm.config</exclude> <exclude>**/jvm.config</exclude>
<exclude>**/.java-version</exclude> <exclude>**/.java-version</exclude>
<exclude>**/.travis.yml</exclude> <exclude>**/.travis.yml</exclude>
<exclude>**/gradlew</exclude>
<exclude>**/.github/**</exclude>
</excludes> </excludes>
<strictCheck>true</strictCheck> <strictCheck>true</strictCheck>
</configuration> </configuration>
@@ -278,9 +313,21 @@
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin-version}</version> <version>${maven-surefire-plugin-version}</version>
<configuration> <configuration>
<argLine>--enable-preview</argLine> <argLine>${enable-preview}</argLine>
</configuration> </configuration>
</plugin> </plugin>
<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>
</plugin>
</plugins> </plugins>
</pluginManagement> </pluginManagement>
@@ -324,7 +371,6 @@
<plugins> <plugins>
<plugin> <plugin>
<artifactId>maven-gpg-plugin</artifactId> <artifactId>maven-gpg-plugin</artifactId>
<version>${maven-gpg-plugin-version}</version>
<configuration> <configuration>
<passphrase>${gpg.passphrase}</passphrase> <passphrase>${gpg.passphrase}</passphrase>
<useAgent>true</useAgent> <useAgent>true</useAgent>
@@ -342,5 +388,13 @@
</plugins> </plugins>
</build> </build>
</profile> </profile>
<profile>
<id>java15</id>
<properties>
<jdk-version>15</jdk-version>
<enable-preview>--enable-preview</enable-preview>
</properties>
</profile>
</profiles> </profiles>
</project> </project>

View File

@@ -3,9 +3,29 @@
<parent> <parent>
<groupId>io.soabase.record-builder</groupId> <groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId> <artifactId>record-builder</artifactId>
<version>1.14.ea-SNAPSHOT</version> <version>21</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>record-builder-core</artifactId> <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> </project>

View File

@@ -15,16 +15,15 @@
*/ */
package io.soabase.recordbuilder.core; package io.soabase.recordbuilder.core;
import java.lang.annotation.ElementType; import java.lang.annotation.*;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
@Inherited
public @interface RecordBuilder { public @interface RecordBuilder {
@Target({ElementType.TYPE, ElementType.PACKAGE}) @Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Inherited
@interface Include { @interface Include {
Class<?>[] value(); Class<?>[] value();
@@ -38,4 +37,116 @@ public @interface RecordBuilder {
*/ */
String packagePattern() default "@"; 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 downcast method
*/
String downCastMethodName() default "_downcast";
/**
* 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;
}
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.ANNOTATION_TYPE)
@Inherited
@interface Template {
RecordBuilder.Options options();
}
} }

View File

@@ -1,144 +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;
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

@@ -16,17 +16,20 @@
package io.soabase.recordbuilder.core; package io.soabase.recordbuilder.core;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
@Inherited
public @interface RecordInterface { public @interface RecordInterface {
boolean addRecordBuilder() default true; boolean addRecordBuilder() default true;
@Target({ElementType.TYPE, ElementType.PACKAGE}) @Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Inherited
@interface Include { @interface Include {
Class<?>[] value(); Class<?>[] value();

View File

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

View File

@@ -19,11 +19,12 @@ import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeVariableName; import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.RecordBuilderMetaData; import io.soabase.recordbuilder.core.RecordBuilder;
import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element; import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.RecordComponentElement; import javax.lang.model.element.RecordComponentElement;
import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeElement;
@@ -107,8 +108,8 @@ public class ElementUtils {
return new ClassType(ParameterizedTypeName.get(builderClassName, typeNames), builderClassName.simpleName()); return new ClassType(ParameterizedTypeName.get(builderClassName, typeNames), builderClassName.simpleName());
} }
public static ClassType getClassType(RecordComponentElement recordComponent) { public static RecordClassType getRecordClassType(RecordComponentElement recordComponent, List<? extends AnnotationMirror> accessorAnnotations, List<? extends AnnotationMirror> canonicalConstructorAnnotations) {
return new ClassType(TypeName.get(recordComponent.asType()), recordComponent.getSimpleName().toString()); return new RecordClassType(TypeName.get(recordComponent.asType()), recordComponent.getSimpleName().toString(), accessorAnnotations, canonicalConstructorAnnotations);
} }
public static String getWithMethodName(ClassType component, String prefix) { public static String getWithMethodName(ClassType component, String prefix) {
@@ -119,12 +120,29 @@ public class ElementUtils {
return prefix + Character.toUpperCase(name.charAt(0)) + name.substring(1); return prefix + Character.toUpperCase(name.charAt(0)) + name.substring(1);
} }
public static String getBuilderName(TypeElement element, RecordBuilderMetaData metaData, ClassType classType, String suffix) { public static String getBuilderName(TypeElement element, RecordBuilder.Options metaData, ClassType classType, String suffix) {
// generate the class name // generate the class name
var baseName = classType.name() + suffix; var baseName = classType.name() + suffix;
return metaData.prefixEnclosingClassNames() ? (getBuilderNamePrefix(element.getEnclosingElement()) + baseName) : baseName; 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) { private static String getBuilderNamePrefix(Element element) {
// prefix enclosing class names if nested in a class // prefix enclosing class names if nested in a class
if (element instanceof TypeElement) { if (element instanceof TypeElement) {

View File

@@ -15,26 +15,13 @@
*/ */
package io.soabase.recordbuilder.processor; package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.ClassName; import com.squareup.javapoet.*;
import com.squareup.javapoet.CodeBlock; import io.soabase.recordbuilder.core.RecordBuilder;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import javax.lang.model.element.Modifier; import javax.lang.model.element.*;
import javax.lang.model.element.TypeElement; import java.util.*;
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.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -43,27 +30,33 @@ import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName; import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation; import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
class InternalRecordBuilderProcessor class InternalRecordBuilderProcessor {
{ private final RecordBuilder.Options metaData;
private final RecordBuilderMetaData metaData;
private final ClassType recordClassType; private final ClassType recordClassType;
private final String packageName; private final String packageName;
private final ClassType builderClassType; private final ClassType builderClassType;
private final List<TypeVariableName> typeVariables; private final List<TypeVariableName> typeVariables;
private final List<ClassType> recordComponents; private final List<RecordClassType> recordComponents;
private final TypeSpec builderType; private final TypeSpec builderType;
private final TypeSpec.Builder builder; private final TypeSpec.Builder builder;
private final String uniqueVarName; private final String uniqueVarName;
private final Pattern notNullPattern;
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData, Optional<String> packageNameOpt) private static final TypeName optionalType = TypeName.get(Optional.class);
{ private static final TypeName optionalIntType = TypeName.get(OptionalInt.class);
this.metaData = metaData; 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");
InternalRecordBuilderProcessor(TypeElement record, RecordBuilder.Options metaData, Optional<String> packageNameOpt) {
this.metaData = getMetaData(record, metaData);
recordClassType = ElementUtils.getClassType(record, record.getTypeParameters()); recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(record)); packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(record));
builderClassType = ElementUtils.getClassType(packageName, getBuilderName(record, metaData, recordClassType, metaData.suffix()), record.getTypeParameters()); builderClassType = ElementUtils.getClassType(packageName, getBuilderName(record, metaData, recordClassType, metaData.suffix()), record.getTypeParameters());
typeVariables = record.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList()); typeVariables = record.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList());
recordComponents = record.getRecordComponents().stream().map(ElementUtils::getClassType).collect(Collectors.toList()); recordComponents = buildRecordComponents(record);
uniqueVarName = getUniqueVarName(); uniqueVarName = getUniqueVarName();
notNullPattern = Pattern.compile(metaData.interpretNotNullsPattern());
builder = TypeSpec.classBuilder(builderClassType.name()) builder = TypeSpec.classBuilder(builderClassType.name())
.addModifiers(Modifier.PUBLIC) .addModifiers(Modifier.PUBLIC)
@@ -91,23 +84,37 @@ class InternalRecordBuilderProcessor
builderType = builder.build(); builderType = builder.build();
} }
String packageName() String packageName() {
{
return packageName; return packageName;
} }
ClassType builderClassType() ClassType builderClassType() {
{
return builderClassType; return builderClassType;
} }
TypeSpec builderType() TypeSpec builderType() {
{
return builderType; return builderType;
} }
private void addWithNestedClass() 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(recordComponents.get(index), thisAccessorAnnotations, thisCanonicalConstructorAnnotations);
})
.collect(Collectors.toList());
}
private RecordBuilder.Options getMetaData(TypeElement record, RecordBuilder.Options metaData) {
var recordSpecificMetaData = record.getAnnotation(RecordBuilder.Options.class);
return (recordSpecificMetaData != null) ? recordSpecificMetaData : metaData;
}
private void addWithNestedClass() {
/* /*
Adds a nested interface that adds withers similar to: Adds a nested interface that adds withers similar to:
@@ -128,21 +135,18 @@ class InternalRecordBuilderProcessor
builder.addType(classBuilder.build()); 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: Adds a method that returns a pre-filled copy builder similar to:
default MyRecord with(Consumer<MyRecordBuilder> consumer) { default MyRecord with(Consumer<MyRecordBuilder> consumer) {
MyRecord r = (MyRecord)(Object)this; MyRecordBuilder builder = with();
MyRecordBuilder builder MyRecordBuilder.builder(r);
consumer.accept(builder); consumer.accept(builder);
return builder.build(); return builder.build();
} }
*/ */
var codeBlockBuilder = CodeBlock.builder() var codeBlockBuilder = CodeBlock.builder()
.add("$T $L = $L(this);\n", recordClassType.typeName(), uniqueVarName, metaData.downCastMethodName()) .add("$T builder = with();\n", builderClassType.typeName())
.add("$T builder = $L.$L($L);\n", builderClassType.typeName(), builderClassType.name(), metaData.copyMethodName(), uniqueVarName)
.add("consumer.accept(builder);\n") .add("consumer.accept(builder);\n")
.add("return builder.build();\n"); .add("return builder.build();\n");
var consumerType = ParameterizedTypeName.get(ClassName.get(Consumer.class), builderClassType.typeName()); var consumerType = ParameterizedTypeName.get(ClassName.get(Consumer.class), builderClassType.typeName());
@@ -158,13 +162,12 @@ class InternalRecordBuilderProcessor
classBuilder.addMethod(methodSpec); 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: Adds a method that returns a pre-filled copy builder similar to:
default MyRecordBuilder with() { default MyRecordBuilder with() {
MyRecord r = (MyRecord)(Object)this; MyRecord r = _downcast(this);
return MyRecordBuilder.builder(r); return MyRecordBuilder.builder(r);
} }
*/ */
@@ -181,27 +184,24 @@ class InternalRecordBuilderProcessor
classBuilder.addMethod(methodSpec); classBuilder.addMethod(methodSpec);
} }
private String getUniqueVarName() private String getUniqueVarName() {
{
return getUniqueVarName(""); return getUniqueVarName("");
} }
private String getUniqueVarName(String prefix) private String getUniqueVarName(String prefix) {
{
var name = prefix + "r"; var name = prefix + "r";
var alreadyExists = recordComponents.stream() var alreadyExists = recordComponents.stream()
.map(ClassType::name) .map(ClassType::name)
.anyMatch(n -> n.equals(name)); .anyMatch(n -> n.equals(name));
return alreadyExists ? getUniqueVarName(prefix + "_") : name; return alreadyExists ? getUniqueVarName(prefix + "_") : name;
} }
private void add1WithMethod(TypeSpec.Builder classBuilder, ClassType component, int index) private void add1WithMethod(TypeSpec.Builder classBuilder, RecordClassType component, int index) {
{
/* /*
Adds a with method for the component similar to: Adds a with method for the component similar to:
default MyRecord withName(String name) { default MyRecord withName(String name) {
MyRecord r = (MyRecord)(Object)this; MyRecord r = _downcast(this);
return new MyRecord(name, r.age()); return new MyRecord(name, r.age());
} }
*/ */
@@ -217,28 +217,27 @@ class InternalRecordBuilderProcessor
ClassType parameterComponent = recordComponents.get(parameterIndex); ClassType parameterComponent = recordComponents.get(parameterIndex);
if (parameterIndex == index) { if (parameterIndex == index) {
codeBlockBuilder.add(parameterComponent.name()); codeBlockBuilder.add(parameterComponent.name());
} } else {
else {
codeBlockBuilder.add("$L.$L()", uniqueVarName, parameterComponent.name()); codeBlockBuilder.add("$L.$L()", uniqueVarName, parameterComponent.name());
} }
}); });
codeBlockBuilder.add(");"); codeBlockBuilder.add(");");
var methodName = getWithMethodName(component, metaData.withClassMethodPrefix()); var methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build(); var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
component.getCanonicalConstructorAnnotations().forEach(annotationMirror -> parameterSpecBuilder.addAnnotation(AnnotationSpec.get(annotationMirror)));
var methodSpec = MethodSpec.methodBuilder(methodName) var methodSpec = MethodSpec.methodBuilder(methodName)
.addAnnotation(generatedRecordBuilderAnnotation) .addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Return a new instance of {@code $L} with a new value for {@code $L}\n", recordClassType.name(), component.name()) .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) .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addParameter(parameterSpec) .addParameter(parameterSpecBuilder.build())
.addCode(codeBlockBuilder.build()) .addCode(codeBlockBuilder.build())
.returns(recordClassType.typeName()) .returns(recordClassType.typeName())
.build(); .build();
classBuilder.addMethod(methodSpec); classBuilder.addMethod(methodSpec);
} }
private void addDefaultConstructor() private void addDefaultConstructor() {
{
/* /*
Adds a default constructor similar to: Adds a default constructor similar to:
@@ -252,8 +251,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(constructor); builder.addMethod(constructor);
} }
private void addStaticBuilder() private void addStaticBuilder() {
{
/* /*
Adds an static builder similar to: Adds an static builder similar to:
@@ -268,13 +266,30 @@ class InternalRecordBuilderProcessor
.addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addAnnotation(generatedRecordBuilderAnnotation) .addAnnotation(generatedRecordBuilderAnnotation)
.returns(recordClassType.typeName()) .returns(recordClassType.typeName())
.addStatement(codeBlock); .addCode(codeBlock);
recordComponents.forEach(component -> builder.addParameter(component.typeName(), component.name())); recordComponents.forEach(component -> {
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
component.getCanonicalConstructorAnnotations().forEach(annotationMirror -> parameterSpecBuilder.addAnnotation(AnnotationSpec.get(annotationMirror)));
builder.addParameter(parameterSpecBuilder.build());
});
this.builder.addMethod(builder.build()); this.builder.addMethod(builder.build());
} }
private void addAllArgsConstructor() private void addNullCheckCodeBlock(CodeBlock.Builder builder) {
{ if (metaData.interpretNotNulls()) {
recordComponents.stream()
.filter(component -> !component.typeName().isPrimitive())
.filter(this::isNullAnnotated)
.forEach(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() {
/* /*
Adds an all-args constructor similar to: Adds an all-args constructor similar to:
@@ -295,8 +310,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(constructorBuilder.build()); builder.addMethod(constructorBuilder.build());
} }
private void addToStringMethod() private void addToStringMethod() {
{
/* /*
add a toString() method similar to: add a toString() method similar to:
@@ -325,8 +339,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec); builder.addMethod(methodSpec);
} }
private void addHashCodeMethod() private void addHashCodeMethod() {
{
/* /*
add a hashCode() method similar to: add a hashCode() method similar to:
@@ -354,8 +367,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec); builder.addMethod(methodSpec);
} }
private void addEqualsMethod() private void addEqualsMethod() {
{
/* /*
add an equals() method similar to: add an equals() method similar to:
@@ -373,8 +385,7 @@ class InternalRecordBuilderProcessor
String name = recordComponent.name(); String name = recordComponent.name();
if (recordComponent.typeName().isPrimitive()) { if (recordComponent.typeName().isPrimitive()) {
codeBuilder.add("\n&& ($L == $L.$L)", name, uniqueVarName, name); codeBuilder.add("\n&& ($L == $L.$L)", name, uniqueVarName, name);
} } else {
else {
codeBuilder.add("\n&& $T.equals($L, $L.$L)", Objects.class, name, uniqueVarName, name); codeBuilder.add("\n&& $T.equals($L, $L.$L)", Objects.class, name, uniqueVarName, name);
} }
}); });
@@ -391,8 +402,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec); builder.addMethod(methodSpec);
} }
private void addBuildMethod() private void addBuildMethod() {
{
/* /*
Adds the build method that generates the record similar to: Adds the build method that generates the record similar to:
@@ -406,7 +416,7 @@ class InternalRecordBuilderProcessor
.addModifiers(Modifier.PUBLIC) .addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation) .addAnnotation(generatedRecordBuilderAnnotation)
.returns(recordClassType.typeName()) .returns(recordClassType.typeName())
.addStatement(codeBlock) .addCode(codeBlock)
.build(); .build();
builder.addMethod(methodSpec); builder.addMethod(methodSpec);
} }
@@ -416,7 +426,13 @@ class InternalRecordBuilderProcessor
Builds the code block for allocating the record from its parts Builds the code block for allocating the record from its parts
*/ */
var codeBuilder = CodeBlock.builder().add("return new $T(", recordClassType.typeName()); var codeBuilder = CodeBlock.builder();
addNullCheckCodeBlock(codeBuilder);
codeBuilder.add("$[return ");
if (metaData.useValidationApi()) {
codeBuilder.add("$T.validate(", validatorTypeName);
}
codeBuilder.add("new $T(", recordClassType.typeName());
IntStream.range(0, recordComponents.size()).forEach(index -> { IntStream.range(0, recordComponents.size()).forEach(index -> {
if (index > 0) { if (index > 0) {
codeBuilder.add(", "); codeBuilder.add(", ");
@@ -424,11 +440,14 @@ class InternalRecordBuilderProcessor
codeBuilder.add("$L", recordComponents.get(index).name()); codeBuilder.add("$L", recordComponents.get(index).name());
}); });
codeBuilder.add(")"); codeBuilder.add(")");
if (metaData.useValidationApi()) {
codeBuilder.add(")");
}
codeBuilder.add(";$]");
return codeBuilder.build(); return codeBuilder.build();
} }
private void addStaticCopyBuilderMethod() private void addStaticCopyBuilderMethod() {
{
/* /*
Adds a copy builder method that pre-fills the builder with existing values similar to: Adds a copy builder method that pre-fills the builder with existing values similar to:
@@ -457,8 +476,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec); builder.addMethod(methodSpec);
} }
private void addStaticDefaultBuilderMethod() private void addStaticDefaultBuilderMethod() {
{
/* /*
Adds a the default builder method similar to: Adds a the default builder method similar to:
@@ -477,8 +495,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec); 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 Adds a static method that converts a record instance into a stream of its component parts
@@ -512,8 +529,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec); builder.addMethod(methodSpec);
} }
private void addStaticDowncastMethod() private void addStaticDowncastMethod() {
{
/* /*
Adds a method that downcasts to the record type Adds a method that downcasts to the record type
@@ -522,41 +538,62 @@ class InternalRecordBuilderProcessor
} }
*/ */
var codeBlockBuilder = CodeBlock.builder() var codeBlockBuilder = CodeBlock.builder()
.add("try {\n") .add("try {\n")
.indent() .indent()
.add("return ($T)obj;\n", recordClassType.typeName()) .add("return ($T)obj;\n", recordClassType.typeName())
.unindent() .unindent()
.add("}\n") .add("}\n")
.add("catch (ClassCastException dummy) {\n") .add("catch (ClassCastException dummy) {\n")
.indent() .indent()
.add("throw new RuntimeException($S);\n", builderClassType.name() + "." + metaData.withClassName() + " can only be implemented for " + recordClassType.name()) .add("throw new RuntimeException($S);\n", builderClassType.name() + "." + metaData.withClassName() + " can only be implemented by " + recordClassType.name())
.unindent() .unindent()
.add("}"); .add("}");
var methodSpec = MethodSpec.methodBuilder(metaData.downCastMethodName()) var methodSpec = MethodSpec.methodBuilder(metaData.downCastMethodName())
.addAnnotation(generatedRecordBuilderAnnotation) .addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Downcast to {@code $L}\n", recordClassType.name()) .addJavadoc("Downcast to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PRIVATE, Modifier.STATIC) .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.addParameter(Object.class, "obj") .addParameter(Object.class, "obj")
.addTypeVariables(typeVariables) .addTypeVariables(typeVariables)
.returns(recordClassType.typeName()) .returns(recordClassType.typeName())
.addCode(codeBlockBuilder.build()) .addCode(codeBlockBuilder.build())
.build(); .build();
builder.addMethod(methodSpec); builder.addMethod(methodSpec);
} }
private void add1Field(ClassType component) private void add1Field(ClassType component) {
{
/* /*
For a single record component, add a field similar to: For a single record component, add a field similar to:
private T p; private T p;
*/ */
var fieldSpec = FieldSpec.builder(component.typeName(), component.name(), Modifier.PRIVATE).build(); var fieldSpecBuilder = FieldSpec.builder(component.typeName(), component.name(), Modifier.PRIVATE);
builder.addField(fieldSpec); 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());
} }
private void add1GetterMethod(ClassType component) private boolean isOptional(ClassType component) {
{ if (component.typeName().equals(optionalType)) {
return true;
}
return (component.typeName() instanceof ParameterizedTypeName) && ((ParameterizedTypeName) component.typeName()).rawType.equals(optionalType);
}
private void add1GetterMethod(RecordClassType component) {
/* /*
For a single record component, add a getter similar to: For a single record component, add a getter similar to:
@@ -564,18 +601,17 @@ class InternalRecordBuilderProcessor
return p; return p;
} }
*/ */
var methodSpec = MethodSpec.methodBuilder(component.name()) var methodSpecBuilder = MethodSpec.methodBuilder(component.name())
.addJavadoc("Return the current value for the {@code $L} record component in the builder\n", component.name()) .addJavadoc("Return the current value for the {@code $L} record component in the builder\n", component.name())
.addModifiers(Modifier.PUBLIC) .addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation) .addAnnotation(generatedRecordBuilderAnnotation)
.returns(component.typeName()) .returns(component.typeName())
.addStatement("return $L", component.name()) .addStatement("return $L", component.name());
.build(); component.getAccessorAnnotations().forEach(annotationMirror -> methodSpecBuilder.addAnnotation(AnnotationSpec.get(annotationMirror)));
builder.addMethod(methodSpec); builder.addMethod(methodSpecBuilder.build());
} }
private void add1SetterMethod(ClassType component) private void add1SetterMethod(RecordClassType component) {
{
/* /*
For a single record component, add a setter similar to: For a single record component, add a setter similar to:
@@ -584,12 +620,14 @@ class InternalRecordBuilderProcessor
return this; return this;
} }
*/ */
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build(); var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
component.getCanonicalConstructorAnnotations().forEach(annotationMirror -> parameterSpecBuilder.addAnnotation(AnnotationSpec.get(annotationMirror)));
var methodSpec = MethodSpec.methodBuilder(component.name()) var methodSpec = MethodSpec.methodBuilder(component.name())
.addJavadoc("Set a new value for the {@code $L} record component in the builder\n", component.name()) .addJavadoc("Set a new value for the {@code $L} record component in the builder\n", component.name())
.addModifiers(Modifier.PUBLIC) .addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation) .addAnnotation(generatedRecordBuilderAnnotation)
.addParameter(parameterSpec) .addParameter(parameterSpecBuilder.build())
.returns(builderClassType.typeName()) .returns(builderClassType.typeName())
.addStatement("this.$L = $L", component.name(), component.name()) .addStatement("this.$L = $L", component.name(), component.name())
.addStatement("return this") .addStatement("return this")
@@ -597,3 +635,4 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec); builder.addMethod(methodSpec);
} }
} }

View File

@@ -22,7 +22,6 @@ import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName; import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.IgnoreDefaultMethod; import io.soabase.recordbuilder.core.IgnoreDefaultMethod;
import io.soabase.recordbuilder.core.RecordBuilder; import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ElementKind; import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.ExecutableElement;
@@ -48,13 +47,18 @@ class InternalRecordInterfaceProcessor {
private final ProcessingEnvironment processingEnv; private final ProcessingEnvironment processingEnv;
private final String packageName; private final String packageName;
private final TypeSpec recordType; private final TypeSpec recordType;
private final List<ExecutableElement> recordComponents; private final List<Component> recordComponents;
private final TypeElement iface; private final TypeElement iface;
private final ClassType recordClassType; private final ClassType recordClassType;
private final List<String> alternateMethods;
private static final String FAKE_METHOD_NAME = "__FAKE__"; private static final String FAKE_METHOD_NAME = "__FAKE__";
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageNameOpt) { private static final Set<String> javaBeanPrefixes = Set.of("get", "is");
private record Component(ExecutableElement element, Optional<String> alternateName){}
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilder.Options metaData, Optional<String> packageNameOpt) {
this.processingEnv = processingEnv; this.processingEnv = processingEnv;
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(iface)); packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(iface));
recordComponents = getRecordComponents(iface); recordComponents = getRecordComponents(iface);
@@ -79,6 +83,8 @@ class InternalRecordInterfaceProcessor {
builder.addSuperinterface(builderClassType.typeName()); builder.addSuperinterface(builderClassType.typeName());
} }
alternateMethods = buildAlternateMethods(recordComponents);
recordType = builder.build(); recordType = builder.build();
} }
@@ -126,22 +132,43 @@ class InternalRecordInterfaceProcessor {
String declaration = matcher.group(1).trim().replace("class", "record"); String declaration = matcher.group(1).trim().replace("class", "record");
String implementsSection = matcher.group(2).trim(); String implementsSection = matcher.group(2).trim();
String argumentList = matcher.group(5).trim(); String argumentList = matcher.group(5).trim();
return declaration + argumentList + " " + implementsSection + " {}";
StringBuilder fixedRecord = new StringBuilder(declaration).append(argumentList).append(' ').append(implementsSection).append(" {");
alternateMethods.forEach(method -> fixedRecord.append('\n').append(method));
fixedRecord.append('}');
return fixedRecord.toString();
} }
private MethodSpec generateArgumentList() private MethodSpec generateArgumentList()
{ {
MethodSpec.Builder builder = MethodSpec.methodBuilder(FAKE_METHOD_NAME); MethodSpec.Builder builder = MethodSpec.methodBuilder(FAKE_METHOD_NAME);
recordComponents.forEach(element -> { recordComponents.forEach(component -> {
ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(element.getReturnType()), element.getSimpleName().toString()).build(); String name = component.alternateName.orElseGet(() -> component.element.getSimpleName().toString());
builder.addTypeVariables(element.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList())); ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(component.element.getReturnType()), name).build();
builder.addTypeVariables(component.element.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList()));
builder.addParameter(parameterSpec); builder.addParameter(parameterSpec);
}); });
return builder.build(); return builder.build();
} }
private List<ExecutableElement> getRecordComponents(TypeElement iface) { private List<String> buildAlternateMethods(List<Component> recordComponents) {
List<ExecutableElement> components = new ArrayList<>(); return recordComponents.stream()
.filter(component -> component.alternateName.isPresent())
.map(component -> {
var method = MethodSpec.methodBuilder(component.element.getSimpleName().toString())
.addAnnotation(Override.class)
.addAnnotation(generatedRecordInterfaceAnnotation)
.returns(ClassName.get(component.element.getReturnType()))
.addModifiers(Modifier.PUBLIC)
.addCode("return $L();", component.alternateName.get())
.build();
return method.toString();
})
.collect(Collectors.toList());
}
private List<Component> getRecordComponents(TypeElement iface) {
List<Component> components = new ArrayList<>();
try { try {
getRecordComponents(iface, components, new HashSet<>(), new HashSet<>()); getRecordComponents(iface, components, new HashSet<>(), new HashSet<>());
if (components.isEmpty()) { if (components.isEmpty()) {
@@ -153,15 +180,15 @@ class InternalRecordInterfaceProcessor {
} }
return components; return components;
} }
private static class IllegalInterface extends RuntimeException private static class IllegalInterface extends RuntimeException
{ {
public IllegalInterface(String message) { public IllegalInterface(String message) {
super(message); super(message);
} }
} }
private void getRecordComponents(TypeElement iface, Collection<? super ExecutableElement> components, Set<String> visitedSet, Set<String> usedNames) { private void getRecordComponents(TypeElement iface, Collection<Component> components, Set<String> visitedSet, Set<String> usedNames) {
if (!visitedSet.add(iface.getQualifiedName().toString())) { if (!visitedSet.add(iface.getQualifiedName().toString())) {
return; return;
} }
@@ -184,10 +211,22 @@ class InternalRecordInterfaceProcessor {
} }
}) })
.filter(element -> usedNames.add(element.getSimpleName().toString())) .filter(element -> usedNames.add(element.getSimpleName().toString()))
.map(element -> new Component(element, stripBeanPrefix(element.getSimpleName().toString())))
.collect(Collectors.toCollection(() -> components)); .collect(Collectors.toCollection(() -> components));
iface.getInterfaces().forEach(parentIface -> { iface.getInterfaces().forEach(parentIface -> {
TypeElement parentIfaceElement = (TypeElement) processingEnv.getTypeUtils().asElement(parentIface); TypeElement parentIfaceElement = (TypeElement) processingEnv.getTypeUtils().asElement(parentIface);
getRecordComponents(parentIfaceElement, components, visitedSet, usedNames); getRecordComponents(parentIfaceElement, components, visitedSet, usedNames);
}); });
} }
private Optional<String> stripBeanPrefix(String name)
{
return javaBeanPrefixes.stream()
.filter(prefix -> name.startsWith(prefix) && (name.length() > prefix.length()))
.findFirst()
.map(prefix -> {
var stripped = name.substring(prefix.length());
return Character.toLowerCase(stripped.charAt(0)) + stripped.substring(1);
});
}
} }

View File

@@ -1,175 +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.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

@@ -1,53 +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.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

@@ -0,0 +1,65 @@
/**
* 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.JavaFile;
import com.squareup.javapoet.TypeSpec; import com.squareup.javapoet.TypeSpec;
import io.soabase.recordbuilder.core.RecordBuilder; import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import io.soabase.recordbuilder.core.RecordInterface; import io.soabase.recordbuilder.core.RecordInterface;
import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer; import javax.annotation.processing.Filer;
import javax.annotation.processing.Generated; import javax.annotation.processing.Generated;
@@ -32,13 +32,16 @@ import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic; import javax.tools.Diagnostic;
import javax.tools.JavaFileObject; import javax.tools.JavaFileObject;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Function; 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 = RecordBuilder.class.getName();
private static final String RECORD_BUILDER_INCLUDE = RecordBuilder.Include.class.getName().replace('$', '.'); private static final String RECORD_BUILDER_INCLUDE = RecordBuilder.Include.class.getName().replace('$', '.');
private static final String RECORD_INTERFACE = RecordInterface.class.getName(); private static final String RECORD_INTERFACE = RecordInterface.class.getName();
@@ -48,18 +51,21 @@ public class RecordBuilderProcessor extends AbstractProcessor {
static final AnnotationSpec generatedRecordInterfaceAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RecordInterface.class.getName()).build(); static final AnnotationSpec generatedRecordInterfaceAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RecordInterface.class.getName()).build();
@Override @Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
{
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> process(annotation, element))); annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> process(annotation, element)));
return true; return false;
} }
@Override @Override
public Set<String> getSupportedAnnotationTypes() { public Set<String> getSupportedAnnotationTypes()
return Set.of(RECORD_BUILDER, RECORD_BUILDER_INCLUDE, RECORD_INTERFACE, RECORD_INTERFACE_INCLUDE); {
return Set.of("*");
} }
@Override @Override
public SourceVersion getSupportedSourceVersion() { public SourceVersion getSupportedSourceVersion()
{
// we don't directly return RELEASE_14 as that may // we don't directly return RELEASE_14 as that may
// not exist in prior releases // not exist in prior releases
// if we're running on an older release, returning latest() // if we're running on an older release, returning latest()
@@ -67,66 +73,57 @@ public class RecordBuilderProcessor extends AbstractProcessor {
return SourceVersion.latest(); return SourceVersion.latest();
} }
private void process(TypeElement annotation, Element element) { 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(); String annotationClass = annotation.getQualifiedName().toString();
if ( annotationClass.equals(RECORD_BUILDER) ) if (annotationClass.equals(RECORD_BUILDER)) {
{ var metaData = RecordBuilderOptions.build(processingEnv.getOptions());
processRecordBuilder((TypeElement)element, metaData, Optional.empty()); processRecordBuilder((TypeElement) element, metaData, Optional.empty());
} }
else if ( annotationClass.equals(RECORD_INTERFACE) ) else if (annotationClass.equals(RECORD_INTERFACE)) {
{ var metaData = RecordBuilderOptions.build(processingEnv.getOptions());
processRecordInterface((TypeElement)element, element.getAnnotation(RecordInterface.class).addRecordBuilder(), metaData, Optional.empty()); processRecordInterface((TypeElement) element, element.getAnnotation(RecordInterface.class).addRecordBuilder(), metaData, Optional.empty());
} }
else if ( annotationClass.equals(RECORD_BUILDER_INCLUDE) || annotationClass.equals(RECORD_INTERFACE_INCLUDE) ) else if (annotationClass.equals(RECORD_BUILDER_INCLUDE) || annotationClass.equals(RECORD_INTERFACE_INCLUDE)) {
{ var metaData = RecordBuilderOptions.build(processingEnv.getOptions());
processIncludes(element, metaData, annotationClass); processIncludes(element, metaData, annotationClass);
} } else {
else var recordBuilderTemplate = annotation.getAnnotation(RecordBuilder.Template.class);
{ if (recordBuilderTemplate != null) {
throw new RuntimeException("Unknown annotation: " + annotation); processRecordBuilder((TypeElement) element, recordBuilderTemplate.options(), Optional.empty());
}
} }
} }
private void processIncludes(Element element, RecordBuilderMetaData metaData, String annotationClass) { private void processIncludes(Element element, RecordBuilder.Options metaData, String annotationClass)
{
var annotationMirrorOpt = ElementUtils.findAnnotationMirror(processingEnv, element, 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); processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get annotation mirror for: " + annotationClass, element);
} }
else else {
{
var values = processingEnv.getElementUtils().getElementValuesWithDefaults(annotationMirrorOpt.get()); var values = processingEnv.getElementUtils().getElementValuesWithDefaults(annotationMirrorOpt.get());
var classes = ElementUtils.getAnnotationValue(values, "value"); var classes = ElementUtils.getAnnotationValue(values, "value");
if ( classes.isEmpty() ) if (classes.isEmpty()) {
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get annotation value for: " + annotationClass, element); processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get annotation value for: " + annotationClass, element);
} }
else else {
{
var packagePattern = ElementUtils.getStringAttribute(ElementUtils.getAnnotationValue(values, "packagePattern").orElse(null), "*"); var packagePattern = ElementUtils.getStringAttribute(ElementUtils.getAnnotationValue(values, "packagePattern").orElse(null), "*");
var classesMirrors = ElementUtils.getClassesAttribute(classes.get()); var classesMirrors = ElementUtils.getClassesAttribute(classes.get());
for ( TypeMirror mirror : classesMirrors ) for (TypeMirror mirror : classesMirrors) {
{ TypeElement typeElement = (TypeElement) processingEnv.getTypeUtils().asElement(mirror);
TypeElement typeElement = (TypeElement)processingEnv.getTypeUtils().asElement(mirror); if (typeElement == null) {
if ( typeElement == null )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get element for: " + mirror, element); processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get element for: " + mirror, element);
} }
else else {
{
var packageName = buildPackageName(packagePattern, element, typeElement); var packageName = buildPackageName(packagePattern, element, typeElement);
if (packageName != null) if (packageName != null) {
{ if (annotationClass.equals(RECORD_INTERFACE_INCLUDE)) {
if ( annotationClass.equals(RECORD_INTERFACE_INCLUDE) )
{
var addRecordBuilderOpt = ElementUtils.getAnnotationValue(values, "addRecordBuilder"); var addRecordBuilderOpt = ElementUtils.getAnnotationValue(values, "addRecordBuilder");
var addRecordBuilder = addRecordBuilderOpt.map(ElementUtils::getBooleanAttribute).orElse(true); var addRecordBuilder = addRecordBuilderOpt.map(ElementUtils::getBooleanAttribute).orElse(true);
processRecordInterface(typeElement, addRecordBuilder, metaData, Optional.of(packageName)); processRecordInterface(typeElement, addRecordBuilder, metaData, Optional.of(packageName));
} }
else else {
{
processRecordBuilder(typeElement, metaData, Optional.of(packageName)); processRecordBuilder(typeElement, metaData, Optional.of(packageName));
} }
} }
@@ -136,50 +133,51 @@ public class RecordBuilderProcessor extends AbstractProcessor {
} }
} }
private String buildPackageName(String packagePattern, Element builderElement, TypeElement includedClass) { private String buildPackageName(String packagePattern, Element builderElement, TypeElement includedClass)
{
PackageElement includedClassPackage = findPackageElement(includedClass, includedClass); PackageElement includedClassPackage = findPackageElement(includedClass, includedClass);
if (includedClassPackage == null) { if (includedClassPackage == null) {
return null; return null;
} }
String replaced = packagePattern.replace("*", includedClassPackage.getQualifiedName().toString()); String replaced = packagePattern.replace("*", includedClassPackage.getQualifiedName().toString());
if (builderElement instanceof PackageElement) { 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) { private PackageElement findPackageElement(Element actualElement, Element includedClass)
{
if (includedClass == null) { if (includedClass == null) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Element has not package", actualElement); processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Element has not package", actualElement);
return null; return null;
} }
if (includedClass.getEnclosingElement() instanceof PackageElement) { if (includedClass.getEnclosingElement() instanceof PackageElement) {
return (PackageElement)includedClass.getEnclosingElement(); return (PackageElement) includedClass.getEnclosingElement();
} }
return findPackageElement(actualElement, includedClass.getEnclosingElement()); return findPackageElement(actualElement, includedClass.getEnclosingElement());
} }
private void processRecordInterface(TypeElement element, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageName) { private void processRecordInterface(TypeElement element, boolean addRecordBuilder, RecordBuilder.Options metaData, Optional<String> packageName)
if ( !element.getKind().isInterface() ) {
{ if (!element.getKind().isInterface()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordInterface only valid for interfaces.", element); processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordInterface only valid for interfaces.", element);
return; return;
} }
var internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, addRecordBuilder, metaData, packageName); var internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, addRecordBuilder, metaData, packageName);
if ( !internalProcessor.isValid() ) if (!internalProcessor.isValid()) {
{
return; return;
} }
writeRecordInterfaceJavaFile(element, internalProcessor.packageName(), internalProcessor.recordClassType(), internalProcessor.recordType(), metaData, internalProcessor::toRecord); writeRecordInterfaceJavaFile(element, internalProcessor.packageName(), internalProcessor.recordClassType(), internalProcessor.recordType(), metaData, internalProcessor::toRecord);
} }
private void processRecordBuilder(TypeElement record, RecordBuilderMetaData metaData, Optional<String> packageName) { private void processRecordBuilder(TypeElement record, RecordBuilder.Options metaData, Optional<String> packageName)
{
// we use string based name comparison for the element kind, // we use string based name comparison for the element kind,
// as the ElementKind.RECORD enum doesn't exist on JRE releases // as the ElementKind.RECORD enum doesn't exist on JRE releases
// older than Java 14, and we don't want to throw unexpected // older than Java 14, and we don't want to throw unexpected
// NoSuchFieldErrors // 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); processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordBuilder only valid for records.", record);
return; return;
} }
@@ -187,26 +185,25 @@ public class RecordBuilderProcessor extends AbstractProcessor {
writeRecordBuilderJavaFile(record, internalProcessor.packageName(), internalProcessor.builderClassType(), internalProcessor.builderType(), metaData); writeRecordBuilderJavaFile(record, internalProcessor.packageName(), internalProcessor.builderClassType(), internalProcessor.builderType(), metaData);
} }
private void writeRecordBuilderJavaFile(TypeElement record, String packageName, ClassType builderClassType, TypeSpec builderType, RecordBuilderMetaData metaData) { private void writeRecordBuilderJavaFile(TypeElement record, String packageName, ClassType builderClassType, TypeSpec builderType, RecordBuilder.Options metaData)
{
// produces the Java file // produces the Java file
JavaFile javaFile = javaFileBuilder(packageName, builderType, metaData); JavaFile javaFile = javaFileBuilder(packageName, builderType, metaData);
Filer filer = processingEnv.getFiler(); Filer filer = processingEnv.getFiler();
try try {
{
String fullyQualifiedName = packageName.isEmpty() ? builderClassType.name() : (packageName + "." + builderClassType.name()); String fullyQualifiedName = packageName.isEmpty() ? builderClassType.name() : (packageName + "." + builderClassType.name());
JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName); JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName);
try (Writer writer = sourceFile.openWriter()) try (Writer writer = sourceFile.openWriter()) {
{
javaFile.writeTo(writer); javaFile.writeTo(writer);
} }
} }
catch ( IOException e ) catch (IOException e) {
{
handleWriteError(record, e); handleWriteError(record, e);
} }
} }
private void writeRecordInterfaceJavaFile(TypeElement element, String packageName, ClassType classType, TypeSpec type, RecordBuilderMetaData metaData, Function<String, String> toRecordProc) { private void writeRecordInterfaceJavaFile(TypeElement element, String packageName, ClassType classType, TypeSpec type, RecordBuilder.Options metaData, Function<String, String> toRecordProc)
{
JavaFile javaFile = javaFileBuilder(packageName, type, metaData); JavaFile javaFile = javaFileBuilder(packageName, type, metaData);
String classSourceCode = javaFile.toString(); String classSourceCode = javaFile.toString();
@@ -214,35 +211,32 @@ public class RecordBuilderProcessor extends AbstractProcessor {
String recordSourceCode = toRecordProc.apply(classSourceCode); String recordSourceCode = toRecordProc.apply(classSourceCode);
Filer filer = processingEnv.getFiler(); Filer filer = processingEnv.getFiler();
try try {
{
String fullyQualifiedName = packageName.isEmpty() ? classType.name() : (packageName + "." + classType.name()); String fullyQualifiedName = packageName.isEmpty() ? classType.name() : (packageName + "." + classType.name());
JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName); JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName);
try (Writer writer = sourceFile.openWriter()) try (Writer writer = sourceFile.openWriter()) {
{
writer.write(recordSourceCode); writer.write(recordSourceCode);
} }
} }
catch ( IOException e ) catch (IOException e) {
{
handleWriteError(element, e); handleWriteError(element, e);
} }
} }
private JavaFile javaFileBuilder(String packageName, TypeSpec type, RecordBuilderMetaData metaData) { private JavaFile javaFileBuilder(String packageName, TypeSpec type, RecordBuilder.Options metaData)
{
var javaFileBuilder = JavaFile.builder(packageName, type).skipJavaLangImports(true).indent(metaData.fileIndent()); var javaFileBuilder = JavaFile.builder(packageName, type).skipJavaLangImports(true).indent(metaData.fileIndent());
var comment = metaData.fileComment(); var comment = metaData.fileComment();
if ( (comment != null) && !comment.isEmpty() ) if ((comment != null) && !comment.isEmpty()) {
{
javaFileBuilder.addFileComment(comment); javaFileBuilder.addFileComment(comment);
} }
return javaFileBuilder.build(); return javaFileBuilder.build();
} }
private void handleWriteError(TypeElement element, IOException e) { private void handleWriteError(TypeElement element, IOException e)
{
String message = "Could not create source file"; String message = "Could not create source file";
if ( e.getMessage() != null ) if (e.getMessage() != null) {
{
message = message + ": " + e.getMessage(); message = message + ": " + e.getMessage();
} }
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, element); processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, element);

View File

@@ -0,0 +1,40 @@
/**
* 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 List<? extends AnnotationMirror> accessorAnnotations;
private final List<? extends AnnotationMirror> canonicalConstructorAnnotations;
public RecordClassType(TypeName typeName, String name, List<? extends AnnotationMirror> accessorAnnotations, List<? extends AnnotationMirror> canonicalConstructorAnnotations) {
super(typeName, name);
this.accessorAnnotations = accessorAnnotations;
this.canonicalConstructorAnnotations = canonicalConstructorAnnotations;
}
public List<? extends AnnotationMirror> getAccessorAnnotations() {
return accessorAnnotations;
}
public List<? extends AnnotationMirror> getCanonicalConstructorAnnotations() {
return canonicalConstructorAnnotations;
}
}

View File

@@ -3,16 +3,43 @@
<parent> <parent>
<groupId>io.soabase.record-builder</groupId> <groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId> <artifactId>record-builder</artifactId>
<version>1.14.ea-SNAPSHOT</version> <version>21</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>record-builder-test</artifactId> <artifactId>record-builder-test</artifactId>
<properties>
<license-file-path>${project.parent.basedir}/src/etc/header.txt</license-file-path>
</properties>
<dependencies> <dependencies>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency> <dependency>
<groupId>io.soabase.record-builder</groupId> <groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-core</artifactId> <artifactId>record-builder-core</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>
<scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
@@ -42,6 +69,10 @@
<annotationProcessors> <annotationProcessors>
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor> <annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
</annotationProcessors> </annotationProcessors>
<release>${jdk-version}</release>
<compilerArgs>
<arg>${enable-preview}</arg>
</compilerArgs>
</configuration> </configuration>
</plugin> </plugin>

View File

@@ -0,0 +1,26 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.test;
import 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

@@ -13,11 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
module io.soabase.record.builder.processor { package io.soabase.recordbuilder.test;
requires com.squareup.javapoet;
requires io.soabase.record.builder.core;
requires java.compiler;
exports io.soabase.recordbuilder.processor; import io.soabase.recordbuilder.core.RecordInterface;
opens io.soabase.recordbuilder.processor; import java.time.Instant;
@RecordInterface
public interface BeanStyle {
String getName();
Instant getDate();
boolean isSomething();
} }

View File

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

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.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

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

View File

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

View File

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

View File

@@ -13,7 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
module io.soabase.record.builder.core { package io.soabase.recordbuilder.test;
exports io.soabase.recordbuilder.core;
opens io.soabase.recordbuilder.core; import java.time.Instant;
@MyTemplate
public record TemplateTest(String text, Instant date) implements TemplateTestBuilder.Com
{
} }

View File

@@ -0,0 +1,101 @@
/**
* 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 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

@@ -0,0 +1,36 @@
/**
* 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

@@ -0,0 +1,31 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.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

@@ -0,0 +1,33 @@
/**
* 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());
}
}

View File

@@ -0,0 +1,31 @@
<?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>21</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>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>io.soabase.recordbuilder.validator</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,80 @@
/**
* 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() {
}
}