From b3c8cb562004ff523b7fba9207fb3a2a6ea372c2 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 14 Oct 2011 10:25:32 +0200 Subject: [PATCH] DATACMNS-88 - Map value types are now resolve correctly for generic types extending Map. Extracted base ParentTypeAwareTypeInformation class as base class for TypeInformations that need to consider the parent's type variable map. --- .../util/GenericArrayTypeInformation.java | 4 +- .../util/ParameterizedTypeInformation.java | 86 +++++++------------ .../util/ParentTypeAwareTypeInformation.java | 81 +++++++++++++++++ .../data/util/TypeDiscoverer.java | 11 +-- .../util/TypeVariableTypeInformation.java | 7 +- .../util/ClassTypeInformationUnitTests.java | 46 ++++++---- .../data/util/ParameterizedTypeUnitTests.java | 50 ++++++++++- 7 files changed, 197 insertions(+), 88 deletions(-) create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/util/ParentTypeAwareTypeInformation.java diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/util/GenericArrayTypeInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/util/GenericArrayTypeInformation.java index dd82e9df8..1ee62fc24 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/util/GenericArrayTypeInformation.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/util/GenericArrayTypeInformation.java @@ -24,7 +24,7 @@ import java.lang.reflect.Type; * * @author Oliver Gierke */ -class GenericArrayTypeInformation extends ParameterizedTypeInformation { +class GenericArrayTypeInformation extends ParentTypeAwareTypeInformation { private GenericArrayType type; @@ -36,7 +36,7 @@ class GenericArrayTypeInformation extends ParameterizedTypeInformation { * @param parent */ protected GenericArrayTypeInformation(GenericArrayType type, TypeDiscoverer parent) { - super(type, parent); + super(type, parent, parent.getTypeVariableMap()); this.type = type; } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/util/ParameterizedTypeInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/util/ParameterizedTypeInformation.java index c602a2df9..3310e6eb5 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/util/ParameterizedTypeInformation.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/util/ParameterizedTypeInformation.java @@ -15,12 +15,12 @@ */ package org.springframework.data.util; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; +import java.util.Arrays; +import java.util.HashSet; import java.util.Map; - -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; +import java.util.Set; /** * Base class for all types that include parameterization of some kind. Crucial as we have to take note of the parent @@ -28,9 +28,9 @@ import org.springframework.util.ObjectUtils; * * @author Oliver Gierke */ -class ParameterizedTypeInformation extends TypeDiscoverer { +class ParameterizedTypeInformation extends ParentTypeAwareTypeInformation { - private final TypeDiscoverer parent; + private final ParameterizedType type; /** * Creates a new {@link ParameterizedTypeInformation} for the given {@link Type} and parent {@link TypeDiscoverer}. @@ -38,60 +38,40 @@ class ParameterizedTypeInformation extends TypeDiscoverer { * @param type must not be {@literal null} * @param parent must not be {@literal null} */ - public ParameterizedTypeInformation(Type type, TypeDiscoverer parent) { - super(type, null); - Assert.notNull(parent); - this.parent = parent; - } - - /** - * Considers the parent's type variable map before invoking the super class method. - * - * @return - */ - @SuppressWarnings("rawtypes") - protected Map getTypeVariableMap() { - - return parent != null ? parent.getTypeVariableMap() : super.getTypeVariableMap(); - } - - /* (non-Javadoc) - * @see org.springframework.data.util.TypeDiscoverer#createInfo(java.lang.reflect.Type) - */ - @Override - protected TypeInformation createInfo(Type fieldType) { - if (parent.getType().equals(fieldType)) { - return parent; - } - - return super.createInfo(fieldType); + public ParameterizedTypeInformation(ParameterizedType type, TypeDiscoverer parent) { + super(type, parent, null); + this.type = type; } + + /* * (non-Javadoc) - * @see org.springframework.data.util.TypeDiscoverer#equals(java.lang.Object) + * @see org.springframework.data.util.TypeDiscoverer#getMapValueType() */ @Override - public boolean equals(Object obj) { - - if (!super.equals(obj)) { - return false; + public TypeInformation getMapValueType() { + + if (Map.class.equals(getType())) { + Type[] arguments = type.getActualTypeArguments(); + return createInfo(arguments[1]); } - - if (!this.getClass().equals(obj.getClass())) { - return false; + + Class rawType = getType(); + + Set supertypes = new HashSet(); + supertypes.add(rawType.getGenericSuperclass()); + supertypes.addAll(Arrays.asList(rawType.getGenericInterfaces())); + + for (Type supertype : supertypes) { + Class rawSuperType = GenericTypeResolver.resolveType(supertype, getTypeVariableMap()); + if (Map.class.isAssignableFrom(rawSuperType)) { + ParameterizedType parameterizedSupertype = (ParameterizedType) supertype; + Type[] arguments = parameterizedSupertype.getActualTypeArguments(); + return createInfo(arguments[1]); + } } - - ParameterizedTypeInformation that = (ParameterizedTypeInformation) obj; - return this.parent == null ? that.parent == null : this.parent.equals(that.parent); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.util.TypeDiscoverer#hashCode() - */ - @Override - public int hashCode() { - return super.hashCode() + 31 * ObjectUtils.nullSafeHashCode(parent); + + return super.getMapValueType(); } } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/util/ParentTypeAwareTypeInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/util/ParentTypeAwareTypeInformation.java new file mode 100644 index 000000000..42a133971 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/util/ParentTypeAwareTypeInformation.java @@ -0,0 +1,81 @@ +package org.springframework.data.util; + +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Map; + +import org.springframework.util.ObjectUtils; + +/** + * Base class for {@link TypeInformation} implementations that need parent type awareness. + * + * @author Oliver Gierke + */ +public abstract class ParentTypeAwareTypeInformation extends TypeDiscoverer { + + private final TypeDiscoverer parent; + + /** + * Creates a new {@link ParentTypeAwareTypeInformation}. + * + * @param type + * @param typeVariableMap + */ + @SuppressWarnings("rawtypes") + protected ParentTypeAwareTypeInformation(Type type, TypeDiscoverer parent, Map map) { + super(type, map); + this.parent = parent; + } + + /** + * Considers the parent's type variable map before invoking the super class method. + * + * @return + */ + @SuppressWarnings("rawtypes") + protected Map getTypeVariableMap() { + return parent == null ? super.getTypeVariableMap() : parent.getTypeVariableMap(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeDiscoverer#createInfo(java.lang.reflect.Type) + */ + @Override + protected TypeInformation createInfo(Type fieldType) { + + if (parent.getType().equals(fieldType)) { + return parent; + } + + return super.createInfo(fieldType); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeDiscoverer#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (!super.equals(obj)) { + return false; + } + + if (!this.getClass().equals(obj.getClass())) { + return false; + } + + ParentTypeAwareTypeInformation that = (ParentTypeAwareTypeInformation) obj; + return this.parent == null ? that.parent == null : this.parent.equals(that.parent); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeDiscoverer#hashCode() + */ + @Override + public int hashCode() { + return super.hashCode() + 31 * ObjectUtils.nullSafeHashCode(parent); + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/util/TypeDiscoverer.java b/spring-data-commons-core/src/main/java/org/springframework/data/util/TypeDiscoverer.java index e8b4d0ac4..6663c8d29 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/util/TypeDiscoverer.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/util/TypeDiscoverer.java @@ -88,6 +88,8 @@ class TypeDiscoverer implements TypeInformation { if (fieldType instanceof Class) { return new ClassTypeInformation((Class) fieldType); } + + Map variableMap = GenericTypeResolver.getTypeVariableMap(resolveType(fieldType)); if (fieldType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) fieldType; @@ -96,7 +98,7 @@ class TypeDiscoverer implements TypeInformation { if (fieldType instanceof TypeVariable) { TypeVariable variable = (TypeVariable) fieldType; - return new TypeVariableTypeInformation(variable, type, this); + return new TypeVariableTypeInformation(variable, type, this, variableMap); } if (fieldType instanceof GenericArrayType) { @@ -263,12 +265,7 @@ class TypeDiscoverer implements TypeInformation { if (!isMap()) { return null; } - - if (type instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType) type; - return createInfo(parameterizedType.getActualTypeArguments()[1]); - } - + return getTypeArgument(getType(), Map.class, 1); } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/util/TypeVariableTypeInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/util/TypeVariableTypeInformation.java index 66683841f..8f7d0b6b3 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/util/TypeVariableTypeInformation.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/util/TypeVariableTypeInformation.java @@ -20,6 +20,7 @@ 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.util.Assert; @@ -29,7 +30,7 @@ import org.springframework.util.Assert; * * @author Oliver Gierke */ -class TypeVariableTypeInformation extends ParameterizedTypeInformation { +class TypeVariableTypeInformation extends ParentTypeAwareTypeInformation { private final TypeVariable variable; private final Type owningType; @@ -42,9 +43,9 @@ class TypeVariableTypeInformation extends ParameterizedTypeInformation { * @param owningType must not be {@literal null} * @param parent */ - public TypeVariableTypeInformation(TypeVariable variable, Type owningType, TypeDiscoverer parent) { + public TypeVariableTypeInformation(TypeVariable variable, Type owningType, TypeDiscoverer parent, Map map) { - super(variable, parent); + super(variable, parent, map); Assert.notNull(variable); this.variable = variable; this.owningType = owningType; diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java index 948dbfd1c..e8ac60a83 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java @@ -55,33 +55,40 @@ public class ClassTypeInformationUnitTests { TypeInformation content = wrapper.getProperty("content"); assertEquals(String.class, content.getType()); - assertEquals(String.class, discoverer.getProperty("wrapped").getProperty("content").getType()); - assertEquals(String.class, discoverer.getProperty("wrapped.content").getType()); + assertEquals(String.class, + discoverer.getProperty("wrapped").getProperty("content").getType()); + assertEquals(String.class, discoverer.getProperty("wrapped.content") + .getType()); } @Test @SuppressWarnings("rawtypes") public void discoversBoundType() { - TypeInformation information = ClassTypeInformation.from(GenericTypeWithBound.class); + TypeInformation information = ClassTypeInformation.from( + GenericTypeWithBound.class); assertEquals(Person.class, information.getProperty("person").getType()); } @Test public void discoversBoundTypeForSpecialization() { - TypeInformation information = ClassTypeInformation - .from(SpecialGenericTypeWithBound.class); - assertEquals(SpecialPerson.class, information.getProperty("person").getType()); + TypeInformation information = ClassTypeInformation.from( + SpecialGenericTypeWithBound.class); + assertEquals(SpecialPerson.class, information.getProperty("person") + .getType()); } @Test @SuppressWarnings("rawtypes") public void discoversBoundTypeForNested() { - TypeInformation information = ClassTypeInformation.from(AnotherGenericType.class); - assertEquals(GenericTypeWithBound.class, information.getProperty("nested").getType()); - assertEquals(Person.class, information.getProperty("nested.person").getType()); + TypeInformation information = ClassTypeInformation.from( + AnotherGenericType.class); + assertEquals(GenericTypeWithBound.class, information.getProperty("nested") + .getType()); + assertEquals(Person.class, information.getProperty("nested.person") + .getType()); } @Test @@ -118,25 +125,25 @@ public class ClassTypeInformationUnitTests { assertEquals(Map.class, map.getType()); assertEquals(Calendar.class, map.getMapValueType().getType()); } - + @Test public void typeInfoDoesNotEqualForGenericTypesWithDifferentParent() { - + TypeInformation first = ClassTypeInformation.from(ConcreteWrapper.class); TypeInformation second = ClassTypeInformation.from(AnotherConcreteWrapper.class); - + assertFalse(first.getProperty("wrapped").equals(second.getProperty("wrapped"))); } @Test public void handlesPropertyFieldMismatchCorrectly() { - + TypeInformation from = ClassTypeInformation.from(PropertyGetter.class); - + TypeInformation property = from.getProperty("_name"); assertThat(property, is(notNullValue())); assertThat(property.getType(), is(typeCompatibleWith(String.class))); - + property = from.getProperty("name"); assertThat(property, is(notNullValue())); assertThat(property.getType(), is(typeCompatibleWith(byte[].class))); @@ -203,7 +210,8 @@ public class ClassTypeInformationUnitTests { S nested; } - static class SpecialGenericTypeWithBound extends GenericTypeWithBound { + static class SpecialGenericTypeWithBound extends + GenericTypeWithBound { } @@ -231,14 +239,14 @@ public class ClassTypeInformationUnitTests { static class ConcreteWrapper extends GenericWrapper { } - + static class AnotherConcreteWrapper extends GenericWrapper { - + } static class PropertyGetter { private String _name; - + public byte[] getName() { return _name.getBytes(); } diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/util/ParameterizedTypeUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/util/ParameterizedTypeUnitTests.java index 3a68c1502..587fac87e 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/util/ParameterizedTypeUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/util/ParameterizedTypeUnitTests.java @@ -15,25 +15,36 @@ */ package org.springframework.data.util; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import java.lang.reflect.ParameterizedType; +import java.util.HashMap; +import java.util.Locale; + import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; /** * Unit tests for {@link ParameterizedTypeInformation}. * * @author Oliver Gierke */ +@RunWith(MockitoJUnitRunner.class) public class ParameterizedTypeUnitTests { + @Mock ParameterizedType one, two; + @Test public void considersTypeInformationsWithDifferingParentsNotEqual() { TypeDiscoverer stringParent = new TypeDiscoverer(String.class, null); TypeDiscoverer objectParent = new TypeDiscoverer(Object.class, null); - ParameterizedTypeInformation first = new ParameterizedTypeInformation(Object.class, stringParent); - ParameterizedTypeInformation second = new ParameterizedTypeInformation(Object.class, objectParent); + ParameterizedTypeInformation first = new ParameterizedTypeInformation(one, stringParent); + ParameterizedTypeInformation second = new ParameterizedTypeInformation(one, objectParent); assertFalse(first.equals(second)); } @@ -43,9 +54,40 @@ public class ParameterizedTypeUnitTests { TypeDiscoverer stringParent = new TypeDiscoverer(String.class, null); - ParameterizedTypeInformation first = new ParameterizedTypeInformation(Object.class, stringParent); - ParameterizedTypeInformation second = new ParameterizedTypeInformation(Object.class, stringParent); + ParameterizedTypeInformation first = new ParameterizedTypeInformation(one, stringParent); + ParameterizedTypeInformation second = new ParameterizedTypeInformation(one, stringParent); assertTrue(first.equals(second)); } + + /** + * @see DATACMNS-88 + */ + @Test + public void resolvesMapValueTypeCorrectly() { + + TypeInformation type = ClassTypeInformation.from(Foo.class); + TypeInformation propertyType = type.getProperty("param"); + assertThat(propertyType.getProperty("value").getType(), is(typeCompatibleWith(String.class))); + assertThat(propertyType.getMapValueType().getType(), is(typeCompatibleWith(String.class))); + + propertyType = type.getProperty("param2"); + assertThat(propertyType.getProperty("value").getType(), is(typeCompatibleWith(String.class))); + assertThat(propertyType.getMapValueType().getType(), is(typeCompatibleWith(Locale.class))); + } + + @SuppressWarnings("serial") + class Localized extends HashMap { + S value; + } + + @SuppressWarnings("serial") + class Localized2 extends HashMap { + S value; + } + + class Foo { + Localized param; + Localized2 param2; + } }