Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix TypeAs.UNWRAP_MAP_VALUE_OF and TypeAs.UNWRAP_MAP_KEY_OF #342

Merged
merged 1 commit into from
Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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