Compare commits
14 Commits
record-bui
...
record-bui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d21994143f | ||
|
|
f49089d26e | ||
|
|
e9a6e79e79 | ||
|
|
f091e094e8 | ||
|
|
72c9332b89 | ||
|
|
41f1e5fa7e | ||
|
|
8f259bd343 | ||
|
|
8e1bc0b45c | ||
|
|
d66fd5cec2 | ||
|
|
d8afe29e0d | ||
|
|
b3e3dc6df3 | ||
|
|
85070cc106 | ||
|
|
6e99b72c0f | ||
|
|
7ac840ae2a |
1
.mvn/jvm.config
Normal file
1
.mvn/jvm.config
Normal file
@@ -0,0 +1 @@
|
||||
--enable-preview
|
||||
3
.travis.yml
Normal file
3
.travis.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
language: java
|
||||
jdk:
|
||||
- openjdk14
|
||||
37
README.md
37
README.md
@@ -1,3 +1,6 @@
|
||||
[](https://travis-ci.org/Randgalt/record-builder)
|
||||
[](https://search.maven.org/search?q=g:io.soabase.record-builder%20a:record-builder)
|
||||
|
||||
# RecordBuilder - Early Access
|
||||
|
||||
## What is RecordBuilder
|
||||
@@ -65,7 +68,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 +76,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 + "]";
|
||||
@@ -159,6 +184,14 @@ Note: records are a preview feature only. You'll need take a number of steps in
|
||||
|
||||
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`.
|
||||
|
||||
## 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`
|
||||
|
||||
## TODOs
|
||||
|
||||
- Document how to integrate with Gradle
|
||||
|
||||
7
pom.xml
7
pom.xml
@@ -5,7 +5,7 @@
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>1.3.ea</version>
|
||||
<version>1.5.ea</version>
|
||||
|
||||
<modules>
|
||||
<module>record-builder-core</module>
|
||||
@@ -70,7 +70,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.3.ea</tag>
|
||||
<tag>record-builder-1.5.ea</tag>
|
||||
</scm>
|
||||
|
||||
<issueManagement>
|
||||
@@ -179,6 +179,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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.3.ea</version>
|
||||
<version>1.5.ea</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -68,13 +68,22 @@ 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";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.3.ea</version>
|
||||
<version>1.5.ea</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
/**
|
||||
* Copyright 2016 Jordan Zimmerman
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.soabase.recordbuilder.processor;
|
||||
|
||||
import com.squareup.javapoet.*;
|
||||
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
|
||||
|
||||
import javax.annotation.processing.Generated;
|
||||
import javax.lang.model.element.Element;
|
||||
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.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
class InternalProcessor {
|
||||
private static final AnnotationSpec generatedAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RecordBuilderProcessor.NAME).build();
|
||||
|
||||
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;
|
||||
|
||||
InternalProcessor(TypeElement record, RecordBuilderMetaData metaData) {
|
||||
this.metaData = metaData;
|
||||
recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
|
||||
packageName = ElementUtils.getPackageName(record);
|
||||
builderClassType = ElementUtils.getClassType(packageName, getBuilderName(record, metaData, recordClassType), 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(generatedAnnotation)
|
||||
.addTypeVariables(typeVariables);
|
||||
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 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();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
private void addDefaultConstructor() {
|
||||
/*
|
||||
Adds a default constructor similar to:
|
||||
|
||||
private MyRecordBuilder() {
|
||||
}
|
||||
*/
|
||||
var constructor = MethodSpec.constructorBuilder()
|
||||
.addModifiers(Modifier.PRIVATE)
|
||||
.addAnnotation(generatedAnnotation)
|
||||
.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(generatedAnnotation);
|
||||
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(generatedAnnotation)
|
||||
.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(generatedAnnotation)
|
||||
.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(generatedAnnotation)
|
||||
.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(generatedAnnotation)
|
||||
.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(generatedAnnotation)
|
||||
.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(generatedAnnotation)
|
||||
.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(generatedAnnotation)
|
||||
.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(generatedAnnotation)
|
||||
.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(generatedAnnotation)
|
||||
.addParameter(parameterSpec)
|
||||
.returns(builderClassType.typeName())
|
||||
.addStatement("this.$L = $L", component.name(), component.name())
|
||||
.addStatement("return this")
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
}
|
||||
@@ -15,28 +15,31 @@
|
||||
*/
|
||||
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.RecordBuilderMetaData;
|
||||
|
||||
import javax.annotation.processing.*;
|
||||
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.TypeElement;
|
||||
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.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
@SupportedAnnotationTypes("io.soabase.recordbuilder.core.RecordBuilder")
|
||||
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.NAME;
|
||||
|
||||
@SupportedAnnotationTypes(NAME)
|
||||
@SupportedSourceVersion(SourceVersion.RELEASE_14)
|
||||
public class RecordBuilderProcessor extends AbstractProcessor {
|
||||
public static final String NAME = "io.soabase.recordbuilder.core.RecordBuilder";
|
||||
|
||||
private static final AnnotationSpec generatedAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", NAME).build();
|
||||
|
||||
@Override
|
||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
||||
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation).forEach(this::process));
|
||||
@@ -54,271 +57,14 @@ public class RecordBuilderProcessor extends AbstractProcessor {
|
||||
}
|
||||
|
||||
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);
|
||||
var internalProcessor = new InternalProcessor(record, metaData);
|
||||
writeJavaFile(record, internalProcessor.packageName(), internalProcessor.builderClassType(), internalProcessor.builderType(), 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();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
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 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 writeJavaFile(TypeElement record, String packageName, ClassType builderClassType, TypeSpec builderType, RecordBuilderMetaData metaData) {
|
||||
// produces the Java file
|
||||
var javaFileBuilder = JavaFile.builder(packageName, builder.build())
|
||||
var javaFileBuilder = JavaFile.builder(packageName, builderType)
|
||||
.skipJavaLangImports(true)
|
||||
.indent(metaData.fileIndent());
|
||||
var comment = metaData.fileComment();
|
||||
if ((comment != null) && !comment.isEmpty()) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.3.ea</version>
|
||||
<version>1.5.ea</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user