diff --git a/src/main/java/org/springframework/data/util/GenericArrayTypeInformation.java b/src/main/java/org/springframework/data/util/GenericArrayTypeInformation.java index 02c07fffa..155054d0e 100644 --- a/src/main/java/org/springframework/data/util/GenericArrayTypeInformation.java +++ b/src/main/java/org/springframework/data/util/GenericArrayTypeInformation.java @@ -18,8 +18,6 @@ package org.springframework.data.util; import java.lang.reflect.Array; import java.lang.reflect.GenericArrayType; import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; -import java.util.Map; import javax.annotation.Nonnull; @@ -38,12 +36,10 @@ class GenericArrayTypeInformation extends ParentTypeAwareTypeInformation { * * @param type must not be {@literal null}. * @param parent must not be {@literal null}. - * @param typeVariableMap must not be {@literal null}. */ - protected GenericArrayTypeInformation(GenericArrayType type, TypeDiscoverer parent, - Map, Type> typeVariableMap) { + protected GenericArrayTypeInformation(GenericArrayType type, TypeDiscoverer parent) { - super(type, parent, typeVariableMap); + super(type, parent); this.type = type; } diff --git a/src/main/java/org/springframework/data/util/ParameterizedTypeInformation.java b/src/main/java/org/springframework/data/util/ParameterizedTypeInformation.java index 4fcc76471..55e0ba812 100644 --- a/src/main/java/org/springframework/data/util/ParameterizedTypeInformation.java +++ b/src/main/java/org/springframework/data/util/ParameterizedTypeInformation.java @@ -21,11 +21,13 @@ import java.lang.reflect.TypeVariable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.IntStream; import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; @@ -49,15 +51,39 @@ class ParameterizedTypeInformation extends ParentTypeAwareTypeInformation * @param type must not be {@literal null} * @param parent must not be {@literal null} */ - public ParameterizedTypeInformation(ParameterizedType type, TypeDiscoverer parent, - Map, Type> typeVariableMap) { + public ParameterizedTypeInformation(ParameterizedType type, Class resolvedType, TypeDiscoverer parent) { - super(type, parent, typeVariableMap); + super(type, parent, calculateTypeVariables(type, resolvedType, parent)); this.type = type; this.resolved = Lazy.of(() -> isResolvedCompletely()); } + /** + * Resolves the type variables to be used. Uses the parent's type variable map but overwrites variables locally + * declared. + * + * @param type must not be {@literal null}. + * @param resolvedType must not be {@literal null}. + * @param parent must not be {@literal null}. + * @return + */ + private static Map, Type> calculateTypeVariables(ParameterizedType type, Class resolvedType, + TypeDiscoverer parent) { + + TypeVariable[] typeParameters = resolvedType.getTypeParameters(); + Type[] arguments = type.getActualTypeArguments(); + + Map, Type> localTypeVariables = new HashMap<>(parent.getTypeVariableMap()); + + IntStream.range(0, typeParameters.length) // + .mapToObj(it -> Pair.of(typeParameters[it], arguments[it])) // + .filter(it -> !(it.getSecond() instanceof TypeVariable)) // + .forEach(it -> localTypeVariables.put(it.getFirst(), it.getSecond())); + + return localTypeVariables; + } + /* * (non-Javadoc) * @see org.springframework.data.util.TypeDiscoverer#doGetMapValueType() diff --git a/src/main/java/org/springframework/data/util/ParentTypeAwareTypeInformation.java b/src/main/java/org/springframework/data/util/ParentTypeAwareTypeInformation.java index 9dd54fe85..b6fd9f4c7 100644 --- a/src/main/java/org/springframework/data/util/ParentTypeAwareTypeInformation.java +++ b/src/main/java/org/springframework/data/util/ParentTypeAwareTypeInformation.java @@ -17,7 +17,6 @@ package org.springframework.data.util; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; -import java.util.HashMap; import java.util.Map; import org.springframework.lang.Nullable; @@ -37,28 +36,15 @@ public abstract class ParentTypeAwareTypeInformation extends TypeDiscoverer parent, Map, Type> map) { - - super(type, mergeMaps(parent, map)); - this.parent = parent; + protected ParentTypeAwareTypeInformation(Type type, TypeDiscoverer parent) { + this(type, parent, parent.getTypeVariableMap()); } - /** - * Merges the type variable maps of the given parent with the new map. - * - * @param parent must not be {@literal null}. - * @param map must not be {@literal null}. - * @return - */ - private static Map, Type> mergeMaps(TypeDiscoverer parent, Map, Type> map) { + protected ParentTypeAwareTypeInformation(Type type, TypeDiscoverer parent, Map, Type> map) { - Map, Type> typeVariableMap = new HashMap<>(); - typeVariableMap.putAll(map); - typeVariableMap.putAll(parent.getTypeVariableMap()); - - return typeVariableMap; + super(type, map); + this.parent = parent; } /* diff --git a/src/main/java/org/springframework/data/util/TypeDiscoverer.java b/src/main/java/org/springframework/data/util/TypeDiscoverer.java index b579015b2..bb193f8f2 100644 --- a/src/main/java/org/springframework/data/util/TypeDiscoverer.java +++ b/src/main/java/org/springframework/data/util/TypeDiscoverer.java @@ -129,31 +129,22 @@ class TypeDiscoverer implements TypeInformation { return ClassTypeInformation.from((Class) fieldType); } - Class resolveType = resolveType(fieldType); - Map variableMap = new HashMap<>(); - variableMap.putAll(GenericTypeResolver.getTypeVariableMap(resolveType)); + Class resolvedType = resolveType(fieldType); if (fieldType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) fieldType; - - TypeVariable>[] typeParameters = resolveType.getTypeParameters(); - Type[] arguments = parameterizedType.getActualTypeArguments(); - - for (int i = 0; i < typeParameters.length; i++) { - variableMap.put(typeParameters[i], arguments[i]); - } - - return new ParameterizedTypeInformation(parameterizedType, this, variableMap); + return new ParameterizedTypeInformation(parameterizedType, resolvedType, this); } if (fieldType instanceof TypeVariable) { + TypeVariable variable = (TypeVariable) fieldType; - return new TypeVariableTypeInformation(variable, type, this, variableMap); + return new TypeVariableTypeInformation(variable, type, this); } if (fieldType instanceof GenericArrayType) { - return new GenericArrayTypeInformation((GenericArrayType) fieldType, this, variableMap); + return new GenericArrayTypeInformation((GenericArrayType) fieldType, this); } if (fieldType instanceof WildcardType) { diff --git a/src/main/java/org/springframework/data/util/TypeVariableTypeInformation.java b/src/main/java/org/springframework/data/util/TypeVariableTypeInformation.java index 02d555791..043487641 100644 --- a/src/main/java/org/springframework/data/util/TypeVariableTypeInformation.java +++ b/src/main/java/org/springframework/data/util/TypeVariableTypeInformation.java @@ -20,7 +20,6 @@ import static org.springframework.util.ObjectUtils.*; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; -import java.util.Map; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -44,11 +43,12 @@ class TypeVariableTypeInformation extends ParentTypeAwareTypeInformation { * @param owningType must not be {@literal null} * @param parent */ - public TypeVariableTypeInformation(TypeVariable variable, Type owningType, TypeDiscoverer parent, - Map, Type> typeVariableMap) { + public TypeVariableTypeInformation(TypeVariable variable, Type owningType, TypeDiscoverer parent) { + + super(variable, parent); - super(variable, parent, typeVariableMap); Assert.notNull(variable, "TypeVariable must not be null!"); + this.variable = variable; this.owningType = owningType; } diff --git a/src/test/java/org/springframework/data/util/ParameterizedTypeUnitTests.java b/src/test/java/org/springframework/data/util/ParameterizedTypeUnitTests.java index 7b598ae88..cd19b4b6e 100755 --- a/src/test/java/org/springframework/data/util/ParameterizedTypeUnitTests.java +++ b/src/test/java/org/springframework/data/util/ParameterizedTypeUnitTests.java @@ -15,24 +15,24 @@ */ package org.springframework.data.util; +import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; -import static org.springframework.data.util.ClassTypeInformation.*; +import static org.springframework.data.util.ClassTypeInformation.from; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; -import java.util.Collections; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Map; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.core.GenericTypeResolver; /** * Unit tests for {@link ParameterizedTypeInformation}. @@ -43,23 +43,23 @@ import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class ParameterizedTypeUnitTests { - static final Map, Type> EMPTY_MAP = Collections.emptyMap(); - @Mock ParameterizedType one; + Class resolvedOne; @Before public void setUp() { when(one.getActualTypeArguments()).thenReturn(new Type[0]); + this.resolvedOne = GenericTypeResolver.resolveType(one, emptyMap()); } @Test public void considersTypeInformationsWithDifferingParentsNotEqual() { - TypeDiscoverer stringParent = new TypeDiscoverer<>(String.class, EMPTY_MAP); - TypeDiscoverer objectParent = new TypeDiscoverer<>(Object.class, EMPTY_MAP); + TypeDiscoverer stringParent = new TypeDiscoverer<>(String.class, emptyMap()); + TypeDiscoverer objectParent = new TypeDiscoverer<>(Object.class, emptyMap()); - ParameterizedTypeInformation first = new ParameterizedTypeInformation<>(one, stringParent, EMPTY_MAP); - ParameterizedTypeInformation second = new ParameterizedTypeInformation<>(one, objectParent, EMPTY_MAP); + ParameterizedTypeInformation first = new ParameterizedTypeInformation<>(one, resolvedOne, stringParent); + ParameterizedTypeInformation second = new ParameterizedTypeInformation<>(one, resolvedOne, objectParent); assertThat(first).isNotEqualTo(second); } @@ -67,10 +67,10 @@ public class ParameterizedTypeUnitTests { @Test public void considersTypeInformationsWithSameParentsNotEqual() { - TypeDiscoverer stringParent = new TypeDiscoverer<>(String.class, EMPTY_MAP); + TypeDiscoverer stringParent = new TypeDiscoverer<>(String.class, emptyMap()); - ParameterizedTypeInformation first = new ParameterizedTypeInformation<>(one, stringParent, EMPTY_MAP); - ParameterizedTypeInformation second = new ParameterizedTypeInformation<>(one, stringParent, EMPTY_MAP); + ParameterizedTypeInformation first = new ParameterizedTypeInformation<>(one, resolvedOne, stringParent); + ParameterizedTypeInformation second = new ParameterizedTypeInformation<>(one, resolvedOne, stringParent); assertThat(first.equals(second)).isTrue(); } @@ -136,6 +136,17 @@ public class ParameterizedTypeUnitTests { assertThat(typeInformation.getMapValueType()).isNull(); } + @Test // DATACMNS-1135 + public void prefersLocalGenericsDeclarationOverParentBound() { + + ClassTypeInformation candidate = ClassTypeInformation.from(Candidate.class); + + TypeInformation componentType = candidate.getRequiredProperty("experiences.values").getRequiredComponentType(); + componentType = componentType.getRequiredProperty("responsibilities.values").getRequiredComponentType(); + + assertThat(componentType.getType()).isEqualTo(Responsibility.class); + } + @SuppressWarnings("serial") class Localized extends HashMap { S value; @@ -183,4 +194,22 @@ public class ParameterizedTypeUnitTests { } class Education {} + + // DATACMNS-1135 + + abstract class CandidateInfo {} + + class Responsibility extends CandidateInfo {} + + class Experience extends CandidateInfo { + CandidateInfoContainer responsibilities; + } + + class CandidateInfoContainer { + List values = new ArrayList<>(); + } + + class Candidate { + CandidateInfoContainer experiences; + } }