From d670af5ec0fcdeb92927bb2065f2a66d407141c8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 16 Mar 2022 16:45:27 +0100 Subject: [PATCH] Create a new conversion context for projection properties. We now create a new conversion context to ensure that we use the correct property type to avoid type retention when mapping complex objects within a projection. Closes #1240 --- .../convert/MappingCassandraConverter.java | 26 +++++--- .../MappingCassandraConverterUnitTests.java | 62 +++++++++++++++++-- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java index 66386051b..a093b278b 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverter.java @@ -27,6 +27,7 @@ import java.util.function.Predicate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.context.ApplicationContext; @@ -39,7 +40,16 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.cassandra.core.mapping.*; import org.springframework.data.cassandra.core.mapping.Embedded.OnEmpty; import org.springframework.data.convert.CustomConversions; -import org.springframework.data.mapping.*; +import org.springframework.data.mapping.AccessOptions; +import org.springframework.data.mapping.InstanceCreatorMetadata; +import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.Parameter; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.PersistentPropertyPathAccessor; +import org.springframework.data.mapping.PreferredConstructor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; @@ -1054,8 +1064,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter @SuppressWarnings({ "rawtypes", "unchecked" }) private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { - if (value == null || target == null - || ClassUtils.isAssignableValue(target, value)) { + if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) { return value; } @@ -1308,10 +1317,9 @@ public class MappingCassandraConverter extends AbstractCassandraConverter final ValueConverter elementConverter; public ConversionContext(org.springframework.data.convert.CustomConversions conversions, - ContainerValueConverter rowConverter, - ContainerValueConverter tupleConverter, ContainerValueConverter udtConverter, - ContainerValueConverter> collectionConverter, ContainerValueConverter> mapConverter, - ValueConverter elementConverter) { + ContainerValueConverter rowConverter, ContainerValueConverter tupleConverter, + ContainerValueConverter udtConverter, ContainerValueConverter> collectionConverter, + ContainerValueConverter> mapConverter, ValueConverter elementConverter) { this.conversions = conversions; this.rowConverter = rowConverter; this.tupleConverter = tupleConverter; @@ -1577,7 +1585,9 @@ public class MappingCassandraConverter extends AbstractCassandraConverter EntityProjection property = projection.findProperty(name); if (property == null) { - return super.forProperty(name); + return new ConversionContext(conversions, MappingCassandraConverter.this::doReadRow, + MappingCassandraConverter.this::doReadTupleValue, MappingCassandraConverter.this::doReadUdtValue, + collectionConverter, mapConverter, elementConverter); } return new ProjectingConversionContext(conversions, rowConverter, tupleConverter, udtConverter, diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUnitTests.java index c992b70d9..936a97f4e 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUnitTests.java @@ -60,6 +60,7 @@ import org.springframework.data.cassandra.domain.TypeWithKeyClass; import org.springframework.data.cassandra.domain.TypeWithMapId; import org.springframework.data.cassandra.domain.User; import org.springframework.data.cassandra.domain.UserToken; +import org.springframework.data.cassandra.support.UserDefinedTypeBuilder; import org.springframework.data.cassandra.test.util.RowMockUtil; import org.springframework.data.projection.EntityProjection; import org.springframework.data.projection.EntityProjectionIntrospector; @@ -67,6 +68,7 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import com.datastax.oss.driver.api.core.CqlIdentifier; import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.data.UdtValue; import com.datastax.oss.driver.api.core.type.DataTypes; import com.datastax.oss.driver.internal.core.data.DefaultTupleValue; import com.datastax.oss.driver.internal.core.type.DefaultTupleType; @@ -1308,9 +1310,8 @@ public class MappingCassandraConverterUnitTests { @CassandraType(type = CassandraType.Name.SET, typeArguments = CassandraType.Name.INT) private Set conditionSet; - @CassandraType(type = CassandraType.Name.MAP, - typeArguments = { CassandraType.Name.INT, - CassandraType.Name.INT }) private Map conditionMap; + @CassandraType(type = CassandraType.Name.MAP, typeArguments = { CassandraType.Name.INT, + CassandraType.Name.INT }) private Map conditionMap; } @@ -1481,7 +1482,8 @@ public class MappingCassandraConverterUnitTests { Row source = RowMockUtil.newRowMock(column("id", "id-1", DataTypes.TEXT), column("prefixage", 30, DataTypes.INT), column("prefixfirstname", "fn", DataTypes.TEXT)); - WithPrefixedNullableEmbeddedType target = mappingCassandraConverter.read(WithPrefixedNullableEmbeddedType.class, source); + WithPrefixedNullableEmbeddedType target = mappingCassandraConverter.read(WithPrefixedNullableEmbeddedType.class, + source); assertThat(target.nested).isEqualTo(new EmbeddedWithSimpleTypes("fn", 30, null)); } @@ -1514,6 +1516,29 @@ public class MappingCassandraConverterUnitTests { assertThat(target.theJson.get("hello")).isEqualTo("world"); } + @Test // GH-1240 + void shouldReadOpenProjectionWithNestedObject() { + + com.datastax.oss.driver.api.core.type.UserDefinedType authorType = UserDefinedTypeBuilder.forName("author") + .withField("firstName", DataTypes.TEXT).withField("lastName", DataTypes.TEXT).build(); + + UdtValue udtValue = authorType.newValue().setString("firstName", "Walter").setString("lastName", "White"); + + Row source = RowMockUtil.newRowMock(column("id", "id-1", DataTypes.TEXT), column("name", "my-book", DataTypes.INT), + column("author", udtValue, authorType)); + + EntityProjectionIntrospector introspector = EntityProjectionIntrospector.create( + mappingCassandraConverter.getProjectionFactory(), + EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() + .and((target, underlyingType) -> !mappingCassandraConverter.getCustomConversions().isSimpleType(target)), + mappingContext); + + BookProjection projection = mappingCassandraConverter + .project(introspector.introspect(BookProjection.class, Book.class), source); + + assertThat(projection.getName()).isEqualTo("my-book by Walter White"); + } + static class TypeWithJsonObject { JSONObject theJson; @@ -1542,4 +1567,33 @@ public class MappingCassandraConverterUnitTests { } } + + interface BookProjection { + + @Value("#{target.name + ' by ' + target.author.firstName + ' ' + target.author.lastName}") + String getName(); + } + + @Data + static class Book { + + @Id String id; + + String name; + + Author author = new Author(); + + } + + @Data + @UserDefinedType + static class Author { + + @Id String id; + + String firstName; + + String lastName; + + } }