Compare commits

..

14 Commits

Author SHA1 Message Date
Jordan Zimmerman
abfc12bdb0 [maven-release-plugin] prepare release record-builder-34 2022-08-02 05:05:14 +02:00
Jordan Zimmerman
6c0fac0dff [maven-release-plugin] rollback the release of record-builder-34 2022-08-02 05:04:43 +02:00
Jordan Zimmerman
ae527cd8e5 [maven-release-plugin] prepare release record-builder-34 2022-08-02 05:04:20 +02:00
Jordan Zimmerman
73ba62057a [maven-release-plugin] rollback the release of record-builder-34 2022-08-02 05:03:00 +02:00
Jordan Zimmerman
87998aba68 [maven-release-plugin] prepare for next development iteration 2022-08-02 05:02:28 +02:00
Jordan Zimmerman
04a0904d3f [maven-release-plugin] prepare release record-builder-34 2022-08-02 05:02:23 +02:00
Johannes
aa072af8e1 Performance feature: copy collections only when they were changed. Fixes #114. (#118) 2022-06-19 21:17:09 +01:00
Jordan Zimmerman
b435b5d3fd Revert "TYPE_USE annotations were being ignored (#115)"
This reverts commit d3c1bb36f3.

A bug was found - the PR needs more work
2022-06-13 08:37:24 +01:00
Jordan Zimmerman
d3c1bb36f3 TYPE_USE annotations were being ignored (#115)
Java's DAG for annotations processors doesn't contain `TYPE_USE` annotations
on the Element for some reason. However, they are on the type. So, use the
type instead.

Note due to limitations of JavaPoet this doesn't fix `TYPE_USE` annotations on
parameterized types or array components. If we want to address those we will need
changes in JavaPoet which has been dormant for a very long time.

Fixes #113
Relates to #111
2022-06-12 08:56:32 +01:00
Lovro Pandžić
c3719326c9 Use Optional.ofNullable() to check for null values
Fixes #107
2022-05-10 16:06:12 +01:00
Jordan Zimmerman
661d0818c0 _FromWith class was missing @Generated on methods/ctor 2022-04-08 09:15:59 +01:00
Jordan Zimmerman
79bc8396f2 Update README.md 2022-04-08 09:03:30 +01:00
Jordan Zimmerman
b2149622e4 Update README.md 2022-04-08 09:01:07 +01:00
Jordan Zimmerman
0718e37f76 [maven-release-plugin] prepare for next development iteration 2022-04-08 08:35:12 +01:00
15 changed files with 570 additions and 209 deletions

264
README.md
View File

@@ -89,168 +89,176 @@ _Hat tip to [Benji Weber](https://benjiweber.co.uk/blog/2020/09/19/fun-with-java
## Builder Class Definition
(Note: you can see a builder class built using `@RecordBuilderFull` here: [SingleItemsBuilder.java](https://gist.github.com/Randgalt/8aa487a847ea2acdd76d702f7cf17d6a))
(Note: you can see a builder class built using `@RecordBuilderFull` here: [FullRecordBuilder.java](https://gist.github.com/Randgalt/8aa487a847ea2acdd76d702f7cf17d6a))
The full builder class is defined as:
```java
public class NameAndAgeBuilder {
private String name;
private String name;
private int age;
private int age;
private NameAndAgeBuilder() {
}
private NameAndAgeBuilder() {
}
private NameAndAgeBuilder(String name, int age) {
this.name = name;
this.age = age;
}
private NameAndAgeBuilder(String name, int age) {
this.name = name;
this.age = age;
}
/**
* Static constructor/builder. Can be used instead of new NameAndAge(...)
*/
public static NameAndAge NameAndAge(String name, int age) {
return new NameAndAge(name, age);
}
/**
* Static constructor/builder. Can be used instead of new NameAndAge(...)
*/
public static NameAndAge NameAndAge(String name, int age) {
return new NameAndAge(name, age);
}
/**
* Return a new builder with all fields set to default Java values
*/
public static NameAndAgeBuilder builder() {
return new NameAndAgeBuilder();
}
/**
* Return a new builder with all fields set to default Java values
*/
public static NameAndAgeBuilder builder() {
return new NameAndAgeBuilder();
}
/**
* Return a new builder with all fields set to the values taken from the given record instance
*/
public static NameAndAgeBuilder builder(NameAndAge from) {
return new NameAndAgeBuilder(from.name(), from.age());
}
/**
* Return a new builder with all fields set to the values taken from the given record instance
*/
public static NameAndAgeBuilder builder(NameAndAge from) {
return new NameAndAgeBuilder(from.name(), from.age());
}
/**
* Return a "with"er for an existing record instance
*/
public static NameAndAgeBuilder.With from(NameAndAge from) {
return new NameAndAgeBuilder.With() {
@Override
public String name() {
return from.name();
}
/**
* Return a "with"er for an existing record instance
*/
public static NameAndAgeBuilder.With from(NameAndAge from) {
return new _FromWith(from);
}
@Override
public int age() {
return from.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.SimpleImmutableEntry<>("name", record.name()),
new AbstractMap.SimpleImmutableEntry<>("age", record.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(Map.entry("name", record.name()),
Map.entry("age", record.age()));
}
/**
* Return a new record instance with all fields set to the current values in this builder
*/
public NameAndAge build() {
return new NameAndAge(name, age);
}
/**
* Return a new record instance with all fields set to the current values in this builder
*/
public NameAndAge build() {
return new NameAndAge(name, age);
}
@Override
public String toString() {
return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
}
@Override
public String toString() {
return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public boolean equals(Object o) {
return (this == o) || ((o instanceof NameAndAgeBuilder r)
&& Objects.equals(name, r.name)
&& (age == r.age));
}
@Override
public boolean equals(Object o) {
return (this == o) || ((o instanceof NameAndAgeBuilder r)
&& Objects.equals(name, r.name)
&& (age == r.age));
}
/**
* Set a new value for the {@code name} record component in the builder
*/
public NameAndAgeBuilder name(String name) {
this.name = name;
return this;
}
/**
* Set a new value for the {@code name} record component in the builder
*/
public NameAndAgeBuilder name(String name) {
this.name = name;
return this;
}
/**
* 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;
}
/**
* Add withers to {@code NameAndAge}
*/
public interface With {
/**
* Return the current value for the {@code name} record component in the builder
*/
String name();
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
*/
int age();
/**
* Return a new record builder using the current values
*/
default NameAndAgeBuilder with() {
return new NameAndAgeBuilder(name(), age());
public int age() {
return age;
}
/**
* Return a new record built from the builder passed to the given consumer
* Add withers to {@code NameAndAge}
*/
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
NameAndAgeBuilder builder = with();
consumer.accept(builder);
return builder.build();
public interface With {
/**
* Return the current value for the {@code name} record component in the builder
*/
String name();
/**
* Return the current value for the {@code age} record component in the builder
*/
int age();
/**
* Return a new record builder using the current values
*/
default NameAndAgeBuilder with() {
return new NameAndAgeBuilder(name(), age());
}
/**
* Return a new record built from the builder passed to the given consumer
*/
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
NameAndAgeBuilder builder = with();
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) {
return new NameAndAge(name, age());
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
*/
default NameAndAge withAge(int age) {
return new NameAndAge(name(), age);
}
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
*/
default NameAndAge withName(String name) {
return new NameAndAge(name, age());
}
private static final class _FromWith implements NameAndAgeBuilder.With {
private final NameAndAge from;
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
*/
default NameAndAge withAge(int age) {
return new NameAndAge(name(), age);
private _FromWith(NameAndAge from) {
this.from = from;
}
@Override
public String name() {
return from.name();
}
@Override
public int age() {
return from.age();
}
}
}
}
```

View File

@@ -5,7 +5,7 @@
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<packaging>pom</packaging>
<version>33</version>
<version>34</version>
<modules>
<module>record-builder-core</module>
@@ -80,7 +80,7 @@
<url>https://github.com/randgalt/record-builder</url>
<connection>scm:git:https://github.com/randgalt/record-builder.git</connection>
<developerConnection>scm:git:git@github.com:randgalt/record-builder.git</developerConnection>
<tag>record-builder-33</tag>
<tag>record-builder-34</tag>
</scm>
<issueManagement>

View File

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

View File

@@ -252,6 +252,24 @@ public @interface RecordBuilder {
* without {@code new}.
*/
boolean addStaticBuilder() default true;
/**
* If {@link #addSingleItemCollectionBuilders()} and {@link #useImmutableCollections()} are enabled the builder
* uses an internal class to track changes to lists. This is the name of that class.
*/
String mutableListClassName() default "_MutableList";
/**
* If {@link #addSingleItemCollectionBuilders()} and {@link #useImmutableCollections()} are enabled the builder
* uses an internal class to track changes to sets. This is the name of that class.
*/
String mutableSetClassName() default "_MutableSet";
/**
* If {@link #addSingleItemCollectionBuilders()} and {@link #useImmutableCollections()} are enabled the builder
* uses an internal class to track changes to maps. This is the name of that class.
*/
String mutableMapClassName() default "_MutableMap";
}
@Retention(RetentionPolicy.CLASS)

View File

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

View File

@@ -22,24 +22,38 @@ import javax.lang.model.element.Modifier;
import java.util.*;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.recordBuilderGeneratedAnnotation;
class CollectionBuilderUtils {
private final boolean useImmutableCollections;
private final boolean addSingleItemCollectionBuilders;
private final boolean addClassRetainedGenerated;
private final String listShimName;
private final String mapShimName;
private final String setShimName;
private final String collectionShimName;
private final String listMakerMethodName;
private final String mapMakerMethodName;
private final String setMakerMethodName;
private boolean needsListShim;
private boolean needsMapShim;
private boolean needsSetShim;
private boolean needsCollectionShim;
private static final TypeName listType = TypeName.get(List.class);
private static final TypeName mapType = TypeName.get(Map.class);
private static final TypeName setType = TypeName.get(Set.class);
private static final TypeName collectionType = TypeName.get(Collection.class);
private boolean needsListMutableMaker;
private boolean needsMapMutableMaker;
private boolean needsSetMutableMaker;
private static final Class<?> listType = List.class;
private static final Class<?> mapType = Map.class;
private static final Class<?> setType = Set.class;
private static final Class<?> collectionType = Collection.class;
private static final TypeName listTypeName = TypeName.get(listType);
private static final TypeName mapTypeName = TypeName.get(mapType);
private static final TypeName setTypeName = TypeName.get(setType);
private static final TypeName collectionTypeName = TypeName.get(collectionType);
private static final TypeVariableName tType = TypeVariableName.get("T");
private static final TypeVariableName kType = TypeVariableName.get("K");
@@ -49,14 +63,33 @@ class CollectionBuilderUtils {
private static final ParameterizedTypeName parameterizedSetType = ParameterizedTypeName.get(ClassName.get(Set.class), tType);
private static final ParameterizedTypeName parameterizedCollectionType = ParameterizedTypeName.get(ClassName.get(Collection.class), tType);
private static final Class<?> mutableListType = ArrayList.class;
private static final Class<?> mutableMapType = HashMap.class;
private static final Class<?> mutableSetType = HashSet.class;
private static final ClassName mutableListTypeName = ClassName.get(mutableListType);
private static final ClassName mutableMapTypeName = ClassName.get(mutableMapType);
private static final ClassName mutableSetTypeName = ClassName.get(mutableSetType);
private final TypeSpec mutableListSpec;
private final TypeSpec mutableSetSpec;
private final TypeSpec mutableMapSpec;
CollectionBuilderUtils(List<RecordClassType> recordComponents, RecordBuilder.Options metaData) {
useImmutableCollections = metaData.useImmutableCollections();
addSingleItemCollectionBuilders = metaData.addSingleItemCollectionBuilders();
addClassRetainedGenerated = metaData.addClassRetainedGenerated();
listShimName = adjustShimName(recordComponents, "__list", 0);
mapShimName = adjustShimName(recordComponents, "__map", 0);
setShimName = adjustShimName(recordComponents, "__set", 0);
collectionShimName = adjustShimName(recordComponents, "__collection", 0);
listShimName = disambiguateGeneratedMethodName(recordComponents, "__list", 0);
mapShimName = disambiguateGeneratedMethodName(recordComponents, "__map", 0);
setShimName = disambiguateGeneratedMethodName(recordComponents, "__set", 0);
collectionShimName = disambiguateGeneratedMethodName(recordComponents, "__collection", 0);
listMakerMethodName = disambiguateGeneratedMethodName(recordComponents, "__ensureListMutable", 0);
setMakerMethodName = disambiguateGeneratedMethodName(recordComponents, "__ensureSetMutable", 0);
mapMakerMethodName = disambiguateGeneratedMethodName(recordComponents, "__ensureMapMutable", 0);
mutableListSpec = buildMutableCollectionSubType(metaData.mutableListClassName(), mutableListTypeName, parameterizedListType, tType);
mutableSetSpec = buildMutableCollectionSubType(metaData.mutableSetClassName(), mutableSetTypeName, parameterizedSetType, tType);
mutableMapSpec = buildMutableCollectionSubType(metaData.mutableMapClassName(), mutableMapTypeName, parameterizedMapType, kType, vType);
}
enum SingleItemsMetaDataMode {
@@ -65,7 +98,8 @@ class CollectionBuilderUtils {
EXCLUDE_WILDCARD_TYPES
}
record SingleItemsMetaData(Class<?> singleItemCollectionClass, List<TypeName> typeArguments, TypeName wildType) {}
record SingleItemsMetaData(Class<?> singleItemCollectionClass, List<TypeName> typeArguments, TypeName wildType) {
}
Optional<SingleItemsMetaData> singleItemsMetaData(RecordClassType component, SingleItemsMetaDataMode mode) {
if (addSingleItemCollectionBuilders && (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName)) {
@@ -73,15 +107,15 @@ class CollectionBuilderUtils {
ClassName wildcardClass = null;
int typeArgumentQty = 0;
if (isList(component)) {
collectionClass = ArrayList.class;
collectionClass = mutableListType;
wildcardClass = ClassName.get(Collection.class);
typeArgumentQty = 1;
} else if (isSet(component)) {
collectionClass = HashSet.class;
collectionClass = mutableSetType;
wildcardClass = ClassName.get(Collection.class);
typeArgumentQty = 1;
} else if (isMap(component)) {
collectionClass = HashMap.class;
collectionClass = mutableMapType;
wildcardClass = (ClassName) component.rawTypeName();
typeArgumentQty = 2;
}
@@ -110,33 +144,36 @@ class CollectionBuilderUtils {
}
boolean isImmutableCollection(RecordClassType component) {
return useImmutableCollections && (isList(component) || isMap(component) || isSet(component) || component.rawTypeName().equals(collectionType));
return useImmutableCollections && (isList(component) || isMap(component) || isSet(component) || component.rawTypeName().equals(collectionTypeName));
}
boolean isList(RecordClassType component) {
return component.rawTypeName().equals(listType);
return component.rawTypeName().equals(listTypeName);
}
boolean isMap(RecordClassType component) {
return component.rawTypeName().equals(mapType);
return component.rawTypeName().equals(mapTypeName);
}
boolean isSet(RecordClassType component) {
return component.rawTypeName().equals(setType);
return component.rawTypeName().equals(setTypeName);
}
void add(CodeBlock.Builder builder, RecordClassType component) {
void addShimCall(CodeBlock.Builder builder, RecordClassType component) {
if (useImmutableCollections) {
if (isList(component)) {
needsListShim = true;
needsListMutableMaker = true;
builder.add("$L($L)", listShimName, component.name());
} else if (isMap(component)) {
needsMapShim = true;
needsMapMutableMaker = true;
builder.add("$L($L)", mapShimName, component.name());
} else if (isSet(component)) {
needsSetShim = true;
needsSetMutableMaker = true;
builder.add("$L($L)", setShimName, component.name());
} else if (component.rawTypeName().equals(collectionType)) {
} else if (component.rawTypeName().equals(collectionTypeName)) {
needsCollectionShim = true;
builder.add("$L($L)", collectionShimName, component.name());
} else {
@@ -147,22 +184,67 @@ class CollectionBuilderUtils {
}
}
String shimName(RecordClassType component) {
if (isList(component)) {
return listShimName;
} else if (isMap(component)) {
return mapShimName;
} else if (isSet(component)) {
return setShimName;
} else if (component.rawTypeName().equals(collectionTypeName)) {
return collectionShimName;
} else {
throw new IllegalArgumentException(component + " is not a supported collection type");
}
}
String mutableMakerName(RecordClassType component) {
if (isList(component)) {
return listMakerMethodName;
} else if (isMap(component)) {
return mapMakerMethodName;
} else if (isSet(component)) {
return setMakerMethodName;
} else {
throw new IllegalArgumentException(component + " is not a supported collection type");
}
}
void addShims(TypeSpec.Builder builder) {
if (!useImmutableCollections) {
return;
}
if (needsListShim) {
builder.addMethod(buildMethod(listShimName, listType, parameterizedListType, tType));
builder.addMethod(buildShimMethod(listShimName, listTypeName, collectionType, parameterizedListType, tType));
}
if (needsSetShim) {
builder.addMethod(buildMethod(setShimName, setType, parameterizedSetType, tType));
builder.addMethod(buildShimMethod(setShimName, setTypeName, collectionType, parameterizedSetType, tType));
}
if (needsMapShim) {
builder.addMethod(buildMethod(mapShimName, mapType, parameterizedMapType, kType, vType));
builder.addMethod(buildShimMethod(mapShimName, mapTypeName, mapType, parameterizedMapType, kType, vType));
}
if (needsCollectionShim) {
builder.addMethod(buildCollectionsMethod());
builder.addMethod(buildCollectionsShimMethod());
}
}
void addMutableMakers(TypeSpec.Builder builder) {
if (!useImmutableCollections) {
return;
}
if (needsListMutableMaker) {
builder.addMethod(buildMutableMakerMethod(listMakerMethodName, mutableListSpec.name, parameterizedListType, tType));
builder.addType(mutableListSpec);
}
if (needsSetMutableMaker) {
builder.addMethod(buildMutableMakerMethod(setMakerMethodName, mutableSetSpec.name, parameterizedSetType, tType));
builder.addType(mutableSetSpec);
}
if (needsMapMutableMaker) {
builder.addMethod(buildMutableMakerMethod(mapMakerMethodName, mutableMapSpec.name, parameterizedMapType, kType, vType));
builder.addType(mutableMapSpec);
}
}
@@ -187,34 +269,71 @@ class CollectionBuilderUtils {
return false;
}
private String adjustShimName(List<RecordClassType> recordComponents, String baseName, int index) {
private String disambiguateGeneratedMethodName(List<RecordClassType> recordComponents, String baseName, int index) {
var name = (index == 0) ? baseName : (baseName + index);
if (recordComponents.stream().anyMatch(component -> component.name().equals(name))) {
return adjustShimName(recordComponents, baseName, index + 1);
return disambiguateGeneratedMethodName(recordComponents, baseName, index + 1);
}
return name;
}
private MethodSpec buildMethod(String name, TypeName mainType, ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) {
private MethodSpec buildShimMethod(String name, TypeName mainType, Class<?> abstractType, ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) {
var code = CodeBlock.of("return (o != null) ? $T.copyOf(o) : $T.of()", mainType, mainType);
TypeName[] wildCardTypeArguments = parameterizedType.typeArguments.stream().map(WildcardTypeName::subtypeOf).toList().toArray(new TypeName[0]);
var extendedParameterizedType = ParameterizedTypeName.get(ClassName.get(abstractType), wildCardTypeArguments);
return MethodSpec.methodBuilder(name)
.addAnnotation(generatedRecordBuilderAnnotation)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.addTypeVariables(Arrays.asList(typeVariables))
.returns(parameterizedType)
.addParameter(extendedParameterizedType, "o")
.addStatement(code)
.build();
}
private MethodSpec buildMutableMakerMethod(String name, String mutableCollectionType, ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) {
var nullCase = CodeBlock.of("if (o == null) return new $L<>()", mutableCollectionType);
var isMutableCase = CodeBlock.of("if (o instanceof $L) return o", mutableCollectionType);
var defaultCase = CodeBlock.of("return new $L<>(o)", mutableCollectionType);
return MethodSpec.methodBuilder(name)
.addAnnotation(generatedRecordBuilderAnnotation)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.addTypeVariables(Arrays.asList(typeVariables))
.returns(parameterizedType)
.addParameter(parameterizedType, "o")
.addStatement(code)
.addStatement(nullCase)
.addStatement(isMutableCase)
.addStatement(defaultCase)
.build();
}
private MethodSpec buildCollectionsMethod() {
private TypeSpec buildMutableCollectionSubType(String className, ClassName mutableCollectionType, ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) {
TypeName[] typeArguments = new TypeName[]{};
typeArguments = Arrays.stream(typeVariables).toList().toArray(typeArguments);
TypeSpec.Builder builder = TypeSpec.classBuilder(className)
.addAnnotation(generatedRecordBuilderAnnotation)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.superclass(ParameterizedTypeName.get(mutableCollectionType, typeArguments))
.addTypeVariables(Arrays.asList(typeVariables))
.addMethod(MethodSpec.constructorBuilder().addAnnotation(generatedRecordBuilderAnnotation).addStatement("super()").build())
.addMethod(MethodSpec.constructorBuilder().addAnnotation(generatedRecordBuilderAnnotation).addParameter(parameterizedType, "o").addStatement("super(o)").build());
if (addClassRetainedGenerated) {
builder.addAnnotation(recordBuilderGeneratedAnnotation);
}
return builder.build();
}
private MethodSpec buildCollectionsShimMethod() {
var code = CodeBlock.builder()
.add("if (o instanceof Set) {\n")
.indent()
.addStatement("return $T.copyOf(o)", setType)
.addStatement("return $T.copyOf(o)", setTypeName)
.unindent()
.addStatement("}")
.addStatement("return (o != null) ? $T.copyOf(o) : $T.of()", listType, listType)
.addStatement("return (o != null) ? $T.copyOf(o) : $T.of()", listTypeName, listTypeName)
.build();
return MethodSpec.methodBuilder(collectionShimName)
.addAnnotation(generatedRecordBuilderAnnotation)

View File

@@ -15,8 +15,12 @@
*/
package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.*;
import io.soabase.recordbuilder.core.RecordBuilder;
import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.EXCLUDE_WILDCARD_TYPES;
import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.STANDARD_FOR_SETTER;
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.recordBuilderGeneratedAnnotation;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
@@ -29,11 +33,8 @@ import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.*;
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.recordBuilderGeneratedAnnotation;
import com.squareup.javapoet.*;
import io.soabase.recordbuilder.core.RecordBuilder;
class InternalRecordBuilderProcessor {
private final RecordBuilder.Options metaData;
@@ -110,6 +111,7 @@ class InternalRecordBuilderProcessor {
collectionMetaData.ifPresent(meta -> add1CollectionBuilders(meta, component));
});
collectionBuilderUtils.addShims(builder);
collectionBuilderUtils.addMutableMakers(builder);
builderType = builder.build();
}
@@ -331,7 +333,7 @@ class InternalRecordBuilderProcessor {
}
RecordClassType parameterComponent = recordComponents.get(parameterIndex);
if (parameterIndex == index) {
collectionBuilderUtils.add(codeBlockBuilder, parameterComponent);
collectionBuilderUtils.addShimCall(codeBlockBuilder, parameterComponent);
} else {
codeBlockBuilder.add("$L()", prefixedName(parameterComponent, true));
}
@@ -415,9 +417,7 @@ class InternalRecordBuilderProcessor {
.addAnnotation(generatedRecordBuilderAnnotation);
recordComponents.forEach(component -> {
constructorBuilder.addParameter(component.typeName(), component.name());
var collectionMetaData = collectionBuilderUtils.singleItemsMetaData(component, STANDARD);
collectionMetaData.ifPresentOrElse(meta -> constructorBuilder.addStatement("this.$L = new $T<>($L)", component.name(), meta.singleItemCollectionClass(), component.name()),
() -> constructorBuilder.addStatement("this.$L = $L", component.name(), component.name()));
constructorBuilder.addStatement("this.$L = $L", component.name(), component.name());
});
builder.addMethod(constructorBuilder.build());
}
@@ -549,7 +549,7 @@ class InternalRecordBuilderProcessor {
var recordComponent = recordComponents.get(index);
if (collectionBuilderUtils.isImmutableCollection(recordComponent)) {
codeBuilder.add("$[$L = ", recordComponent.name());
collectionBuilderUtils.add(codeBuilder, recordComponents.get(index));
collectionBuilderUtils.addShimCall(codeBuilder, recordComponents.get(index));
codeBuilder.add(";\n$]");
}
});
@@ -616,6 +616,7 @@ class InternalRecordBuilderProcessor {
.addParameter(recordClassType.typeName(), "from")
.addStatement("this.from = from")
.addModifiers(Modifier.PRIVATE)
.addAnnotation(generatedRecordBuilderAnnotation)
.build();
fromWithClassBuilder.addMethod(constructorSpec);
@@ -626,6 +627,7 @@ class InternalRecordBuilderProcessor {
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addStatement("return from.$L()", component.name())
.addAnnotation(generatedRecordBuilderAnnotation)
.build();
fromWithClassBuilder.addMethod(methodSpec);
});
@@ -815,34 +817,34 @@ class InternalRecordBuilderProcessor {
For a single map record component, add a methods similar to:
public T addP(K key, V value) {
if (this.p == null) {
this.p = new HashMap<>();
}
this.p = __ensureMapMutable(p);
this.p.put(key, value);
return this;
}
public T addP(Stream<? extends Map.Entry<K, V> i) {
if (p == null) {
p = new HashMap<>();
}
this.p = __ensureMapMutable(p);
i.forEach(this.p::put);
return this;
}
public T addP(Iterable<? extends Map.Entry<K, V> i) {
if (p == null) {
p = new HashMap<>();
}
this.p = __ensureMapMutable(p);
i.forEach(this.p::put);
return this;
}
*/
for (var i = 0; i < 3; ++i) {
var codeBlockBuilder = CodeBlock.builder()
.beginControlFlow("if (this.$L == null)", component.name())
.addStatement("this.$L = new $T<>()", component.name(), HashMap.class)
.endControlFlow();
var codeBlockBuilder = CodeBlock.builder();
if (collectionBuilderUtils.isImmutableCollection(component)) {
codeBlockBuilder
.addStatement("this.$L = $L($L)", component.name(), collectionBuilderUtils.mutableMakerName(component), component.name());
} else {
codeBlockBuilder
.beginControlFlow("if (this.$L == null)", component.name())
.addStatement("this.$L = new $T<>()", component.name(), meta.singleItemCollectionClass())
.endControlFlow();
}
var methodSpecBuilder = MethodSpec.methodBuilder(metaData.singleItemBuilderPrefix() + capitalize(component.name()))
.addJavadoc("Add to the internally allocated {@code HashMap} for {@code $L}\n", component.name())
.addModifiers(Modifier.PUBLIC)
@@ -870,25 +872,19 @@ class InternalRecordBuilderProcessor {
For a single list or set record component, add methods similar to:
public T addP(I i) {
if (this.p == null) {
this.p = new ArrayList<>();
}
this.list = __ensureListMutable(list);
this.p.add(i);
return this;
}
public T addP(Stream<? extends I> i) {
if (this.p == null) {
this.p = new ArrayList<>();
}
this.list = __ensureListMutable(list);
this.p.addAll(i);
return this;
}
public T addP(Iterable<? extends I> i) {
if (this.p == null) {
this.p = new ArrayList<>();
}
this.list = __ensureListMutable(list);
this.p.addAll(i);
return this;
}
@@ -904,10 +900,17 @@ class InternalRecordBuilderProcessor {
var parameterClass = ClassName.get((i == 1) ? Stream.class : Iterable.class);
parameter = ParameterizedTypeName.get(parameterClass, WildcardTypeName.subtypeOf(meta.typeArguments().get(0)));
}
var codeBlockBuilder = CodeBlock.builder()
.beginControlFlow("if (this.$L == null)", component.name())
.addStatement("this.$L = new $T<>()", component.name(), meta.singleItemCollectionClass())
.endControlFlow()
var codeBlockBuilder = CodeBlock.builder();
if (collectionBuilderUtils.isImmutableCollection(component)) {
codeBlockBuilder
.addStatement("this.$L = $L($L)", component.name(), collectionBuilderUtils.mutableMakerName(component), component.name());
} else {
codeBlockBuilder
.beginControlFlow("if (this.$L == null)", component.name())
.addStatement("this.$L = new $T<>()", component.name(), meta.singleItemCollectionClass())
.endControlFlow();
}
codeBlockBuilder
.add(addClockBlock.build())
.addStatement("return this");
var methodSpecBuilder = MethodSpec.methodBuilder(metaData.singleItemBuilderPrefix() + capitalize(component.name()))
@@ -956,8 +959,8 @@ class InternalRecordBuilderProcessor {
var collectionMetaData = collectionBuilderUtils.singleItemsMetaData(component, STANDARD_FOR_SETTER);
var parameterSpecBuilder = collectionMetaData.map(meta -> {
CodeBlock.Builder codeSpec = CodeBlock.builder();
codeSpec.addStatement("this.$L = ($L != null) ? new $T<>($L) : null", component.name(), component.name(), meta.singleItemCollectionClass(), component.name());
methodSpec.addJavadoc("Re-create the internally allocated {@code $L} for {@code $L} by copying the argument\n", meta.singleItemCollectionClass().getSimpleName(), component.name())
codeSpec.addStatement("this.$L = $L($L)", component.name(), collectionBuilderUtils.shimName(component), component.name());
methodSpec.addJavadoc("Re-create the internally allocated {@code $T} for {@code $L} by copying the argument\n", component.typeName(), component.name())
.addCode(codeSpec.build());
return ParameterSpec.builder(meta.wildType(), component.name());
}).orElseGet(() -> {
@@ -991,12 +994,21 @@ class InternalRecordBuilderProcessor {
var parameterSpecBuilder = ParameterSpec.builder(type.valueType(), component.name());
methodSpec.addJavadoc("Set a new value for the {@code $L} record component in the builder\n", component.name())
.addStatement("this.$L = $T.of($L)", component.name(), type.typeName(), component.name());
.addStatement(getOptionalStatement(type), component.name(), type.typeName(), component.name());
addConstructorAnnotations(component, parameterSpecBuilder);
methodSpec.addStatement("return this").addParameter(parameterSpecBuilder.build());
builder.addMethod(methodSpec.build());
}
private String getOptionalStatement(OptionalType type) {
if(type.isOptional()) {
return "this.$L = $T.ofNullable($L)";
}
return "this.$L = $T.of($L)";
}
private List<TypeVariableName> typeVariablesWithReturn() {
var variables = new ArrayList<TypeVariableName>();
variables.add(rType);

View File

@@ -19,6 +19,7 @@ import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
@@ -58,4 +59,8 @@ public record OptionalType(TypeName typeName, TypeName valueType) {
}
return Optional.empty();
}
public boolean isOptional() {
return typeName.equals(optionalType);
}
}

View File

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

View File

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

View File

@@ -15,6 +15,8 @@
*/
package io.soabase.recordbuilder.test;
import javax.validation.constraints.NotNull;
import io.soabase.recordbuilder.core.RecordBuilder;
import java.util.Optional;
@@ -24,4 +26,4 @@ import java.util.OptionalLong;
@RecordBuilder.Options(emptyDefaultForOptional = true, addConcreteSettersForOptional = true)
@RecordBuilder
public record RecordWithOptional(Optional<String> value, Optional raw, OptionalInt i, OptionalLong l, OptionalDouble d) {}
public record RecordWithOptional(@NotNull Optional<String> value, Optional raw, OptionalInt i, OptionalLong l, OptionalDouble d) {}

View File

@@ -27,7 +27,7 @@ class TestCollections {
@Test
void testRecordBuilderOptionsCopied() {
try {
assertNotNull(CollectionInterfaceRecordBuilder.class.getDeclaredMethod("__list", List.class));
assertNotNull(CollectionInterfaceRecordBuilder.class.getDeclaredMethod("__list", Collection.class));
} catch (NoSuchMethodException e) {
Assertions.fail(e);
}

View File

@@ -0,0 +1,148 @@
/**
* 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;
import java.util.*;
public class TestImmutableCollections {
@Test
public void testImmutableListNotCopiedWhenNotChanged() {
var item = CollectionCopyingBuilder.<String>builder()
.addList("a")
.addList("b")
.addList("c")
.build();
Assertions.assertEquals(item.list(), List.of("a", "b", "c"));
var oldList = item.list();
var copy = item.with()
.count(1)
.build();
Assertions.assertSame(oldList, copy.list());
var otherCopy = item.with()
.count(2)
.build();
Assertions.assertSame(oldList, otherCopy.list());
}
@Test
public void testImmutableSetNotCopiedWhenNotChanged() {
var item = CollectionCopyingBuilder.<String>builder()
.addSet(Arrays.asList("1", "2", "3"))
.build();
Assertions.assertEquals(item.set(), Set.of("1", "2", "3"));
var oldSet = item.set();
var copy = item.with()
.count(1)
.build();
Assertions.assertSame(oldSet, copy.set());
var otherCopy = item.with()
.count(2)
.build();
Assertions.assertSame(oldSet, otherCopy.set());
}
@Test
public void testImmutableCollectionNotCopiedWhenNotChanged() {
var item = CollectionCopyingBuilder.<String>builder()
.collection(List.of("foo", "bar", "baz"))
.build();
Assertions.assertEquals(item.collection(), List.of("foo", "bar", "baz"));
var oldCollection = item.collection();
var copy = item.with()
.count(1)
.build();
Assertions.assertSame(oldCollection, copy.collection());
var otherCopy = item.with()
.count(2)
.build();
Assertions.assertSame(oldCollection, otherCopy.collection());
}
@Test
public void testImmutableMapNotCopiedWhenNotChanged() {
var item = CollectionCopyingBuilder.<String>builder()
.addMap(Instant.MAX, "future")
.addMap(Instant.MIN, "before")
.build();
Assertions.assertEquals(item.map(), Map.of(Instant.MAX, "future", Instant.MIN, "before"));
var oldMap = item.map();
var copy = item.with()
.count(1)
.build();
Assertions.assertSame(oldMap, copy.map());
var otherCopy = item.with()
.count(2)
.build();
Assertions.assertSame(oldMap, otherCopy.map());
}
@Test
void testSourceListNotModified() {
var item = new CollectionCopying<>(new ArrayList<>(), null, null, null, 0);
var modifiedItem = CollectionCopyingBuilder.builder(item)
.addList("a")
.build();
Assertions.assertEquals(modifiedItem.list(), List.of("a"));
Assertions.assertTrue(item.list().isEmpty());
}
@Test
void testSourceSetNotModified() {
var item = new CollectionCopying<>(null, new HashSet<>(), null, null, 0);
var modifiedItem = CollectionCopyingBuilder.builder(item)
.addSet("a")
.build();
Assertions.assertEquals(modifiedItem.set(), Set.of("a"));
Assertions.assertTrue(item.set().isEmpty());
}
@Test
void testSourceMapNotModified() {
var item = new CollectionCopying<>(null, null, new HashMap<>(), null, 0);
var modifiedItem = CollectionCopyingBuilder.builder(item)
.addMap(Instant.MIN, "a")
.build();
Assertions.assertEquals(modifiedItem.map(), Map.of(Instant.MIN, "a"));
Assertions.assertTrue(item.map().isEmpty());
}
}

View File

@@ -15,14 +15,14 @@
*/
package io.soabase.recordbuilder.test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class TestOptional {
@Test
void testDefaultEmpty() {
@@ -65,4 +65,19 @@ class TestOptional {
Assertions.assertEquals(OptionalLong.of(424242L), record.l());
Assertions.assertEquals(OptionalDouble.of(42.42), record.d());
}
@Test
void shouldAcceptNullForOptionalRawSetter() {
// given
String value = null;
// when
var record = RecordWithOptionalBuilder.builder()
.value(value)
.build();
// then
Assertions.assertEquals(Optional.empty(), record.value());
}
}

View File

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