Skip to content

Commit

Permalink
Fixes Randgalt/record-builder/#153 (#154)
Browse files Browse the repository at this point in the history
  • Loading branch information
pawellabaj authored Jul 3, 2023
1 parent 1b22341 commit c6cf239
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 11 deletions.
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@

<javapoet-version>1.12.1</javapoet-version>
<junit-jupiter-version>5.5.2</junit-jupiter-version>
<assertj-core.version>3.24.2</assertj-core.version>
<asm-version>7.2</asm-version>
<validation-api-version>2.0.1.Final</validation-api-version>
<hibernate-validator-version>6.0.20.Final</hibernate-validator-version>
Expand Down Expand Up @@ -151,6 +152,12 @@
<version>${junit-jupiter-version}</version>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj-core.version}</version>
</dependency>

<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*
* <p>
* 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -50,10 +51,12 @@ class CollectionBuilderUtils {
private static final Class<?> mapType = Map.class;
private static final Class<?> setType = Set.class;
private static final Class<?> collectionType = Collection.class;
private static final Class<?> collectionsType = Collections.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 TypeName collectionsTypeName = TypeName.get(collectionsType);

private static final TypeVariableName tType = TypeVariableName.get("T");
private static final TypeVariableName kType = TypeVariableName.get("K");
Expand All @@ -79,6 +82,7 @@ class CollectionBuilderUtils {

CollectionBuilderUtils(List<RecordClassType> recordComponents, RecordBuilder.Options metaData) {
useImmutableCollections = metaData.useImmutableCollections();
useUnmodifiableCollections = !useImmutableCollections && metaData.useUnmodifiableCollections();
addSingleItemCollectionBuilders = metaData.addSingleItemCollectionBuilders();
addClassRetainedGenerated = metaData.addClassRetainedGenerated();

Expand Down Expand Up @@ -154,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) {
Expand All @@ -170,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;
Expand All @@ -184,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 {
Expand All @@ -202,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");
Expand All @@ -222,7 +230,7 @@ String mutableMakerName(RecordClassType component) {
}

void addShims(TypeSpec.Builder builder) {
if (!useImmutableCollections) {
if (!useImmutableCollections && !useUnmodifiableCollections) {
return;
}

Expand All @@ -242,7 +250,7 @@ void addShims(TypeSpec.Builder builder) {
}

void addMutableMakers(TypeSpec.Builder builder) {
if (!useImmutableCollections) {
if (!useImmutableCollections && !useUnmodifiableCollections) {
return;
}

Expand Down Expand Up @@ -298,7 +306,8 @@ private String disambiguateGeneratedMethodName(List<RecordClassType> recordCompo

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 = buildShimMethodBody(mainType, parameterizedType);

TypeName[] wildCardTypeArguments = parameterizedType.typeArguments.stream().map(WildcardTypeName::subtypeOf)
.toList().toArray(new TypeName[0]);
var extendedParameterizedType = ParameterizedTypeName.get(ClassName.get(abstractType), wildCardTypeArguments);
Expand All @@ -307,6 +316,29 @@ 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) {
var nullCase = CodeBlock.of("if (o == null) return new $L<>()", mutableCollectionType);
Expand Down Expand Up @@ -340,12 +372,25 @@ 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions record-builder-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 io.soabase.recordbuilder.core.RecordBuilder;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

@RecordBuilder
@RecordBuilder.Options(useUnmodifiableCollections = true)
record UnmodifiableCollectionsRecord(List<Integer> aList, Set<String> orderedSet, Map<String, Integer> orderedMap,
Collection<String> aCollection) {
}
Original file line number Diff line number Diff line change
@@ -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<Integer>();
list.add(2);
list.add(1);
list.add(0);

var orderedSet = new LinkedHashSet<String>();
orderedSet.add("C");
orderedSet.add("B");
orderedSet.add("A");

var orderedMap = new LinkedHashMap<String, Integer>();
orderedMap.put("C", 2);
orderedMap.put("B", 1);
orderedMap.put("A", 0);

var collection = new HashSet<String>();
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"));
}
}

0 comments on commit c6cf239

Please sign in to comment.