Compare commits

...

21 Commits

Author SHA1 Message Date
randgalt
af69217eb5 [maven-release-plugin] prepare release record-builder-1.6.ea 2020-02-06 08:14:59 -05:00
randgalt
9133d5f66d Updated the README 2020-02-06 08:12:39 -05:00
sipkab
456b6f0f62 Fix indentation in RecordBuilderProcessor 2020-02-05 08:50:31 +01:00
sipkab
0fc82139de Fix indentation in OptionBasedRecordBuilderMetaData as well 2020-02-05 08:49:02 +01:00
sipkab
0774278032 Fix indentation 2020-02-05 08:47:38 +01:00
sipkab
578c7ad532 Allow -A option based processor configuration 2020-02-05 08:46:21 +01:00
sipkab
6e10d3a3c0 Mitigate possible runtime Errors when running on older JVMs 2020-02-05 08:30:43 +01:00
randgalt
a9fa609911 updated copyright date in license 2019-12-27 18:50:08 -05:00
randgalt
43db1586ac [maven-release-plugin] prepare for next development iteration 2019-12-24 18:43:17 -05:00
randgalt
d21994143f [maven-release-plugin] prepare release record-builder-1.5.ea 2019-12-24 18:43:10 -05:00
randgalt
f49089d26e Merge branch 'master' of github.com:Randgalt/record-builder 2019-12-24 12:34:08 -05:00
randgalt
e9a6e79e79 Added a component stream method 2019-12-24 12:34:02 -05:00
Jordan Zimmerman
f091e094e8 Update README.md 2019-12-23 09:38:36 -05:00
Jordan Zimmerman
72c9332b89 Update README.md 2019-12-23 09:37:47 -05:00
randgalt
41f1e5fa7e [maven-release-plugin] prepare for next development iteration 2019-12-22 13:21:26 -05:00
randgalt
8f259bd343 [maven-release-plugin] prepare release record-builder-1.4.ea 2019-12-22 13:21:19 -05:00
randgalt
8e1bc0b45c Merge branch 'master' of github.com:Randgalt/record-builder 2019-12-22 13:16:21 -05:00
randgalt
d66fd5cec2 Added getter methods, generated annotation and a few other tweaks 2019-12-22 12:09:28 -05:00
Jordan Zimmerman
d8afe29e0d Update README.md 2019-12-20 15:45:02 -05:00
randgalt
b3e3dc6df3 Merge branch 'master' of github.com:Randgalt/record-builder 2019-12-20 15:39:09 -05:00
Jordan Zimmerman
6e99b72c0f Create .travis.yml 2019-12-20 15:38:27 -05:00
18 changed files with 631 additions and 296 deletions

3
.travis.yml Normal file
View File

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

View File

@@ -1,3 +1,6 @@
[![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 # RecordBuilder - Early Access
## What is RecordBuilder ## 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) { public NameAndAgeBuilder name(String name) {
this.name = 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) { public NameAndAgeBuilder age(int age) {
this.age = age; this.age = age;
return this; 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 @Override
public String toString() { public String toString() {
return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]"; return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
@@ -159,6 +184,24 @@ 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`. 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`
Alternatively, you can provide values for each individual meta data (or combinations):
- `javac ... -AcopyMethodName=foo`
- `javac ... -AbuilderMethodName=foo`
- `javac ... -AbuildMethodName=foo`
- `javac ... -AcomponentsMethodName=foo`
- `javac ... -AfileComment=foo`
- `javac ... -AfileIndent=foo`
- `javac ... -AprefixEnclosingClassNames=foo`
## TODOs ## TODOs
- Document how to integrate with Gradle - Document how to integrate with Gradle

View File

@@ -5,7 +5,7 @@
<groupId>io.soabase.record-builder</groupId> <groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId> <artifactId>record-builder</artifactId>
<packaging>pom</packaging> <packaging>pom</packaging>
<version>1.4.ea-SNAPSHOT</version> <version>1.6.ea</version>
<modules> <modules>
<module>record-builder-core</module> <module>record-builder-core</module>
@@ -70,7 +70,7 @@
<url>https://github.com/randgalt/record-builder</url> <url>https://github.com/randgalt/record-builder</url>
<connection>scm:git:https://github.com/randgalt/record-builder.git</connection> <connection>scm:git:https://github.com/randgalt/record-builder.git</connection>
<developerConnection>scm:git:git@github.com:randgalt/record-builder.git</developerConnection> <developerConnection>scm:git:git@github.com:randgalt/record-builder.git</developerConnection>
<tag>HEAD</tag> <tag>record-builder-1.6.ea</tag>
</scm> </scm>
<issueManagement> <issueManagement>
@@ -179,6 +179,9 @@
<exclude>**/io/soabase/com/google/**</exclude> <exclude>**/io/soabase/com/google/**</exclude>
<exclude>**/com/company/**</exclude> <exclude>**/com/company/**</exclude>
<exclude>**/META-INF/services/**</exclude> <exclude>**/META-INF/services/**</exclude>
<exclude>**/jvm.config</exclude>
<exclude>**/.java-version</exclude>
<exclude>**/.travis.yml</exclude>
</excludes> </excludes>
<strictCheck>true</strictCheck> <strictCheck>true</strictCheck>
</configuration> </configuration>

View File

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

View File

@@ -1,5 +1,5 @@
/** /**
* Copyright 2016 Jordan Zimmerman * Copyright 2019 Jordan Zimmerman
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -68,13 +68,22 @@ public interface RecordBuilderMetaData {
return "build"; 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 the comment to place at the top of generated files. Return null or an empty string for no comment.
* *
* @return comment or empty * @return comment or empty
*/ */
default String fileComment() { 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

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

View File

@@ -1,5 +1,5 @@
/** /**
* Copyright 2016 Jordan Zimmerman * Copyright 2019 Jordan Zimmerman
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

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

View File

@@ -0,0 +1,122 @@
/**
* 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 java.util.Map;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
/**
* @see #suffix()
*/
public static final String OPTION_SUFFIX = "suffix";
/**
* @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";
private final String suffix;
private final String copyMethodName;
private final String builderMethodName;
private final String buildMethodName;
private final String componentsMethodName;
private final String fileComment;
private final String fileIndent;
private final boolean prefixEnclosingClassNames;
public OptionBasedRecordBuilderMetaData(Map<String, String> options) {
suffix = options.getOrDefault(OPTION_SUFFIX, "Builder");
builderMethodName = options.getOrDefault(OPTION_BUILDER_METHOD_NAME, "builder");
copyMethodName = options.getOrDefault(OPTION_COPY_METHOD_NAME, builderMethodName);
buildMethodName = options.getOrDefault(OPTION_BUILD_METHOD_NAME, "build");
componentsMethodName = options.getOrDefault(OPTION_COMPONENTS_METHOD_NAME, "stream");
fileComment = options.getOrDefault(OPTION_FILE_COMMENT,
"Auto generated by io.soabase.recordbuilder.core.RecordBuilder: https://github.com/Randgalt/record-builder");
fileIndent = options.getOrDefault(OPTION_FILE_INDENT, " ");
String prefixenclosingclassnamesopt = options.get(OPTION_PREFIX_ENCLOSING_CLASS_NAMES);
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 fileComment() {
return fileComment;
}
@Override
public String fileIndent() {
return fileIndent;
}
@Override
public boolean prefixEnclosingClassNames() {
return prefixEnclosingClassNames;
}
}

View File

@@ -1,5 +1,5 @@
/** /**
* Copyright 2016 Jordan Zimmerman * Copyright 2019 Jordan Zimmerman
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 io.soabase.recordbuilder.core.RecordBuilderMetaData;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import javax.annotation.processing.ProcessingEnvironment;
class RecordBuilderMetaDataLoader { class RecordBuilderMetaDataLoader {
private final RecordBuilderMetaData metaData; private final RecordBuilderMetaData metaData;
RecordBuilderMetaDataLoader(String metaDataClassName, Consumer<String> logger) { RecordBuilderMetaDataLoader(ProcessingEnvironment processingEnv, Consumer<String> logger) {
RecordBuilderMetaData localMetaData = null; Map<String, String> options = processingEnv.getOptions();
String metaDataClassName = options.get(RecordBuilderMetaData.JAVAC_OPTION_NAME);
if ((metaDataClassName != null) && !metaDataClassName.isEmpty()) { if ((metaDataClassName != null) && !metaDataClassName.isEmpty()) {
RecordBuilderMetaData loadedMetaData = null;
try { try {
Class<?> clazz = Class.forName(metaDataClassName); Class<?> clazz = Class.forName(metaDataClassName);
localMetaData = (RecordBuilderMetaData) clazz.getDeclaredConstructor().newInstance(); loadedMetaData = (RecordBuilderMetaData) clazz.getDeclaredConstructor().newInstance();
logger.accept("Found meta data: " + localMetaData.getClass()); 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) { } 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() { 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -15,310 +15,68 @@
*/ */
package io.soabase.recordbuilder.processor; 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 io.soabase.recordbuilder.core.RecordBuilderMetaData;
import javax.annotation.processing.*; import javax.annotation.processing.*;
import javax.lang.model.SourceVersion; import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element; import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind; import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic; import javax.tools.Diagnostic;
import javax.tools.JavaFileObject; import javax.tools.JavaFileObject;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.util.List;
import java.util.Objects;
import java.util.Set; 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;
@SupportedSourceVersion(SourceVersion.RELEASE_14)
@SupportedAnnotationTypes(NAME)
public class RecordBuilderProcessor extends AbstractProcessor { 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 @Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation).forEach(this::process)); annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation).forEach(this::process));
return true; return true;
} }
@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(Element element) { private void process(Element element) {
var messager = processingEnv.getMessager(); var messager = processingEnv.getMessager();
if (element.getKind() != ElementKind.RECORD) { // 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(element.getKind().name())) {
messager.printMessage(Diagnostic.Kind.ERROR, "RecordBuilder only valid for records.", element); messager.printMessage(Diagnostic.Kind.ERROR, "RecordBuilder only valid for records.", element);
return; return;
} }
var metaData = new RecordBuilderMetaDataLoader(processingEnv.getOptions().get(RecordBuilderMetaData.JAVAC_OPTION_NAME), s -> messager.printMessage(Diagnostic.Kind.NOTE, s)).getMetaData(); var metaData = new RecordBuilderMetaDataLoader(processingEnv, s -> messager.printMessage(Diagnostic.Kind.NOTE, s)).getMetaData();
process((TypeElement) element, metaData); process((TypeElement) element, metaData);
} }
private void process(TypeElement record, RecordBuilderMetaData metaData) { private void process(TypeElement record, RecordBuilderMetaData metaData) {
var recordClassType = ElementUtils.getClassType(record, record.getTypeParameters()); var internalProcessor = new InternalProcessor(record, metaData);
var packageName = ElementUtils.getPackageName(record); writeJavaFile(record, internalProcessor.packageName(), internalProcessor.builderClassType(), internalProcessor.builderType(), metaData);
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) { private void writeJavaFile(TypeElement record, String packageName, ClassType builderClassType, TypeSpec builderType, RecordBuilderMetaData metaData) {
// 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) {
// produces the Java file // produces the Java file
var javaFileBuilder = JavaFile.builder(packageName, builder.build()) var javaFileBuilder = JavaFile.builder(packageName, builderType)
.skipJavaLangImports(true)
.indent(metaData.fileIndent()); .indent(metaData.fileIndent());
var comment = metaData.fileComment(); var comment = metaData.fileComment();
if ((comment != null) && !comment.isEmpty()) { if ((comment != null) && !comment.isEmpty()) {

View File

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

View File

@@ -1,5 +1,5 @@
/** /**
* Copyright 2016 Jordan Zimmerman * Copyright 2019 Jordan Zimmerman
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@@ -1,4 +1,4 @@
Copyright 2016 Jordan Zimmerman Copyright 2019 Jordan Zimmerman
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.