From 01c6350cb3a2dfc2ab8802024c8c5ae7c0b4076d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 12 Mar 2021 09:10:21 +0100 Subject: [PATCH] Correctly resolve user type from CassandraType(userTypeName) for Maps. We now correctly resolve user types for type arguments configured to UDT. These can either apply to keys or values depending on the typeArguments. Closes #1098 --- .../convert/DefaultColumnTypeResolver.java | 16 ++++--- .../mapping/CassandraSimpleTypeHolder.java | 44 +++++++++++++++++++ .../cassandra/core/mapping/CassandraType.java | 6 ++- ...MappingCassandraConverterUDTUnitTests.java | 25 +++++++++++ 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/DefaultColumnTypeResolver.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/DefaultColumnTypeResolver.java index 0846fbd7b..7b1470e60 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/DefaultColumnTypeResolver.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/DefaultColumnTypeResolver.java @@ -225,9 +225,9 @@ class DefaultColumnTypeResolver implements ColumnTypeResolver { assertTypeArguments(annotation.typeArguments().length, 2); CassandraColumnType keyType = createCassandraTypeDescriptor( - CassandraSimpleTypeHolder.getDataTypeFor(annotation.typeArguments()[0])); + getRequiredDataType(annotation, 0)); CassandraColumnType valueType = createCassandraTypeDescriptor( - CassandraSimpleTypeHolder.getDataTypeFor(annotation.typeArguments()[1])); + getRequiredDataType(annotation, 1)); return ColumnType.mapOf(keyType, valueType); @@ -235,8 +235,7 @@ class DefaultColumnTypeResolver implements ColumnTypeResolver { case SET: assertTypeArguments(annotation.typeArguments().length, 1); - DataType componentType = annotation.typeArguments()[0] == Name.UDT ? getUserType(annotation.userTypeName()) - : CassandraSimpleTypeHolder.getDataTypeFor(annotation.typeArguments()[0]); + DataType componentType = getRequiredDataType(annotation, 0); if (type == Name.SET) { return ColumnType.setOf(createCassandraTypeDescriptor(componentType)); @@ -259,7 +258,7 @@ class DefaultColumnTypeResolver implements ColumnTypeResolver { return createCassandraTypeDescriptor(getUserType(annotation.userTypeName())); default: - return createCassandraTypeDescriptor(CassandraSimpleTypeHolder.getDataTypeFor(type)); + return createCassandraTypeDescriptor(CassandraSimpleTypeHolder.getRequiredDataTypeFor(type)); } } @@ -443,6 +442,13 @@ class DefaultColumnTypeResolver implements ColumnTypeResolver { return new DefaultCassandraColumnType(typeInformation, dataType); } + private DataType getRequiredDataType(CassandraType annotation, int typeIndex) { + + Name typeName = annotation.typeArguments()[typeIndex]; + return typeName == Name.UDT ? getUserType(annotation.userTypeName()) + : CassandraSimpleTypeHolder.getRequiredDataTypeFor(typeName); + } + private Class resolveToJavaType(DataType dataType) { TypeCodec codec = getCodecRegistry().codecFor(dataType); return codec.getJavaType().getRawType(); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraSimpleTypeHolder.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraSimpleTypeHolder.java index 77f4ee342..7c4762be3 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraSimpleTypeHolder.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraSimpleTypeHolder.java @@ -189,14 +189,58 @@ public class CassandraSimpleTypeHolder extends SimpleTypeHolder { return javaType.isEnum() ? DataTypes.TEXT : classToDataType.get(javaType); } + /** + * Returns the required default {@link DataType} for a {@link Class}. This method resolves only simple types to a + * Cassandra {@link DataType}. Throws {@link IllegalStateException} if the {@link Class} cannot be resolved to a + * {@link DataType}. + * + * @param javaType must not be {@literal null}. + * @return the {@link DataType} for {@code javaClass} if resolvable, otherwise {@literal null}. + * @throws IllegalStateException if the {@link Class} cannot be resolved to a {@link DataType}. + * @since 3.1.6 + * @see #getDataTypeFor(Class) + */ + public static DataType getRequiredDataTypeFor(Class javaType) { + + DataType dataType = getDataTypeFor(javaType); + + if (dataType == null) { + throw new IllegalStateException(String.format("Required DataType cannot be resolved for %s", javaType.getName())); + } + + return dataType; + } + /** * Returns the {@link DataType} for a {@link CassandraType.Name}. * * @param dataTypeName must not be {@literal null}. * @return the {@link DataType} for {@link CassandraType.Name}. */ + @Nullable public static DataType getDataTypeFor(CassandraType.Name dataTypeName) { return nameToDataType.get(dataTypeName); } + /** + * Returns the required {@link DataType} for a {@link CassandraType.Name}. Throws {@link IllegalStateException} if the + * {@link CassandraType.Name} cannot be resolved to a {@link DataType}. + * + * @param dataTypeName must not be {@literal null}. + * @return the {@link DataType} for {@link CassandraType.Name}. + * @throws IllegalStateException if the {@link CassandraType.Name} cannot be resolved to a {@link DataType}. + * @since 3.1.6 + * @see #getDataTypeFor(CassandraType.Name) + */ + public static DataType getRequiredDataTypeFor(CassandraType.Name dataTypeName) { + + DataType dataType = getDataTypeFor(dataTypeName); + + if (dataType == null) { + throw new IllegalStateException(String.format("Required DataType cannot be resolved for %s", dataTypeName)); + } + + return dataType; + } + } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraType.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraType.java index f425ce1c0..1c3658086 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraType.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraType.java @@ -57,8 +57,10 @@ public @interface CassandraType { /** * If the property maps to a User-Defined Type (UDT) then this attribute holds the user type name. For - * {@link java.util.Collection Collection-like} properties the user type name applies to the component type. The user - * type name is only required if the UDT does not map to a class annotated with {@link UserDefinedType}. + * {@link java.util.Collection Collection-like} properties the user type name applies to the component type. For + * {@link java.util.Map} properties, {@link #typeArguments()} configured to {@link Name#UDT} are resolved using the + * user type name. The user type name is only required if the UDT does not map to a class annotated with + * {@link UserDefinedType}. * * @return {@link String name} of the user type * @since 1.5 diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUDTUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUDTUnitTests.java index fc085d5e6..b2c552034 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUDTUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterUDTUnitTests.java @@ -40,7 +40,9 @@ import org.mockito.quality.Strictness; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.core.mapping.CassandraType; import org.springframework.data.cassandra.core.mapping.Embedded; +import org.springframework.data.cassandra.core.mapping.Frozen; import org.springframework.data.cassandra.core.mapping.UserDefinedType; import org.springframework.data.cassandra.core.mapping.UserTypeResolver; import org.springframework.data.cassandra.support.UserDefinedTypeBuilder; @@ -237,6 +239,21 @@ class MappingCassandraConverterUDTUnitTests { assertThat(target.udtValue.nested.age).isEqualTo(30); } + @Test // #1098 + void shouldWriteMapWithTypeHintToUdtValue() { + + when(userTypeResolver.resolveType(CqlIdentifier.fromCql("udt"))).thenReturn(manufacturer); + + MapWithUdt mapWithUdt = new MapWithUdt(); + mapWithUdt.map = Collections.singletonMap("key", new Manufacturer("name", "display")); + + Map sink = new LinkedHashMap<>(); + mappingCassandraConverter.write(mapWithUdt, sink); + + Map map = (Map) sink.get(CqlIdentifier.fromCql("map")); + assertThat(map.get("key")).isInstanceOf(UdtValue.class); + } + @UserDefinedType @Data @AllArgsConstructor @@ -317,4 +334,12 @@ class MappingCassandraConverterUDTUnitTests { } } + static class MapWithUdt { + + @Id String id; + + @CassandraType(type = CassandraType.Name.MAP, userTypeName = "udt", typeArguments = { CassandraType.Name.TEXT, + CassandraType.Name.UDT }) private Map map; + } + }