Skip to content

Commit

Permalink
Issue #123: support nullable collections (#156)
Browse files Browse the repository at this point in the history
* Support nullable collections based on 'allowNullableCollections' and 'interpretNotNulls' options

* nit & typos

* Emit warning if allowNullableCollections is used in combination with non-specialized collections

---------

Co-authored-by: Dominik Hoftych <[email protected]>
  • Loading branch information
hofiisek and Dominik Hoftych authored Nov 23, 2023
1 parent 6781824 commit e7b7963
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,16 @@
*/
boolean useUnmodifiableCollections() 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 immutable collection (e.g.
* {@code List.copyOf(o)}), or {@code null} if the component is {@code null}. If nulls are interpreted (see
* {@link #interpretNotNulls()}), the record component will return {@code null} only if it's NOT annotated by
* any of the not-null annotations (defined by {@link #interpretNotNullsPattern()}).
*/
boolean allowNullableCollections() 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 @@ -20,20 +20,31 @@

import javax.lang.model.element.Modifier;
import java.util.*;
import java.util.regex.Pattern;

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 useUnmodifiableCollections;
private final boolean allowNullableCollections;
private final boolean addSingleItemCollectionBuilders;
private final boolean addClassRetainedGenerated;

private final boolean interpretNotNulls;
private final Pattern notNullPattern;

private final String listShimName;
private final String mapShimName;
private final String setShimName;
private final String collectionShimName;

private final String nullableListShimName;
private final String nullableMapShimName;
private final String nullableSetShimName;
private final String nullableCollectionShimName;

private final String listMakerMethodName;
private final String mapMakerMethodName;
private final String setMakerMethodName;
Expand All @@ -43,6 +54,11 @@ class CollectionBuilderUtils {
private boolean needsSetShim;
private boolean needsCollectionShim;

private boolean needsNullableListShim;
private boolean needsNullableMapShim;
private boolean needsNullableSetShim;
private boolean needsNullableCollectionShim;

private boolean needsListMutableMaker;
private boolean needsMapMutableMaker;
private boolean needsSetMutableMaker;
Expand Down Expand Up @@ -83,14 +99,23 @@ class CollectionBuilderUtils {
CollectionBuilderUtils(List<RecordClassType> recordComponents, RecordBuilder.Options metaData) {
useImmutableCollections = metaData.useImmutableCollections();
useUnmodifiableCollections = !useImmutableCollections && metaData.useUnmodifiableCollections();
allowNullableCollections = metaData.allowNullableCollections();
addSingleItemCollectionBuilders = metaData.addSingleItemCollectionBuilders();
addClassRetainedGenerated = metaData.addClassRetainedGenerated();

interpretNotNulls = metaData.interpretNotNulls();
notNullPattern = Pattern.compile(metaData.interpretNotNullsPattern());

listShimName = disambiguateGeneratedMethodName(recordComponents, "__list", 0);
mapShimName = disambiguateGeneratedMethodName(recordComponents, "__map", 0);
setShimName = disambiguateGeneratedMethodName(recordComponents, "__set", 0);
collectionShimName = disambiguateGeneratedMethodName(recordComponents, "__collection", 0);

nullableListShimName = disambiguateGeneratedMethodName(recordComponents, "__nullableList", 0);
nullableMapShimName = disambiguateGeneratedMethodName(recordComponents, "__nullableMap", 0);
nullableSetShimName = disambiguateGeneratedMethodName(recordComponents, "__nullableSet", 0);
nullableCollectionShimName = disambiguateGeneratedMethodName(recordComponents, "__nullableCollection", 0);

listMakerMethodName = disambiguateGeneratedMethodName(recordComponents, "__ensureListMutable", 0);
setMakerMethodName = disambiguateGeneratedMethodName(recordComponents, "__ensureSetMutable", 0);
mapMakerMethodName = disambiguateGeneratedMethodName(recordComponents, "__ensureMapMutable", 0);
Expand Down Expand Up @@ -162,6 +187,16 @@ boolean isImmutableCollection(RecordClassType component) {
&& (isList(component) || isMap(component) || isSet(component) || isCollection(component));
}

boolean isNullableCollection(RecordClassType component) {
return allowNullableCollections && (!interpretNotNulls || !isNotNullAnnotated(component))
&& (isList(component) || isMap(component) || isSet(component) || isCollection(component));
}

private boolean isNotNullAnnotated(RecordClassType component) {
return component.getCanonicalConstructorAnnotations().stream().anyMatch(annotation -> notNullPattern
.matcher(annotation.getAnnotationType().asElement().getSimpleName().toString()).matches());
}

boolean isList(RecordClassType component) {
return component.rawTypeName().equals(listTypeName);
}
Expand All @@ -181,20 +216,40 @@ private boolean isCollection(RecordClassType component) {
void addShimCall(CodeBlock.Builder builder, RecordClassType component) {
if (useImmutableCollections || useUnmodifiableCollections) {
if (isList(component)) {
needsListShim = true;
needsListMutableMaker = true;
builder.add("$L($L)", listShimName, component.name());
if (isNullableCollection(component)) {
needsNullableListShim = true;
builder.add("$L($L)", nullableListShimName, component.name());
} else {
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());
if (isNullableCollection(component)) {
needsNullableMapShim = true;
builder.add("$L($L)", nullableMapShimName, component.name());
} else {
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());
if (isNullableCollection(component)) {
needsNullableSetShim = true;
builder.add("$L($L)", nullableSetShimName, component.name());
} else {
needsSetShim = true;
needsSetMutableMaker = true;
builder.add("$L($L)", setShimName, component.name());
}
} else if (isCollection(component)) {
needsCollectionShim = true;
builder.add("$L($L)", collectionShimName, component.name());
if (isNullableCollection(component)) {
needsNullableCollectionShim = true;
builder.add("$L($L)", nullableCollectionShimName, component.name());
} else {
needsCollectionShim = true;
builder.add("$L($L)", collectionShimName, component.name());
}
} else {
builder.add("$L", component.name());
}
Expand All @@ -205,13 +260,13 @@ void addShimCall(CodeBlock.Builder builder, RecordClassType component) {

String shimName(RecordClassType component) {
if (isList(component)) {
return listShimName;
return isNullableCollection(component) ? nullableListShimName : listShimName;
} else if (isMap(component)) {
return mapShimName;
return isNullableCollection(component) ? nullableMapShimName : mapShimName;
} else if (isSet(component)) {
return setShimName;
return isNullableCollection(component) ? nullableSetShimName : setShimName;
} else if (isCollection(component)) {
return collectionShimName;
return isNullableCollection(component) ? nullableCollectionShimName : collectionShimName;
} else {
throw new IllegalArgumentException(component + " is not a supported collection type");
}
Expand All @@ -238,15 +293,33 @@ void addShims(TypeSpec.Builder builder) {
builder.addMethod(
buildShimMethod(listShimName, listTypeName, collectionType, parameterizedListType, tType));
}
if (needsNullableListShim) {
builder.addMethod(buildNullableShimMethod(nullableListShimName, listTypeName, collectionType,
parameterizedListType, tType));
}

if (needsSetShim) {
builder.addMethod(buildShimMethod(setShimName, setTypeName, collectionType, parameterizedSetType, tType));
}
if (needsNullableSetShim) {
builder.addMethod(buildNullableShimMethod(nullableSetShimName, setTypeName, collectionType,
parameterizedSetType, tType));
}

if (needsMapShim) {
builder.addMethod(buildShimMethod(mapShimName, mapTypeName, mapType, parameterizedMapType, kType, vType));
}
if (needsNullableMapShim) {
builder.addMethod(buildNullableShimMethod(nullableMapShimName, mapTypeName, mapType, parameterizedMapType,
kType, vType));
}

if (needsCollectionShim) {
builder.addMethod(buildCollectionsShimMethod());
}
if (needsNullableCollectionShim) {
builder.addMethod(buildNullableCollectionsShimMethod());
}
}

void addMutableMakers(TypeSpec.Builder builder) {
Expand Down Expand Up @@ -336,7 +409,42 @@ private CodeBlock buildShimMethodBody(TypeName mainType, ParameterizedTypeName p
collectionsTypeName, kType, vType, parameterizedType, collectionsTypeName, kType, vType);
}

throw new IllegalStateException("Cannot build shim method for" + mainType);
throw new IllegalStateException("Cannot build shim method for " + mainType);
}

private MethodSpec buildNullableShimMethod(String name, TypeName mainType, Class<?> abstractType,
ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) {
var code = buildNullableShimMethodBody(mainType, parameterizedType);

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 CodeBlock buildNullableShimMethodBody(TypeName mainType, ParameterizedTypeName parameterizedType) {
if (!useUnmodifiableCollections) {
return CodeBlock.of("return (o != null) ? $T.copyOf(o) : null", mainType);
}

if (mainType.equals(listTypeName)) {
return CodeBlock.of("return (o != null) ? $T.<$T>unmodifiableList(($T) o) : null", collectionsTypeName,
tType, parameterizedType);
}

if (mainType.equals(setTypeName)) {
return CodeBlock.of("return (o != null) ? $T.<$T>unmodifiableSet(($T) o) : null", collectionsTypeName,
tType, parameterizedType);
}

if (mainType.equals(mapTypeName)) {
return CodeBlock.of("return (o != null) ? $T.<$T>unmodifiableMap(($T) o) : null", collectionsTypeName,
tType, parameterizedType);
}

throw new IllegalStateException("Cannot build shim method for " + mainType);
}

private MethodSpec buildMutableMakerMethod(String name, String mutableCollectionType,
Expand Down Expand Up @@ -393,4 +501,28 @@ private CodeBlock buildCollectionShimMethodBody() {
.addStatement("return $T.<$T>unmodifiableSet(($T) o)", collectionsTypeName, tType, parameterizedSetType)
.endControlFlow().addStatement("return $T.<$T>emptyList()", collectionsTypeName, tType).build();
}

private MethodSpec buildNullableCollectionsShimMethod() {
var code = buildNullableCollectionShimMethodBody();

return MethodSpec.methodBuilder(nullableCollectionShimName).addAnnotation(generatedRecordBuilderAnnotation)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC).addTypeVariable(tType)
.returns(parameterizedCollectionType).addParameter(parameterizedCollectionType, "o").addCode(code)
.build();
}

private CodeBlock buildNullableCollectionShimMethodBody() {
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) : null", 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 null").build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,16 @@ private void processRecordBuilder(TypeElement record, RecordBuilder.Options meta
private void validateMetaData(RecordBuilder.Options metaData, Element record) {
var useImmutableCollections = metaData.useImmutableCollections();
var useUnmodifiableCollections = metaData.useUnmodifiableCollections();
var allowNullableCollections = metaData.allowNullableCollections();

if (useImmutableCollections && useUnmodifiableCollections) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING,
"Options.useUnmodifiableCollections property is ignored as Options.useImmutableCollections is set to true",
record);
} else if (!useImmutableCollections && !useUnmodifiableCollections && allowNullableCollections) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING,
"Options.allowNullableCollections property will have no effect as Options.useImmutableCollections and Options.useUnmodifiableCollections are set to false",
record);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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;

/**
* To test that the compilation warning is emitted for this combination of options
*/
@RecordBuilder
@RecordBuilder.Options(useImmutableCollections = false, useUnmodifiableCollections = false, allowNullableCollections = true)
public record NonspecializedNullabeCollectionRecord<T, X extends Point>(List<T> l, Set<T> s, Map<T, X> m,
Collection<X> c) implements CollectionRecordBuilder.With<T, X> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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(useImmutableCollections = true, allowNullableCollections = true)
public record NullableCollectionRecord<T, X extends Point>(List<T> l, Set<T> s, Map<T, X> m, Collection<X> c)
implements CollectionRecordBuilder.With<T, X> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 javax.validation.constraints.NotNull;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

@RecordBuilder
@RecordBuilder.Options(useImmutableCollections = true, allowNullableCollections = true, interpretNotNulls = true)
public record NullableCollectionRecordInterpretingNotNulls<T, X extends Point>(@NotNull List<T> l, Set<T> s,
Map<T, X> m, @NotNull Collection<X> c) implements CollectionRecordBuilder.With<T, X> {

}
Loading

0 comments on commit e7b7963

Please sign in to comment.