Compare commits

...

36 Commits

Author SHA1 Message Date
Jordan Zimmerman
445c33fc1d [maven-release-plugin] prepare release record-builder-26 2021-08-18 08:19:14 -05:00
Jordan Zimmerman
2b266bd65d [maven-release-plugin] prepare for next development iteration 2021-08-18 08:15:23 -05:00
Jordan Zimmerman
821718ac3f [maven-release-plugin] prepare release record-builder-25-java15 2021-08-18 08:15:19 -05:00
Jordan Zimmerman
8ac34529e7 Match record visibility
If the builder is in the same package as the record and the record
is package-private, make the builder package-private too.

Closes #52
2021-08-18 08:01:48 -05:00
Jordan Zimmerman
c701a09039 @Override should not be inherited. Also, the inheritComponentAnnotations
option was being ignored.

Closes #57
2021-08-18 07:48:28 -05:00
Jordan Zimmerman
328bb7d660 Adds special handling for record components of type java.util.List, java.util.Set, java.util.Map and java.util.Collection. When the record is built, any components of these types are passed through an added shim method that uses the corresponding immutable collection (e.g. List.copyOf(o)) or an empty immutable collection if the component is null.
Closes #56
Closes #58
2021-08-18 07:48:04 -05:00
Jordan Zimmerman
41eddd46a3 [maven-release-plugin] prepare for next development iteration 2021-08-04 08:38:31 -05:00
Jordan Zimmerman
b0f4bfbe88 [maven-release-plugin] prepare release record-builder-24 2021-08-04 08:38:27 -05:00
Jordan Zimmerman
09168b6104 [maven-release-plugin] prepare for next development iteration 2021-08-04 08:34:03 -05:00
Jordan Zimmerman
d4e24993c9 [maven-release-plugin] prepare release record-builder-23-java15 2021-08-04 08:33:59 -05:00
Jordan Zimmerman
8304cb1f83 Remove downcast in favor of methods
Great suggestion from @Twisol. There's no need for the downcasting
if record component methods are added to the Wither interface.

Closes #27
2021-08-04 08:16:12 -05:00
Jordan Zimmerman
7215ad3241 Validation and null checks are missing for withers. This PR adds them.
Closes #47
2021-07-01 08:37:52 +01:00
Jordan Zimmerman
f7d65c7619 [maven-release-plugin] prepare for next development iteration 2021-06-26 05:55:30 +01:00
Jordan Zimmerman
29ebe52914 [maven-release-plugin] prepare release record-builder-22 2021-06-26 05:55:23 +01:00
Jordan Zimmerman
d9c143aa8b [maven-release-plugin] prepare for next development iteration 2021-06-25 19:21:43 +01:00
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
802dd1f880 Update README.md 2021-02-04 10:12:17 -05:00
42 changed files with 1750 additions and 667 deletions

View File

@@ -1,5 +1,5 @@
[![Build Status](https://github.com/Randgalt/record-builder/workflows/Java%20CI%20with%20Maven/badge.svg)](https://github.com/Randgalt/record-builder/actions)
[![Maven Central](https://img.shields.io/maven-central/v/io.soabase.record-builder/record-builder.svg)](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
@@ -21,7 +21,7 @@ _Details:_
- [Record From Interface Details](#RecordInterface-Example)
- [Generation Via Includes](#generation-via-includes)
- [Usage](#usage)
- [Customizing](#customizing)
- [Customizing](customizing.md)
- [Java 15 Versions](#java-15-versions)
## RecordBuilder Example
@@ -71,6 +71,15 @@ NameAndAge r4 = r3.with().age(101).name("baz").build();
// alternate method of accessing the builder (note: no need to call "build()")
NameAndAge r5 = r4.with(b -> b.age(200).name("whatever"));
// perform some logic in addition to changing values
NameAndAge r5 = r4.with(b -> {
if (b.age() > 13) {
b.name("Teen " + b.name());
} else {
b.name("whatever"));
}
});
```
_Hat tip to [Benji Weber](https://benjiweber.co.uk/blog/2020/09/19/fun-with-java-records/) for the Withers idea._
@@ -175,29 +184,26 @@ public class NameAndAgeBuilder {
&& Objects.equals(name, b.name)
&& (age == b.age));
}
/**
* Downcast to {@code NameAndAge}
*/
private static NameAndAge _downcast(Object obj) {
try {
return (NameAndAge)obj;
}
catch (ClassCastException dummy) {
throw new RuntimeException("NameAndAgeBuilder.With can only be implemented for NameAndAge");
}
}
/**
* Add withers to {@code NameAndAge}
*/
public interface With {
/**
* Return the current value for the {@code name} record component in the builder
*/
String name();
/**
* Return the current value for the {@code age} record component in the builder
*/
int age();
/**
* Return a new record builder using the current values
*/
default NameAndAgeBuilder with() {
NameAndAge r = _downcast(this);
return NameAndAgeBuilder.builder(r);
return new NameAndAgeBuilder(name(), age());
}
/**
@@ -213,16 +219,14 @@ public class NameAndAgeBuilder {
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
*/
default NameAndAge withName(String name) {
NameAndAge r = _downcast(this);
return new NameAndAge(name, r.age());
return new NameAndAge(name, age());
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
*/
default NameAndAge withAge(int age) {
NameAndAge r = _downcast(this);
return new NameAndAge(r.name(), age);
return new NameAndAge(name(), age);
}
}
}
@@ -268,7 +272,7 @@ libraries where you are not able to annotate the source.
E.g.
```
```java
import some.library.code.ImportedRecord
import some.library.code.ImportedInterface
@@ -291,7 +295,7 @@ annotation. Use `packagePattern` to change this (see Javadoc for details).
1) Add the dependency that contains the `@RecordBuilder` annotation.
```
```xml
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-core</artifactId>
@@ -303,7 +307,7 @@ annotation. Use `packagePattern` to change this (see Javadoc for details).
2) Enable the annotation processing for the Maven Compiler Plugin:
```
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
@@ -330,7 +334,7 @@ annotation. Use `packagePattern` to change this (see Javadoc for details).
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'
@@ -343,30 +347,13 @@ Depending on your IDE you are likely to need to enable Annotation Processing in
## 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`
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. They are the same versions
as the Java 16 versions with `-java15` appended.
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:
@@ -380,7 +367,7 @@ You will need to enable preview in your build tools:
### Maven
```
```xml
<dependencies>
<dependency>
<groupId>io.soabase.record-builder</groupId>
@@ -421,7 +408,7 @@ Create a file in your project's root named `.mvn/jvm.config`. The file should ha
### Gradle
```
```groovy
dependencies {
annotationProcessor 'io.soabase.record-builder:record-builder-processor:$record-builder-version-java15'
compileOnly 'io.soabase.record-builder:record-builder-core:$record-builder-version-java15'

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.

View File

@@ -16,6 +16,6 @@
#
jenv local 16-ea
jenv local 16
javahome
rm -fr .mvn

52
pom.xml
View File

@@ -5,12 +5,13 @@
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<packaging>pom</packaging>
<version>1.19-SNAPSHOT</version>
<version>26</version>
<modules>
<module>record-builder-core</module>
<module>record-builder-processor</module>
<module>record-builder-test</module>
<module>record-builder-validator</module>
</modules>
<properties>
@@ -33,10 +34,16 @@
<maven-shade-plugin-version>3.2.1</maven-shade-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-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>
<junit-jupiter-version>5.5.2</junit-jupiter-version>
<asm-version>7.2</asm-version>
<validation-api-version>2.0.1.Final</validation-api-version>
<hibernate-validator-version>6.0.20.Final</hibernate-validator-version>
<javax-el-version>3.0.1-b09</javax-el-version>
</properties>
<name>Record Builder</name>
@@ -73,7 +80,7 @@
<url>https://github.com/randgalt/record-builder</url>
<connection>scm:git:https://github.com/randgalt/record-builder.git</connection>
<developerConnection>scm:git:git@github.com:randgalt/record-builder.git</developerConnection>
<tag>record-builder-1.16</tag>
<tag>record-builder-26</tag>
</scm>
<issueManagement>
@@ -108,11 +115,35 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-validator</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit-jupiter-version}</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>${validation-api-version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator-version}</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>${javax-el-version}</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -159,7 +190,7 @@
<artifactId>maven-license-plugin</artifactId>
<version>${maven-license-plugin-version}</version>
<configuration>
<header>src/etc/header.txt</header>
<header>${license-file-path}</header>
<excludes>
<exclude>**/*.apt</exclude>
<exclude>**/*.md</exclude>
@@ -185,6 +216,8 @@
<exclude>**/jvm.config</exclude>
<exclude>**/.java-version</exclude>
<exclude>**/.travis.yml</exclude>
<exclude>**/gradlew</exclude>
<exclude>**/.github/**</exclude>
</excludes>
<strictCheck>true</strictCheck>
</configuration>
@@ -283,6 +316,18 @@
<argLine>${enable-preview}</argLine>
</configuration>
</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>
</pluginManagement>
@@ -326,7 +371,6 @@
<plugins>
<plugin>
<artifactId>maven-gpg-plugin</artifactId>
<version>${maven-gpg-plugin-version}</version>
<configuration>
<passphrase>${gpg.passphrase}</passphrase>
<useAgent>true</useAgent>

View File

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

View File

@@ -15,16 +15,15 @@
*/
package io.soabase.recordbuilder.core;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.*;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
public @interface RecordBuilder {
@Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE)
@Inherited
@interface Include {
Class<?>[] value();
@@ -38,4 +37,119 @@ public @interface RecordBuilder {
*/
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 method that returns the record components as a stream
*/
String componentsMethodName() default "stream";
/**
* The name to use for the nested With class
*/
String withClassName() default "With";
/**
* The prefix to use for the methods in the With class
*/
String withClassMethodPrefix() default "with";
/**
* Return the comment to place at the top of generated files. Return null or an empty string for no comment.
*/
String fileComment() default "Auto generated by io.soabase.recordbuilder.core.RecordBuilder: https://github.com/Randgalt/record-builder";
/**
* Return the file indent to use
*/
String fileIndent() default " ";
/**
* If the record is declared inside of another class, the outer class's name will
* be prefixed to the builder name if this returns true.
*/
boolean prefixEnclosingClassNames() default true;
/**
* If true, any annotations (if applicable) on record components are copied
* to the builder methods
*
* @return true/false
*/
boolean inheritComponentAnnotations() default true;
/**
* Set the default value of {@code Optional} record components to
* {@code Optional.empty()}
*/
boolean emptyDefaultForOptional() default true;
/**
* Add not-null checks for record components annotated with any annotation named either "NotNull",
* "NoNull", or "NonNull" (see {@link #interpretNotNullsPattern()} for the actual regex matching pattern).
*/
boolean interpretNotNulls() default false;
/**
* If {@link #interpretNotNulls()} is true, this is the regex pattern used to determine if an annotation name
* means "not null"
*/
String interpretNotNullsPattern() default "(?i)((notnull)|(nonnull)|(nonull))";
/**
* <p>Pass built records through the Java Validation API if it's available in the classpath.</p>
*
* <p>IMPORTANT:
* if this option is enabled you must include the {@code record-builder-validator} dependency in addition
* to {@code record-builder-core}. {@code record-builder-validator} is implemented completely via reflection and
* does not require other dependencies. Alternatively, you can define your own class with the package {@code package io.soabase.recordbuilder.validator;}
* named {@code RecordBuilderValidator} which has a public static method: {@code public static <T> T validate(T o)}.</p>
*/
boolean useValidationApi() default false;
/**
* Adds special handling for record components of type {@link java.util.List}, {@link java.util.Set},
* {@link java.util.Map} and {@link java.util.Collection}. When the record is built, any components
* of these types are passed through an added shim method that uses the corresponding immutable collection
* (e.g. {@code List.copyOf(o)}) or an empty immutable collection if the component is {@code null}.
*/
boolean useImmutableCollections() default false;
}
@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;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
public @interface RecordInterface {
boolean addRecordBuilder() default true;
@Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE)
@Inherited
@interface Include {
Class<?>[] value();

View File

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

View File

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

View File

@@ -19,11 +19,12 @@ import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.RecordComponentElement;
import javax.lang.model.element.TypeElement;
@@ -107,8 +108,10 @@ public class ElementUtils {
return new ClassType(ParameterizedTypeName.get(builderClassName, typeNames), builderClassName.simpleName());
}
public static ClassType getClassType(RecordComponentElement recordComponent) {
return new ClassType(TypeName.get(recordComponent.asType()), recordComponent.getSimpleName().toString());
public static RecordClassType getRecordClassType(ProcessingEnvironment processingEnv, RecordComponentElement recordComponent, List<? extends AnnotationMirror> accessorAnnotations, List<? extends AnnotationMirror> canonicalConstructorAnnotations) {
var typeName = TypeName.get(recordComponent.asType());
var rawTypeName = TypeName.get(processingEnv.getTypeUtils().erasure(recordComponent.asType()));
return new RecordClassType(typeName, rawTypeName, recordComponent.getSimpleName().toString(), accessorAnnotations, canonicalConstructorAnnotations);
}
public static String getWithMethodName(ClassType component, String prefix) {
@@ -119,12 +122,29 @@ public class ElementUtils {
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
var baseName = classType.name() + suffix;
return metaData.prefixEnclosingClassNames() ? (getBuilderNamePrefix(element.getEnclosingElement()) + baseName) : baseName;
}
public static Optional<? extends Element> findCanonicalConstructor(TypeElement record) {
if ( record.getKind() != ElementKind.RECORD ) {
return Optional.empty();
}
// based on https://github.com/openjdk/jdk/pull/3556/files#diff-a6270f4b50989abe733607c69038b2036306d13f77276af005d023b7fc57f1a2R2368
var componentList = record.getRecordComponents().stream().map(e -> e.asType().toString()).collect(Collectors.toList());
return record.getEnclosedElements().stream()
.filter(element -> element.getKind() == ElementKind.CONSTRUCTOR)
.filter(element -> {
var parameters = ((ExecutableElement)element).getParameters();
var parametersList = parameters.stream().map(e -> e.asType().toString()).collect(Collectors.toList());
return componentList.equals(parametersList);
})
.findFirst();
}
private static String getBuilderNamePrefix(Element element) {
// prefix enclosing class names if nested in a class
if (element instanceof TypeElement) {

View File

@@ -15,26 +15,14 @@
*/
package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import com.squareup.javapoet.*;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import java.util.*;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@@ -43,32 +31,44 @@ import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
class InternalRecordBuilderProcessor
{
private final RecordBuilderMetaData metaData;
class InternalRecordBuilderProcessor {
private final RecordBuilder.Options metaData;
private final ClassType recordClassType;
private final String packageName;
private final ClassType builderClassType;
private final List<TypeVariableName> typeVariables;
private final List<ClassType> recordComponents;
private final List<RecordClassType> recordComponents;
private final TypeSpec builderType;
private final TypeSpec.Builder builder;
private final String uniqueVarName;
private final Pattern notNullPattern;
private final CollectionBuilderUtils collectionBuilderUtils;
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData, Optional<String> packageNameOpt)
{
this.metaData = metaData;
private static final TypeName overrideType = TypeName.get(Override.class);
private static final TypeName optionalType = TypeName.get(Optional.class);
private static final TypeName optionalIntType = TypeName.get(OptionalInt.class);
private static final TypeName optionalLongType = TypeName.get(OptionalLong.class);
private static final TypeName optionalDoubleType = TypeName.get(OptionalDouble.class);
private static final TypeName validatorTypeName = ClassName.get("io.soabase.recordbuilder.validator", "RecordBuilderValidator");
private final ProcessingEnvironment processingEnv;
InternalRecordBuilderProcessor(ProcessingEnvironment processingEnv, TypeElement record, RecordBuilder.Options metaData, Optional<String> packageNameOpt) {
this.processingEnv = processingEnv;
var recordActualPackage = ElementUtils.getPackageName(record);
this.metaData = getMetaData(record, metaData);
recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(record));
packageName = packageNameOpt.orElse(recordActualPackage);
builderClassType = ElementUtils.getClassType(packageName, getBuilderName(record, metaData, recordClassType, metaData.suffix()), record.getTypeParameters());
typeVariables = record.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList());
recordComponents = record.getRecordComponents().stream().map(ElementUtils::getClassType).collect(Collectors.toList());
recordComponents = buildRecordComponents(record);
uniqueVarName = getUniqueVarName();
notNullPattern = Pattern.compile(metaData.interpretNotNullsPattern());
collectionBuilderUtils = new CollectionBuilderUtils(recordComponents, this.metaData.useImmutableCollections());
builder = TypeSpec.classBuilder(builderClassType.name())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables);
addVisibility(recordActualPackage.equals(packageName), record.getModifiers());
addWithNestedClass();
addDefaultConstructor();
addStaticBuilder();
@@ -87,27 +87,53 @@ class InternalRecordBuilderProcessor
add1SetterMethod(component);
add1GetterMethod(component);
});
addStaticDowncastMethod();
collectionBuilderUtils.addShims(builder);
builderType = builder.build();
}
String packageName()
{
String packageName() {
return packageName;
}
ClassType builderClassType()
{
ClassType builderClassType() {
return builderClassType;
}
TypeSpec builderType()
{
TypeSpec builderType() {
return builderType;
}
private void addWithNestedClass()
{
private void addVisibility(boolean builderIsInRecordPackage, Set<Modifier> modifiers) {
if (builderIsInRecordPackage) {
if (modifiers.contains(Modifier.PUBLIC) || modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.PROTECTED)) {
builder.addModifiers(Modifier.PUBLIC); // builders are top level classes - can only be public or package-private
}
// is package-private
}
else {
builder.addModifiers(Modifier.PUBLIC);
}
}
private List<RecordClassType> buildRecordComponents(TypeElement record) {
var accessorAnnotations = record.getRecordComponents().stream().map(e -> e.getAccessor().getAnnotationMirrors()).collect(Collectors.toList());
var canonicalConstructorAnnotations = ElementUtils.findCanonicalConstructor(record).map(constructor -> ((ExecutableElement) constructor).getParameters().stream().map(Element::getAnnotationMirrors).collect(Collectors.toList())).orElse(List.of());
var recordComponents = record.getRecordComponents();
return IntStream.range(0, recordComponents.size())
.mapToObj(index -> {
var thisAccessorAnnotations = (accessorAnnotations.size() > index) ? accessorAnnotations.get(index) : List.<AnnotationMirror>of();
var thisCanonicalConstructorAnnotations = (canonicalConstructorAnnotations.size() > index) ? canonicalConstructorAnnotations.get(index) : List.<AnnotationMirror>of();
return ElementUtils.getRecordClassType(processingEnv, recordComponents.get(index), thisAccessorAnnotations, thisCanonicalConstructorAnnotations);
})
.collect(Collectors.toList());
}
private 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:
@@ -122,14 +148,14 @@ class InternalRecordBuilderProcessor
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PUBLIC)
.addTypeVariables(typeVariables);
recordComponents.forEach(component -> addWithGetterMethod(classBuilder, component));
addWithBuilderMethod(classBuilder);
addWithSuppliedBuilderMethod(classBuilder);
IntStream.range(0, recordComponents.size()).forEach(index -> add1WithMethod(classBuilder, recordComponents.get(index), index));
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:
@@ -156,19 +182,18 @@ class InternalRecordBuilderProcessor
classBuilder.addMethod(methodSpec);
}
private void addWithBuilderMethod(TypeSpec.Builder classBuilder)
{
private void addWithBuilderMethod(TypeSpec.Builder classBuilder) {
/*
Adds a method that returns a pre-filled copy builder similar to:
default MyRecordBuilder with() {
MyRecord r = _downcast(this);
return MyRecordBuilder.builder(r);
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("$T $L = $L(this);\n", recordClassType.typeName(), uniqueVarName, metaData.downCastMethodName())
.add("return $L.$L($L);", builderClassType.name(), metaData.copyMethodName(), uniqueVarName);
.add("return new $L(", builderClassType.name());
addComponentCallsAsArguments(-1, codeBlockBuilder);
codeBlockBuilder.add(");");
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Return a new record builder using the current values")
@@ -179,64 +204,69 @@ class InternalRecordBuilderProcessor
classBuilder.addMethod(methodSpec);
}
private String getUniqueVarName()
{
private String getUniqueVarName() {
return getUniqueVarName("");
}
private String getUniqueVarName(String prefix)
{
private String getUniqueVarName(String prefix) {
var name = prefix + "r";
var alreadyExists = recordComponents.stream()
.map(ClassType::name)
.anyMatch(n -> n.equals(name));
.map(ClassType::name)
.anyMatch(n -> n.equals(name));
return alreadyExists ? getUniqueVarName(prefix + "_") : name;
}
private void add1WithMethod(TypeSpec.Builder classBuilder, ClassType component, int index)
{
private void add1WithMethod(TypeSpec.Builder classBuilder, RecordClassType component, int index) {
/*
Adds a with method for the component similar to:
default MyRecord withName(String name) {
MyRecord r = _downcast(this);
return new MyRecord(name, r.age());
}
*/
var codeBlockBuilder = CodeBlock.builder();
if (recordComponents.size() > 1) {
codeBlockBuilder.add("$T $L = $L(this);\n", recordClassType.typeName(), uniqueVarName, metaData.downCastMethodName());
addNullCheckCodeBlock(codeBlockBuilder, index);
codeBlockBuilder.add("$[return ");
if (metaData.useValidationApi()) {
codeBlockBuilder.add("$T.validate(", validatorTypeName);
}
codeBlockBuilder.add("return new $T(", recordClassType.typeName());
IntStream.range(0, recordComponents.size()).forEach(parameterIndex -> {
if (parameterIndex > 0) {
codeBlockBuilder.add(", ");
}
ClassType parameterComponent = recordComponents.get(parameterIndex);
if (parameterIndex == index) {
codeBlockBuilder.add(parameterComponent.name());
}
else {
codeBlockBuilder.add("$L.$L()", uniqueVarName, parameterComponent.name());
}
});
codeBlockBuilder.add(");");
codeBlockBuilder.add("new $T(", recordClassType.typeName());
addComponentCallsAsArguments(index, codeBlockBuilder);
codeBlockBuilder.add(")");
if (metaData.useValidationApi()) {
codeBlockBuilder.add(")");
}
codeBlockBuilder.add(";$]");
var methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
addConstructorAnnotations(component, parameterSpecBuilder);
var methodSpec = MethodSpec.methodBuilder(methodName)
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Return a new instance of {@code $L} with a new value for {@code $L}\n", recordClassType.name(), component.name())
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addParameter(parameterSpec)
.addParameter(parameterSpecBuilder.build())
.addCode(codeBlockBuilder.build())
.returns(recordClassType.typeName())
.build();
classBuilder.addMethod(methodSpec);
}
private void addDefaultConstructor()
{
private void addComponentCallsAsArguments(int index, CodeBlock.Builder codeBlockBuilder) {
IntStream.range(0, recordComponents.size()).forEach(parameterIndex -> {
if (parameterIndex > 0) {
codeBlockBuilder.add(", ");
}
RecordClassType parameterComponent = recordComponents.get(parameterIndex);
if (parameterIndex == index) {
collectionBuilderUtils.add(codeBlockBuilder, parameterComponent);
} else {
codeBlockBuilder.add("$L()", parameterComponent.name());
}
});
}
private void addDefaultConstructor() {
/*
Adds a default constructor similar to:
@@ -250,8 +280,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(constructor);
}
private void addStaticBuilder()
{
private void addStaticBuilder() {
/*
Adds an static builder similar to:
@@ -266,13 +295,38 @@ class InternalRecordBuilderProcessor
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(recordClassType.typeName())
.addStatement(codeBlock);
recordComponents.forEach(component -> builder.addParameter(component.typeName(), component.name()));
.addCode(codeBlock);
recordComponents.forEach(component -> {
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
addConstructorAnnotations(component, parameterSpecBuilder);
builder.addParameter(parameterSpecBuilder.build());
});
this.builder.addMethod(builder.build());
}
private void addAllArgsConstructor()
{
private void addNullCheckCodeBlock(CodeBlock.Builder builder) {
if (metaData.interpretNotNulls()) {
for (int i = 0; i < recordComponents.size(); ++i) {
addNullCheckCodeBlock(builder, i);
}
}
}
private void addNullCheckCodeBlock(CodeBlock.Builder builder, int index) {
if (metaData.interpretNotNulls()) {
var component = recordComponents.get(index);
if (!component.typeName().isPrimitive() && isNullAnnotated(component)) {
builder.addStatement("$T.requireNonNull($L, $S)", Objects.class, component.name(), component.name() + " is required");
}
}
}
private boolean isNullAnnotated(RecordClassType component) {
return component.getCanonicalConstructorAnnotations().stream()
.anyMatch(annotation -> notNullPattern.matcher(annotation.getAnnotationType().asElement().getSimpleName().toString()).matches());
}
private void addAllArgsConstructor() {
/*
Adds an all-args constructor similar to:
@@ -293,8 +347,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(constructorBuilder.build());
}
private void addToStringMethod()
{
private void addToStringMethod() {
/*
add a toString() method similar to:
@@ -323,8 +376,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec);
}
private void addHashCodeMethod()
{
private void addHashCodeMethod() {
/*
add a hashCode() method similar to:
@@ -352,8 +404,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec);
}
private void addEqualsMethod()
{
private void addEqualsMethod() {
/*
add an equals() method similar to:
@@ -371,8 +422,7 @@ class InternalRecordBuilderProcessor
String name = recordComponent.name();
if (recordComponent.typeName().isPrimitive()) {
codeBuilder.add("\n&& ($L == $L.$L)", name, uniqueVarName, name);
}
else {
} else {
codeBuilder.add("\n&& $T.equals($L, $L.$L)", Objects.class, name, uniqueVarName, name);
}
});
@@ -389,8 +439,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec);
}
private void addBuildMethod()
{
private void addBuildMethod() {
/*
Adds the build method that generates the record similar to:
@@ -404,7 +453,7 @@ class InternalRecordBuilderProcessor
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(recordClassType.typeName())
.addStatement(codeBlock)
.addCode(codeBlock)
.build();
builder.addMethod(methodSpec);
}
@@ -414,19 +463,28 @@ class InternalRecordBuilderProcessor
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 -> {
if (index > 0) {
codeBuilder.add(", ");
}
codeBuilder.add("$L", recordComponents.get(index).name());
collectionBuilderUtils.add(codeBuilder, recordComponents.get(index));
});
codeBuilder.add(")");
if (metaData.useValidationApi()) {
codeBuilder.add(")");
}
codeBuilder.add(";$]");
return codeBuilder.build();
}
private void addStaticCopyBuilderMethod()
{
private void addStaticCopyBuilderMethod() {
/*
Adds a copy builder method that pre-fills the builder with existing values similar to:
@@ -455,8 +513,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec);
}
private void addStaticDefaultBuilderMethod()
{
private void addStaticDefaultBuilderMethod() {
/*
Adds a the default builder method similar to:
@@ -475,8 +532,7 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec);
}
private void addStaticComponentsMethod()
{
private void addStaticComponentsMethod() {
/*
Adds a static method that converts a record instance into a stream of its component parts
@@ -510,51 +566,79 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec);
}
private void addStaticDowncastMethod()
{
/*
Adds a method that downcasts to the record type
private static MyRecord _downcast(Object this) {
return (MyRecord)this;
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("try {\n")
.indent()
.add("return ($T)obj;\n", recordClassType.typeName())
.unindent()
.add("}\n")
.add("catch (ClassCastException dummy) {\n")
.indent()
.add("throw new RuntimeException($S);\n", builderClassType.name() + "." + metaData.withClassName() + " can only be implemented by " + recordClassType.name())
.unindent()
.add("}");
var methodSpec = MethodSpec.methodBuilder(metaData.downCastMethodName())
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Downcast to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.addParameter(Object.class, "obj")
.addTypeVariables(typeVariables)
.returns(recordClassType.typeName())
.addCode(codeBlockBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void add1Field(ClassType component)
{
private void add1Field(ClassType component) {
/*
For a single record component, add a field similar to:
private T p;
*/
var fieldSpec = FieldSpec.builder(component.typeName(), component.name(), Modifier.PRIVATE).build();
builder.addField(fieldSpec);
var fieldSpecBuilder = FieldSpec.builder(component.typeName(), component.name(), Modifier.PRIVATE);
if (metaData.emptyDefaultForOptional()) {
TypeName thisOptionalType = null;
if (isOptional(component)) {
thisOptionalType = optionalType;
} else if (component.typeName().equals(optionalIntType)) {
thisOptionalType = optionalIntType;
} else if (component.typeName().equals(optionalLongType)) {
thisOptionalType = optionalLongType;
} else if (component.typeName().equals(optionalDoubleType)) {
thisOptionalType = optionalDoubleType;
}
if (thisOptionalType != null) {
var codeBlock = CodeBlock.builder().add("$T.empty()", thisOptionalType).build();
fieldSpecBuilder.initializer(codeBlock);
}
}
builder.addField(fieldSpecBuilder.build());
}
private void add1GetterMethod(ClassType component)
{
private boolean isOptional(ClassType component) {
if (component.typeName().equals(optionalType)) {
return true;
}
return (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName) && parameterizedTypeName.rawType.equals(optionalType);
}
private void addWithGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component) {
/*
For a single record component, add a getter similar to:
T p();
*/
var methodSpecBuilder = MethodSpec.methodBuilder(component.name())
.addJavadoc("Return the current value for the {@code $L} record component in the builder\n", component.name())
.addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(component.typeName());
addAccessorAnnotations(component, methodSpecBuilder);
classBuilder.addMethod(methodSpecBuilder.build());
}
private boolean filterOutOverride(AnnotationSpec annotationSpec) {
return !annotationSpec.type.equals(overrideType);
}
private void addConstructorAnnotations(RecordClassType component, ParameterSpec.Builder parameterSpecBuilder) {
if (metaData.inheritComponentAnnotations()) {
component.getCanonicalConstructorAnnotations()
.stream()
.map(AnnotationSpec::get)
.filter(this::filterOutOverride)
.forEach(parameterSpecBuilder::addAnnotation);
}
}
private void addAccessorAnnotations(RecordClassType component, MethodSpec.Builder methodSpecBuilder) {
if (metaData.inheritComponentAnnotations()) {
component.getAccessorAnnotations()
.stream()
.map(AnnotationSpec::get)
.filter(this::filterOutOverride)
.forEach(methodSpecBuilder::addAnnotation);
}
}
private void add1GetterMethod(RecordClassType component) {
/*
For a single record component, add a getter similar to:
@@ -562,18 +646,17 @@ class InternalRecordBuilderProcessor
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())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(component.typeName())
.addStatement("return $L", component.name())
.build();
builder.addMethod(methodSpec);
.addStatement("return $L", component.name());
addAccessorAnnotations(component, methodSpecBuilder);
builder.addMethod(methodSpecBuilder.build());
}
private void add1SetterMethod(ClassType component)
{
private void add1SetterMethod(RecordClassType component) {
/*
For a single record component, add a setter similar to:
@@ -582,12 +665,14 @@ class InternalRecordBuilderProcessor
return this;
}
*/
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
addConstructorAnnotations(component, parameterSpecBuilder);
var methodSpec = MethodSpec.methodBuilder(component.name())
.addJavadoc("Set a new value for the {@code $L} record component in the builder\n", component.name())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addParameter(parameterSpec)
.addParameter(parameterSpecBuilder.build())
.returns(builderClassType.typeName())
.addStatement("this.$L = $L", component.name(), component.name())
.addStatement("return this")
@@ -595,3 +680,4 @@ class InternalRecordBuilderProcessor
builder.addMethod(methodSpec);
}
}

View File

@@ -22,7 +22,6 @@ import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.IgnoreDefaultMethod;
import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
@@ -59,7 +58,7 @@ class InternalRecordInterfaceProcessor {
private record Component(ExecutableElement element, Optional<String> alternateName){}
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageNameOpt) {
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilder.Options metaData, Optional<String> packageNameOpt) {
this.processingEnv = processingEnv;
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(iface));
recordComponents = getRecordComponents(iface);

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

View File

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

View File

@@ -3,19 +3,45 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>1.19-SNAPSHOT</version>
<version>26</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>record-builder-test</artifactId>
<properties>
<license-file-path>${project.parent.basedir}/src/etc/header.txt</license-file-path>
</properties>
<dependencies>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-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>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>

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

@@ -0,0 +1,28 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RecordBuilder
@RecordBuilder.Options(useImmutableCollections = true)
public record CollectionRecord<T, X extends Point>(List<T> l, Set<T> s, Map<T, X> m, Collection<X> c) implements CollectionRecordBuilder.With<T, X> {
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RecordBuilder
@RecordBuilder.Options(useImmutableCollections = true)
public record CollectionRecordConflicts(List<String> __list, Set<String> __set, Map<String, String> __map, Collection<String> __collection) implements CollectionRecordConflictsBuilder.With {
}

View File

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

View File

@@ -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()
@RecordBuilder.Options(inheritComponentAnnotations = false)
public record IgnoreAnnotated(@NotNull String s) {
}

View File

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

View File

@@ -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) implements RequiredRecordBuilder.With {
}

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) implements RequiredRecord2Builder.With {
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,107 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import java.lang.reflect.AnnotatedElement;
class TestAnnotated {
@Test
void testInheritComponentAnnotationsFalse() throws NoSuchMethodException {
var method = IgnoreAnnotatedBuilder.class.getMethod("s");
Assertions.assertNull(method.getAnnotation(NotNull.class));
}
@Test
void testStaticConstructor() throws NoSuchMethodException {
var method = AnnotatedBuilder.class.getMethod("Annotated", String.class, Integer.TYPE, Double.TYPE);
var parameters = method.getParameters();
Assertions.assertEquals(3, parameters.length);
assertHey(parameters[0]);
assertI(parameters[1]);
assertD(parameters[2]);
}
@Test
void testSetters() throws NoSuchMethodException {
var method = AnnotatedBuilder.class.getMethod("hey", String.class);
var parameters = method.getParameters();
Assertions.assertEquals(1, parameters.length);
assertHey(parameters[0]);
method = AnnotatedBuilder.class.getMethod("i", Integer.TYPE);
parameters = method.getParameters();
Assertions.assertEquals(1, parameters.length);
assertI(parameters[0]);
method = AnnotatedBuilder.class.getMethod("d", Double.TYPE);
parameters = method.getParameters();
Assertions.assertEquals(1, parameters.length);
assertD(parameters[0]);
}
@Test
void testGetters() throws NoSuchMethodException {
var method = AnnotatedBuilder.class.getMethod("hey");
assertHey(method);
method = AnnotatedBuilder.class.getMethod("i");
assertI(method);
method = AnnotatedBuilder.class.getMethod("d");
assertD(method);
}
@Test
void testWitherSetters() throws NoSuchMethodException {
var method = AnnotatedBuilder.With.class.getMethod("withHey", String.class);
var parameters = method.getParameters();
Assertions.assertEquals(1, parameters.length);
assertHey(parameters[0]);
method = AnnotatedBuilder.With.class.getMethod("withI", Integer.TYPE);
parameters = method.getParameters();
Assertions.assertEquals(1, parameters.length);
assertI(parameters[0]);
method = AnnotatedBuilder.With.class.getMethod("withD", Double.TYPE);
parameters = method.getParameters();
Assertions.assertEquals(1, parameters.length);
assertD(parameters[0]);
}
private void assertD(AnnotatedElement d) {
Assertions.assertEquals(0, d.getAnnotations().length);
}
private void assertI(AnnotatedElement i) {
Assertions.assertNotNull(i.getAnnotation(Min.class));
Assertions.assertEquals(i.getAnnotation(Min.class).value(), 10);
Assertions.assertNotNull(i.getAnnotation(Max.class));
Assertions.assertEquals(i.getAnnotation(Max.class).value(), 100);
}
private void assertHey(AnnotatedElement hey) {
Assertions.assertNotNull(hey.getAnnotation(NotNull.class));
Assertions.assertNotNull(hey.getAnnotation(Null.class));
}
}

View File

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

View File

@@ -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,45 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import javax.validation.ValidationException;
class TestValidation {
@Test
void testNotNulls() {
Assertions.assertThrows(NullPointerException.class, () -> RequiredRecordBuilder.builder().build());
}
@Test
void testValidation() {
Assertions.assertThrows(ValidationException.class, () -> RequiredRecord2Builder.builder().build());
}
@Test
void testNotNullsWithNewProperty() {
var valid = RequiredRecordBuilder.builder().hey("hey").i(1).build();
Assertions.assertThrows(NullPointerException.class, () -> valid.withHey(null));
}
@Test
void testValidationWithNewProperty() {
var valid = RequiredRecord2Builder.builder().hey("hey").i(1).build();
Assertions.assertThrows(ValidationException.class, () -> valid.withHey(null));
}
}

View File

@@ -59,11 +59,4 @@ class TestWithers {
Assertions.assertEquals(20, r3.i());
Assertions.assertEquals("twenty", r3.s());
}
private static class BadSubclass implements PersonRecordBuilder.With {}
@Test
void testBadWithSubclass() {
Assertions.assertThrows(RuntimeException.class, () -> new BadSubclass().withAge(10));
}
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.test.visibility;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Modifier;
class TestVisibility {
@Test
void testMatches() {
Assertions.assertFalse(Modifier.isPublic(PackagePrivateRecordBuilder.class.getModifiers()));
Assertions.assertFalse(Modifier.isPrivate(PackagePrivateRecordBuilder.class.getModifiers()));
Assertions.assertFalse(Modifier.isProtected(PackagePrivateRecordBuilder.class.getModifiers()));
Assertions.assertTrue(Modifier.isPublic(WrapperProtectedRecordBuilder.class.getModifiers()));
}
}

View File

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