DATACMNS-1135 - Fixed generics lookup for recursively nested container references.

Revert the merging of a parent type's type variable map and only apply the locally available declared generics in case of parameterized types. Moved that augmentation into ParameterizedTypeInformation.
This commit is contained in:
Oliver Gierke
2017-08-01 17:19:35 +02:00
parent 0ff80930a3
commit 756396a065
6 changed files with 87 additions and 59 deletions

View File

@@ -18,8 +18,6 @@ package org.springframework.data.util;
import java.lang.reflect.Array; import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType; import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Map;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@@ -38,12 +36,10 @@ class GenericArrayTypeInformation<S> extends ParentTypeAwareTypeInformation<S> {
* *
* @param type must not be {@literal null}. * @param type must not be {@literal null}.
* @param parent 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, protected GenericArrayTypeInformation(GenericArrayType type, TypeDiscoverer<?> parent) {
Map<TypeVariable<?>, Type> typeVariableMap) {
super(type, parent, typeVariableMap); super(type, parent);
this.type = type; this.type = type;
} }

View File

@@ -21,11 +21,13 @@ import java.lang.reflect.TypeVariable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.IntStream;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@@ -49,15 +51,39 @@ class ParameterizedTypeInformation<T> extends ParentTypeAwareTypeInformation<T>
* @param type must not be {@literal null} * @param type must not be {@literal null}
* @param parent must not be {@literal null} * @param parent must not be {@literal null}
*/ */
public ParameterizedTypeInformation(ParameterizedType type, TypeDiscoverer<?> parent, public ParameterizedTypeInformation(ParameterizedType type, Class<?> resolvedType, TypeDiscoverer<?> parent) {
Map<TypeVariable<?>, Type> typeVariableMap) {
super(type, parent, typeVariableMap); super(type, parent, calculateTypeVariables(type, resolvedType, parent));
this.type = type; this.type = type;
this.resolved = Lazy.of(() -> isResolvedCompletely()); 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<TypeVariable<?>, Type> calculateTypeVariables(ParameterizedType type, Class<?> resolvedType,
TypeDiscoverer<?> parent) {
TypeVariable<?>[] typeParameters = resolvedType.getTypeParameters();
Type[] arguments = type.getActualTypeArguments();
Map<TypeVariable<?>, 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) * (non-Javadoc)
* @see org.springframework.data.util.TypeDiscoverer#doGetMapValueType() * @see org.springframework.data.util.TypeDiscoverer#doGetMapValueType()

View File

@@ -17,7 +17,6 @@ package org.springframework.data.util;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable; import java.lang.reflect.TypeVariable;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@@ -37,28 +36,15 @@ public abstract class ParentTypeAwareTypeInformation<S> extends TypeDiscoverer<S
* *
* @param type must not be {@literal null}. * @param type must not be {@literal null}.
* @param parent must not be {@literal null}. * @param parent must not be {@literal null}.
* @param map must not be {@literal null}.
*/ */
protected ParentTypeAwareTypeInformation(Type type, TypeDiscoverer<?> parent, Map<TypeVariable<?>, Type> map) { protected ParentTypeAwareTypeInformation(Type type, TypeDiscoverer<?> parent) {
this(type, parent, parent.getTypeVariableMap());
super(type, mergeMaps(parent, map));
this.parent = parent;
} }
/** protected ParentTypeAwareTypeInformation(Type type, TypeDiscoverer<?> parent, Map<TypeVariable<?>, Type> map) {
* 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<TypeVariable<?>, Type> mergeMaps(TypeDiscoverer<?> parent, Map<TypeVariable<?>, Type> map) {
Map<TypeVariable<?>, Type> typeVariableMap = new HashMap<>(); super(type, map);
typeVariableMap.putAll(map); this.parent = parent;
typeVariableMap.putAll(parent.getTypeVariableMap());
return typeVariableMap;
} }
/* /*

View File

@@ -129,31 +129,22 @@ class TypeDiscoverer<S> implements TypeInformation<S> {
return ClassTypeInformation.from((Class<?>) fieldType); return ClassTypeInformation.from((Class<?>) fieldType);
} }
Class<S> resolveType = resolveType(fieldType); Class<S> resolvedType = resolveType(fieldType);
Map<TypeVariable, Type> variableMap = new HashMap<>();
variableMap.putAll(GenericTypeResolver.getTypeVariableMap(resolveType));
if (fieldType instanceof ParameterizedType) { if (fieldType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) fieldType; ParameterizedType parameterizedType = (ParameterizedType) fieldType;
return new ParameterizedTypeInformation(parameterizedType, resolvedType, this);
TypeVariable<Class<S>>[] 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);
} }
if (fieldType instanceof TypeVariable) { if (fieldType instanceof TypeVariable) {
TypeVariable<?> variable = (TypeVariable<?>) fieldType; TypeVariable<?> variable = (TypeVariable<?>) fieldType;
return new TypeVariableTypeInformation(variable, type, this, variableMap); return new TypeVariableTypeInformation(variable, type, this);
} }
if (fieldType instanceof GenericArrayType) { if (fieldType instanceof GenericArrayType) {
return new GenericArrayTypeInformation((GenericArrayType) fieldType, this, variableMap); return new GenericArrayTypeInformation((GenericArrayType) fieldType, this);
} }
if (fieldType instanceof WildcardType) { if (fieldType instanceof WildcardType) {

View File

@@ -20,7 +20,6 @@ import static org.springframework.util.ObjectUtils.*;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable; import java.lang.reflect.TypeVariable;
import java.util.Map;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@@ -44,11 +43,12 @@ class TypeVariableTypeInformation<T> extends ParentTypeAwareTypeInformation<T> {
* @param owningType must not be {@literal null} * @param owningType must not be {@literal null}
* @param parent * @param parent
*/ */
public TypeVariableTypeInformation(TypeVariable<?> variable, Type owningType, TypeDiscoverer<?> parent, public TypeVariableTypeInformation(TypeVariable<?> variable, Type owningType, TypeDiscoverer<?> parent) {
Map<TypeVariable<?>, Type> typeVariableMap) {
super(variable, parent);
super(variable, parent, typeVariableMap);
Assert.notNull(variable, "TypeVariable must not be null!"); Assert.notNull(variable, "TypeVariable must not be null!");
this.variable = variable; this.variable = variable;
this.owningType = owningType; this.owningType = owningType;
} }

View File

@@ -15,24 +15,24 @@
*/ */
package org.springframework.data.util; package org.springframework.data.util;
import static java.util.Collections.*;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*; 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.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.core.GenericTypeResolver;
/** /**
* Unit tests for {@link ParameterizedTypeInformation}. * Unit tests for {@link ParameterizedTypeInformation}.
@@ -43,23 +43,23 @@ import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class ParameterizedTypeUnitTests { public class ParameterizedTypeUnitTests {
static final Map<TypeVariable<?>, Type> EMPTY_MAP = Collections.emptyMap();
@Mock ParameterizedType one; @Mock ParameterizedType one;
Class<?> resolvedOne;
@Before @Before
public void setUp() { public void setUp() {
when(one.getActualTypeArguments()).thenReturn(new Type[0]); when(one.getActualTypeArguments()).thenReturn(new Type[0]);
this.resolvedOne = GenericTypeResolver.resolveType(one, emptyMap());
} }
@Test @Test
public void considersTypeInformationsWithDifferingParentsNotEqual() { public void considersTypeInformationsWithDifferingParentsNotEqual() {
TypeDiscoverer<String> stringParent = new TypeDiscoverer<>(String.class, EMPTY_MAP); TypeDiscoverer<String> stringParent = new TypeDiscoverer<>(String.class, emptyMap());
TypeDiscoverer<Object> objectParent = new TypeDiscoverer<>(Object.class, EMPTY_MAP); TypeDiscoverer<Object> objectParent = new TypeDiscoverer<>(Object.class, emptyMap());
ParameterizedTypeInformation<Object> first = new ParameterizedTypeInformation<>(one, stringParent, EMPTY_MAP); ParameterizedTypeInformation<Object> first = new ParameterizedTypeInformation<>(one, resolvedOne, stringParent);
ParameterizedTypeInformation<Object> second = new ParameterizedTypeInformation<>(one, objectParent, EMPTY_MAP); ParameterizedTypeInformation<Object> second = new ParameterizedTypeInformation<>(one, resolvedOne, objectParent);
assertThat(first).isNotEqualTo(second); assertThat(first).isNotEqualTo(second);
} }
@@ -67,10 +67,10 @@ public class ParameterizedTypeUnitTests {
@Test @Test
public void considersTypeInformationsWithSameParentsNotEqual() { public void considersTypeInformationsWithSameParentsNotEqual() {
TypeDiscoverer<String> stringParent = new TypeDiscoverer<>(String.class, EMPTY_MAP); TypeDiscoverer<String> stringParent = new TypeDiscoverer<>(String.class, emptyMap());
ParameterizedTypeInformation<Object> first = new ParameterizedTypeInformation<>(one, stringParent, EMPTY_MAP); ParameterizedTypeInformation<Object> first = new ParameterizedTypeInformation<>(one, resolvedOne, stringParent);
ParameterizedTypeInformation<Object> second = new ParameterizedTypeInformation<>(one, stringParent, EMPTY_MAP); ParameterizedTypeInformation<Object> second = new ParameterizedTypeInformation<>(one, resolvedOne, stringParent);
assertThat(first.equals(second)).isTrue(); assertThat(first.equals(second)).isTrue();
} }
@@ -136,6 +136,17 @@ public class ParameterizedTypeUnitTests {
assertThat(typeInformation.getMapValueType()).isNull(); assertThat(typeInformation.getMapValueType()).isNull();
} }
@Test // DATACMNS-1135
public void prefersLocalGenericsDeclarationOverParentBound() {
ClassTypeInformation<Candidate> 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") @SuppressWarnings("serial")
class Localized<S> extends HashMap<Locale, S> { class Localized<S> extends HashMap<Locale, S> {
S value; S value;
@@ -183,4 +194,22 @@ public class ParameterizedTypeUnitTests {
} }
class Education {} class Education {}
// DATACMNS-1135
abstract class CandidateInfo {}
class Responsibility extends CandidateInfo {}
class Experience extends CandidateInfo {
CandidateInfoContainer<Responsibility> responsibilities;
}
class CandidateInfoContainer<E extends CandidateInfo> {
List<E> values = new ArrayList<>();
}
class Candidate {
CandidateInfoContainer<Experience> experiences;
}
} }