Compare commits

...

15 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
21 changed files with 525 additions and 128 deletions

View File

@@ -57,8 +57,11 @@ var r1 = new NameAndAge("foo", 123);
var r2 = r1.withName("bar");
var r3 = r2.withAge(456);
// access the builder as well:
// 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._
@@ -161,27 +164,29 @@ public class NameAndAgeBuilder {
* Add withers to {@code NameAndAge}
*/
public interface With {
/**
* Cast this to the record type
*/
default NameAndAge internalGetThis() {
Object obj = this;
return (PersonRecord)obj;
}
/**
* Return a new record builder using the current values
*/
default NameAndAgeBuilder with() {
NameAndAge r = internalGetThis();
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) {
NameAndAge r = internalGetThis();
var r = (NameAndAge)(Object)this;
return new NameAndAge(name, r.age());
}
@@ -189,7 +194,7 @@ public class NameAndAgeBuilder {
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
*/
default NameAndAge withAge(int age) {
NameAndAge r = internalGetThis();
var r = (NameAndAge)(Object)this;
return new NameAndAge(r.name(), age);
}
}
@@ -210,7 +215,8 @@ This will generate a record ala:
```java
@RecordBuilder
public record NameAndAgeRecord(String name, int age) implements NameAndAge {}
public record NameAndAgeRecord(String name, int age) implements
NameAndAge, NameAndAgeRecordBuilder.With {}
```
Note that the generated record is annotated with `@RecordBuilder` so a record
@@ -225,6 +231,31 @@ Notes:
- 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
@@ -327,7 +358,6 @@ Alternatively, you can provide values for each individual meta data (or combinat
- `javac ... -AcomponentsMethodName=foo`
- `javac ... -AwithClassName=foo`
- `javac ... -AwithClassMethodPrefix=foo`
- `javac ... -AwithClassGetThisMethodName=foo`
- `javac ... -AfileComment=foo`
- `javac ... -AfileIndent=foo`
- `javac ... -AprefixEnclosingClassNames=foo`

View File

@@ -5,7 +5,7 @@
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<packaging>pom</packaging>
<version>1.8.ea</version>
<version>1.11.ea</version>
<modules>
<module>record-builder-core</module>
@@ -71,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>record-builder-1.8.ea</tag>
<tag>record-builder-1.11.ea</tag>
</scm>
<issueManagement>

View File

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

View File

@@ -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

@@ -105,15 +105,6 @@ public interface RecordBuilderMetaData {
return "with";
}
/**
* The name to use for the method that returns "this" cast to the record type
*
* @return method name
*/
default String withClassGetThisMethodName() {
return "internalGetThis";
}
/**
* Return the comment to place at the top of generated files. Return null or an empty string for no comment.
*

View File

@@ -24,4 +24,22 @@ import java.lang.annotation.Target;
@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.8.ea</version>
<version>1.11.ea</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -20,14 +20,63 @@ 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();

View File

@@ -15,15 +15,26 @@
*/
package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.*;
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;
@@ -32,7 +43,8 @@ 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 {
class InternalRecordBuilderProcessor
{
private final RecordBuilderMetaData metaData;
private final ClassType recordClassType;
private final String packageName;
@@ -42,10 +54,11 @@ class InternalRecordBuilderProcessor {
private final TypeSpec builderType;
private final TypeSpec.Builder builder;
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData) {
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData, Optional<String> packageNameOpt)
{
this.metaData = metaData;
recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
packageName = ElementUtils.getPackageName(record);
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());
@@ -72,19 +85,23 @@ class InternalRecordBuilderProcessor {
builderType = builder.build();
}
String packageName() {
String packageName()
{
return packageName;
}
ClassType builderClassType() {
ClassType builderClassType()
{
return builderClassType;
}
TypeSpec builderType() {
TypeSpec builderType()
{
return builderType;
}
private void addWithNestedClass() {
private void addWithNestedClass()
{
/*
Adds a nested interface that adds withers similar to:
@@ -94,45 +111,61 @@ class InternalRecordBuilderProcessor {
}
}
*/
TypeSpec.Builder classBuilder = TypeSpec.interfaceBuilder(metaData.withClassName())
var classBuilder = TypeSpec.interfaceBuilder(metaData.withClassName())
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PUBLIC)
.addTypeVariables(typeVariables);
addWithGetThisMethod(classBuilder);
addWithBuilderMethod(classBuilder);
addWithSuppliedBuilderMethod(classBuilder);
IntStream.range(0, recordComponents.size()).forEach(index -> add1WithMethod(classBuilder, recordComponents.get(index), index));
builder.addType(classBuilder.build());
}
private void addWithGetThisMethod(TypeSpec.Builder classBuilder) {
private void addWithSuppliedBuilderMethod(TypeSpec.Builder classBuilder)
{
/*
Adds a method that returns "this" cast to the record similar to:
Adds a method that returns a pre-filled copy builder similar to:
default MyRecord internalGetThis() {
Object obj = this;
return (MyRecord)obj;
default MyRecord with(Consumer<MyRecordBuilder> consumer) {
MyRecord r = (MyRecord)(Object)this;
MyRecordBuilder builder MyRecordBuilder.builder(r);
consumer.accept(builder);
return builder.build();
}
*/
CodeBlock codeBlock = CodeBlock.builder()
.add("Object obj = this;\n")
.add("return ($T)obj;", recordClassType.typeName())
.build();
MethodSpec methodSpec = MethodSpec.methodBuilder(metaData.withClassGetThisMethodName())
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("Cast this to the record type")
.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(codeBlock)
.addCode(codeBlockBuilder.build())
.build();
classBuilder.addMethod(methodSpec);
}
private void addWithBuilderMethod(TypeSpec.Builder classBuilder) {
CodeBlock.Builder codeBlockBuilder = CodeBlock.builder()
.add("$T r = $L();\n", recordClassType.typeName(), metaData.withClassGetThisMethodName())
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());
MethodSpec methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Return a new record builder using the current values")
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
@@ -142,17 +175,18 @@ class InternalRecordBuilderProcessor {
classBuilder.addMethod(methodSpec);
}
private void add1WithMethod(TypeSpec.Builder classBuilder, ClassType component, int index) {
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 = internalGetThis();
MyRecord r = (MyRecord)(Object)this;
return new MyRecord(name, r.age());
}
*/
CodeBlock.Builder codeBlockBuilder = CodeBlock.builder()
.add("$T r = $L();\n", recordClassType.typeName(), metaData.withClassGetThisMethodName())
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) {
@@ -161,15 +195,16 @@ class InternalRecordBuilderProcessor {
ClassType parameterComponent = recordComponents.get(parameterIndex);
if (parameterIndex == index) {
codeBlockBuilder.add(parameterComponent.name());
} else {
}
else {
codeBlockBuilder.add("r.$L()", parameterComponent.name());
}
});
codeBlockBuilder.add(");");
String methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
var methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
MethodSpec methodSpec = MethodSpec.methodBuilder(methodName)
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)
@@ -180,7 +215,8 @@ class InternalRecordBuilderProcessor {
classBuilder.addMethod(methodSpec);
}
private void addDefaultConstructor() {
private void addDefaultConstructor()
{
/*
Adds a default constructor similar to:
@@ -194,7 +230,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(constructor);
}
private void addAllArgsConstructor() {
private void addAllArgsConstructor()
{
/*
Adds an all-args constructor similar to:
@@ -215,7 +252,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(constructorBuilder.build());
}
private void addToStringMethod() {
private void addToStringMethod()
{
/*
add a toString() method similar to:
@@ -244,7 +282,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addHashCodeMethod() {
private void addHashCodeMethod()
{
/*
add a hashCode() method similar to:
@@ -272,7 +311,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addEqualsMethod() {
private void addEqualsMethod()
{
/*
add an equals() method similar to:
@@ -290,7 +330,8 @@ class InternalRecordBuilderProcessor {
String name = recordComponent.name();
if (recordComponent.typeName().isPrimitive()) {
codeBuilder.add("\n&& ($L == b.$L)", name, name);
} else {
}
else {
codeBuilder.add("\n&& $T.equals($L, b.$L)", Objects.class, name, name);
}
});
@@ -307,7 +348,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addBuildMethod() {
private void addBuildMethod()
{
/*
Adds the build method that generates the record similar to:
@@ -334,7 +376,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addStaticCopyBuilderMethod() {
private void addStaticCopyBuilderMethod()
{
/*
Adds a copy builder method that pre-fills the builder with existing values similar to:
@@ -363,7 +406,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addStaticDefaultBuilderMethod() {
private void addStaticDefaultBuilderMethod()
{
/*
Adds a the default builder method similar to:
@@ -382,7 +426,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void addStaticComponentsMethod() {
private void addStaticComponentsMethod()
{
/*
Adds a static method that converts a record instance into a stream of its component parts
@@ -416,7 +461,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void add1Field(ClassType component) {
private void add1Field(ClassType component)
{
/*
For a single record component, add a field similar to:
@@ -426,7 +472,8 @@ class InternalRecordBuilderProcessor {
builder.addField(fieldSpec);
}
private void add1GetterMethod(ClassType component) {
private void add1GetterMethod(ClassType component)
{
/*
For a single record component, add a getter similar to:
@@ -444,7 +491,8 @@ class InternalRecordBuilderProcessor {
builder.addMethod(methodSpec);
}
private void add1SetterMethod(ClassType component) {
private void add1SetterMethod(ClassType component)
{
/*
For a single record component, add a setter similar to:

View File

@@ -15,12 +15,14 @@
*/
package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.*;
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 io.soabase.recordbuilder.core.RecordInterface;
import io.soabase.recordbuilder.core.IgnoreDefaultMethod;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
@@ -28,7 +30,13 @@ 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.*;
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;
@@ -46,9 +54,9 @@ class InternalRecordInterfaceProcessor {
private static final String FAKE_METHOD_NAME = "__FAKE__";
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, RecordInterface recordInterface, RecordBuilderMetaData metaData) {
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageNameOpt) {
this.processingEnv = processingEnv;
packageName = ElementUtils.getPackageName(iface);
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(iface));
recordComponents = getRecordComponents(iface);
this.iface = iface;
@@ -65,8 +73,10 @@ class InternalRecordInterfaceProcessor {
.addAnnotation(generatedRecordInterfaceAnnotation)
.addTypeVariables(typeVariables);
if (recordInterface.addRecordBuilder()) {
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();

View File

@@ -75,11 +75,6 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
*/
public static final String OPTION_WITH_CLASS_METHOD_PREFIX = "withClassMethodPrefix";
/**
* @see #withClassGetThisMethodName()
*/
public static final String OPTION_WITH_CLASS_GET_THIS_METHOD_NAME = "withClassGetThisMethodName";
private final String suffix;
private final String interfaceSuffix;
private final String copyMethodName;
@@ -88,7 +83,6 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
private final String componentsMethodName;
private final String withClassName;
private final String withClassMethodPrefix;
private final String withClassGetThisMethodName;
private final String fileComment;
private final String fileIndent;
private final boolean prefixEnclosingClassNames;
@@ -102,7 +96,6 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
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());
withClassGetThisMethodName = options.getOrDefault(OPTION_WITH_CLASS_GET_THIS_METHOD_NAME, DEFAULT.withClassGetThisMethodName());
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()));
@@ -148,11 +141,6 @@ public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
return withClassMethodPrefix;
}
@Override
public String withClassGetThisMethodName() {
return withClassGetThisMethodName;
}
@Override
public String fileComment() {
return fileComment;

View File

@@ -18,39 +18,46 @@ package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.TypeSpec;
import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import io.soabase.recordbuilder.core.RecordInterface;
import javax.annotation.processing.*;
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.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.Optional;
import java.util.Set;
import java.util.function.Function;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.RECORD_BUILDER;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.RECORD_INTERFACE;
@SupportedAnnotationTypes({RECORD_BUILDER, RECORD_INTERFACE})
public class RecordBuilderProcessor extends AbstractProcessor {
public static final String RECORD_BUILDER = "io.soabase.recordbuilder.core.RecordBuilder";
public static final String RECORD_INTERFACE = "io.soabase.recordbuilder.core.RecordInterface";
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", RECORD_BUILDER).build();
static final AnnotationSpec generatedRecordInterfaceAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RECORD_INTERFACE).build();
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(element -> process(annotation, element))
);
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> process(annotation, element)));
return true;
}
@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
@@ -63,37 +70,102 @@ public class RecordBuilderProcessor extends AbstractProcessor {
private void process(TypeElement annotation, Element element) {
var metaData = new RecordBuilderMetaDataLoader(processingEnv, s -> processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, s)).getMetaData();
if (annotation.getQualifiedName().toString().equals(RECORD_BUILDER)) {
processRecordBuilder((TypeElement) element, metaData);
} else if (annotation.getQualifiedName().toString().equals(RECORD_INTERFACE)) {
processRecordInterface((TypeElement) element, element.getAnnotation(RecordInterface.class), metaData);
} else {
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 processRecordInterface(TypeElement element, RecordInterface recordInterface, RecordBuilderMetaData metaData) {
if (!element.getKind().isInterface()) {
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 internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, recordInterface, metaData);
if (!internalProcessor.isValid()) {
var internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, addRecordBuilder, metaData, packageName);
if ( !internalProcessor.isValid() )
{
return;
}
writeRecordInterfaceJavaFile(element, internalProcessor.packageName(), internalProcessor.recordClassType(), internalProcessor.recordType(), metaData, internalProcessor::toRecord);
}
private void processRecordBuilder(TypeElement record, RecordBuilderMetaData metaData) {
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())) {
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);
var internalProcessor = new InternalRecordBuilderProcessor(record, metaData, packageName);
writeRecordBuilderJavaFile(record, internalProcessor.packageName(), internalProcessor.builderClassType(), internalProcessor.builderType(), metaData);
}
@@ -105,7 +177,7 @@ public class RecordBuilderProcessor extends AbstractProcessor {
{
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);
}
@@ -128,7 +200,7 @@ public class RecordBuilderProcessor extends AbstractProcessor {
{
String fullyQualifiedName = packageName.isEmpty() ? classType.name() : (packageName + "." + classType.name());
JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName);
try ( Writer writer = sourceFile.openWriter() )
try (Writer writer = sourceFile.openWriter())
{
writer.write(recordSourceCode);
}
@@ -140,11 +212,10 @@ public class RecordBuilderProcessor extends AbstractProcessor {
}
private JavaFile javaFileBuilder(String packageName, TypeSpec type, RecordBuilderMetaData metaData) {
var javaFileBuilder = JavaFile.builder(packageName, type)
.skipJavaLangImports(true)
.indent(metaData.fileIndent());
var javaFileBuilder = JavaFile.builder(packageName, type).skipJavaLangImports(true).indent(metaData.fileIndent());
var comment = metaData.fileComment();
if ((comment != null) && !comment.isEmpty()) {
if ( (comment != null) && !comment.isEmpty() )
{
javaFileBuilder.addFileComment(comment);
}
return javaFileBuilder.build();
@@ -152,7 +223,8 @@ public class RecordBuilderProcessor extends AbstractProcessor {
private void handleWriteError(TypeElement element, IOException e) {
String message = "Could not create source file";
if (e.getMessage() != null) {
if ( e.getMessage() != null )
{
message = message + ": " + e.getMessage();
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, element);

View File

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

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,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,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

@@ -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

@@ -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

@@ -46,4 +46,17 @@ class TestWithers {
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());
}
}