diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/ColumnType.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/ColumnType.java index 83575e1cc..2e2ff7cb8 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/ColumnType.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/ColumnType.java @@ -88,8 +88,18 @@ public interface ColumnType { * @return */ static CassandraColumnType listOf(CassandraColumnType componentType) { + return listOf(componentType, FrozenInfo.NOT_FROZEN); + } + /** + * Creates a List {@link ColumnType} given its {@link CassandraColumnType component type}. + * + * @param componentType must not be {@literal null}. + * @param frozen + * @return + */ + static CassandraColumnType listOf(CassandraColumnType componentType, FrozenInfo frozen) { return new DefaultCassandraColumnType(ClassTypeInformation.LIST, - () -> DataTypes.listOf(componentType.getDataType()), componentType); + () -> DataTypes.listOf(componentType.getDataType(), frozen.self), componentType); } /** @@ -114,7 +124,17 @@ public interface ColumnType { * @return */ static CassandraColumnType setOf(CassandraColumnType componentType) { - return new DefaultCassandraColumnType(ClassTypeInformation.SET, () -> DataTypes.setOf(componentType.getDataType()), + return setOf(componentType, FrozenInfo.NOT_FROZEN); + } + /** + * Creates a Set {@link ColumnType} given its {@link CassandraColumnType component type}. + * + * @param componentType must not be {@literal null}. + * @param frozen must not be {@literal null}. + * @return + */ + static CassandraColumnType setOf(CassandraColumnType componentType, FrozenInfo frozen) { + return new DefaultCassandraColumnType(ClassTypeInformation.SET, () -> DataTypes.setOf(componentType.getDataType(),frozen.self), componentType); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/ColumnTypeResolver.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/ColumnTypeResolver.java index 8d0b89279..c9ac0557f 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/ColumnTypeResolver.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/ColumnTypeResolver.java @@ -17,6 +17,7 @@ package org.springframework.data.cassandra.core.convert; import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty; import org.springframework.data.cassandra.core.mapping.CassandraType; +import org.springframework.data.cassandra.core.mapping.Frozen; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -62,6 +63,21 @@ public interface ColumnTypeResolver { */ CassandraColumnType resolve(TypeInformation typeInformation); + /** + * Resolve a {@link CassandraColumnType} from {@link TypeInformation}. Considers potentially registered custom + * converters and simple type rules. + * + * @param typeInformation must not be {@literal null}. + * @param frozen must not be {@literal null}. + * @return + * @see org.springframework.data.cassandra.core.mapping.CassandraSimpleTypeHolder + * @see CassandraCustomConversions + * @throws org.springframework.dao.InvalidDataAccessApiUsageException + */ + default CassandraColumnType resolve(TypeInformation typeInformation, FrozenInfo frozen) { + return resolve(typeInformation, FrozenInfo.NOT_FROZEN); + } + /** * Resolve a {@link CassandraColumnType} from a {@link CassandraType} annotation. * 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 774e3ed7e..0d9392053 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 @@ -15,6 +15,8 @@ */ package org.springframework.data.cassandra.core.convert; +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -25,13 +27,14 @@ import java.util.stream.StreamSupport; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - +import org.jetbrains.annotations.NotNull; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty; import org.springframework.data.cassandra.core.mapping.CassandraSimpleTypeHolder; import org.springframework.data.cassandra.core.mapping.CassandraType; import org.springframework.data.cassandra.core.mapping.CassandraType.Name; +import org.springframework.data.cassandra.core.mapping.Frozen; import org.springframework.data.cassandra.core.mapping.UserTypeResolver; import org.springframework.data.convert.CustomConversions; import org.springframework.data.mapping.MappingException; @@ -95,6 +98,8 @@ class DefaultColumnTypeResolver implements ColumnTypeResolver { Assert.notNull(property, "Property must not be null"); + FrozenInfo frozen = getFrozenInfo(property); + if (property.isAnnotationPresent(CassandraType.class)) { CassandraType annotation = property.getRequiredAnnotation(CassandraType.class); @@ -122,7 +127,32 @@ class DefaultColumnTypeResolver implements ColumnTypeResolver { return resolve(annotation); } - return resolve(property.getTypeInformation()); + TypeInformation typeInformation = property.getTypeInformation(); + return resolve(typeInformation, frozen); + } + + private FrozenInfo getFrozenInfo(CassandraPersistentProperty property) { + + FrozenInfo frozen = FrozenInfo.frozen(property.isAnnotationPresent(Frozen.class)); + AnnotatedType annotatedType = property.findAnnotatedType(Frozen.class); + + if (annotatedType instanceof AnnotatedParameterizedType) { + + AnnotatedParameterizedType apt = (AnnotatedParameterizedType) annotatedType; + AnnotatedType[] annotatedTypes = apt.getAnnotatedActualTypeArguments(); + + if (annotatedTypes.length > 0) { + + AnnotatedType type1 = annotatedTypes[0]; + frozen = frozen.withFirstTypeParameter(type1.isAnnotationPresent(Frozen.class)); + if (annotatedTypes.length > 1) { + + AnnotatedType type2 = annotatedTypes[1]; + frozen = frozen.withSecondTypeParameter(type2.isAnnotationPresent(Frozen.class)); + } + } + } + return frozen; } /* @@ -131,6 +161,15 @@ class DefaultColumnTypeResolver implements ColumnTypeResolver { */ @Override public CassandraColumnType resolve(TypeInformation typeInformation) { + return resolve(typeInformation, FrozenInfo.NOT_FROZEN); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.core.convert.ColumnTypeResolver#resolve(org.springframework.data.util.TypeInformation, org.springframework.data.cassandra.core.convert.FrozenInfo) + */ + @Override + public CassandraColumnType resolve(TypeInformation typeInformation, FrozenInfo frozen) { return getCustomWriteTarget(typeInformation).map(it -> { return createCassandraTypeDescriptor(tryResolve(it), ClassTypeInformation.from(it)); @@ -140,7 +179,7 @@ class DefaultColumnTypeResolver implements ColumnTypeResolver { return ColumnType.create(String.class, DataTypes.TEXT); } - return createCassandraTypeDescriptor(typeInformation); + return createCassandraTypeDescriptor(typeInformation, frozen); }); } @@ -350,19 +389,27 @@ class DefaultColumnTypeResolver implements ColumnTypeResolver { return new DefaultCassandraColumnType(typeInformation, dataType); } - private CassandraColumnType createCassandraTypeDescriptor(TypeInformation typeInformation) { + private CassandraColumnType createCassandraTypeDescriptor(TypeInformation typeInformation, FrozenInfo frozen) { if (List.class.isAssignableFrom(typeInformation.getType())) { - return ColumnType.listOf(resolve(typeInformation.getRequiredComponentType())); + + FrozenInfo elementFrozen = frozen.forFirstTypeParameter(); + return ColumnType.listOf(resolve(typeInformation.getRequiredComponentType(), elementFrozen), frozen); } if (Set.class.isAssignableFrom(typeInformation.getType())) { - return ColumnType.setOf(resolve(typeInformation.getRequiredComponentType())); + + FrozenInfo elementFrozen = frozen.forFirstTypeParameter(); + return ColumnType.setOf(resolve(typeInformation.getRequiredComponentType(), elementFrozen), frozen); } if (typeInformation.isMap()) { - return ColumnType.mapOf(resolve(typeInformation.getRequiredComponentType()), - resolve(typeInformation.getRequiredMapValueType())); + + FrozenInfo frozenKey = frozen.forFirstTypeParameter(); + FrozenInfo frozenValue = frozen.forSecondTypeParameter(); + + return ColumnType.mapOf(resolve(typeInformation.getRequiredComponentType(), frozenKey), + resolve(typeInformation.getRequiredMapValueType(), frozenValue)); } CassandraPersistentEntity persistentEntity = mappingContext.getPersistentEntity(typeInformation); @@ -370,7 +417,7 @@ class DefaultColumnTypeResolver implements ColumnTypeResolver { if (persistentEntity != null) { if (persistentEntity.isUserDefinedType()) { - return new DefaultCassandraColumnType(typeInformation, getUserType(persistentEntity)); + return new DefaultCassandraColumnType(typeInformation, getUserType(persistentEntity, frozen.self)); } if (persistentEntity.isTupleType()) { @@ -394,10 +441,11 @@ class DefaultColumnTypeResolver implements ColumnTypeResolver { return new DefaultCassandraColumnType(typeInformation, dataType); } - private DataType getUserType(CassandraPersistentEntity persistentEntity) { + private DataType getUserType(CassandraPersistentEntity persistentEntity, boolean frozen) { CqlIdentifier identifier = persistentEntity.getTableName(); - com.datastax.oss.driver.api.core.type.UserDefinedType userType = userTypeResolver.resolveType(identifier); + com.datastax.oss.driver.api.core.type.UserDefinedType userType = userTypeResolver.resolveType(identifier) + .copy(frozen); if (userType == null) { throw new MappingException(String.format("User type [%s] not found", identifier)); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/FrozenInfo.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/FrozenInfo.java new file mode 100644 index 000000000..d5c7f8e64 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/FrozenInfo.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.core.convert; + +/** + * Information about if a type or its elements should be frozen or not. + */ +public class FrozenInfo { + + public static final FrozenInfo NOT_FROZEN = new FrozenInfo(); + + final boolean self; + final boolean firstTypeParameter; + final boolean secondTypeParameter; + + public static FrozenInfo frozen(boolean frozen) { + return new FrozenInfo(frozen); + } + + private FrozenInfo() { + this(false, false, false); + } + + private FrozenInfo(boolean selfFrozen) { + this(selfFrozen, false, false); + } + + private FrozenInfo(boolean self, boolean firstTypeParameter, boolean secondTypeParameter) { + + this.self = self; + this.firstTypeParameter = firstTypeParameter; + this.secondTypeParameter = secondTypeParameter; + } + + FrozenInfo withFirstTypeParameter(boolean firstTypeParameter) { + return new FrozenInfo(self, firstTypeParameter, secondTypeParameter); + } + + FrozenInfo withSecondTypeParameter(boolean secondTypeParameter) { + return new FrozenInfo(self, firstTypeParameter, secondTypeParameter); + } + + FrozenInfo forFirstTypeParameter() { + return new FrozenInfo(firstTypeParameter); + } + + FrozenInfo forSecondTypeParameter() { + return new FrozenInfo(secondTypeParameter); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/Frozen.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/Frozen.java new file mode 100644 index 000000000..2a00e20fb --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/Frozen.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.core.mapping; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies a type as "frozen". + * + * @author Jens Schauder + * @see Documentation about frozen collections + * @since 3.0 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(value = {ElementType.TYPE_USE, ElementType.FIELD, ElementType.METHOD}) +public @interface Frozen { + +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/ColumnTypeResolverUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/ColumnTypeResolverUnitTests.java index d2a87d028..fa9707802 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/ColumnTypeResolverUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/ColumnTypeResolverUnitTests.java @@ -23,17 +23,23 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import org.assertj.core.api.SoftAssertions; import org.junit.Test; - import org.springframework.data.cassandra.core.mapping.BasicCassandraPersistentEntity; import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty; import org.springframework.data.cassandra.core.mapping.CassandraType; +import org.springframework.data.cassandra.core.mapping.Frozen; +import org.springframework.data.cassandra.core.mapping.UserDefinedType; import org.springframework.data.mapping.MappingException; import org.springframework.data.util.ClassTypeInformation; import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.type.DataType; import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.ListType; +import com.datastax.oss.driver.api.core.type.MapType; +import com.datastax.oss.driver.api.core.type.SetType; import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; /** @@ -44,7 +50,8 @@ import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; public class ColumnTypeResolverUnitTests { CassandraMappingContext mappingContext = new CassandraMappingContext(); - ColumnTypeResolver resolver = new DefaultColumnTypeResolver(mappingContext, null, () -> CodecRegistry.DEFAULT, + ColumnTypeResolver resolver = new DefaultColumnTypeResolver(mappingContext, + SchemaFactory.ShallowUserTypeResolver.INSTANCE, () -> CodecRegistry.DEFAULT, mappingContext::getCustomConversions); @Test // DATACASS-743 @@ -183,8 +190,7 @@ public class ColumnTypeResolverUnitTests { BasicCassandraPersistentEntity entity = mappingContext.getRequiredPersistentEntity(Person.class); CassandraColumnType columnType = resolver.resolve(entity.getRequiredPersistentProperty("tupleValue")); - assertThat(columnType.getType()) - .isEqualTo(TupleValue.class); + assertThat(columnType.getType()).isEqualTo(TupleValue.class); assertThatThrownBy(columnType::getDataType).isInstanceOf(MappingException.class); } @@ -201,6 +207,108 @@ public class ColumnTypeResolverUnitTests { assertThat(resolver.resolve(timeUUIDProperty).getDataType()).isEqualTo(DataTypes.TIMEUUID); } + @Test // DATACASS-465 + public void listPropertyWithFrozenAnnotation() { + + BasicCassandraPersistentEntity entity = mappingContext.getRequiredPersistentEntity(Person.class); + + DataType dataType = resolver.resolve(entity.getRequiredPersistentProperty("frozenList")).getDataType(); + assertThat(dataType).isInstanceOf(ListType.class); + assertThat(((ListType) dataType).isFrozen()).describedAs("The list itself should be frozen").isTrue(); + } + + @Test // DATACASS-465 + public void listPropertyWithFrozenAnnotationOnElement() { + + BasicCassandraPersistentEntity entity = mappingContext.getRequiredPersistentEntity(Person.class); + + DataType dataType = resolver.resolve(entity.getRequiredPersistentProperty("frozenListContent")).getDataType(); + + assertThat(dataType).isInstanceOf(ListType.class); + assertThat(((ListType) dataType).isFrozen()).describedAs("The collection itself should not be frozen.").isFalse(); + + DataType elementType = ((ListType) dataType).getElementType(); + assertThat(elementType).isInstanceOf(com.datastax.oss.driver.api.core.type.UserDefinedType.class); + assertThat(((com.datastax.oss.driver.api.core.type.UserDefinedType) elementType).isFrozen()) + .describedAs("The element type should be frozen").isTrue(); + } + + @Test // DATACASS-465 + public void setPropertyWithFrozenAnnotation() { + + BasicCassandraPersistentEntity entity = mappingContext.getRequiredPersistentEntity(Person.class); + + DataType dataType = resolver.resolve(entity.getRequiredPersistentProperty("frozenSet")).getDataType(); + assertThat(dataType).isInstanceOf(SetType.class); + assertThat(((SetType) dataType).isFrozen()).describedAs("The set itself should be frozen").isTrue(); + } + + @Test // DATACASS-465 + public void setPropertyWithFrozenAnnotationOnElement() { + + BasicCassandraPersistentEntity entity = mappingContext.getRequiredPersistentEntity(Person.class); + + DataType dataType = resolver.resolve(entity.getRequiredPersistentProperty("frozenSetContent")).getDataType(); + + assertThat(dataType).isInstanceOf(SetType.class); + assertThat(((SetType) dataType).isFrozen()).describedAs("The collection itself should not be frozen.").isFalse(); + + DataType elementType = ((SetType) dataType).getElementType(); + assertThat(elementType).isInstanceOf(com.datastax.oss.driver.api.core.type.UserDefinedType.class); + assertThat(((com.datastax.oss.driver.api.core.type.UserDefinedType) elementType).isFrozen()) + .describedAs("The element type should be frozen").isTrue(); + } + + @Test // DATACASS-465 + public void mapPropertyWithFrozenAnnotationOnKey() { + + BasicCassandraPersistentEntity entity = mappingContext.getRequiredPersistentEntity(Person.class); + + DataType dataType = resolver.resolve(entity.getRequiredPersistentProperty("frozenMapKey")).getDataType(); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(dataType).isInstanceOf(MapType.class); + softly.assertThat(((MapType) dataType).isFrozen()).describedAs("The collection itself should not be frozen.") + .isFalse(); + + DataType keyType = ((MapType) dataType).getKeyType(); + softly.assertThat(keyType).isInstanceOf(com.datastax.oss.driver.api.core.type.UserDefinedType.class); + softly.assertThat(((com.datastax.oss.driver.api.core.type.UserDefinedType) keyType).isFrozen()) + .describedAs("The key type should be frozen").isTrue(); + + DataType valueType = ((MapType) dataType).getValueType(); + softly.assertThat(valueType).isInstanceOf(com.datastax.oss.driver.api.core.type.UserDefinedType.class); + softly.assertThat(((com.datastax.oss.driver.api.core.type.UserDefinedType) valueType).isFrozen()) + .describedAs("The value type should not be frozen").isFalse(); + }); + } + + @Test // DATACASS-465 + public void mapPropertyWithFrozenAnnotationOnValue() { + + BasicCassandraPersistentEntity entity = mappingContext.getRequiredPersistentEntity(Person.class); + + DataType dataType = resolver.resolve(entity.getRequiredPersistentProperty("frozenMapValue")).getDataType(); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(dataType).isInstanceOf(MapType.class); + softly.assertThat(((MapType) dataType).isFrozen()).describedAs("The collection itself should not be frozen.") + .isFalse(); + + DataType keyType = ((MapType) dataType).getKeyType(); + softly.assertThat(keyType).isInstanceOf(com.datastax.oss.driver.api.core.type.UserDefinedType.class); + softly.assertThat(((com.datastax.oss.driver.api.core.type.UserDefinedType) keyType).isFrozen()) + .describedAs("The key type should not be frozen").isFalse(); + + DataType valueType = ((MapType) dataType).getValueType(); + softly.assertThat(valueType).isInstanceOf(com.datastax.oss.driver.api.core.type.UserDefinedType.class); + softly.assertThat(((com.datastax.oss.driver.api.core.type.UserDefinedType) valueType).isFrozen()) + .describedAs("The value type should be frozen").isTrue(); + }); + } + static class Person { String name; @@ -229,6 +337,22 @@ public class ColumnTypeResolverUnitTests { typeArguments = { CassandraType.Name.INT, CassandraType.Name.TEXT }) Map enumMapAsInt; TupleValue tupleValue; + + @Frozen List frozenList; + List<@Frozen MyUdt> frozenListContent; + + @Frozen Set frozenSet; + Set<@Frozen MyUdt> frozenSetContent; + + Map<@Frozen MyUdt, MyUdt> frozenMapKey; + + Map frozenMapValue; + + @UserDefinedType + private static class MyUdt { + String one; + Integer two; + } } static class TypeWithUUIDColumn { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterMappedTupleUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterMappedTupleUnitTests.java index 1e2f0ebcb..e513e39c0 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterMappedTupleUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/MappingCassandraConverterMappedTupleUnitTests.java @@ -28,7 +28,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; - import org.springframework.data.cassandra.core.mapping.BasicCassandraPersistentEntity; import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; import org.springframework.data.cassandra.core.mapping.Element; diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/BasicCassandraPersistentPropertyUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/BasicCassandraPersistentPropertyUnitTests.java index c9c5b9fdc..f1228c8c3 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/BasicCassandraPersistentPropertyUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/BasicCassandraPersistentPropertyUnitTests.java @@ -20,14 +20,15 @@ import static org.springframework.data.cassandra.core.mapping.CassandraType.*; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.AnnotatedParameterizedType; import java.lang.reflect.AnnotatedType; import java.lang.reflect.Field; +import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Map; import org.junit.Test; - import org.springframework.core.annotation.AliasFor; import org.springframework.data.mapping.model.Property; import org.springframework.data.util.ClassTypeInformation; @@ -131,6 +132,24 @@ public class BasicCassandraPersistentPropertyUnitTests { assertThat(persistentProperty.findAnnotation(CassandraType.class)).isNotNull(); } + /** + * Demonstrates how to access annotations on type parameters. + */ + @Test // DATACASS-465 + public void parameterAnnotations() { + + AnnotatedType annotatedType = findAnnotatedType(TypeWithMaps.class, "parameterizedWithParameterAnnotation"); + assertThat(annotatedType).isNotNull().isInstanceOf(AnnotatedParameterizedType.class); + + AnnotatedParameterizedType apt = (AnnotatedParameterizedType) annotatedType; + AnnotatedType[] annotatedActualTypeArguments = apt.getAnnotatedActualTypeArguments(); + assertThat(annotatedActualTypeArguments) + .extracting(a -> a.getType(), a -> Arrays.stream(a.getAnnotations()).map(an -> an instanceof Indexed).toArray()) + .containsExactly(tuple(String.class, new boolean[] {}), // annotation on key type + tuple(String.class, new boolean[] { true }) // annotation on value type + ); + } + private AnnotatedType findAnnotatedType(Class type, String parameterized) { return getPropertyFor(TypeWithMaps.class, parameterized).findAnnotatedType(Indexed.class); } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java index c52758f6e..dae9ab3d9 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java @@ -89,6 +89,7 @@ public class PartTreeCassandraQueryUnitTests { when(attachmentPoint.getCodecRegistry()).thenReturn(CodecRegistry.DEFAULT); when(attachmentPoint.getProtocolVersion()).thenReturn(ProtocolVersion.DEFAULT); + when(userTypeMock.copy(anyBoolean())).thenReturn(userTypeMock); when(userTypeMock.getAttachmentPoint()).thenReturn(attachmentPoint); when(userTypeMock.getFieldNames()) .thenReturn(Arrays.asList("city", "country").stream().map(CqlIdentifier::fromCql).collect(Collectors.toList()));