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; + } + }