DATACASS-465 - Introduces @Frozen annotation to mark schema elements as Frozen.

`@Frozen` is supported on UDTs, collection like properties (`Set`, `List` and `Map`), and their type parameters if those are UDTs.

For example

    @Frozen Map<String, @Frozen MyUdt> propertyName;

Original pull request: #172.
This commit is contained in:
Jens Schauder
2020-03-26 10:31:59 +01:00
committed by Mark Paluch
parent 2b05a68eea
commit 17adf20a14
9 changed files with 345 additions and 19 deletions

View File

@@ -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);
}

View File

@@ -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.
*

View File

@@ -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));

View File

@@ -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);
}
}

View File

@@ -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 <a href="https://express-cassandra.readthedocs.io/en/stable/advanced/#frozen-collections">Documentation about frozen collections</a>
* @since 3.0
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE_USE, ElementType.FIELD, ElementType.METHOD})
public @interface Frozen {
}

View File

@@ -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<MyEnum, MyEnum> enumMapAsInt;
TupleValue tupleValue;
@Frozen List<String> frozenList;
List<@Frozen MyUdt> frozenListContent;
@Frozen Set<String> frozenSet;
Set<@Frozen MyUdt> frozenSetContent;
Map<@Frozen MyUdt, MyUdt> frozenMapKey;
Map<MyUdt, @Frozen MyUdt> frozenMapValue;
@UserDefinedType
private static class MyUdt {
String one;
Integer two;
}
}
static class TypeWithUUIDColumn {

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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()));