Compare commits

...

48 Commits

Author SHA1 Message Date
Jordan Zimmerman
e690800a02 [maven-release-plugin] prepare release record-builder-1.11.ea 2020-10-05 14:47:11 -05:00
Jordan Zimmerman
410cae4d32 Support Include versions of the annotation 2020-10-05 14:42:02 -05:00
Jordan Zimmerman
7811ff8823 Update README.md 2020-10-05 11:30:04 -05:00
Jordan Zimmerman
4c5076690d Update README.md 2020-10-05 11:29:32 -05:00
Jordan Zimmerman
39a800f10e Update README.md 2020-10-05 11:29:00 -05:00
Jordan Zimmerman
610081b27e [maven-release-plugin] prepare for next development iteration 2020-10-05 11:22:40 -05:00
Jordan Zimmerman
1eb91d612e [maven-release-plugin] prepare release record-builder-1.10.ea 2020-10-05 11:22:31 -05:00
Jordan Zimmerman
7c84f26972 Generated record from interface should implement the wither 2020-10-05 11:20:43 -05:00
Jordan Zimmerman
82cc4f4cad Update README.md 2020-10-04 14:54:54 -05:00
Jordan Zimmerman
f16e1b1d0e Update README.md 2020-10-04 14:51:40 -05:00
Jordan Zimmerman
c92bf78ec5 [maven-release-plugin] prepare for next development iteration 2020-10-04 14:48:03 -05:00
Jordan Zimmerman
4c4baa015f [maven-release-plugin] prepare release record-builder-1.9.ea 2020-10-04 14:47:55 -05:00
Jordan Zimmerman
81b7b93a5b Added alternate build wither that takes the builder as a consumer argument 2020-10-04 14:43:43 -05:00
Jordan Zimmerman
c39983e342 Some refactoring and simplifying. No need for the method to return the cast "this". It can be done inline, etc. 2020-09-28 08:06:23 -05:00
Jordan Zimmerman
400caa2943 [maven-release-plugin] prepare for next development iteration 2020-09-27 22:23:31 -05:00
Jordan Zimmerman
a2edd7299f [maven-release-plugin] prepare release record-builder-1.8.ea 2020-09-27 22:23:22 -05:00
Jordan Zimmerman
6661c2ae0e Added support for withers and moved to Java 15 2020-09-27 22:18:26 -05:00
Jordan Zimmerman
74c8480b43 Update README.md 2020-08-03 08:48:59 -05:00
Jordan Zimmerman
8dbdb43391 Update README.md 2020-05-30 08:21:30 -05:00
Jordan Zimmerman
44064d656e Update README.md 2020-05-28 21:38:31 -05:00
Jordan Zimmerman
791eb02faf [maven-release-plugin] prepare for next development iteration 2020-05-28 21:36:11 -05:00
Jordan Zimmerman
aa3aefa39c [maven-release-plugin] prepare release record-builder-1.7.ea 2020-05-28 21:36:03 -05:00
Jordan Zimmerman
de6946030f [maven-release-plugin] rollback the release of record-builder-1.7.ea 2020-05-28 21:34:44 -05:00
Jordan Zimmerman
091c663520 [maven-release-plugin] prepare release record-builder-1.7.ea 2020-05-28 21:34:12 -05:00
Jordan Zimmerman
6a45a2cbd9 Added support for RecordInterface to generate records from Interfaces 2020-05-28 21:32:53 -05:00
Jordan Zimmerman
c47e290363 Update README.md 2020-05-28 21:32:53 -05:00
Jordan Zimmerman
d7abf4c60d Update README.md 2020-05-28 21:32:53 -05:00
Jordan Zimmerman
6fbb0d0330 Update README.md 2020-05-28 21:32:53 -05:00
Jordan Zimmerman
e235fc078f Update README.md 2020-05-28 21:32:53 -05:00
randgalt
cf8f277018 [maven-release-plugin] prepare for next development iteration 2020-05-28 21:32:53 -05:00
randgalt
e8e74cce1a [maven-release-plugin] prepare release record-builder-1.6.ea 2020-05-28 21:32:53 -05:00
randgalt
3b425d4dce Updated the README 2020-05-28 21:32:53 -05:00
sipkab
395f0879ac Fix indentation in RecordBuilderProcessor 2020-05-28 21:32:53 -05:00
sipkab
8969a17053 Fix indentation in OptionBasedRecordBuilderMetaData as well 2020-05-28 21:32:53 -05:00
sipkab
5ef0662d99 Fix indentation 2020-05-28 21:32:52 -05:00
sipkab
c6de55d5ad Allow -A option based processor configuration 2020-05-28 21:32:52 -05:00
sipkab
d19a0c2dc5 Mitigate possible runtime Errors when running on older JVMs 2020-05-28 21:32:52 -05:00
randgalt
feb334e6ea updated copyright date in license 2020-05-28 21:32:52 -05:00
randgalt
f812e173e3 [maven-release-plugin] prepare for next development iteration 2020-05-28 21:32:52 -05:00
randgalt
d50303ca09 [maven-release-plugin] prepare release record-builder-1.5.ea 2020-05-28 21:32:52 -05:00
Jordan Zimmerman
13319e643b Update README.md 2020-05-28 21:32:52 -05:00
Jordan Zimmerman
83802125aa Update README.md 2020-05-28 21:32:52 -05:00
randgalt
b1e343f733 Added a component stream method 2020-05-28 21:32:52 -05:00
randgalt
75fce6bb84 [maven-release-plugin] prepare for next development iteration 2020-05-28 21:32:52 -05:00
randgalt
c676436f5e [maven-release-plugin] prepare release record-builder-1.4.ea 2020-05-28 21:32:52 -05:00
Jordan Zimmerman
b861d0f4b1 Update README.md 2020-05-28 21:32:52 -05:00
randgalt
933098b07d Added getter methods, generated annotation and a few other tweaks 2020-05-28 21:32:52 -05:00
Jordan Zimmerman
8560666ec6 Create .travis.yml 2020-05-28 21:32:52 -05:00
33 changed files with 1865 additions and 353 deletions

3
.travis.yml Normal file
View File

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

222
README.md
View File

@@ -1,10 +1,26 @@
[![Build Status](https://travis-ci.org/Randgalt/record-builder.svg?branch=master)](https://travis-ci.org/Randgalt/record-builder)
[![Maven Central](https://img.shields.io/maven-central/v/io.soabase.record-builder/record-builder.svg)](https://search.maven.org/search?q=g:io.soabase.record-builder%20a:record-builder)
# RecordBuilder - Early Access
## What is RecordBuilder
Java 14 is introducing [Records](https://cr.openjdk.java.net/~briangoetz/amber/datum.html) as a preview feature. Since Java 9, features in Java are being released in stages. While the Java 14 version of records is fantastic, it's currently missing an important feature for data classes: a builder. This project is an annotation processor that creates companion builder classes for Java records.
Java 15 introduced [Records](https://cr.openjdk.java.net/~briangoetz/amber/datum.html) as a preview feature. Since Java 9,
features in Java are being released in stages. While the Java 15 version of records is fantastic, it's currently missing important features
for data classes: a builder and "with"ers. This project is an annotation processor that creates:
- a companion builder class for Java records
- an interface that adds "with" copy methods
- an annotation that generates a Java record from an Interface template
## Example
_Details:_
- [RecordBuilder Details](#RecordBuilder-Example)
- [Wither Details](#Wither-Example)
- [RecordBuilder Full Definition](#Builder-Class-Definition)
- [Record From Interface Details](#RecordInterface-Example)
## RecordBuilder Example
```java
@RecordBuilder
@@ -27,6 +43,31 @@ setAge(builder);
var n3 = builder.build();
```
## Wither Example
```java
@RecordBuilder
public record NameAndAge(String name, int age) implements NameAndAgeBuilder.With {}
```
In addition to creating a builder, your record is enhanced by "wither" methods ala:
```java
var r1 = new NameAndAge("foo", 123);
var r2 = r1.withName("bar");
var r3 = r2.withAge(456);
// access the builder as well
var r4 = r3.with().age(101).name("baz").build();
// alternate method of accessing the builder (note: no need to call "build()")
var r5 = r4.with(b -> b.age(200).name("whatever"));
```
_Hat tip to [Benji Weber](https://benjiweber.co.uk/blog/2020/09/19/fun-with-java-records/) for the Withers idea._
## Builder Class Definition
The full builder class is defined as:
```java
@@ -65,7 +106,7 @@ public class NameAndAgeBuilder {
}
/**
* Set a new value for this record component in the builder
* Set a new value for the {@code name} record component in the builder
*/
public NameAndAgeBuilder name(String name) {
this.name = name;
@@ -73,13 +114,35 @@ public class NameAndAgeBuilder {
}
/**
* Set a new value for this record component in the builder
* Return the current value for the {@code name} record component in the builder
*/
public String name() {
return name;
}
/**
* Set a new value for the {@code age} record component in the builder
*/
public NameAndAgeBuilder age(int age) {
this.age = age;
return this;
}
/**
* Return the current value for the {@code age} record component in the builder
*/
public int age() {
return age;
}
/**
* Return a stream of the record components as map entries keyed with the component name and the value as the component value
*/
public static Stream<Map.Entry<String, Object>> stream(NameAndAge record) {
return Stream.of(new AbstractMap.SimpleEntry<>("name", record.name()),
new AbstractMap.SimpleEntry<>("age", record.age()));
}
@Override
public String toString() {
return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
@@ -96,9 +159,103 @@ public class NameAndAgeBuilder {
&& Objects.equals(name, b.name)
&& (age == b.age));
}
/**
* Add withers to {@code NameAndAge}
*/
public interface With {
/**
* Return a new record builder using the current values
*/
default NameAndAgeBuilder with() {
var r = (NameAndAge)(Object)this;
return NameAndAgeBuilder.builder(r);
}
/**
* Return a new record built from the builder passed to the given consumer
*/
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
var r = (NameAndAge)(Object)this;
NameAndAgeBuilder builder = NameAndAgeBuilder.builder(r);
consumer.accept(builder);
return builder.build();
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
*/
default NameAndAge withName(String name) {
var r = (NameAndAge)(Object)this;
return new NameAndAge(name, r.age());
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
*/
default NameAndAge withAge(int age) {
var r = (NameAndAge)(Object)this;
return new NameAndAge(r.name(), age);
}
}
}
```
## RecordInterface Example
```java
@RecordInterface
public interface NameAndAge {
String name();
int age();
}
```
This will generate a record ala:
```java
@RecordBuilder
public record NameAndAgeRecord(String name, int age) implements
NameAndAge, NameAndAgeRecordBuilder.With {}
```
Note that the generated record is annotated with `@RecordBuilder` so a record
builder is generated for the new record as well.
Notes:
- Non static methods in the interface...
- ...cannot have arguments
- ...must return a value
- ...cannot have type parameters
- Methods with default implementations are used in the generation unless they are annotated with `@IgnoreDefaultMethod`
- If you do not want a record builder generated, annotate your interface as `@RecordInterface(addRecordBuilder = false)`
## Generation Via Includes
An alternate method of generation is to use the Include variants of the annotations. These variants
act on lists of specified classes. This allows the source classes to be pristine or even come from
libraries where you are not able to annotate the source.
E.g.
```
import some.library.code.ImportedRecord
import some.library.code.ImportedInterface
@RecordBuilder.Include({
ImportedRecord.class // generates a record builder for ImportedRecord
})
@RecordInterface.Include({
ImportedInterface.class // generates a record interface for ImportedInterface
})
public void Placeholder {
}
```
The target package for generation is the same as the package that contains the "Include"
annotation. Use `packagePattern` to change this (see Javadoc for details).
## Usage
### Maven
@@ -135,7 +292,7 @@ public class NameAndAgeBuilder {
<!-- "release" and "enable-preview" are required while records are preview features -->
<release>14</release>
<release>15</release>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
@@ -145,6 +302,30 @@ public class NameAndAgeBuilder {
</plugin>
```
3\. Enable Preview for Maven
Create a file in your project's root named `.mvn/jvm.config`. The file should have 1 line with the value: `--enable-preview`. (see: https://stackoverflow.com/questions/58023240)
### Gradle
Add the following to your build.gradle file:
```
dependencies {
annotationProcessor 'io.soabase.record-builder:record-builder-processor:$version-goes-here'
implementation 'io.soabase.record-builder:record-builder-core:$version-goes-here'
}
tasks.withType(JavaCompile) {
options.fork = true
options.forkOptions.jvmArgs += '--enable-preview'
options.compilerArgs += '--enable-preview'
}
tasks.withType(Test) {
jvmArgs += "--enable-preview"
}
```
### IDE
Depending on your IDE you are likely to need to enable Annotation Processing in your IDE settings.
@@ -153,15 +334,30 @@ Depending on your IDE you are likely to need to enable Annotation Processing in
Note: records are a preview feature only. You'll need take a number of steps in order to try RecordBuilder:
- Install and make active Java 14 or later
- Make sure your development tool is using Java 14 or later and is configured to enable preview features (for Maven I've documented how to do this here: [https://stackoverflow.com/a/59363152/2048051](https://stackoverflow.com/a/59363152/2048051))
- Install and make active Java 15 or later
- Make sure your development tool is using Java 15 or later and is configured to enable preview features (for Maven I've documented how to do this here: [https://stackoverflow.com/a/59363152/2048051](https://stackoverflow.com/a/59363152/2048051))
- Bear in mind that this is not yet meant for production and there are numerous bugs in the tools and JDKs.
Note: I've seen some very odd compilation bugs with the current Java 14 and Maven. If you get internal Javac errors I suggest rebuilding with `mvn clean package` and/or `mvn clean install`.
Note: I've seen some very odd compilation bugs with the current Java 15 and Maven. If you get internal Javac errors I suggest rebuilding with `mvn clean package` and/or `mvn clean install`.
## TODOs
## Customizing
- Document how to integrate with Gradle
- Keep up with changes
- Testing
- Etc.
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`

21
pom.xml
View File

@@ -5,7 +5,7 @@
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<packaging>pom</packaging>
<version>1.4.ea-SNAPSHOT</version>
<version>1.11.ea</version>
<modules>
<module>record-builder-core</module>
@@ -18,7 +18,7 @@
<project.build.resourceEncoding>UTF-8</project.build.resourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<jdk-version>14</jdk-version>
<jdk-version>15</jdk-version>
<maven-compiler-plugin-version>3.8.1</maven-compiler-plugin-version>
<maven-source-plugin-version>3.2.0</maven-source-plugin-version>
@@ -30,8 +30,9 @@
<maven-clean-plugin-version>3.1.0</maven-clean-plugin-version>
<maven-shade-plugin-version>3.2.1</maven-shade-plugin-version>
<maven-release-plugin-version>2.5.3</maven-release-plugin-version>
<maven-surefire-plugin-version>3.0.0-M5</maven-surefire-plugin-version>
<javapoet-version>1.11.0</javapoet-version>
<javapoet-version>1.12.1</javapoet-version>
<junit-jupiter-version>5.5.2</junit-jupiter-version>
<asm-version>7.2</asm-version>
</properties>
@@ -70,7 +71,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>HEAD</tag>
<tag>record-builder-1.11.ea</tag>
</scm>
<issueManagement>
@@ -179,6 +180,9 @@
<exclude>**/io/soabase/com/google/**</exclude>
<exclude>**/com/company/**</exclude>
<exclude>**/META-INF/services/**</exclude>
<exclude>**/jvm.config</exclude>
<exclude>**/.java-version</exclude>
<exclude>**/.travis.yml</exclude>
</excludes>
<strictCheck>true</strictCheck>
</configuration>
@@ -268,6 +272,15 @@
<tagNameFormat>record-builder-@{project.version}</tagNameFormat>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin-version}</version>
<configuration>
<argLine>--enable-preview</argLine>
</configuration>
</plugin>
</plugins>
</pluginManagement>

View File

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

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.core;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface IgnoreDefaultMethod {
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2016 Jordan Zimmerman
* 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.
@@ -23,4 +23,19 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface RecordBuilder {
@Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE)
@interface Include {
Class<?>[] value();
/**
* Pattern used to generate the package for the generated class. The value
* is the literal package name however two replacement values can be used. '@'
* is replaced with the package of the Include annotation. '*' is replaced with
* the package of the included class.
*
* @return package pattern
*/
String packagePattern() default "@";
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2016 Jordan Zimmerman
* 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.
@@ -41,6 +41,16 @@ public interface RecordBuilderMetaData {
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
*
@@ -68,13 +78,40 @@ public interface RecordBuilderMetaData {
return "build";
}
/**
* 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 RecordBuilder: https://github.com/Randgalt/record-builder";
return "Auto generated by io.soabase.recordbuilder.core.RecordBuilder: https://github.com/Randgalt/record-builder";
}
/**

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.core;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface RecordInterface {
boolean addRecordBuilder() default true;
@Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE)
@interface Include {
Class<?>[] value();
boolean addRecordBuilder() default true;
/**
* Pattern used to generate the package for the generated class. The value
* is the literal package name however two replacement values can be used. '@'
* is replaced with the package of the Include annotation. '*' is replaced with
* the package of the included class.
*
* @return package pattern
*/
String packagePattern() default "@";
}
}

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>1.4.ea-SNAPSHOT</version>
<version>1.11.ea</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -24,30 +24,12 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<proc>none</proc>
</configuration>
</plugin>
<!--plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<artifactSet>
<includes>
<include>com.squareup:javapoet</include>
<include>io.soabase.record-builder:record-builder-processor</include>
</includes>
</artifactSet>
<relocations>
<relocation>
<pattern>com.squareup.javapoet</pattern>
<shadedPattern>io.soabase.com.squareup.javapoet</shadedPattern>
</relocation>
</relocations>
</configuration>
</plugin-->
</plugins>
</build>
</project>

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2016 Jordan Zimmerman
* 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.

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2016 Jordan Zimmerman
* 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.
@@ -19,14 +19,64 @@ 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 javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.RecordComponentElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.type.TypeMirror;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
public class ElementUtils {
public static Optional<? extends AnnotationMirror> findAnnotationMirror(ProcessingEnvironment processingEnv, Element element, String annotationClass) {
return processingEnv.getElementUtils().getAllAnnotationMirrors(element).stream()
.filter(e -> e.getAnnotationType().toString().equals(annotationClass))
.findFirst();
}
public static Optional<? extends AnnotationValue> getAnnotationValue(Map<? extends ExecutableElement, ? extends AnnotationValue> values, String name) {
return values.entrySet()
.stream()
.filter(e -> e.getKey().getSimpleName().toString().equals(name))
.map(Map.Entry::getValue)
.findFirst();
}
@SuppressWarnings("unchecked")
public static List<TypeMirror> getClassesAttribute(AnnotationValue attribute)
{
List<? extends AnnotationValue> values = (attribute != null) ? (List<? extends AnnotationValue>)attribute.getValue() : Collections.emptyList();
return values.stream().map(v -> (TypeMirror)v.getValue()).collect(Collectors.toList());
}
public static boolean getBooleanAttribute(AnnotationValue attribute)
{
Object value = (attribute != null) ? attribute.getValue() : null;
if ( value != null )
{
return Boolean.parseBoolean(String.valueOf(value));
}
return false;
}
public static String getStringAttribute(AnnotationValue attribute, String defaultValue)
{
Object value = (attribute != null) ? attribute.getValue() : null;
if ( value != null )
{
return String.valueOf(value);
}
return defaultValue;
}
public static String getPackageName(TypeElement typeElement) {
while (typeElement.getNestingKind().isNested()) {
Element enclosingElement = typeElement.getEnclosingElement();
@@ -60,6 +110,28 @@ public class ElementUtils {
return new ClassType(TypeName.get(recordComponent.asType()), recordComponent.getSimpleName().toString());
}
public static String getWithMethodName(ClassType component, String prefix) {
var name = component.name();
if (name.length() == 1) {
return prefix + name.toUpperCase();
}
return prefix + Character.toUpperCase(name.charAt(0)) + name.substring(1);
}
public static String getBuilderName(TypeElement element, RecordBuilderMetaData metaData, ClassType classType, String suffix) {
// generate the class name
var baseName = classType.name() + suffix;
return metaData.prefixEnclosingClassNames() ? (getBuilderNamePrefix(element.getEnclosingElement()) + baseName) : baseName;
}
private static String getBuilderNamePrefix(Element element) {
// prefix enclosing class names if nested in a class
if (element instanceof TypeElement) {
return getBuilderNamePrefix(element.getEnclosingElement()) + element.getSimpleName().toString();
}
return "";
}
private ElementUtils() {
}
}

View File

@@ -0,0 +1,516 @@
/**
* 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.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
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;
private final ClassType recordClassType;
private final String packageName;
private final ClassType builderClassType;
private final List<TypeVariableName> typeVariables;
private final List<ClassType> recordComponents;
private final TypeSpec builderType;
private final TypeSpec.Builder builder;
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData, Optional<String> packageNameOpt)
{
this.metaData = metaData;
recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(record));
builderClassType = ElementUtils.getClassType(packageName, getBuilderName(record, metaData, recordClassType, metaData.suffix()), record.getTypeParameters());
typeVariables = record.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList());
recordComponents = record.getRecordComponents().stream().map(ElementUtils::getClassType).collect(Collectors.toList());
builder = TypeSpec.classBuilder(builderClassType.name())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables);
addWithNestedClass();
addDefaultConstructor();
addAllArgsConstructor();
addStaticDefaultBuilderMethod();
addStaticCopyBuilderMethod();
addStaticComponentsMethod();
addBuildMethod();
addToStringMethod();
addHashCodeMethod();
addEqualsMethod();
recordComponents.forEach(component -> {
add1Field(component);
add1SetterMethod(component);
add1GetterMethod(component);
});
builderType = builder.build();
}
String packageName()
{
return packageName;
}
ClassType builderClassType()
{
return builderClassType;
}
TypeSpec builderType()
{
return builderType;
}
private void addWithNestedClass()
{
/*
Adds a nested interface that adds withers similar to:
public class MyRecordBuilder {
public interface With {
// with methods
}
}
*/
var classBuilder = TypeSpec.interfaceBuilder(metaData.withClassName())
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PUBLIC)
.addTypeVariables(typeVariables);
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)
{
/*
Adds a method that returns a pre-filled copy builder similar to:
default MyRecord with(Consumer<MyRecordBuilder> consumer) {
MyRecord r = (MyRecord)(Object)this;
MyRecordBuilder builder MyRecordBuilder.builder(r);
consumer.accept(builder);
return builder.build();
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
.add("$T builder = $L.$L(r);\n", builderClassType.typeName(), builderClassType.name(), metaData.copyMethodName())
.add("consumer.accept(builder);\n")
.add("return builder.build();\n");
var consumerType = ParameterizedTypeName.get(ClassName.get(Consumer.class), builderClassType.typeName());
var parameter = ParameterSpec.builder(consumerType, "consumer").build();
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Return a new record built from the builder passed to the given consumer")
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addParameter(parameter)
.returns(recordClassType.typeName())
.addCode(codeBlockBuilder.build())
.build();
classBuilder.addMethod(methodSpec);
}
private void addWithBuilderMethod(TypeSpec.Builder classBuilder)
{
/*
Adds a method that returns a pre-filled copy builder similar to:
default MyRecordBuilder with() {
MyRecord r = (MyRecord)(Object)this;
return MyRecordBuilder.builder(r);
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
.add("return $L.$L(r);", builderClassType.name(), metaData.copyMethodName());
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Return a new record builder using the current values")
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.returns(builderClassType.typeName())
.addCode(codeBlockBuilder.build())
.build();
classBuilder.addMethod(methodSpec);
}
private void add1WithMethod(TypeSpec.Builder classBuilder, ClassType component, int index)
{
/*
Adds a with method for the component similar to:
default MyRecord withName(String name) {
MyRecord r = (MyRecord)(Object)this;
return new MyRecord(name, r.age());
}
*/
var codeBlockBuilder = CodeBlock.builder()
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
.add("return new $T(", recordClassType.typeName());
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("r.$L()", parameterComponent.name());
}
});
codeBlockBuilder.add(");");
var methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
var methodSpec = MethodSpec.methodBuilder(methodName)
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Return a new instance of {@code $L} with a new value for {@code $L}\n", recordClassType.name(), component.name())
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addParameter(parameterSpec)
.addCode(codeBlockBuilder.build())
.returns(recordClassType.typeName())
.build();
classBuilder.addMethod(methodSpec);
}
private void addDefaultConstructor()
{
/*
Adds a default constructor similar to:
private MyRecordBuilder() {
}
*/
var constructor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PRIVATE)
.addAnnotation(generatedRecordBuilderAnnotation)
.build();
builder.addMethod(constructor);
}
private void addAllArgsConstructor()
{
/*
Adds an all-args constructor similar to:
private MyRecordBuilder(int p1, T p2, ...) {
this.p1 = p1;
this.p2 = p2;
...
}
*/
var constructorBuilder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PRIVATE)
.addAnnotation(generatedRecordBuilderAnnotation);
recordComponents.forEach(component -> {
constructorBuilder.addParameter(component.typeName(), component.name());
var codeBuilder = CodeBlock.builder().add("this.$L = $L", component.name(), component.name());
constructorBuilder.addStatement(codeBuilder.build());
});
builder.addMethod(constructorBuilder.build());
}
private void addToStringMethod()
{
/*
add a toString() method similar to:
@Override
public String toString() {
return "MyRecord[p1=blah, p2=blah]";
}
*/
var codeBuilder = CodeBlock.builder().add("return \"$L[", builderClassType.name());
IntStream.range(0, recordComponents.size()).forEach(index -> {
if (index > 0) {
codeBuilder.add(", ");
}
String name = recordComponents.get(index).name();
codeBuilder.add("$L=\" + $L + \"", name, name);
});
codeBuilder.add("]\"");
var methodSpec = MethodSpec.methodBuilder("toString")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addAnnotation(Override.class)
.returns(String.class)
.addStatement(codeBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void addHashCodeMethod()
{
/*
add a hashCode() method similar to:
@Override
public int hashCode() {
return Objects.hash(p1, p2);
}
*/
var codeBuilder = CodeBlock.builder().add("return $T.hash(", Objects.class);
IntStream.range(0, recordComponents.size()).forEach(index -> {
if (index > 0) {
codeBuilder.add(", ");
}
codeBuilder.add("$L", recordComponents.get(index).name());
});
codeBuilder.add(")");
var methodSpec = MethodSpec.methodBuilder("hashCode")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addAnnotation(Override.class)
.returns(TypeName.INT)
.addStatement(codeBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void addEqualsMethod()
{
/*
add an equals() method similar to:
@Override
public boolean equals(Object o) {
if (this == o) || ((o instanceof MyRecordBuilder b)
&& Objects.equals(p1, b.p1)
&& Objects.equals(p2, b.p2));
}
*/
var codeBuilder = CodeBlock.builder();
codeBuilder.add("return (this == o) || (");
codeBuilder.add("(o instanceof $L b)", builderClassType.name());
recordComponents.forEach(recordComponent -> {
String name = recordComponent.name();
if (recordComponent.typeName().isPrimitive()) {
codeBuilder.add("\n&& ($L == b.$L)", name, name);
}
else {
codeBuilder.add("\n&& $T.equals($L, b.$L)", Objects.class, name, name);
}
});
codeBuilder.add(")");
var methodSpec = MethodSpec.methodBuilder("equals")
.addParameter(Object.class, "o")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addAnnotation(Override.class)
.returns(TypeName.BOOLEAN)
.addStatement(codeBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void addBuildMethod()
{
/*
Adds the build method that generates the record similar to:
public MyRecord build() {
return new MyRecord(p1, p2, ...);
}
*/
var codeBuilder = CodeBlock.builder().add("return new $T(", recordClassType.typeName());
IntStream.range(0, recordComponents.size()).forEach(index -> {
if (index > 0) {
codeBuilder.add(", ");
}
codeBuilder.add("$L", recordComponents.get(index).name());
});
codeBuilder.add(")");
var methodSpec = MethodSpec.methodBuilder(metaData.buildMethodName())
.addJavadoc("Return a new record instance with all fields set to the current values in this builder\n")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(recordClassType.typeName())
.addStatement(codeBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void addStaticCopyBuilderMethod()
{
/*
Adds a copy builder method that pre-fills the builder with existing values similar to:
public static MyRecordBuilder builder(MyRecord from) {
return new MyRecordBuilder(from.p1(), from.p2(), ...);
}
*/
var codeBuilder = CodeBlock.builder().add("return new $T(", builderClassType.typeName());
IntStream.range(0, recordComponents.size()).forEach(index -> {
if (index > 0) {
codeBuilder.add(", ");
}
codeBuilder.add("from.$L()", recordComponents.get(index).name());
});
codeBuilder.add(")");
var methodSpec = MethodSpec.methodBuilder(metaData.copyMethodName())
.addJavadoc("Return a new builder with all fields set to the values taken from the given record instance\n")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables)
.addParameter(recordClassType.typeName(), "from")
.returns(builderClassType.typeName())
.addStatement(codeBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void addStaticDefaultBuilderMethod()
{
/*
Adds a the default builder method similar to:
public static MyRecordBuilder builder() {
return new MyRecordBuilder();
}
*/
var methodSpec = MethodSpec.methodBuilder(metaData.builderMethodName())
.addJavadoc("Return a new builder with all fields set to default Java values\n")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables)
.returns(builderClassType.typeName())
.addStatement("return new $T()", builderClassType.typeName())
.build();
builder.addMethod(methodSpec);
}
private void addStaticComponentsMethod()
{
/*
Adds a static method that converts a record instance into a stream of its component parts
public static Stream<Map.Entry<String, Object>> stream(MyRecord record) {
return Stream.of(
new AbstractMap.SimpleEntry<>("p1", record.p1()),
new AbstractMap.SimpleEntry<>("p2", record.p2())
);
}
*/
var codeBuilder = CodeBlock.builder().add("return $T.of(", Stream.class);
IntStream.range(0, recordComponents.size()).forEach(index -> {
if (index > 0) {
codeBuilder.add(",\n ");
}
var name = recordComponents.get(index).name();
codeBuilder.add("new $T<>($S, record.$L())", AbstractMap.SimpleEntry.class, name, name);
});
codeBuilder.add(")");
var mapEntryTypeVariables = ParameterizedTypeName.get(Map.Entry.class, String.class, Object.class);
var mapEntryType = ParameterizedTypeName.get(ClassName.get(Stream.class), mapEntryTypeVariables);
var methodSpec = MethodSpec.methodBuilder(metaData.componentsMethodName())
.addJavadoc("Return a stream of the record components as map entries keyed with the component name and the value as the component value\n")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(recordClassType.typeName(), "record")
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables)
.returns(mapEntryType)
.addStatement(codeBuilder.build())
.build();
builder.addMethod(methodSpec);
}
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);
}
private void add1GetterMethod(ClassType component)
{
/*
For a single record component, add a getter similar to:
public T p() {
return p;
}
*/
var methodSpec = MethodSpec.methodBuilder(component.name())
.addJavadoc("Return the current value for the {@code $L} record component in the builder\n", component.name())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.returns(component.typeName())
.addStatement("return $L", component.name())
.build();
builder.addMethod(methodSpec);
}
private void add1SetterMethod(ClassType component)
{
/*
For a single record component, add a setter similar to:
public MyRecordBuilder p(T p) {
this.p = p;
return this;
}
*/
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
var methodSpec = MethodSpec.methodBuilder(component.name())
.addJavadoc("Set a new value for the {@code $L} record component in the builder\n", component.name())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addParameter(parameterSpec)
.returns(builderClassType.typeName())
.addStatement("this.$L = $L", component.name(), component.name())
.addStatement("return this")
.build();
builder.addMethod(methodSpec);
}
}

View File

@@ -0,0 +1,193 @@
/**
* 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.ClassName;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.IgnoreDefaultMethod;
import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.tools.Diagnostic;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordInterfaceAnnotation;
class InternalRecordInterfaceProcessor {
private final ProcessingEnvironment processingEnv;
private final String packageName;
private final TypeSpec recordType;
private final List<ExecutableElement> recordComponents;
private final TypeElement iface;
private final ClassType recordClassType;
private static final String FAKE_METHOD_NAME = "__FAKE__";
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageNameOpt) {
this.processingEnv = processingEnv;
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(iface));
recordComponents = getRecordComponents(iface);
this.iface = iface;
ClassType ifaceClassType = ElementUtils.getClassType(iface, iface.getTypeParameters());
recordClassType = ElementUtils.getClassType(packageName, getBuilderName(iface, metaData, ifaceClassType, metaData.interfaceSuffix()), iface.getTypeParameters());
List<TypeVariableName> typeVariables = iface.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList());
MethodSpec methodSpec = generateArgumentList();
TypeSpec.Builder builder = TypeSpec.classBuilder(recordClassType.name())
.addSuperinterface(iface.asType())
.addMethod(methodSpec)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordInterfaceAnnotation)
.addTypeVariables(typeVariables);
if (addRecordBuilder) {
ClassType builderClassType = ElementUtils.getClassType(packageName, getBuilderName(iface, metaData, recordClassType, metaData.suffix()) + "." + metaData.withClassName(), iface.getTypeParameters());
builder.addAnnotation(RecordBuilder.class);
builder.addSuperinterface(builderClassType.typeName());
}
recordType = builder.build();
}
boolean isValid()
{
return !recordComponents.isEmpty();
}
TypeSpec recordType() {
return recordType;
}
String packageName() {
return packageName;
}
ClassType recordClassType() {
return recordClassType;
}
String toRecord(String classSource)
{
// javapoet does yet support records - so a class was created and we can reshape it
// The class will look something like this:
/*
// Auto generated by io.soabase.recordbuilder.core.RecordBuilder: https://github.com/Randgalt/record-builder
package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.annotation.processing.Generated;
@Generated("io.soabase.recordbuilder.core.RecordInterface")
@RecordBuilder
public class MyRecord implements MyInterface {
void __FAKE__(String name, int age) {
}
}
*/
Pattern pattern = Pattern.compile("(.*)(implements.*)(\\{)(.*" + FAKE_METHOD_NAME + ")(\\(.*\\))(.*)", Pattern.MULTILINE | Pattern.DOTALL);
Matcher matcher = pattern.matcher(classSource);
if (!matcher.find() || matcher.groupCount() != 6) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Internal error generating record. Group count: " + matcher.groupCount(), iface);
}
String declaration = matcher.group(1).trim().replace("class", "record");
String implementsSection = matcher.group(2).trim();
String argumentList = matcher.group(5).trim();
return declaration + argumentList + " " + implementsSection + " {}";
}
private MethodSpec generateArgumentList()
{
MethodSpec.Builder builder = MethodSpec.methodBuilder(FAKE_METHOD_NAME);
recordComponents.forEach(element -> {
ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(element.getReturnType()), element.getSimpleName().toString()).build();
builder.addTypeVariables(element.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList()));
builder.addParameter(parameterSpec);
});
return builder.build();
}
private List<ExecutableElement> getRecordComponents(TypeElement iface) {
List<ExecutableElement> components = new ArrayList<>();
try {
getRecordComponents(iface, components, new HashSet<>(), new HashSet<>());
if (components.isEmpty()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Annotated interface has no component methods", iface);
}
} catch (IllegalInterface e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage(), iface);
components = Collections.emptyList();
}
return components;
}
private static class IllegalInterface extends RuntimeException
{
public IllegalInterface(String message) {
super(message);
}
}
private void getRecordComponents(TypeElement iface, Collection<? super ExecutableElement> components, Set<String> visitedSet, Set<String> usedNames) {
if (!visitedSet.add(iface.getQualifiedName().toString())) {
return;
}
iface.getEnclosedElements().stream()
.filter(element -> (element.getKind() == ElementKind.METHOD) && !(element.getModifiers().contains(Modifier.STATIC)))
.map(element -> ((ExecutableElement) element))
.filter(element -> {
if (element.isDefault()) {
return element.getAnnotation(IgnoreDefaultMethod.class) == null;
}
return true;
})
.peek(element -> {
if (!element.getParameters().isEmpty() || element.getReturnType().getKind() == TypeKind.VOID) {
throw new IllegalInterface(String.format("Non-static, non-default methods must take no arguments and must return a value. Bad method: %s.%s()", iface.getSimpleName(), element.getSimpleName()));
}
if (!element.getTypeParameters().isEmpty()) {
throw new IllegalInterface(String.format("Interface methods cannot have type parameters. Bad method: %s.%s()", iface.getSimpleName(), element.getSimpleName()));
}
})
.filter(element -> usedNames.add(element.getSimpleName().toString()))
.collect(Collectors.toCollection(() -> components));
iface.getInterfaces().forEach(parentIface -> {
TypeElement parentIfaceElement = (TypeElement) processingEnv.getTypeUtils().asElement(parentIface);
getRecordComponents(parentIfaceElement, components, visitedSet, usedNames);
});
}
}

View File

@@ -0,0 +1,163 @@
/**
* 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 #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 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());
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 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,5 +1,5 @@
/**
* Copyright 2016 Jordan Zimmerman
* 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.
@@ -17,23 +17,34 @@ 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(String metaDataClassName, Consumer<String> logger) {
RecordBuilderMetaData localMetaData = null;
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);
localMetaData = (RecordBuilderMetaData) clazz.getDeclaredConstructor().newInstance();
logger.accept("Found meta data: " + localMetaData.getClass());
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.getMessage());
logger.accept("Could not load meta data: " + metaDataClassName + " - " + e);
}
metaData = (loadedMetaData != null) ? loadedMetaData : RecordBuilderMetaData.DEFAULT;
} else {
metaData = new OptionBasedRecordBuilderMetaData(options);
}
metaData = (localMetaData != null) ? localMetaData : RecordBuilderMetaData.DEFAULT;
}
RecordBuilderMetaData getMetaData() {

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2016 Jordan Zimmerman
* 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.
@@ -15,334 +15,218 @@
*/
package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.*;
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 javax.annotation.processing.*;
import io.soabase.recordbuilder.core.RecordInterface;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Generated;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.function.Function;
@SupportedAnnotationTypes("io.soabase.recordbuilder.core.RecordBuilder")
@SupportedSourceVersion(SourceVersion.RELEASE_14)
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();
private static final String RECORD_INTERFACE_INCLUDE = RecordInterface.Include.class.getName().replace('$', '.');
static final AnnotationSpec generatedRecordBuilderAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RecordBuilder.class.getName()).build();
static final AnnotationSpec generatedRecordInterfaceAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RecordInterface.class.getName()).build();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation).forEach(this::process));
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> process(annotation, element)));
return true;
}
private void process(Element element) {
var messager = processingEnv.getMessager();
if (element.getKind() != ElementKind.RECORD) {
messager.printMessage(Diagnostic.Kind.ERROR, "RecordBuilder only valid for records.", element);
@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of(RECORD_BUILDER, RECORD_BUILDER_INCLUDE, RECORD_INTERFACE, RECORD_INTERFACE_INCLUDE);
}
@Override
public SourceVersion getSupportedSourceVersion() {
// we don't directly return RELEASE_14 as that may
// not exist in prior releases
// if we're running on an older release, returning latest()
// is fine as we won't encounter any records anyway
return SourceVersion.latest();
}
private void process(TypeElement annotation, Element element) {
var metaData = new RecordBuilderMetaDataLoader(processingEnv, s -> processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, s)).getMetaData();
String annotationClass = annotation.getQualifiedName().toString();
if ( annotationClass.equals(RECORD_BUILDER) )
{
processRecordBuilder((TypeElement)element, metaData, Optional.empty());
}
else if ( annotationClass.equals(RECORD_INTERFACE) )
{
processRecordInterface((TypeElement)element, element.getAnnotation(RecordInterface.class).addRecordBuilder(), metaData, Optional.empty());
}
else if ( annotationClass.equals(RECORD_BUILDER_INCLUDE) || annotationClass.equals(RECORD_INTERFACE_INCLUDE) )
{
processIncludes(element, metaData, annotationClass);
}
else
{
throw new RuntimeException("Unknown annotation: " + annotation);
}
}
private void processIncludes(Element element, RecordBuilderMetaData metaData, String annotationClass) {
var annotationMirrorOpt = ElementUtils.findAnnotationMirror(processingEnv, element, annotationClass);
if ( annotationMirrorOpt.isEmpty() )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get annotation mirror for: " + annotationClass, element);
}
else
{
var values = processingEnv.getElementUtils().getElementValuesWithDefaults(annotationMirrorOpt.get());
var classes = ElementUtils.getAnnotationValue(values, "value");
if ( classes.isEmpty() )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get annotation value for: " + annotationClass, element);
}
else
{
var packagePattern = ElementUtils.getStringAttribute(ElementUtils.getAnnotationValue(values, "packagePattern").orElse(null), "*");
var classesMirrors = ElementUtils.getClassesAttribute(classes.get());
for ( TypeMirror mirror : classesMirrors )
{
TypeElement typeElement = (TypeElement)processingEnv.getTypeUtils().asElement(mirror);
if ( typeElement == null )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get element for: " + mirror, element);
}
else
{
var packageName = buildPackageName(packagePattern, element, typeElement);
if ( annotationClass.equals(RECORD_INTERFACE_INCLUDE) )
{
var addRecordBuilderOpt = ElementUtils.getAnnotationValue(values, "addRecordBuilder");
var addRecordBuilder = addRecordBuilderOpt.map(ElementUtils::getBooleanAttribute).orElse(true);
processRecordInterface(typeElement, addRecordBuilder, metaData, Optional.of(packageName));
}
else
{
processRecordBuilder(typeElement, metaData, Optional.of(packageName));
}
}
}
}
}
}
private String buildPackageName(String packagePattern, Element builderElement, TypeElement includedClass) {
String replaced = packagePattern.replace("*", ((PackageElement)includedClass.getEnclosingElement()).getQualifiedName().toString());
if (builderElement instanceof PackageElement) {
return replaced.replace("@", ((PackageElement)builderElement).getQualifiedName().toString());
}
return replaced.replace("@", ((PackageElement)builderElement.getEnclosingElement()).getQualifiedName().toString());
}
private void processRecordInterface(TypeElement element, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageName) {
if ( !element.getKind().isInterface() )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordInterface only valid for interfaces.", element);
return;
}
var metaData = new RecordBuilderMetaDataLoader(processingEnv.getOptions().get(RecordBuilderMetaData.JAVAC_OPTION_NAME), s -> messager.printMessage(Diagnostic.Kind.NOTE, s)).getMetaData();
process((TypeElement) element, metaData);
}
private void process(TypeElement record, RecordBuilderMetaData metaData) {
var recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
var packageName = ElementUtils.getPackageName(record);
var builderClassType = ElementUtils.getClassType(packageName, getBuilderName(record, metaData, recordClassType), record.getTypeParameters());
var typeVariables = record.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList());
var recordComponents = record.getRecordComponents().stream().map(ElementUtils::getClassType).collect(Collectors.toList());
var builder = TypeSpec.classBuilder(builderClassType.name()).addModifiers(Modifier.PUBLIC);
builder.addTypeVariables(typeVariables);
addDefaultConstructor(builder);
addAllArgsConstructor(builder, recordComponents);
addStaticDefaultBuilderMethod(builder, builderClassType, typeVariables, metaData);
addStaticCopyMethod(builder, builderClassType, recordClassType, recordComponents, typeVariables, metaData);
addBuildMethod(builder, recordClassType, recordComponents, metaData);
addToStringMethod(builder, builderClassType, recordComponents);
addHashCodeMethod(builder, recordComponents);
addEqualsMethod(builder, builderClassType, recordComponents);
recordComponents.forEach(component -> {
add1Field(builder, component);
add1SetterMethod(builder, component, builderClassType);
});
writeJavaFile(record, packageName, builderClassType, builder, metaData);
}
private String getBuilderName(TypeElement record, RecordBuilderMetaData metaData, ClassType recordClassType) {
// generate the record builder class name
var baseName = recordClassType.name() + metaData.suffix();
return metaData.prefixEnclosingClassNames() ? (getBuilderNamePrefix(record.getEnclosingElement()) + baseName) : baseName;
}
private String getBuilderNamePrefix(Element element) {
// prefix enclosing class names if this record is nested in a class
if (element instanceof TypeElement) {
return getBuilderNamePrefix(element.getEnclosingElement()) + element.getSimpleName().toString();
var internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, addRecordBuilder, metaData, packageName);
if ( !internalProcessor.isValid() )
{
return;
}
return "";
writeRecordInterfaceJavaFile(element, internalProcessor.packageName(), internalProcessor.recordClassType(), internalProcessor.recordType(), metaData, internalProcessor::toRecord);
}
private void addDefaultConstructor(TypeSpec.Builder builder) {
/*
Adds a default constructor similar to:
private MyRecordBuilder() {
}
*/
var constructorBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE);
builder.addMethod(constructorBuilder.build());
private void processRecordBuilder(TypeElement record, RecordBuilderMetaData metaData, Optional<String> packageName) {
// we use string based name comparison for the element kind,
// as the ElementKind.RECORD enum doesn't exist on JRE releases
// older than Java 14, and we don't want to throw unexpected
// NoSuchFieldErrors
if ( !"RECORD".equals(record.getKind().name()) )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordBuilder only valid for records.", record);
return;
}
var internalProcessor = new InternalRecordBuilderProcessor(record, metaData, packageName);
writeRecordBuilderJavaFile(record, internalProcessor.packageName(), internalProcessor.builderClassType(), internalProcessor.builderType(), metaData);
}
private void addAllArgsConstructor(TypeSpec.Builder builder, List<ClassType> recordComponents) {
/*
Adds an all-args constructor similar to:
private MyRecordBuilder(int p1, T p2, ...) {
this.p1 = p1;
this.p2 = p2;
...
}
*/
var constructorBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE);
recordComponents.forEach(component -> {
constructorBuilder.addParameter(component.typeName(), component.name());
var codeBuilder = CodeBlock.builder().add("this.$L = $L", component.name(), component.name());
constructorBuilder.addStatement(codeBuilder.build());
});
builder.addMethod(constructorBuilder.build());
}
private void addToStringMethod(TypeSpec.Builder builder, ClassType builderClassType, List<ClassType> recordComponents) {
/*
add a toString() method similar to:
@Override
public String toString() {
return "MyRecord[p1=blah, p2=blah]";
}
*/
var codeBuilder = CodeBlock.builder().add("return \"$L[", builderClassType.name());
IntStream.range(0, recordComponents.size()).forEach(index -> {
if (index > 0) {
codeBuilder.add(", ");
}
String name = recordComponents.get(index).name();
codeBuilder.add("$L=\" + $L + \"", name, name);
});
codeBuilder.add("]\"");
var methodSpec = MethodSpec.methodBuilder("toString")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(String.class)
.addStatement(codeBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void addHashCodeMethod(TypeSpec.Builder builder, List<ClassType> recordComponents) {
/*
add an hashCode() method similar to:
@Override
public int hashCode() {
return Objects.hash(p1, p2);
}
*/
var codeBuilder = CodeBlock.builder().add("return $T.hash(", Objects.class);
IntStream.range(0, recordComponents.size()).forEach(index -> {
if (index > 0) {
codeBuilder.add(", ");
}
codeBuilder.add("$L", recordComponents.get(index).name());
});
codeBuilder.add(")");
var methodSpec = MethodSpec.methodBuilder("hashCode")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(TypeName.INT)
.addStatement(codeBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void addEqualsMethod(TypeSpec.Builder builder, ClassType builderClassType, List<ClassType> recordComponents) {
/*
add an equals() method similar to:
@Override
public boolean equals(Object o) {
if (this == o) return true;
return (o instanceof MyRecordBuilder b)
&& Objects.equals(p1, b.p1)
&& Objects.equals(p2, b.p2);
}
*/
var codeBuilder = CodeBlock.builder();
codeBuilder.add("return (this == o) || (");
codeBuilder.add("(o instanceof $L b)", builderClassType.name());
recordComponents.forEach(recordComponent -> {
String name = recordComponent.name();
if (recordComponent.typeName().isPrimitive()) {
codeBuilder.add("\n&& ($L == b.$L)", name, name);
} else {
codeBuilder.add("\n&& $T.equals($L, b.$L)", Objects.class, name, name);
}
});
codeBuilder.add(")");
var methodSpec = MethodSpec.methodBuilder("equals")
.addParameter(Object.class, "o")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(TypeName.BOOLEAN)
.addStatement(codeBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void addBuildMethod(TypeSpec.Builder builder, ClassType recordClassType, List<ClassType> recordComponents, RecordBuilderMetaData metaData) {
/*
Adds the build method that generates the record similar to:
public MyRecord build() {
return new MyRecord(p1, p2, ...);
}
*/
var codeBuilder = CodeBlock.builder().add("return new $T(", recordClassType.typeName());
IntStream.range(0, recordComponents.size()).forEach(index -> {
if (index > 0) {
codeBuilder.add(", ");
}
codeBuilder.add("$L", recordComponents.get(index).name());
});
codeBuilder.add(")");
var methodSpec = MethodSpec.methodBuilder(metaData.buildMethodName())
.addJavadoc("Return a new record instance with all fields set to the current values in this builder\n")
.addModifiers(Modifier.PUBLIC)
.returns(recordClassType.typeName())
.addStatement(codeBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void addStaticCopyMethod(TypeSpec.Builder builder, ClassType builderClassType, ClassType recordClassType, List<ClassType> recordComponents, List<TypeVariableName> typeVariables, RecordBuilderMetaData metaData) {
/*
Adds a copy builder method that pre-fills the builder with existing values similar to:
public static MyRecordBuilder builder(MyRecord from) {
return new MyRecordBuilder(from.p1(), from.p2(), ...);
}
*/
var codeBuilder = CodeBlock.builder().add("return new $T(", builderClassType.typeName());
IntStream.range(0, recordComponents.size()).forEach(index -> {
if (index > 0) {
codeBuilder.add(", ");
}
codeBuilder.add("from.$L()", recordComponents.get(index).name());
});
codeBuilder.add(")");
var methodSpec = MethodSpec.methodBuilder(metaData.copyMethodName())
.addJavadoc("Return a new builder with all fields set to the values taken from the given record instance\n")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addTypeVariables(typeVariables)
.addParameter(recordClassType.typeName(), "from")
.returns(builderClassType.typeName())
.addStatement(codeBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void addStaticDefaultBuilderMethod(TypeSpec.Builder builder, ClassType builderClassType, List<TypeVariableName> typeVariables, RecordBuilderMetaData metaData) {
/*
Adds a the default builder method similar to:
public static MyRecordBuilder builder() {
return new MyRecordBuilder();
}
*/
var methodSpec = MethodSpec.methodBuilder(metaData.builderMethodName())
.addJavadoc("Return a new builder with all fields set to default Java values\n")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addTypeVariables(typeVariables)
.returns(builderClassType.typeName())
.addStatement("return new $T()", builderClassType.typeName())
.build();
builder.addMethod(methodSpec);
}
private void add1Field(TypeSpec.Builder builder, 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);
}
private void add1SetterMethod(TypeSpec.Builder builder, ClassType component, ClassType builderClassType) {
/*
For a single record component, add a setter similar to:
public MyRecordBuilder p(T p) {
this.p = p;
return this;
}
*/
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
var methodSpec = MethodSpec.methodBuilder(component.name())
.addJavadoc("Set a new value for this record component in the builder\n")
.addModifiers(Modifier.PUBLIC)
.addParameter(parameterSpec)
.returns(builderClassType.typeName())
.addStatement("this.$L = $L", component.name(), component.name())
.addStatement("return this")
.build();
builder.addMethod(methodSpec);
}
private void writeJavaFile(TypeElement record, String packageName, ClassType builderClassType, TypeSpec.Builder builder, RecordBuilderMetaData metaData) {
private void writeRecordBuilderJavaFile(TypeElement record, String packageName, ClassType builderClassType, TypeSpec builderType, RecordBuilderMetaData metaData) {
// produces the Java file
var javaFileBuilder = JavaFile.builder(packageName, builder.build())
.indent(metaData.fileIndent());
var comment = metaData.fileComment();
if ((comment != null) && !comment.isEmpty()) {
javaFileBuilder.addFileComment(comment);
}
JavaFile javaFile = javaFileBuilder.build();
JavaFile javaFile = javaFileBuilder(packageName, builderType, metaData);
Filer filer = processingEnv.getFiler();
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 )
{
String message = "Could not create source file";
if ( e.getMessage() != null )
{
message = message + ": " + e.getMessage();
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, record);
handleWriteError(record, e);
}
}
private void writeRecordInterfaceJavaFile(TypeElement element, String packageName, ClassType classType, TypeSpec type, RecordBuilderMetaData metaData, Function<String, String> toRecordProc) {
JavaFile javaFile = javaFileBuilder(packageName, type, metaData);
String classSourceCode = javaFile.toString();
int generatedIndex = classSourceCode.indexOf("@Generated");
String recordSourceCode = toRecordProc.apply(classSourceCode);
Filer filer = processingEnv.getFiler();
try
{
String fullyQualifiedName = packageName.isEmpty() ? classType.name() : (packageName + "." + classType.name());
JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName);
try (Writer writer = sourceFile.openWriter())
{
writer.write(recordSourceCode);
}
}
catch ( IOException e )
{
handleWriteError(element, e);
}
}
private JavaFile javaFileBuilder(String packageName, TypeSpec type, RecordBuilderMetaData metaData) {
var javaFileBuilder = JavaFile.builder(packageName, type).skipJavaLangImports(true).indent(metaData.fileIndent());
var comment = metaData.fileComment();
if ( (comment != null) && !comment.isEmpty() )
{
javaFileBuilder.addFileComment(comment);
}
return javaFileBuilder.build();
}
private void handleWriteError(TypeElement element, IOException e) {
String message = "Could not create source file";
if ( e.getMessage() != null )
{
message = message + ": " + e.getMessage();
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, element);
}
}

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>1.4.ea-SNAPSHOT</version>
<version>1.11.ea</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -24,6 +24,11 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>

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.RecordInterface;
@RecordInterface.Include({
Thingy.class
})
public class Builder {
}

View File

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

View File

@@ -0,0 +1,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 io.soabase.recordbuilder.core.IgnoreDefaultMethod;
import io.soabase.recordbuilder.core.RecordInterface;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
@RecordInterface
public interface HasDefaults {
Instant time();
default Instant tomorrow() {
return Instant.now().plusMillis(TimeUnit.DAYS.toMillis(1));
}
@IgnoreDefaultMethod
default void complexMethod(String s1, String s2) {
// do nothing
}
}

View File

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

View File

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

View File

@@ -0,0 +1,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.RecordInterface;
@RecordInterface
public interface Person {
String name();
int age();
}

View File

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

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2016 Jordan Zimmerman
* 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.
@@ -18,5 +18,5 @@ package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder
public record SimpleGenericRecord<T>(int i, T s) {
public record SimpleGenericRecord<T>(int i, T s) implements SimpleGenericRecordBuilder.With<T> {
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2016 Jordan Zimmerman
* 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.

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2016 Jordan Zimmerman
* 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.
@@ -15,13 +15,20 @@
*/
package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.test.SimpleRecordBuilder;
public class Usage {
public static void main(String[] args) {
var hey = SimpleRecordBuilder.builder().i(10).s("hey").build();
System.out.println(hey);
var hey2 = SimpleRecordBuilder.builder(hey).i(100).build();
System.out.println(hey2);
var person = new PersonRecord("me", 42);
outputPerson(person);
var aged = PersonRecordBuilder.builder(person).age(100).build();
outputPerson(aged);
}
private static void outputPerson(Person p) {
System.out.println(p.toString());
}
}

View File

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

View File

@@ -0,0 +1,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 org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.Instant;
public class TestRecordInterface
{
@Test
public void testHasDefaults()
{
var r1 = new HasDefaultsRecord(Instant.MIN, Instant.MAX);
var r2 = r1.with(b -> b.tomorrow(Instant.MIN));
Assertions.assertEquals(Instant.MIN, r1.time());
Assertions.assertEquals(Instant.MAX, r1.tomorrow());
Assertions.assertEquals(Instant.MIN, r2.time());
Assertions.assertEquals(Instant.MIN, r2.tomorrow());
}
}

View File

@@ -0,0 +1,62 @@
/**
* 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.List;
class TestWithers {
@Test
void testWithers() {
var r1 = new SimpleGenericRecord<>(10, List.of("1", "2", "3"));
var r2 = r1.withS(List.of("4", "5"));
var r3 = r2.withI(20);
Assertions.assertEquals(10, r1.i());
Assertions.assertEquals(List.of("1", "2", "3"), r1.s());
Assertions.assertEquals(10, r2.i());
Assertions.assertEquals(List.of("4", "5"), r2.s());
Assertions.assertEquals(20, r3.i());
Assertions.assertEquals(List.of("4", "5"), r2.s());
}
@Test
void testWitherBuilder() {
var r1 = new SimpleGenericRecord<>(10, "ten");
var r2 = r1.with().i(20).s("twenty").build();
var r3 = r2.with().s("changed");
Assertions.assertEquals(10, r1.i());
Assertions.assertEquals("ten", r1.s());
Assertions.assertEquals(20, r2.i());
Assertions.assertEquals("twenty", r2.s());
Assertions.assertEquals(20, r3.i());
Assertions.assertEquals("changed", r3.s());
}
@Test
void testWitherBuilderConsumer() {
var r1 = new SimpleGenericRecord<>(10, "ten");
var r2 = r1.with(r -> r.i(15));
var r3 = r1.with(r -> r.s("twenty").i(20));
Assertions.assertEquals(10, r1.i());
Assertions.assertEquals("ten", r1.s());
Assertions.assertEquals(15, r2.i());
Assertions.assertEquals("ten", r2.s());
Assertions.assertEquals(20, r3.i());
Assertions.assertEquals("twenty", r3.s());
}
}

View File

@@ -1,4 +1,4 @@
Copyright 2016 Jordan Zimmerman
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.