From d2737983c3e782548ad181082c86d457b0896711 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 13 Nov 2015 16:52:09 +0100 Subject: [PATCH] DATACMNS-783 - DefaultTypeMapper now specializes raw generic types. If the type lookup from the store source returns a raw generic type (e.g. resolving the value of a generic property against a value of that generic type - i.e. not a more concrete type binding the generic information) in the context of a generic property, we previously did not apply the generics information of the contextual instance to that very raw type. We now expose a TypeInformation.specialize(ClassTypeInformation) which applies the current generics context to the given raw type and basically creates a synthetic parameterized TypeInformation instance. DefaultTypeMapper applies this specialization by default now with ClassTypeInformation simply returning the given type as is so that we don't create any resolution overhead in case no generics are involved in the first place. --- .../data/convert/DefaultTypeMapper.java | 14 +++- .../data/util/ClassTypeInformation.java | 9 +++ .../data/util/TypeDiscoverer.java | 73 +++++++++++++++++++ .../data/util/TypeInformation.java | 10 +++ .../convert/DefaultTypeMapperUnitTests.java | 38 +++++++++- .../util/ClassTypeInformationUnitTests.java | 54 ++++++++++++-- 6 files changed, 184 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/springframework/data/convert/DefaultTypeMapper.java b/src/main/java/org/springframework/data/convert/DefaultTypeMapper.java index cae8a12b6..9bc99009b 100644 --- a/src/main/java/org/springframework/data/convert/DefaultTypeMapper.java +++ b/src/main/java/org/springframework/data/convert/DefaultTypeMapper.java @@ -147,10 +147,16 @@ public class DefaultTypeMapper implements TypeMapper { Class rawType = basicType == null ? null : basicType.getType(); - boolean isMoreConcreteCustomType = rawType == null ? true : rawType.isAssignableFrom(documentsTargetType) - && !rawType.equals(documentsTargetType); - return isMoreConcreteCustomType ? (TypeInformation) ClassTypeInformation.from(documentsTargetType) - : basicType; + boolean isMoreConcreteCustomType = rawType == null ? true + : rawType.isAssignableFrom(documentsTargetType) && !rawType.equals(documentsTargetType); + + if (!isMoreConcreteCustomType) { + return basicType; + } + + ClassTypeInformation targetType = ClassTypeInformation.from(documentsTargetType); + + return (TypeInformation) (basicType != null ? basicType.specialize(targetType) : targetType); } /** diff --git a/src/main/java/org/springframework/data/util/ClassTypeInformation.java b/src/main/java/org/springframework/data/util/ClassTypeInformation.java index aecfc1a8f..ee288dd10 100644 --- a/src/main/java/org/springframework/data/util/ClassTypeInformation.java +++ b/src/main/java/org/springframework/data/util/ClassTypeInformation.java @@ -167,6 +167,15 @@ public class ClassTypeInformation extends TypeDiscoverer { return getType().isAssignableFrom(target.getType()); } + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeDiscoverer#specialize(org.springframework.data.util.ClassTypeInformation) + */ + @Override + public TypeInformation specialize(ClassTypeInformation type) { + return type; + } + /* * (non-Javadoc) * @see java.lang.Object#toString() diff --git a/src/main/java/org/springframework/data/util/TypeDiscoverer.java b/src/main/java/org/springframework/data/util/TypeDiscoverer.java index cbe4e209e..9f587e498 100644 --- a/src/main/java/org/springframework/data/util/TypeDiscoverer.java +++ b/src/main/java/org/springframework/data/util/TypeDiscoverer.java @@ -493,6 +493,20 @@ class TypeDiscoverer implements TypeInformation { return target.getSuperTypeInformation(getType()).equals(this); } + /* + * (non-Javadoc) + * @see org.springframework.data.util.TypeInformation#specialize(org.springframework.data.util.ClassTypeInformation) + */ + @Override + public TypeInformation specialize(ClassTypeInformation type) { + + Assert.isTrue(getType().isAssignableFrom(type.getType())); + + List> arguments = getTypeArguments(); + + return arguments.isEmpty() ? type : createInfo(new SyntheticParamterizedType(type, arguments)); + } + private TypeInformation getTypeArgument(Class bound, int index) { Class[] arguments = GenericTypeResolver.resolveTypeArguments(getType(), bound); @@ -537,4 +551,63 @@ class TypeDiscoverer implements TypeInformation { public int hashCode() { return hashCode; } + + /** + * A synthetic {@link ParameterizedType}. + * + * @author Oliver Gierke + * @since 1.11 + */ + private static class SyntheticParamterizedType implements ParameterizedType { + + private final ClassTypeInformation typeInformation; + private final List> typeParameters; + + /** + * @param typeInformation must not be {@literal null}. + * @param typeParameters must not be {@literal null}. + */ + public SyntheticParamterizedType(ClassTypeInformation typeInformation, List> typeParameters) { + + Assert.notNull(typeInformation, "Type must not be null!"); + Assert.notNull(typeParameters, "Type parameters must not be null!"); + + this.typeInformation = typeInformation; + this.typeParameters = typeParameters; + } + + /* + * (non-Javadoc) + * @see java.lang.reflect.ParameterizedType#getRawType() + */ + @Override + public Type getRawType() { + return typeInformation.getType(); + } + + /* + * (non-Javadoc) + * @see java.lang.reflect.ParameterizedType#getOwnerType() + */ + @Override + public Type getOwnerType() { + return null; + } + + /* + * (non-Javadoc) + * @see java.lang.reflect.ParameterizedType#getActualTypeArguments() + */ + @Override + public Type[] getActualTypeArguments() { + + Type[] result = new Type[typeParameters.size()]; + + for (int i = 0; i < typeParameters.size(); i++) { + result[i] = typeParameters.get(0).getType(); + } + + return result; + } + } } diff --git a/src/main/java/org/springframework/data/util/TypeInformation.java b/src/main/java/org/springframework/data/util/TypeInformation.java index a71275904..8d875abbf 100644 --- a/src/main/java/org/springframework/data/util/TypeInformation.java +++ b/src/main/java/org/springframework/data/util/TypeInformation.java @@ -140,4 +140,14 @@ public interface TypeInformation { * @return */ List> getTypeArguments(); + + /** + * Specializes the given (raw) {@link ClassTypeInformation} using the context of the current potentially parameterized + * type, basically turning the given raw type into a parameterized one. Will return the given type as is if no + * generics are involved. + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + TypeInformation specialize(ClassTypeInformation type); } diff --git a/src/test/java/org/springframework/data/convert/DefaultTypeMapperUnitTests.java b/src/test/java/org/springframework/data/convert/DefaultTypeMapperUnitTests.java index d07a3251a..e25b38dde 100644 --- a/src/test/java/org/springframework/data/convert/DefaultTypeMapperUnitTests.java +++ b/src/test/java/org/springframework/data/convert/DefaultTypeMapperUnitTests.java @@ -15,7 +15,7 @@ */ package org.springframework.data.convert; -import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; @@ -50,14 +50,13 @@ public class DefaultTypeMapperUnitTests { Map source; @Before - @SuppressWarnings({ "rawtypes", "unchecked" }) public void setUp() { this.typeMapper = new DefaultTypeMapper>(accessor, Arrays.asList(mapper)); this.source = Collections.singletonMap("key", STRING); - when(accessor.readAliasFrom(source)).thenReturn(STRING); - when(mapper.resolveTypeFrom(STRING)).thenReturn((TypeInformation) STRING_TYPE_INFO); + doReturn(STRING).when(accessor).readAliasFrom(source); + doReturn(STRING_TYPE_INFO).when(mapper).resolveTypeFrom(STRING); } @Test @@ -83,4 +82,35 @@ public class DefaultTypeMapperUnitTests { assertThat(this.typeMapper.getAliasFor(STRING_TYPE_INFO), is(alias)); } + + /** + * @see DATACMNS-783 + */ + @Test + public void specializesRawSourceTypeUsingGenericContext() { + + ClassTypeInformation root = ClassTypeInformation.from(Foo.class); + TypeInformation propertyType = root.getProperty("abstractBar"); + TypeInformation barType = ClassTypeInformation.from(Bar.class); + + doReturn(barType).when(accessor).readAliasFrom(source); + doReturn(barType).when(mapper).resolveTypeFrom(barType); + + TypeInformation result = typeMapper.readType(source, propertyType); + + assertThat(result.getType(), is((Object) Bar.class)); + assertThat(result.getProperty("field").getType(), is((Object) Character.class)); + } + + static class TypeWithAbstractGenericType { + AbstractBar abstractBar; + } + + static class Foo extends TypeWithAbstractGenericType {} + + static abstract class AbstractBar {} + + static class Bar extends AbstractBar { + T field; + } } diff --git a/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java b/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java index 844bea797..ef1ef5355 100644 --- a/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java +++ b/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java @@ -224,8 +224,8 @@ public class ClassTypeInformationUnitTests { 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)); + assertThat(parameterType + .isAssignableFrom(from(StringImplementation.class).getSuperTypeInformation(GenericInterface.class)), is(true)); } @Test @@ -238,8 +238,8 @@ public class ClassTypeInformationUnitTests { 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)); + assertThat(parameterType + .isAssignableFrom(from(StringImplementation.class).getSuperTypeInformation(GenericInterface.class)), is(false)); } @Test @@ -252,8 +252,8 @@ public class ClassTypeInformationUnitTests { 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)); + assertThat(parameterType + .isAssignableFrom(from(StringImplementation.class).getSuperTypeInformation(GenericInterface.class)), is(false)); } @Test @@ -353,6 +353,35 @@ public class ClassTypeInformationUnitTests { assertThat(leafType.getType(), is((Object) Leaf.class)); } + /** + * @see DATACMNS-783 + */ + @Test + public void specializesTypeUsingTypeVariableContext() { + + ClassTypeInformation root = ClassTypeInformation.from(Foo.class); + TypeInformation property = root.getProperty("abstractBar"); + + TypeInformation specialized = property.specialize(ClassTypeInformation.from(Bar.class)); + + assertThat(specialized.getType(), is((Object) Bar.class)); + assertThat(specialized.getProperty("field").getType(), is((Object) Character.class)); + } + + /** + * @see DATACMNS-783 + */ + @Test + @SuppressWarnings("rawtypes") + public void usesTargetTypeDirectlyIfNoGenericsAreInvolved() { + + ClassTypeInformation root = ClassTypeInformation.from(Foo.class); + TypeInformation property = root.getProperty("object"); + + ClassTypeInformation from = ClassTypeInformation.from(Bar.class); + assertThat(property.specialize(from), is((TypeInformation) from)); + } + static class StringMapContainer extends MapContainer { } @@ -527,4 +556,17 @@ public class ClassTypeInformationUnitTests { static class ConcreteInnerIntermediate extends GenericInnerIntermediate {} static class Leaf {} + + static class TypeWithAbstractGenericType { + AbstractBar abstractBar; + Object object; + } + + static class Foo extends TypeWithAbstractGenericType {} + + static abstract class AbstractBar {} + + static class Bar extends AbstractBar { + T field; + } }