Performance feature: copy collections only when they were changed. Fixes #114. (#118)

This commit is contained in:
Johannes
2022-06-19 22:17:09 +02:00
committed by GitHub
parent b435b5d3fd
commit aa072af8e1
6 changed files with 382 additions and 64 deletions

View File

@@ -252,6 +252,24 @@ public @interface RecordBuilder {
* without {@code new}. * without {@code new}.
*/ */
boolean addStaticBuilder() default true; 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) @Retention(RetentionPolicy.CLASS)

View File

@@ -22,24 +22,38 @@ import javax.lang.model.element.Modifier;
import java.util.*; import java.util.*;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation; import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.recordBuilderGeneratedAnnotation;
class CollectionBuilderUtils { class CollectionBuilderUtils {
private final boolean useImmutableCollections; private final boolean useImmutableCollections;
private final boolean addSingleItemCollectionBuilders; private final boolean addSingleItemCollectionBuilders;
private final boolean addClassRetainedGenerated;
private final String listShimName; private final String listShimName;
private final String mapShimName; private final String mapShimName;
private final String setShimName; private final String setShimName;
private final String collectionShimName; private final String collectionShimName;
private final String listMakerMethodName;
private final String mapMakerMethodName;
private final String setMakerMethodName;
private boolean needsListShim; private boolean needsListShim;
private boolean needsMapShim; private boolean needsMapShim;
private boolean needsSetShim; private boolean needsSetShim;
private boolean needsCollectionShim; private boolean needsCollectionShim;
private static final TypeName listType = TypeName.get(List.class); private boolean needsListMutableMaker;
private static final TypeName mapType = TypeName.get(Map.class); private boolean needsMapMutableMaker;
private static final TypeName setType = TypeName.get(Set.class); private boolean needsSetMutableMaker;
private static final TypeName collectionType = TypeName.get(Collection.class);
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 tType = TypeVariableName.get("T");
private static final TypeVariableName kType = TypeVariableName.get("K"); 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 parameterizedSetType = ParameterizedTypeName.get(ClassName.get(Set.class), tType);
private static final ParameterizedTypeName parameterizedCollectionType = ParameterizedTypeName.get(ClassName.get(Collection.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) { CollectionBuilderUtils(List<RecordClassType> recordComponents, RecordBuilder.Options metaData) {
useImmutableCollections = metaData.useImmutableCollections(); useImmutableCollections = metaData.useImmutableCollections();
addSingleItemCollectionBuilders = metaData.addSingleItemCollectionBuilders(); addSingleItemCollectionBuilders = metaData.addSingleItemCollectionBuilders();
addClassRetainedGenerated = metaData.addClassRetainedGenerated();
listShimName = adjustShimName(recordComponents, "__list", 0); listShimName = disambiguateGeneratedMethodName(recordComponents, "__list", 0);
mapShimName = adjustShimName(recordComponents, "__map", 0); mapShimName = disambiguateGeneratedMethodName(recordComponents, "__map", 0);
setShimName = adjustShimName(recordComponents, "__set", 0); setShimName = disambiguateGeneratedMethodName(recordComponents, "__set", 0);
collectionShimName = adjustShimName(recordComponents, "__collection", 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 { enum SingleItemsMetaDataMode {
@@ -65,7 +98,8 @@ class CollectionBuilderUtils {
EXCLUDE_WILDCARD_TYPES 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) { Optional<SingleItemsMetaData> singleItemsMetaData(RecordClassType component, SingleItemsMetaDataMode mode) {
if (addSingleItemCollectionBuilders && (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName)) { if (addSingleItemCollectionBuilders && (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName)) {
@@ -73,15 +107,15 @@ class CollectionBuilderUtils {
ClassName wildcardClass = null; ClassName wildcardClass = null;
int typeArgumentQty = 0; int typeArgumentQty = 0;
if (isList(component)) { if (isList(component)) {
collectionClass = ArrayList.class; collectionClass = mutableListType;
wildcardClass = ClassName.get(Collection.class); wildcardClass = ClassName.get(Collection.class);
typeArgumentQty = 1; typeArgumentQty = 1;
} else if (isSet(component)) { } else if (isSet(component)) {
collectionClass = HashSet.class; collectionClass = mutableSetType;
wildcardClass = ClassName.get(Collection.class); wildcardClass = ClassName.get(Collection.class);
typeArgumentQty = 1; typeArgumentQty = 1;
} else if (isMap(component)) { } else if (isMap(component)) {
collectionClass = HashMap.class; collectionClass = mutableMapType;
wildcardClass = (ClassName) component.rawTypeName(); wildcardClass = (ClassName) component.rawTypeName();
typeArgumentQty = 2; typeArgumentQty = 2;
} }
@@ -110,33 +144,36 @@ class CollectionBuilderUtils {
} }
boolean isImmutableCollection(RecordClassType component) { 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) { boolean isList(RecordClassType component) {
return component.rawTypeName().equals(listType); return component.rawTypeName().equals(listTypeName);
} }
boolean isMap(RecordClassType component) { boolean isMap(RecordClassType component) {
return component.rawTypeName().equals(mapType); return component.rawTypeName().equals(mapTypeName);
} }
boolean isSet(RecordClassType component) { 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 (useImmutableCollections) {
if (isList(component)) { if (isList(component)) {
needsListShim = true; needsListShim = true;
needsListMutableMaker = true;
builder.add("$L($L)", listShimName, component.name()); builder.add("$L($L)", listShimName, component.name());
} else if (isMap(component)) { } else if (isMap(component)) {
needsMapShim = true; needsMapShim = true;
needsMapMutableMaker = true;
builder.add("$L($L)", mapShimName, component.name()); builder.add("$L($L)", mapShimName, component.name());
} else if (isSet(component)) { } else if (isSet(component)) {
needsSetShim = true; needsSetShim = true;
needsSetMutableMaker = true;
builder.add("$L($L)", setShimName, component.name()); builder.add("$L($L)", setShimName, component.name());
} else if (component.rawTypeName().equals(collectionType)) { } else if (component.rawTypeName().equals(collectionTypeName)) {
needsCollectionShim = true; needsCollectionShim = true;
builder.add("$L($L)", collectionShimName, component.name()); builder.add("$L($L)", collectionShimName, component.name());
} else { } 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) { void addShims(TypeSpec.Builder builder) {
if (!useImmutableCollections) { if (!useImmutableCollections) {
return; return;
} }
if (needsListShim) { if (needsListShim) {
builder.addMethod(buildMethod(listShimName, listType, parameterizedListType, tType)); builder.addMethod(buildShimMethod(listShimName, listTypeName, collectionType, parameterizedListType, tType));
} }
if (needsSetShim) { if (needsSetShim) {
builder.addMethod(buildMethod(setShimName, setType, parameterizedSetType, tType)); builder.addMethod(buildShimMethod(setShimName, setTypeName, collectionType, parameterizedSetType, tType));
} }
if (needsMapShim) { if (needsMapShim) {
builder.addMethod(buildMethod(mapShimName, mapType, parameterizedMapType, kType, vType)); builder.addMethod(buildShimMethod(mapShimName, mapTypeName, mapType, parameterizedMapType, kType, vType));
} }
if (needsCollectionShim) { 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; 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); var name = (index == 0) ? baseName : (baseName + index);
if (recordComponents.stream().anyMatch(component -> component.name().equals(name))) { if (recordComponents.stream().anyMatch(component -> component.name().equals(name))) {
return adjustShimName(recordComponents, baseName, index + 1); return disambiguateGeneratedMethodName(recordComponents, baseName, index + 1);
} }
return name; 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); 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) return MethodSpec.methodBuilder(name)
.addAnnotation(generatedRecordBuilderAnnotation) .addAnnotation(generatedRecordBuilderAnnotation)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC) .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.addTypeVariables(Arrays.asList(typeVariables)) .addTypeVariables(Arrays.asList(typeVariables))
.returns(parameterizedType) .returns(parameterizedType)
.addParameter(parameterizedType, "o") .addParameter(parameterizedType, "o")
.addStatement(code) .addStatement(nullCase)
.addStatement(isMutableCase)
.addStatement(defaultCase)
.build(); .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() var code = CodeBlock.builder()
.add("if (o instanceof Set) {\n") .add("if (o instanceof Set) {\n")
.indent() .indent()
.addStatement("return $T.copyOf(o)", setType) .addStatement("return $T.copyOf(o)", setTypeName)
.unindent() .unindent()
.addStatement("}") .addStatement("}")
.addStatement("return (o != null) ? $T.copyOf(o) : $T.of()", listType, listType) .addStatement("return (o != null) ? $T.copyOf(o) : $T.of()", listTypeName, listTypeName)
.build(); .build();
return MethodSpec.methodBuilder(collectionShimName) return MethodSpec.methodBuilder(collectionShimName)
.addAnnotation(generatedRecordBuilderAnnotation) .addAnnotation(generatedRecordBuilderAnnotation)

View File

@@ -16,7 +16,6 @@
package io.soabase.recordbuilder.processor; package io.soabase.recordbuilder.processor;
import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.EXCLUDE_WILDCARD_TYPES; import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.EXCLUDE_WILDCARD_TYPES;
import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.STANDARD;
import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.STANDARD_FOR_SETTER; 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.getBuilderName;
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName; import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
@@ -112,6 +111,7 @@ class InternalRecordBuilderProcessor {
collectionMetaData.ifPresent(meta -> add1CollectionBuilders(meta, component)); collectionMetaData.ifPresent(meta -> add1CollectionBuilders(meta, component));
}); });
collectionBuilderUtils.addShims(builder); collectionBuilderUtils.addShims(builder);
collectionBuilderUtils.addMutableMakers(builder);
builderType = builder.build(); builderType = builder.build();
} }
@@ -333,7 +333,7 @@ class InternalRecordBuilderProcessor {
} }
RecordClassType parameterComponent = recordComponents.get(parameterIndex); RecordClassType parameterComponent = recordComponents.get(parameterIndex);
if (parameterIndex == index) { if (parameterIndex == index) {
collectionBuilderUtils.add(codeBlockBuilder, parameterComponent); collectionBuilderUtils.addShimCall(codeBlockBuilder, parameterComponent);
} else { } else {
codeBlockBuilder.add("$L()", prefixedName(parameterComponent, true)); codeBlockBuilder.add("$L()", prefixedName(parameterComponent, true));
} }
@@ -417,9 +417,7 @@ class InternalRecordBuilderProcessor {
.addAnnotation(generatedRecordBuilderAnnotation); .addAnnotation(generatedRecordBuilderAnnotation);
recordComponents.forEach(component -> { recordComponents.forEach(component -> {
constructorBuilder.addParameter(component.typeName(), component.name()); constructorBuilder.addParameter(component.typeName(), component.name());
var collectionMetaData = collectionBuilderUtils.singleItemsMetaData(component, STANDARD); constructorBuilder.addStatement("this.$L = $L", component.name(), component.name());
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()));
}); });
builder.addMethod(constructorBuilder.build()); builder.addMethod(constructorBuilder.build());
} }
@@ -551,7 +549,7 @@ class InternalRecordBuilderProcessor {
var recordComponent = recordComponents.get(index); var recordComponent = recordComponents.get(index);
if (collectionBuilderUtils.isImmutableCollection(recordComponent)) { if (collectionBuilderUtils.isImmutableCollection(recordComponent)) {
codeBuilder.add("$[$L = ", recordComponent.name()); codeBuilder.add("$[$L = ", recordComponent.name());
collectionBuilderUtils.add(codeBuilder, recordComponents.get(index)); collectionBuilderUtils.addShimCall(codeBuilder, recordComponents.get(index));
codeBuilder.add(";\n$]"); codeBuilder.add(";\n$]");
} }
}); });
@@ -819,34 +817,34 @@ class InternalRecordBuilderProcessor {
For a single map record component, add a methods similar to: For a single map record component, add a methods similar to:
public T addP(K key, V value) { public T addP(K key, V value) {
if (this.p == null) { this.p = __ensureMapMutable(p);
this.p = new HashMap<>();
}
this.p.put(key, value); this.p.put(key, value);
return this; return this;
} }
public T addP(Stream<? extends Map.Entry<K, V> i) { public T addP(Stream<? extends Map.Entry<K, V> i) {
if (p == null) { this.p = __ensureMapMutable(p);
p = new HashMap<>();
}
i.forEach(this.p::put); i.forEach(this.p::put);
return this; return this;
} }
public T addP(Iterable<? extends Map.Entry<K, V> i) { public T addP(Iterable<? extends Map.Entry<K, V> i) {
if (p == null) { this.p = __ensureMapMutable(p);
p = new HashMap<>();
}
i.forEach(this.p::put); i.forEach(this.p::put);
return this; return this;
} }
*/ */
for (var i = 0; i < 3; ++i) { for (var i = 0; i < 3; ++i) {
var codeBlockBuilder = CodeBlock.builder() var codeBlockBuilder = CodeBlock.builder();
.beginControlFlow("if (this.$L == null)", component.name()) if (collectionBuilderUtils.isImmutableCollection(component)) {
.addStatement("this.$L = new $T<>()", component.name(), HashMap.class) codeBlockBuilder
.endControlFlow(); .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())) var methodSpecBuilder = MethodSpec.methodBuilder(metaData.singleItemBuilderPrefix() + capitalize(component.name()))
.addJavadoc("Add to the internally allocated {@code HashMap} for {@code $L}\n", component.name()) .addJavadoc("Add to the internally allocated {@code HashMap} for {@code $L}\n", component.name())
.addModifiers(Modifier.PUBLIC) .addModifiers(Modifier.PUBLIC)
@@ -874,25 +872,19 @@ class InternalRecordBuilderProcessor {
For a single list or set record component, add methods similar to: For a single list or set record component, add methods similar to:
public T addP(I i) { public T addP(I i) {
if (this.p == null) { this.list = __ensureListMutable(list);
this.p = new ArrayList<>();
}
this.p.add(i); this.p.add(i);
return this; return this;
} }
public T addP(Stream<? extends I> i) { public T addP(Stream<? extends I> i) {
if (this.p == null) { this.list = __ensureListMutable(list);
this.p = new ArrayList<>();
}
this.p.addAll(i); this.p.addAll(i);
return this; return this;
} }
public T addP(Iterable<? extends I> i) { public T addP(Iterable<? extends I> i) {
if (this.p == null) { this.list = __ensureListMutable(list);
this.p = new ArrayList<>();
}
this.p.addAll(i); this.p.addAll(i);
return this; return this;
} }
@@ -908,10 +900,17 @@ class InternalRecordBuilderProcessor {
var parameterClass = ClassName.get((i == 1) ? Stream.class : Iterable.class); var parameterClass = ClassName.get((i == 1) ? Stream.class : Iterable.class);
parameter = ParameterizedTypeName.get(parameterClass, WildcardTypeName.subtypeOf(meta.typeArguments().get(0))); parameter = ParameterizedTypeName.get(parameterClass, WildcardTypeName.subtypeOf(meta.typeArguments().get(0)));
} }
var codeBlockBuilder = CodeBlock.builder() var codeBlockBuilder = CodeBlock.builder();
.beginControlFlow("if (this.$L == null)", component.name()) if (collectionBuilderUtils.isImmutableCollection(component)) {
.addStatement("this.$L = new $T<>()", component.name(), meta.singleItemCollectionClass()) codeBlockBuilder
.endControlFlow() .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()) .add(addClockBlock.build())
.addStatement("return this"); .addStatement("return this");
var methodSpecBuilder = MethodSpec.methodBuilder(metaData.singleItemBuilderPrefix() + capitalize(component.name())) var methodSpecBuilder = MethodSpec.methodBuilder(metaData.singleItemBuilderPrefix() + capitalize(component.name()))
@@ -960,8 +959,8 @@ class InternalRecordBuilderProcessor {
var collectionMetaData = collectionBuilderUtils.singleItemsMetaData(component, STANDARD_FOR_SETTER); var collectionMetaData = collectionBuilderUtils.singleItemsMetaData(component, STANDARD_FOR_SETTER);
var parameterSpecBuilder = collectionMetaData.map(meta -> { var parameterSpecBuilder = collectionMetaData.map(meta -> {
CodeBlock.Builder codeSpec = CodeBlock.builder(); CodeBlock.Builder codeSpec = CodeBlock.builder();
codeSpec.addStatement("this.$L = ($L != null) ? new $T<>($L) : null", component.name(), component.name(), meta.singleItemCollectionClass(), component.name()); codeSpec.addStatement("this.$L = $L($L)", component.name(), collectionBuilderUtils.shimName(component), component.name());
methodSpec.addJavadoc("Re-create the internally allocated {@code $L} for {@code $L} by copying the argument\n", meta.singleItemCollectionClass().getSimpleName(), 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()); .addCode(codeSpec.build());
return ParameterSpec.builder(meta.wildType(), component.name()); return ParameterSpec.builder(meta.wildType(), component.name());
}).orElseGet(() -> { }).orElseGet(() -> {

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

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