From 583619907cdcfdb96ca8fcffbb4ab370e1c4faf5 Mon Sep 17 00:00:00 2001 From: pawel_labaj Date: Wed, 21 Jun 2023 13:26:48 +0200 Subject: [PATCH] Introduced useUnmodifiableCollections option --- .../recordbuilder/core/RecordBuilder.java | 18 +++ .../processor/CollectionBuilderUtils.java | 124 +++++++++++++----- .../processor/RecordBuilderProcessor.java | 17 +++ ...ava => UnmodifiableCollectionsRecord.java} | 7 +- .../test/TestOrderedSetsBuilder.java | 40 ------ .../TestUnmodifiableCollectionsBuilder.java | 72 ++++++++++ 6 files changed, 201 insertions(+), 77 deletions(-) rename record-builder-test/src/main/java/io/soabase/recordbuilder/test/{OrderedSetRecord.java => UnmodifiableCollectionsRecord.java} (72%) delete mode 100644 record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestOrderedSetsBuilder.java create mode 100644 record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestUnmodifiableCollectionsBuilder.java diff --git a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java index e5209633..34c6c0a6 100644 --- a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java +++ b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java @@ -183,9 +183,27 @@ * {@link java.util.Map} and {@link java.util.Collection}. When the record is built, any components of these * types are passed through an added shim method that uses the corresponding immutable collection (e.g. * {@code List.copyOf(o)}) or an empty immutable collection if the component is {@code null}. + * + * @see #useUnmodifiableCollections() */ boolean useImmutableCollections() default false; + /** + * Adds special handling for record components of type: {@link java.util.List}, {@link java.util.Set}, + * {@link java.util.Map} and {@link java.util.Collection}. When the record is built, any components of these + * types are passed through an added shim method that uses the corresponding unmodifiable collection (e.g. + * {@code Collections.unmodifiableList(o)}) or an empty immutable collection if the component is {@code null}. + * + *

+ * For backward compatibility, when {@link #useImmutableCollections()} returns {@code true}, this property is + * ignored. + * + * @see #useImmutableCollections() + * + * @since 37 + */ + boolean useUnmodifiableCollections() default false; + /** * When enabled, collection types ({@code List}, {@code Set} and {@code Map}) are handled specially. The setters * for these types now create an internal collection and items are added to that collection. Additionally, diff --git a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/CollectionBuilderUtils.java b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/CollectionBuilderUtils.java index 9452f48e..fbb32f86 100644 --- a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/CollectionBuilderUtils.java +++ b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/CollectionBuilderUtils.java @@ -26,6 +26,7 @@ class CollectionBuilderUtils { private final boolean useImmutableCollections; + private final boolean useUnmodifiableCollections; private final boolean addSingleItemCollectionBuilders; private final boolean addClassRetainedGenerated; private final String listShimName; @@ -81,6 +82,7 @@ class CollectionBuilderUtils { CollectionBuilderUtils(List recordComponents, RecordBuilder.Options metaData) { useImmutableCollections = metaData.useImmutableCollections(); + useUnmodifiableCollections = !useImmutableCollections && metaData.useUnmodifiableCollections(); addSingleItemCollectionBuilders = metaData.addSingleItemCollectionBuilders(); addClassRetainedGenerated = metaData.addClassRetainedGenerated(); @@ -130,25 +132,25 @@ Optional singleItemsMetaData(RecordClassType component, Sin var hasWildcardTypeArguments = hasWildcardTypeArguments(parameterizedTypeName, typeArgumentQty); if (collectionClass != null) { return switch (mode) { - case STANDARD -> singleItemsMetaDataWithWildType(parameterizedTypeName, collectionClass, wildcardClass, - typeArgumentQty); + case STANDARD -> singleItemsMetaDataWithWildType(parameterizedTypeName, collectionClass, wildcardClass, + typeArgumentQty); - case STANDARD_FOR_SETTER -> { - if (hasWildcardTypeArguments) { - yield Optional.of(new SingleItemsMetaData(collectionClass, parameterizedTypeName.typeArguments, - component.typeName())); + case STANDARD_FOR_SETTER -> { + if (hasWildcardTypeArguments) { + yield Optional.of(new SingleItemsMetaData(collectionClass, parameterizedTypeName.typeArguments, + component.typeName())); + } + yield singleItemsMetaDataWithWildType(parameterizedTypeName, collectionClass, wildcardClass, + typeArgumentQty); } - yield singleItemsMetaDataWithWildType(parameterizedTypeName, collectionClass, wildcardClass, - typeArgumentQty); - } - case EXCLUDE_WILDCARD_TYPES -> { - if (hasWildcardTypeArguments) { - yield Optional.empty(); + case EXCLUDE_WILDCARD_TYPES -> { + if (hasWildcardTypeArguments) { + yield Optional.empty(); + } + yield singleItemsMetaDataWithWildType(parameterizedTypeName, collectionClass, wildcardClass, + typeArgumentQty); } - yield singleItemsMetaDataWithWildType(parameterizedTypeName, collectionClass, wildcardClass, - typeArgumentQty); - } }; } } @@ -156,8 +158,8 @@ yield singleItemsMetaDataWithWildType(parameterizedTypeName, collectionClass, wi } boolean isImmutableCollection(RecordClassType component) { - return useImmutableCollections && (isList(component) || isMap(component) || isSet(component) - || component.rawTypeName().equals(collectionTypeName)); + return (useImmutableCollections || useUnmodifiableCollections) + && (isList(component) || isMap(component) || isSet(component) || isCollection(component)); } boolean isList(RecordClassType component) { @@ -172,8 +174,12 @@ boolean isSet(RecordClassType component) { return component.rawTypeName().equals(setTypeName); } + private boolean isCollection(RecordClassType component) { + return component.rawTypeName().equals(collectionTypeName); + } + void addShimCall(CodeBlock.Builder builder, RecordClassType component) { - if (useImmutableCollections) { + if (useImmutableCollections || useUnmodifiableCollections) { if (isList(component)) { needsListShim = true; needsListMutableMaker = true; @@ -186,7 +192,7 @@ void addShimCall(CodeBlock.Builder builder, RecordClassType component) { needsSetShim = true; needsSetMutableMaker = true; builder.add("$L($L)", setShimName, component.name()); - } else if (component.rawTypeName().equals(collectionTypeName)) { + } else if (isCollection(component)) { needsCollectionShim = true; builder.add("$L($L)", collectionShimName, component.name()); } else { @@ -204,7 +210,7 @@ String shimName(RecordClassType component) { return mapShimName; } else if (isSet(component)) { return setShimName; - } else if (component.rawTypeName().equals(collectionTypeName)) { + } else if (isCollection(component)) { return collectionShimName; } else { throw new IllegalArgumentException(component + " is not a supported collection type"); @@ -224,7 +230,7 @@ String mutableMakerName(RecordClassType component) { } void addShims(TypeSpec.Builder builder) { - if (!useImmutableCollections) { + if (!useImmutableCollections && !useUnmodifiableCollections) { return; } @@ -244,7 +250,7 @@ void addShims(TypeSpec.Builder builder) { } void addMutableMakers(TypeSpec.Builder builder) { - if (!useImmutableCollections) { + if (!useImmutableCollections && !useUnmodifiableCollections) { return; } @@ -266,7 +272,7 @@ void addMutableMakers(TypeSpec.Builder builder) { } private Optional singleItemsMetaDataWithWildType(ParameterizedTypeName parameterizedTypeName, - Class collectionClass, ClassName wildcardClass, int typeArgumentQty) { + Class collectionClass, ClassName wildcardClass, int typeArgumentQty) { TypeName wildType; if (typeArgumentQty == 1) { wildType = ParameterizedTypeName.get(wildcardClass, @@ -299,13 +305,8 @@ private String disambiguateGeneratedMethodName(List recordCompo } private MethodSpec buildShimMethod(String name, TypeName mainType, Class abstractType, - ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) { - CodeBlock code; - if (mainType.equals(setTypeName)) { - code = CodeBlock.of("return (o != null) ? $T.unmodifiableSet(($T) o) : $T.of()", collectionsTypeName, parameterizedType, mainType); - } else { - code = CodeBlock.of("return (o != null) ? $T.copyOf(o) : $T.of()", mainType, mainType); - } + ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) { + var code = buildShimMethodBody(mainType, parameterizedType); TypeName[] wildCardTypeArguments = parameterizedType.typeArguments.stream().map(WildcardTypeName::subtypeOf) .toList().toArray(new TypeName[0]); @@ -315,8 +316,45 @@ private MethodSpec buildShimMethod(String name, TypeName mainType, Class abst .returns(parameterizedType).addParameter(extendedParameterizedType, "o").addStatement(code).build(); } + private CodeBlock buildShimMethodBody(TypeName mainType, ParameterizedTypeName parameterizedType) { + if (!useUnmodifiableCollections) { + return CodeBlock.of("return (o != null) ? $T.copyOf(o) : $T.of()", mainType, mainType); + } + + if (mainType.equals(listTypeName)) { + return CodeBlock.of("return (o != null) ? $T.<$T>unmodifiableList(($T) o) : $T.<$T>emptyList()", + collectionsTypeName, + tType, + parameterizedType, + collectionsTypeName, + tType); + } + + if (mainType.equals(setTypeName)) { + return CodeBlock.of("return (o != null) ? $T.<$T>unmodifiableSet(($T) o) : $T.<$T>emptySet()", + collectionsTypeName, + tType, + parameterizedType, + collectionsTypeName, + tType); + } + + if (mainType.equals(mapTypeName)) { + return CodeBlock.of("return (o != null) ? $T.<$T, $T>unmodifiableMap(($T) o) : $T.<$T, $T>emptyMap()", + collectionsTypeName, + kType, + vType, + parameterizedType, + collectionsTypeName, + kType, + vType); + } + + throw new IllegalStateException("Cannot build shim method for" + mainType); + } + private MethodSpec buildMutableMakerMethod(String name, String mutableCollectionType, - ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) { + 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); @@ -327,7 +365,7 @@ private MethodSpec buildMutableMakerMethod(String name, String mutableCollection } private TypeSpec buildMutableCollectionSubType(String className, ClassName mutableCollectionType, - ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) { + ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) { TypeName[] typeArguments = new TypeName[] {}; typeArguments = Arrays.stream(typeVariables).toList().toArray(typeArguments); @@ -348,12 +386,28 @@ private TypeSpec buildMutableCollectionSubType(String className, ClassName mutab } private MethodSpec buildCollectionsShimMethod() { - var code = CodeBlock.builder().add("if (o instanceof Set) {\n").indent() - .addStatement("return $T.copyOf(o)", setTypeName).unindent().addStatement("}") - .addStatement("return (o != null) ? $T.copyOf(o) : $T.of()", listTypeName, listTypeName).build(); + var code = buildCollectionShimMethodBody(); return MethodSpec.methodBuilder(collectionShimName).addAnnotation(generatedRecordBuilderAnnotation) .addModifiers(Modifier.PRIVATE, Modifier.STATIC).addTypeVariable(tType) .returns(parameterizedCollectionType).addParameter(parameterizedCollectionType, "o").addCode(code) .build(); } + + private CodeBlock buildCollectionShimMethodBody() { + if (!useUnmodifiableCollections) { + return CodeBlock.builder().add("if (o instanceof Set) {\n").indent() + .addStatement("return $T.copyOf(o)", setTypeName).unindent().addStatement("}") + .addStatement("return (o != null) ? $T.copyOf(o) : $T.of()", listTypeName, listTypeName).build(); + } + + return CodeBlock.builder() + .beginControlFlow("if (o instanceof $T)", listType) + .addStatement("return $T.<$T>unmodifiableList(($T) o)", collectionsTypeName, tType, parameterizedListType) + .endControlFlow() + .beginControlFlow("if (o instanceof $T)", setType) + .addStatement("return $T.<$T>unmodifiableSet(($T) o)", collectionsTypeName, tType, parameterizedSetType) + .endControlFlow() + .addStatement("return $T.<$T>emptyList()", collectionsTypeName, tType) + .build(); + } } diff --git a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/RecordBuilderProcessor.java b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/RecordBuilderProcessor.java index 322867ce..c3381095 100644 --- a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/RecordBuilderProcessor.java +++ b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/RecordBuilderProcessor.java @@ -164,6 +164,9 @@ private void processRecordInterface(TypeElement element, boolean addRecordBuilde "RecordInterface only valid for interfaces.", element); return; } + + validateMetaData(metaData, element); + var internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, addRecordBuilder, metaData, packageName, fromTemplate); if (!internalProcessor.isValid()) { @@ -184,11 +187,25 @@ private void processRecordBuilder(TypeElement record, RecordBuilder.Options meta record); return; } + + validateMetaData(metaData, record); + var internalProcessor = new InternalRecordBuilderProcessor(processingEnv, record, metaData, packageName); writeRecordBuilderJavaFile(record, internalProcessor.packageName(), internalProcessor.builderClassType(), internalProcessor.builderType(), metaData); } + private void validateMetaData(RecordBuilder.Options metaData, Element record) { + var useImmutableCollections = metaData.useImmutableCollections(); + var useUnmodifiableCollections = metaData.useUnmodifiableCollections(); + + if (useImmutableCollections && useUnmodifiableCollections) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, + "Options.useUnmodifiableCollections property is ignored as Options.useImmutableCollections is set to true", + record); + } + } + private void writeRecordBuilderJavaFile(TypeElement record, String packageName, ClassType builderClassType, TypeSpec builderType, RecordBuilder.Options metaData) { // produces the Java file diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/OrderedSetRecord.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/UnmodifiableCollectionsRecord.java similarity index 72% rename from record-builder-test/src/main/java/io/soabase/recordbuilder/test/OrderedSetRecord.java rename to record-builder-test/src/main/java/io/soabase/recordbuilder/test/UnmodifiableCollectionsRecord.java index da1c713d..396f613e 100644 --- a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/OrderedSetRecord.java +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/UnmodifiableCollectionsRecord.java @@ -17,9 +17,12 @@ import io.soabase.recordbuilder.core.RecordBuilder; +import java.util.Collection; +import java.util.List; +import java.util.Map; import java.util.Set; @RecordBuilder -@RecordBuilder.Options(useImmutableCollections = true) -record OrderedSetRecord(Set orderedSet) { +@RecordBuilder.Options(useUnmodifiableCollections = true) +record UnmodifiableCollectionsRecord(List aList, Set orderedSet, Map orderedMap, Collection aCollection) { } diff --git a/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestOrderedSetsBuilder.java b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestOrderedSetsBuilder.java deleted file mode 100644 index caca475e..00000000 --- a/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestOrderedSetsBuilder.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2019 The original author or authors - * - * 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.Test; - -import java.util.LinkedHashSet; - -import static org.assertj.core.api.Assertions.assertThat; - -public class TestOrderedSetsBuilder { - - @Test - void shouldKeepOrderInSetIfProvided() { - // given - var orderedSet = new LinkedHashSet(); - orderedSet.add("C"); - orderedSet.add("B"); - orderedSet.add("A"); - - // when - var record = OrderedSetRecordBuilder.builder().orderedSet(orderedSet).build(); - - // then - assertThat(record.orderedSet()).containsExactly("C", "B", "A"); - } -} diff --git a/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestUnmodifiableCollectionsBuilder.java b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestUnmodifiableCollectionsBuilder.java new file mode 100644 index 00000000..93f32967 --- /dev/null +++ b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestUnmodifiableCollectionsBuilder.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 The original author or authors + * + * 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.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import static java.util.Map.entry; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class TestUnmodifiableCollectionsBuilder { + + @Test + void shouldWrapCollectionsWithUnmodifiableView() { + // given + var list = new ArrayList(); + list.add(2); + list.add(1); + list.add(0); + + var orderedSet = new LinkedHashSet(); + orderedSet.add("C"); + orderedSet.add("B"); + orderedSet.add("A"); + + var orderedMap = new LinkedHashMap(); + orderedMap.put("C", 2); + orderedMap.put("B", 1); + orderedMap.put("A", 0); + + var collection = new HashSet(); + collection.add("C"); + collection.add("B"); + collection.add("A"); + + // when + var record = UnmodifiableCollectionsRecordBuilder.builder().aList(list).orderedSet(orderedSet) + .orderedMap(orderedMap).aCollection(collection).build(); + + // then + assertAll(() -> assertThrows(UnsupportedOperationException.class, () -> record.aList().add(9)), + () -> assertThat(record.aList()).containsExactly(2, 1, 0), + () -> assertThrows(UnsupportedOperationException.class, () -> record.orderedSet().add("newElement")), + () -> assertThat(record.orderedSet()).containsExactly("C", "B", "A"), + () -> assertThrows(UnsupportedOperationException.class, () -> record.orderedMap().put("newElement", 9)), + () -> assertThat(record.orderedMap()).containsExactly(entry("C", 2), entry("B", 1), entry("A", 0)), + () -> assertThrows(UnsupportedOperationException.class, () -> record.aCollection().add("newElement")), + () -> assertThat(record.aCollection()).containsExactlyInAnyOrder("C", "B", "A")); + } +}