Skip to content

Commit

Permalink
Fix TypeAs.UNWRAP_MAP_VALUE_OF and TypeAs.UNWRAP_MAP_KEY_OF return wr…
Browse files Browse the repository at this point in the history
…ong results for multimaps (& other more complicated generic references)

Fixes #341

This PR adds a new reusable routine `TypeCast`, which is used to investigate type relations with respect to generic arguments (like improved `isInstanceOf`).

`TypeCast` is then used by three new utility methods to `Collections`: `getCollectionElementType`, `getMapKeyType` and `getMapValueType`.
These are similar to `TypeAs.UNWRAP_MAP_VALUE_OF` & friends, but return `Optional.empty()` when the supplied type is not a Collection / Map (as opposed to the supplied type itself).
In fact, the `UNWRAP_*` functions now delegate to `Collections` and therefore now return more accurate results.

Last but not least, I have identified a new public routine `TypeArguments.getGenericArgumentsMappings(ClassRef ref, TypeDef definition)`.
This creates a mapping from generic types on a TypeDef to their instantiation on ClassRef. This code has been used at multiple places both in sundrio, but also in kubernetes-client. So I think it deserves a common implementation.
  • Loading branch information
xRodney authored and iocanel committed Oct 14, 2022
1 parent 906eb04 commit 1262be0
Show file tree
Hide file tree
Showing 9 changed files with 815 additions and 181 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,8 @@ public ClassRef apply(TypeRef item) {
public static final Function<TypeRef, TypeRef> ARRAY_AS_LIST = FunctionFactory
.cache(item -> LIST_OF.apply(UNWRAP_ARRAY_OF.apply(item)));

public static final Function<TypeRef, TypeRef> UNWRAP_COLLECTION_OF = type -> {
if (type instanceof ClassRef) {
return extractArgument((ClassRef) type, Collections.IS_COLLECTION, 0).orElse(type);
}
return type;
};
public static final Function<TypeRef, TypeRef> UNWRAP_COLLECTION_OF = type -> Collections.getCollectionElementType(type)
.orElse(type);

private static Optional<TypeRef> extractArgument(ClassRef classRef, Function<TypeRef, Boolean> typeCheckFn,
int argumentIndex) {
Expand All @@ -240,16 +236,9 @@ private static Optional<TypeRef> extractArgument(ClassRef classRef, Function<Typ
}
}

private static TypeRef unwrapMapOf(TypeRef type, int argumentIndex) {
if (type instanceof ClassRef) {
return extractArgument((ClassRef) type, Collections.IS_MAP, argumentIndex).orElse(type);
}
return type;
}

public static final Function<TypeRef, TypeRef> UNWRAP_MAP_KEY_OF = type -> unwrapMapOf(type, 0);
public static final Function<TypeRef, TypeRef> UNWRAP_MAP_KEY_OF = type -> Collections.getMapKeyType(type).orElse(type);

public static final Function<TypeRef, TypeRef> UNWRAP_MAP_VALUE_OF = type -> unwrapMapOf(type, 1);
public static final Function<TypeRef, TypeRef> UNWRAP_MAP_VALUE_OF = type -> Collections.getMapValueType(type).orElse(type);

public static final Function<TypeRef, TypeRef> UNWRAP_OPTIONAL_OF = type -> {
if (type instanceof ClassRef) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static org.junit.Assert.assertTrue;

import java.util.Iterator;
import java.util.List;

import org.junit.Test;

Expand All @@ -41,6 +42,7 @@ public class SimpleClassTest extends AbstractProcessorTest {
private final AdapterContext context = AdapterContext.create(DefinitionRepository.getRepository());

TypeDef stringDef = Adapters.adaptType(String.class, context);
TypeDef listDef = Adapters.adaptType(List.class, context);
RichTypeDef simpleClassDef = TypeArguments.apply(Sources.readTypeDefFromResource("SimpleClass.java", context));

@Test
Expand Down
134 changes: 134 additions & 0 deletions model/utils/src/main/java/io/sundr/model/functions/TypeCast.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* Copyright 2015 The original 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.sundr.model.functions;

import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.sundr.model.ClassRef;
import io.sundr.model.ClassRefBuilder;
import io.sundr.model.TypeDef;
import io.sundr.model.TypeRef;
import io.sundr.model.WildcardRef;
import io.sundr.model.utils.TypeArguments;
import io.sundr.model.utils.Types;
import io.sundr.model.visitors.ApplyTypeParamMappingToTypeArguments;

/**
* This function can be thought as {@link Types#isInstanceOf(TypeRef, TypeDef, Function)} with added bonus that generic
* arguments
* are resolved.
* <p>
* For example, when {@code TypeCast.to(Map<?,?>)} is called on {@code HashMap<String, Integer}, the result will be
* {@code Optional.of(Map<String, Integer>)}.
* <p>
* This works also for complex hierarchies with non-trivial type argument substitutions.
* <p>
* Limitation: Arguments involving wildcards are currently not supported.
*/
public class TypeCast implements Function<TypeRef, Optional<ClassRef>> {

private final ClassRef expectedType;

private TypeCast(ClassRef expectedType) {
this.expectedType = expectedType;
}

/**
* Create the function which casts to the specified target type.
*
* @param expectedType
* The type to which to cast. It must not be an array, and all type arguments (if any) must be unbounded wildcards
*/
public static TypeCast to(ClassRef expectedType) {
assertNoArray(expectedType);
assertAllArgumentsAreWildcards(expectedType);
return new TypeCast(expectedType);
}

private static void assertNoArray(ClassRef expectedType) {
if (expectedType.getDimensions() != 0) {
throw new IllegalArgumentException("Arrays are not supported: " + expectedType);
}
}

private static void assertAllArgumentsAreWildcards(ClassRef expectedType) {
for (TypeRef argument : expectedType.getArguments()) {
if (!(argument instanceof WildcardRef) || !((WildcardRef) argument).getBounds().isEmpty()) {
throw new IllegalArgumentException("Argument " + argument + " is not an unbounded wildcard in " + expectedType);
}
}
}

/**
* Perform the type cast, if possible.
*
* @param type
* The type which will be cast
* @return If the type can be cast to target type, {@code Optional.of(targetType<...>)} is returned with the type arguments
* resolved. If the
* type cannot be cast, {@code Optional.empty()} is returned.
* @throws IllegalStateException
* when the type implements or extends target type multiple times with different arguments. Currently, this may also
* apply to multiple inheritance of wildcard types, even if they were compatible.
*/
@Override
public Optional<ClassRef> apply(TypeRef type) {
if (type instanceof ClassRef) {
Set<ClassRef> types = findMatchingTypes((ClassRef) type).collect(Collectors.toSet());

if (types.size() > 1) {
throw new IllegalStateException("Type " + type +
" extends or implements " + expectedType + " multiple times: "
+ types + ". This is not legal in Java");
}

return types.stream().findAny();
} else {
return Optional.empty();
}
}

private Stream<ClassRef> findMatchingTypes(ClassRef type) {
if (type.getFullyQualifiedName().equals(expectedType.getFullyQualifiedName())) {
return Stream.of(type);
}

TypeDef definition = GetDefinition.of(type);
Stream<ClassRef> supertypes = Stream.concat(
definition.getImplementsList().stream(),
definition.getExtendsList().stream());

return supertypes
// as a corner-case, java.lang.Object extends itself:
.filter(supertype -> !type.equals(supertype))
.flatMap(this::findMatchingTypes)
.map(supertype -> bindArguments(type, supertype));
}

private ClassRef bindArguments(ClassRef type, ClassRef supertype) {
Map<String, TypeRef> mappings = TypeArguments.getGenericArgumentsMappings(type);
return new ClassRefBuilder(supertype)
.accept(new ApplyTypeParamMappingToTypeArguments(mappings))
.build();
}
}
40 changes: 39 additions & 1 deletion model/utils/src/main/java/io/sundr/model/utils/Collections.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;

import io.sundr.model.ClassRef;
import io.sundr.model.Kind;
import io.sundr.model.TypeDef;
import io.sundr.model.TypeDefBuilder;
import io.sundr.model.TypeParamDef;
import io.sundr.model.TypeRef;
import io.sundr.model.VoidRef;
import io.sundr.model.functions.TypeCast;

public class Collections {

Expand Down Expand Up @@ -72,11 +75,15 @@ public class Collections {
.withExtendsList(ITERABLE.toReference(E.toReference()))
.build();

public static final ClassRef COLLECTION_REF = COLLECTION.toReference();

public static final TypeDef MAP = new TypeDefBuilder(TypeDef.forName(Map.class.getName()))
.withKind(Kind.INTERFACE)
.withParameters(K, V)
.build();

public static final ClassRef MAP_REF = MAP.toReference();

public static final TypeDef MAP_ENTRY = new TypeDefBuilder(TypeDef.forName(Map.Entry.class.getName()))
.withKind(Kind.INTERFACE)
.withParameters(K, V)
Expand Down Expand Up @@ -170,9 +177,40 @@ public Boolean apply(TypeRef type) {
}
};

public static final TypeCast AS_MAP = TypeCast.to(MAP_REF);
public static final TypeCast AS_COLLECTION = TypeCast.to(COLLECTION_REF);

/**
* If the supplied type implements {@link java.util.Collection} (directly or indirectly), determine its generic element type.
* Otherwise, return {@link Optional#empty()}
*/
public static Optional<TypeRef> getCollectionElementType(TypeRef collectionType) {
return extractArgument(collectionType, AS_COLLECTION, 0);
}

/**
* If the supplied type implements {@link java.util.Map} (directly or indirectly), determine its generic key type. Otherwise,
* return {@link Optional#empty()}
*/
public static Optional<TypeRef> getMapKeyType(TypeRef mapType) {
return extractArgument(mapType, AS_MAP, 0);
}

/**
* If the supplied type implements {@link java.util.Map} (directly or indirectly), determine its generic value type.
* Otherwise, return {@link Optional#empty()}
*/
public static Optional<TypeRef> getMapValueType(TypeRef mapType) {
return extractArgument(mapType, AS_MAP, 1);
}

private static Optional<TypeRef> extractArgument(TypeRef type, TypeCast typeCast, int index) {
return typeCast.apply(type).map(castType -> castType.getArguments().get(index));
}

/**
* Checks if a {@link TypeRef} is a {@link Collection}.
*
*
* @param type The type to check.
* @return True if its a Collection.
*/
Expand Down
76 changes: 60 additions & 16 deletions model/utils/src/main/java/io/sundr/model/utils/TypeArguments.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@

package io.sundr.model.utils;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.sundr.model.AttributeKey;
import io.sundr.model.ClassRef;
import io.sundr.model.Method;
Expand All @@ -32,13 +41,6 @@
import io.sundr.model.visitors.ApplyTypeParamMappingToProperty;
import io.sundr.model.visitors.ApplyTypeParamMappingToTypeArguments;
import io.sundr.utils.Predicates;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class TypeArguments {

Expand Down Expand Up @@ -93,11 +95,41 @@ public static RichTypeDef apply(TypeDef definition) {
definition.getModifiers(), definition.getAttributes());
}

private static TypeDef applyGenericArguments(ClassRef ref) {
/**
* Given a reference to a generic class, determine a mapping between generic arguments definitions and instantiations.
* <p>
* For example, given a definition of {@code interface Map<K,V> {...}} and a reference {@code Map<String,Integer>},
* the mapping will be {@code {K -> String, V -> Integer}}.
* <p>
* Raw references, that is, references that do not contain generic arguments (like {@code Map}) are accepted and always return
* an empty result.
* <p>
* However, if the reference does contain generic arguments, their count must be equal to the definition.
*
* @param ref The class reference to evaluate. The corresponding definition will be loaded using {@link GetDefinition}
*/
public static Map<String, TypeRef> getGenericArgumentsMappings(ClassRef ref) {
return getGenericArgumentsMappings(ref, GetDefinition.of(ref));
}

/**
* Given a reference to a generic class, determine a mapping between generic arguments definitions and instantiations.
* <p>
* For example, given a definition of {@code interface Map<K,V> {...}} and a reference {@code Map<String,Integer>},
* the mapping will be {@code {K -> String, V -> Integer}}.
* <p>
* Raw references, that is, references that do not contain generic arguments (like {@code Map}) are accepted and always return
* an empty result.
* <p>
* However, if the reference does contain generic arguments, their count must be equal to the definition.
*
* @param ref The class reference to evaluate.
* @param definition The corresponding definition
*/
public static Map<String, TypeRef> getGenericArgumentsMappings(ClassRef ref, TypeDef definition) {
List<TypeRef> arguments = ref.getArguments();
TypeDef definition = GetDefinition.of(ref);
if (arguments.isEmpty()) {
return definition;
return Collections.emptyMap();
}

List<TypeParamDef> parameters = definition.getParameters();
Expand All @@ -112,11 +144,22 @@ private static TypeDef applyGenericArguments(ClassRef ref) {
mappings.put(name, typeRef);
}

return new TypeDefBuilder(definition)
.accept(new ApplyTypeParamMappingToTypeArguments(mappings)) // existing type arguments must be handled before methods and properties
.accept(new ApplyTypeParamMappingToProperty(mappings, ORIGINAL_TYPE_PARAMETER),
new ApplyTypeParamMappingToMethod(mappings, ORIGINAL_TYPE_PARAMETER))
.build();
return mappings;
}

private static TypeDef applyGenericArguments(ClassRef ref) {
TypeDef definition = GetDefinition.of(ref);
Map<String, TypeRef> mappings = getGenericArgumentsMappings(ref, definition);

if (mappings.isEmpty()) {
return definition;
} else {
return new TypeDefBuilder(definition)
.accept(new ApplyTypeParamMappingToTypeArguments(mappings)) // existing type arguments must be handled before methods and properties
.accept(new ApplyTypeParamMappingToProperty(mappings, ORIGINAL_TYPE_PARAMETER),
new ApplyTypeParamMappingToMethod(mappings, ORIGINAL_TYPE_PARAMETER))
.build();
}
}

private static List<Property> applyToProperties(TypeDef definition) {
Expand All @@ -130,7 +173,8 @@ private static List<Property> applyToProperties(TypeDef definition) {
private static List<Method> applyToMethods(TypeDef definition) {
return Stream
.concat(definition.getMethods().stream(),
definition.getExtendsList().stream().filter(INTERNAL_JDK.negate()).flatMap(e -> applyToMethods(applyGenericArguments(e)).stream()))
definition.getExtendsList().stream().filter(INTERNAL_JDK.negate())
.flatMap(e -> applyToMethods(applyGenericArguments(e)).stream()))
.filter(Predicates.distinct(m -> m.withErasure().getSignature())).collect(Collectors.toList());
}

Expand Down

0 comments on commit 1262be0

Please sign in to comment.