diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/util/ClassTypeInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/util/ClassTypeInformation.java index 773f00ff0..afc61296b 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/util/ClassTypeInformation.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/util/ClassTypeInformation.java @@ -102,10 +102,9 @@ public class ClassTypeInformation extends TypeDiscoverer { } /* - * (non-Javadoc) - * - * @see org.springframework.data.document.mongodb.TypeDiscovererTest.FieldInformation#getType() - */ + * (non-Javadoc) + * @see org.springframework.data.util.TypeDiscoverer#getType() + */ @Override public Class getType() { return type; @@ -125,9 +124,18 @@ public class ClassTypeInformation extends TypeDiscoverer { return super.getComponentType(); } + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeDiscoverer#isAssignableFrom(org.springframework.data.util.TypeInformation) + */ + @Override + public boolean isAssignableFrom(TypeInformation target) { + return getType().isAssignableFrom(target.getType()); + } + private static Type resolveArrayType(Class type) { Assert.isTrue(type.isArray()); Class componentType = type.getComponentType(); return componentType.isArray() ? resolveArrayType(componentType) : componentType; } -} \ No newline at end of file +} 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 706e77a9b..b3188dc2e 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 @@ -17,8 +17,10 @@ package org.springframework.data.util; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -74,4 +76,116 @@ class ParameterizedTypeInformation extends ParentTypeAwareTypeInformation return super.getMapValueType(); } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeDiscoverer#getTypeParameters() + */ + @Override + public List> getTypeArguments() { + + List> result = new ArrayList>(); + + for (Type argument : type.getActualTypeArguments()) { + result.add(createInfo(argument)); + } + + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeDiscoverer#isAssignableFrom(org.springframework.data.util.TypeInformation) + */ + @Override + public boolean isAssignableFrom(TypeInformation target) { + + if (this.equals(target)) { + return true; + } + + Class rawType = getType(); + Class rawTargetType = target.getType(); + + if (!rawType.isAssignableFrom(rawTargetType)) { + return false; + } + + TypeInformation otherTypeInformation = rawType.equals(rawTargetType) ? target : target + .getSuperTypeInformation(rawType); + + List> myParameters = getTypeArguments(); + List> typeParameters = otherTypeInformation.getTypeArguments(); + + if (myParameters.size() != typeParameters.size()) { + return false; + } + + for (int i = 0; i < myParameters.size(); i++) { + if (!myParameters.get(i).isAssignableFrom(typeParameters.get(i))) { + return false; + } + } + + return true; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeDiscoverer#getComponentType() + */ + @Override + public TypeInformation getComponentType() { + return createInfo(type.getActualTypeArguments()[0]); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.ParentTypeAwareTypeInformation#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (obj == this) { + return true; + } + + if (!(obj instanceof ParameterizedTypeInformation)) { + return false; + } + + ParameterizedTypeInformation that = (ParameterizedTypeInformation) obj; + + if (this.isResolvedCompletely() && that.isResolvedCompletely()) { + return this.type.equals(that.type); + } + + return super.equals(obj); + } + + private boolean isResolvedCompletely() { + + Type[] types = type.getActualTypeArguments(); + + if (types.length == 0) { + return false; + } + + for (Type type : types) { + + TypeInformation info = createInfo(type); + + if (info instanceof ParameterizedTypeInformation) { + if (!((ParameterizedTypeInformation) info).isResolvedCompletely()) { + return false; + } + } + + if (!(info instanceof ClassTypeInformation)) { + return false; + } + } + + return true; + } } 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 538a9284a..e87f6ca4d 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 @@ -27,7 +27,9 @@ import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -144,6 +146,7 @@ class TypeDiscoverer implements TypeInformation { */ public List> getParameterTypes(Constructor constructor) { + Assert.notNull(constructor); List> result = new ArrayList>(); for (Type type : constructor.getGenericParameterTypes()) { @@ -238,13 +241,22 @@ class TypeDiscoverer implements TypeInformation { * @see org.springframework.data.util.TypeInformation#getActualType() */ public TypeInformation getActualType() { + if (isMap()) { return getMapValueType(); - } else if (isCollectionLike()) { + } + + if (isCollectionLike()) { return getComponentType(); - } else { + } + + List> arguments = getTypeArguments(); + + if (arguments.isEmpty()) { return this; } + + return arguments.get(arguments.size() - 1); } /* @@ -261,16 +273,17 @@ class TypeDiscoverer implements TypeInformation { */ public TypeInformation getMapValueType() { - if (!isMap()) { - return null; + if (isMap()) { + return getTypeArgument(getType(), Map.class, 1); } - return getTypeArgument(getType(), Map.class, 1); - } + List> arguments = getTypeArguments(); - private TypeInformation getTypeArgument(Class type, Class bound, int index) { - Class[] arguments = GenericTypeResolver.resolveTypeArguments(type, bound); - return arguments == null ? null : createInfo(arguments[index]); + if (arguments.size() > 1) { + return arguments.get(1); + } + + return null; } /* @@ -296,13 +309,8 @@ class TypeDiscoverer implements TypeInformation { Class rawType = getType(); - if (!(isMap() || isCollectionLike() || Iterable.class.isAssignableFrom(rawType))) { - return null; - } - - if (type instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType) type; - return createInfo(parameterizedType.getActualTypeArguments()[0]); + if (rawType.isArray()) { + return createInfo(rawType.getComponentType()); } if (isMap()) { @@ -313,8 +321,10 @@ class TypeDiscoverer implements TypeInformation { return getTypeArgument(rawType, Iterable.class, 0); } - if (rawType.isArray()) { - return createInfo(rawType.getComponentType()); + List> arguments = getTypeArguments(); + + if (arguments.size() > 0) { + return arguments.get(0); } return null; @@ -330,6 +340,85 @@ class TypeDiscoverer implements TypeInformation { return createInfo(method.getGenericReturnType()); } + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#getMethodParameterTypes(java.lang.reflect.Method) + */ + public List> getParameterTypes(Method method) { + + Assert.notNull(method); + + Type[] parameterTypes = method.getGenericParameterTypes(); + List> result = new ArrayList>(parameterTypes.length); + + for (Type type : parameterTypes) { + result.add(createInfo(type)); + } + + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#getSuperTypeInformation(java.lang.Class) + */ + public TypeInformation getSuperTypeInformation(Class superType) { + + Class type = getType(); + + if (!superType.isAssignableFrom(type)) { + return null; + } + + if (getType().equals(superType)) { + return this; + } + + List candidates = new ArrayList(); + + Type genericSuperclass = type.getGenericSuperclass(); + if (genericSuperclass != null) { + candidates.add(genericSuperclass); + } + candidates.addAll(Arrays.asList(type.getGenericInterfaces())); + + for (Type candidate : candidates) { + + TypeInformation candidateInfo = createInfo(candidate); + + if (superType.equals(candidateInfo.getType())) { + return candidateInfo; + } else { + TypeInformation nestedSuperType = candidateInfo.getSuperTypeInformation(superType); + if (nestedSuperType != null) { + return nestedSuperType; + } + } + } + + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#getTypeParameters() + */ + public List> getTypeArguments() { + return Collections.emptyList(); + } + + /* (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#isAssignableFrom(org.springframework.data.util.TypeInformation) + */ + public boolean isAssignableFrom(TypeInformation target) { + return target.getSuperTypeInformation(getType()).equals(this); + } + + private TypeInformation getTypeArgument(Class type, Class bound, int index) { + Class[] arguments = GenericTypeResolver.resolveTypeArguments(type, bound); + return arguments == null ? null : createInfo(arguments[index]); + } + /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) @@ -368,4 +457,4 @@ class TypeDiscoverer implements TypeInformation { result += nullSafeHashCode(typeVariableMap); return result; } -} \ No newline at end of file +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/util/TypeInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/util/TypeInformation.java index ee2a50b58..869cc2ce6 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/util/TypeInformation.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/util/TypeInformation.java @@ -27,6 +27,12 @@ import java.util.List; */ public interface TypeInformation { + /** + * Returns the {@link TypeInformation}s for the parameters of the given {@link Constructor}. + * + * @param constructor must not be {@literal null}. + * @return + */ List> getParameterTypes(Constructor constructor); /** @@ -92,4 +98,38 @@ public interface TypeInformation { * @return */ TypeInformation getReturnType(Method method); -} \ No newline at end of file + + /** + * Returns the {@link TypeInformation}s for the parameters of the given {@link Method}. + * + * @param method must not be {@literal null}. + * @return + */ + List> getParameterTypes(Method method); + + /** + * Returns the {@link TypeInformation} for the given raw super type. + * + * @param superType must not be {@literal null}. + * @return the {@link TypeInformation} for the given raw super type or {@literal null} in case the current + * {@link TypeInformation} does not implement the given type. + */ + TypeInformation getSuperTypeInformation(Class superType); + + /** + * Returns if the current {@link TypeInformation} can be safely assigned to the given one. Mimics semantics of + * {@link Class#isAssignableFrom(Class)} but takes generics into account. Thus it will allow to detect that a + * {@code List} is assignable to {@code List}. + * + * @param target + * @return + */ + boolean isAssignableFrom(TypeInformation target); + + /** + * Returns the {@link TypeInformation} for the type arguments of the current {@link TypeInformation}. + * + * @return + */ + List> getTypeArguments(); +} 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 0b412dc36..3af2ec8db 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 @@ -17,7 +17,9 @@ package org.springframework.data.util; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.springframework.data.util.ClassTypeInformation.*; +import java.lang.reflect.Method; import java.util.Calendar; import java.util.Collection; import java.util.List; @@ -172,6 +174,86 @@ public class ClassTypeInformationUnitTests { assertThat(component.getComponentType().getType(), is(typeCompatibleWith(String.class))); } + @Test + public void resolvesTypeParametersCorrectly() { + + TypeInformation information = ClassTypeInformation.from(ConcreteType.class); + TypeInformation superTypeInformation = information.getSuperTypeInformation(GenericType.class); + + List> parameters = superTypeInformation.getTypeArguments(); + assertThat(parameters, hasSize(2)); + assertThat(parameters.get(0).getType(), is((Object) String.class)); + assertThat(parameters.get(1).getType(), is((Object) Object.class)); + } + + @Test + public void resolvesNestedInheritedTypeParameters() { + + TypeInformation information = ClassTypeInformation.from(SecondExtension.class); + TypeInformation superTypeInformation = information.getSuperTypeInformation(Base.class); + + List> parameters = superTypeInformation.getTypeArguments(); + assertThat(parameters, hasSize(1)); + assertThat(parameters.get(0).getType(), is((Object) String.class)); + } + + @Test + public void discoveresMethodParameterTypesCorrectly() throws Exception { + + TypeInformation information = ClassTypeInformation.from(SecondExtension.class); + Method method = SecondExtension.class.getMethod("foo", Base.class); + List> informations = information.getParameterTypes(method); + TypeInformation returnTypeInformation = information.getReturnType(method); + + assertThat(informations, hasSize(1)); + assertThat(informations.get(0).getType(), is((Object) Base.class)); + assertThat(informations.get(0), is((Object) returnTypeInformation)); + } + + @Test + public void discoversImplementationBindingCorrectlyForString() throws Exception { + + TypeInformation information = from(TypedClient.class); + Method method = TypedClient.class.getMethod("stringMethod", GenericInterface.class); + + TypeInformation parameterType = information.getParameterTypes(method).get(0); + + TypeInformation stringInfo = from(StringImplementation.class); + assertThat(parameterType.isAssignableFrom(stringInfo), is(true)); + assertThat(stringInfo.getSuperTypeInformation(GenericInterface.class), is((Object) parameterType)); + assertThat(parameterType.isAssignableFrom(from(LongImplementation.class)), is(false)); + assertThat(parameterType.isAssignableFrom(from(StringImplementation.class).getSuperTypeInformation( + GenericInterface.class)), is(true)); + } + + @Test + public void discoversImplementationBindingCorrectlyForLong() throws Exception { + + TypeInformation information = from(TypedClient.class); + Method method = TypedClient.class.getMethod("longMethod", GenericInterface.class); + + TypeInformation parameterType = information.getParameterTypes(method).get(0); + + assertThat(parameterType.isAssignableFrom(from(StringImplementation.class)), is(false)); + assertThat(parameterType.isAssignableFrom(from(LongImplementation.class)), is(true)); + assertThat(parameterType.isAssignableFrom(from(StringImplementation.class).getSuperTypeInformation( + GenericInterface.class)), is(false)); + } + + @Test + public void discoversImplementationBindingCorrectlyForNumber() throws Exception { + + TypeInformation information = from(TypedClient.class); + Method method = TypedClient.class.getMethod("boundToNumberMethod", GenericInterface.class); + + TypeInformation parameterType = information.getParameterTypes(method).get(0); + + assertThat(parameterType.isAssignableFrom(from(StringImplementation.class)), is(false)); + assertThat(parameterType.isAssignableFrom(from(LongImplementation.class)), is(true)); + assertThat(parameterType.isAssignableFrom(from(StringImplementation.class).getSuperTypeInformation( + GenericInterface.class)), is(false)); + } + static class StringMapContainer extends MapContainer { } @@ -248,4 +330,40 @@ public class ClassTypeInformationUnitTests { List wildcard; List> complexWildcard; } + + static class Base { + + } + + static class FirstExtension extends Base { + + public Base> foo(Base> param) { + return null; + } + } + + static class SecondExtension extends FirstExtension { + + } + + interface GenericInterface { + + } + + interface TypedClient { + + void stringMethod(GenericInterface param); + + void longMethod(GenericInterface param); + + void boundToNumberMethod(GenericInterface param); + } + + class StringImplementation implements GenericInterface { + + } + + class LongImplementation implements GenericInterface { + + } } 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 6dadb7417..1063323d4 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 @@ -17,11 +17,14 @@ package org.springframework.data.util; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.HashMap; import java.util.Locale; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -36,7 +39,12 @@ import org.mockito.runners.MockitoJUnitRunner; public class ParameterizedTypeUnitTests { @Mock - ParameterizedType one, two; + ParameterizedType one; + + @Before + public void setUp() { + when(one.getActualTypeArguments()).thenReturn(new Type[0]); + } @Test public void considersTypeInformationsWithDifferingParentsNotEqual() { diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/util/TypeDiscovererUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/util/TypeDiscovererUnitTests.java index 9a1eb008f..5b4a034cd 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/util/TypeDiscovererUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/util/TypeDiscovererUnitTests.java @@ -25,7 +25,6 @@ import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Properties; import org.junit.Test; import org.junit.runner.RunWith; @@ -93,7 +92,7 @@ public class TypeDiscovererUnitTests { @Test public void returnsComponentAndValueTypesForMapExtensions() { - TypeDiscoverer discoverer = new TypeDiscoverer(CustomMap.class, null); + TypeInformation discoverer = new TypeDiscoverer(CustomMap.class, null); assertEquals(Locale.class, discoverer.getMapValueType().getType()); assertEquals(String.class, discoverer.getComponentType().getType()); }