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 2e2ff7cb8..5e02c5fd7 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,9 @@ public interface ColumnType { * @return */ static CassandraColumnType listOf(CassandraColumnType componentType) { - return listOf(componentType, FrozenInfo.NOT_FROZEN); + return listOf(componentType, false); } + /** * Creates a List {@link ColumnType} given its {@link CassandraColumnType component type}. * @@ -97,9 +98,9 @@ public interface ColumnType { * @param frozen * @return */ - static CassandraColumnType listOf(CassandraColumnType componentType, FrozenInfo frozen) { + static CassandraColumnType listOf(CassandraColumnType componentType, boolean frozen) { return new DefaultCassandraColumnType(ClassTypeInformation.LIST, - () -> DataTypes.listOf(componentType.getDataType(), frozen.self), componentType); + () -> DataTypes.listOf(componentType.getDataType(), frozen), componentType); } /** @@ -124,18 +125,19 @@ public interface ColumnType { * @return */ static CassandraColumnType setOf(CassandraColumnType componentType) { - return setOf(componentType, FrozenInfo.NOT_FROZEN); + return setOf(componentType, false); } + /** * 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}. + * @param frozen * @return */ - static CassandraColumnType setOf(CassandraColumnType componentType, FrozenInfo frozen) { - return new DefaultCassandraColumnType(ClassTypeInformation.SET, () -> DataTypes.setOf(componentType.getDataType(),frozen.self), - componentType); + static CassandraColumnType setOf(CassandraColumnType componentType, boolean frozen) { + return new DefaultCassandraColumnType(ClassTypeInformation.SET, + () -> DataTypes.setOf(componentType.getDataType(), frozen), 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 c9ac0557f..8d0b89279 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,7 +17,6 @@ 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; @@ -63,21 +62,6 @@ 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 0d9392053..b955c5d3c 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 @@ -27,7 +27,8 @@ 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.core.annotation.AnnotatedElementUtils; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty; @@ -98,8 +99,6 @@ 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); @@ -128,48 +127,51 @@ class DefaultColumnTypeResolver implements ColumnTypeResolver { } TypeInformation typeInformation = property.getTypeInformation(); - return resolve(typeInformation, frozen); + return resolve(typeInformation, getFrozenInfo(property)); } - private FrozenInfo getFrozenInfo(CassandraPersistentProperty property) { + private FrozenIndicator getFrozenInfo(CassandraPersistentProperty property) { - FrozenInfo frozen = FrozenInfo.frozen(property.isAnnotationPresent(Frozen.class)); AnnotatedType annotatedType = property.findAnnotatedType(Frozen.class); + if (annotatedType == null) { + return FrozenIndicator.NOT_FROZEN; + } + + return getFrozenIndicator(annotatedType); + } + + private FrozenIndicator getFrozenIndicator(AnnotatedType annotatedType) { + + FrozenIndicator frozen = FrozenIndicator.frozen(isFrozen(annotatedType)); + 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)); - } + for (AnnotatedType type : annotatedTypes) { + frozen.addNested(getFrozenIndicator(type)); } } + return frozen; } + private boolean isFrozen(AnnotatedType type) { + return AnnotatedElementUtils.hasAnnotation(type, Frozen.class); + } + /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.convert.ColumnTypeResolver#resolve(org.springframework.data.util.TypeInformation) */ @Override public CassandraColumnType resolve(TypeInformation typeInformation) { - return resolve(typeInformation, FrozenInfo.NOT_FROZEN); + return resolve(typeInformation, FrozenIndicator.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) { + private CassandraColumnType resolve(TypeInformation typeInformation, FrozenIndicator frozen) { return getCustomWriteTarget(typeInformation).map(it -> { return createCassandraTypeDescriptor(tryResolve(it), ClassTypeInformation.from(it)); @@ -389,24 +391,23 @@ class DefaultColumnTypeResolver implements ColumnTypeResolver { return new DefaultCassandraColumnType(typeInformation, dataType); } - private CassandraColumnType createCassandraTypeDescriptor(TypeInformation typeInformation, FrozenInfo frozen) { + private CassandraColumnType createCassandraTypeDescriptor(TypeInformation typeInformation, + FrozenIndicator frozen) { if (List.class.isAssignableFrom(typeInformation.getType())) { - - FrozenInfo elementFrozen = frozen.forFirstTypeParameter(); - return ColumnType.listOf(resolve(typeInformation.getRequiredComponentType(), elementFrozen), frozen); + return ColumnType.listOf(resolve(typeInformation.getRequiredComponentType(), frozen.getFrozen(0)), + frozen.isFrozen()); } if (Set.class.isAssignableFrom(typeInformation.getType())) { - - FrozenInfo elementFrozen = frozen.forFirstTypeParameter(); - return ColumnType.setOf(resolve(typeInformation.getRequiredComponentType(), elementFrozen), frozen); + return ColumnType.setOf(resolve(typeInformation.getRequiredComponentType(), frozen.getFrozen(0)), + frozen.isFrozen()); } if (typeInformation.isMap()) { - FrozenInfo frozenKey = frozen.forFirstTypeParameter(); - FrozenInfo frozenValue = frozen.forSecondTypeParameter(); + FrozenIndicator frozenKey = frozen.getFrozen(0); + FrozenIndicator frozenValue = frozen.getFrozen(1); return ColumnType.mapOf(resolve(typeInformation.getRequiredComponentType(), frozenKey), resolve(typeInformation.getRequiredMapValueType(), frozenValue)); @@ -417,7 +418,7 @@ class DefaultColumnTypeResolver implements ColumnTypeResolver { if (persistentEntity != null) { if (persistentEntity.isUserDefinedType()) { - return new DefaultCassandraColumnType(typeInformation, getUserType(persistentEntity, frozen.self)); + return new DefaultCassandraColumnType(typeInformation, getUserType(persistentEntity, frozen.isFrozen())); } if (persistentEntity.isTupleType()) { diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/FrozenIndicator.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/FrozenIndicator.java new file mode 100644 index 000000000..d77367b61 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/FrozenIndicator.java @@ -0,0 +1,58 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.List; + +/** + * Information about if a type or its elements should be frozen or not. + * + * @author Jens Schauder + * @author Mark Paluch + * @since 3.0 + */ +class FrozenIndicator { + + public static final FrozenIndicator NOT_FROZEN = new FrozenIndicator(); + + private final boolean frozen; + private final List nested = new ArrayList<>(); + + public static FrozenIndicator frozen(boolean frozen) { + return new FrozenIndicator(frozen); + } + + private FrozenIndicator() { + this(false); + } + + private FrozenIndicator(boolean frozen) { + this.frozen = frozen; + } + + public void addNested(FrozenIndicator frozenIndicator) { + this.nested.add(frozenIndicator); + } + + boolean isFrozen() { + return frozen; + } + + FrozenIndicator getFrozen(int parameterIndex) { + return parameterIndex < this.nested.size() ? this.nested.get(parameterIndex) : NOT_FROZEN; + } +} 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 deleted file mode 100644 index d5c7f8e64..000000000 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/FrozenInfo.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 index 2a00e20fb..0e5b68bc8 100644 --- 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 @@ -22,15 +22,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Specifies a type as "frozen". + * Indicates that a persistent property should use a {@code frozen} column type. * * @author Jens Schauder - * @see Documentation about frozen collections + * @see Documentation about + * frozen collections * @since 3.0 */ @Documented @Retention(RetentionPolicy.RUNTIME) -@Target(value = {ElementType.TYPE_USE, ElementType.FIELD, ElementType.METHOD}) +@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 fa9707802..a0b21df5c 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 @@ -46,6 +46,7 @@ import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; * Unit tests for {@link DefaultColumnTypeResolver}. * * @author Mark Paluch + * @author Jens Schauder */ public class ColumnTypeResolverUnitTests { diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index e1dc4aa40..5b47f6b27 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -8,6 +8,7 @@ This chapter summarizes changes and new features for each release. * Upgrade to Cassandra Driver version 4. See the <>. * Support for `NamingStrategy`. +* Support for frozen collections and UDT columns in schema creation. [[new-features.2-2-0]] == What's new in Spring Data for Apache Cassandra 2.2 diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 7c0c2523d..20fde7cf6 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -377,6 +377,8 @@ Describes the index to be created at session initialization. Allows SASI index creation during session initialization. * `@CassandraType`: Applied at the field level to specify a Cassandra data type. Types are derived from the property declaration by default. +* `@Frozen`: Applied at the field level to class-types and parametrized types. +Declares a frozen UDT column or frozen collection like `List<@Frozen UserDefinedPersonType>`. * `@UserDefinedType`: Applied at the type level to specify a Cassandra User-defined Data Type (UDT). Types are derived from the declaration by default. * `@Tuple`: Applied at the type level to use a type as a mapped tuple.