diff --git a/pom.xml b/pom.xml
index 13dc1aa7e..178e43de0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -91,7 +91,7 @@
17001
17000
3.9
- 3.1.3
+ 3.2.0
spring-data-cassandra
1.0
2.16
diff --git a/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/CreateUserTypeCqlGeneratorIntegrationTests.java b/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/CreateUserTypeCqlGeneratorIntegrationTests.java
index 02bb14681..c85036839 100644
--- a/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/CreateUserTypeCqlGeneratorIntegrationTests.java
+++ b/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/CreateUserTypeCqlGeneratorIntegrationTests.java
@@ -71,7 +71,7 @@ public class CreateUserTypeCqlGeneratorIntegrationTests extends AbstractKeyspace
assertThat(address.getFieldNames()).contains("zip", "city");
}
- @Test // DATACASS-172
+ @Test // DATACASS-172, DATACASS-424
public void createNestedUserType() {
CreateUserTypeSpecification addressSpec = CreateUserTypeSpecification //
@@ -89,6 +89,12 @@ public class CreateUserTypeCqlGeneratorIntegrationTests extends AbstractKeyspace
.name("person").ifNotExists().field("address", address) //
.field("city", DataType.varchar());
- session.execute(toCql(personSpec));
+ // Cassandra driver compatibility code: driver 3.0.x for frozen UDT in UDT types.
+ String cql = toCql(personSpec);
+ if (!cql.contains("frozen<") && !cql.contains(".address>")) {
+ cql = cql.replaceAll("address .*\\.address", "address frozen
");
+ }
+
+ session.execute(cql);
}
}
diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java
index 3f7d2bcab..ec868d40e 100644
--- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java
+++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java
@@ -36,6 +36,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.cassandra.convert.CustomConversions;
+import org.springframework.data.cassandra.mapping.UserTypeUtil.FrozenLiteralDataType;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.context.AbstractMappingContext;
@@ -376,13 +377,13 @@ public class BasicCassandraMappingContext
entity.getPersistentProperties().filter((property) -> !property.isCompositePrimaryKey()).forEach(property -> {
if (property.isIdProperty() || property.isPartitionKeyColumn()) {
- specification.partitionKeyColumn(property.getColumnName(), getDataType(property));
+ specification.partitionKeyColumn(property.getColumnName(),
+ UserTypeUtil.potentiallyFreeze(getDataType(property)));
} else if (property.isClusterKeyColumn()) {
- specification.clusteredKeyColumn(property.getColumnName(), getDataType(property),
- property.getPrimaryKeyOrdering());
+ specification.clusteredKeyColumn(property.getColumnName(),
+ UserTypeUtil.potentiallyFreeze(getDataType(property)), property.getPrimaryKeyOrdering());
} else {
- specification.column(property.getColumnName(), getDataType(property));
-
+ specification.column(property.getColumnName(), UserTypeUtil.potentiallyFreeze(getDataType(property)));
}
});
@@ -575,36 +576,4 @@ public class BasicCassandraMappingContext
*/
abstract DataType getDataType(CassandraPersistentEntity> entity);
}
-
- /**
- * @author Jens Schauder
- * @since 1.5.1
- */
- static class FrozenLiteralDataType extends DataType {
-
- private final CqlIdentifier type;
-
- protected FrozenLiteralDataType(CqlIdentifier type) {
-
- super(Name.UDT);
-
- this.type = type;
- }
-
- /* (non-Javadoc)
- * @see com.datastax.driver.core.DataType#isFrozen()
- */
- @Override
- public boolean isFrozen() {
- return true;
- }
-
- /* (non-Javadoc)
- * @see java.lang.Object#toString()
- */
- @Override
- public String toString() {
- return String.format("frozen<%s>", type.toCql());
- }
- }
}
diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/UserTypeUtil.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/UserTypeUtil.java
new file mode 100644
index 000000000..3417bfe9b
--- /dev/null
+++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/UserTypeUtil.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2017 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
+ *
+ * http://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.mapping;
+
+import static org.springframework.cassandra.core.cql.CqlIdentifier.*;
+
+import org.springframework.cassandra.core.cql.CqlIdentifier;
+import org.springframework.util.Assert;
+
+import com.datastax.driver.core.DataType;
+import com.datastax.driver.core.DataType.CollectionType;
+import com.datastax.driver.core.DataType.Name;
+import com.datastax.driver.core.UserType;
+
+/**
+ * {@link com.datastax.driver.core.UserType} utility methods. Mainly for internal use within the framework.
+ *
+ * @author Mark Paluch
+ * @since 2.0
+ */
+class UserTypeUtil {
+
+ /**
+ * Potentially create a frozen variant of {@code dataType}. Frozen types are required for nested UDTs (a user-type
+ * referencing another user-type) or UDTs within collection types.
+ *
+ * @param dataType must not be {@literal null}.
+ * @return the potentially frozen {@link DataType}.
+ */
+ static DataType potentiallyFreeze(DataType dataType) {
+
+ Assert.notNull(dataType, "DataType must not be null");
+
+ if (dataType.getName() == Name.LIST && dataType instanceof CollectionType) {
+
+ CollectionType collectionType = (CollectionType) dataType;
+
+ DataType typeArgument = collectionType.getTypeArguments().get(0);
+
+ if (typeArgument instanceof CollectionType || isNonFrozenUdt(typeArgument)) {
+ return DataType.list(potentiallyFreeze(typeArgument), collectionType.isFrozen());
+ }
+ }
+
+ if (dataType.getName() == Name.SET && dataType instanceof CollectionType) {
+
+ CollectionType collectionType = (CollectionType) dataType;
+
+ DataType typeArgument = collectionType.getTypeArguments().get(0);
+
+ if (typeArgument instanceof CollectionType || isNonFrozenUdt(typeArgument)) {
+ return DataType.set(potentiallyFreeze(typeArgument), collectionType.isFrozen());
+ }
+ }
+
+ if (dataType.getName() == Name.MAP && dataType instanceof CollectionType) {
+
+ CollectionType collectionType = (CollectionType) dataType;
+
+ DataType keyType = collectionType.getTypeArguments().get(0);
+ DataType valueType = collectionType.getTypeArguments().get(1);
+
+ if (keyType instanceof CollectionType || valueType instanceof CollectionType || isNonFrozenUdt(keyType)
+ || isNonFrozenUdt(valueType)) {
+ return DataType.map(potentiallyFreeze(keyType), potentiallyFreeze(valueType), collectionType.isFrozen());
+ }
+ }
+
+ return isNonFrozenUdt(dataType) ? new FrozenLiteralDataType(getTypeName(dataType)) : dataType;
+ }
+
+ private static CqlIdentifier getTypeName(DataType dataType) {
+
+ if (dataType instanceof UserType) {
+ return CqlIdentifier.cqlId(((UserType) dataType).getTypeName());
+ }
+
+ return cqlId(dataType.asFunctionParameterString());
+ }
+
+ private static boolean isNonFrozenUdt(DataType dataType) {
+ return dataType.getName() == Name.UDT && !dataType.isFrozen();
+ }
+
+ /**
+ * @author Jens Schauder
+ * @since 1.5.1
+ */
+ static class FrozenLiteralDataType extends DataType {
+
+ private final CqlIdentifier type;
+
+ FrozenLiteralDataType(CqlIdentifier type) {
+
+ super(Name.UDT);
+
+ this.type = type;
+ }
+
+ /* (non-Javadoc)
+ * @see com.datastax.driver.core.DataType#isFrozen()
+ */
+ @Override
+ public boolean isFrozen() {
+ return true;
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return String.format("frozen<%s>", type.toCql());
+ }
+ }
+}
diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CreateUserTypeIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CreateUserTypeIntegrationTests.java
new file mode 100644
index 000000000..fea1cde36
--- /dev/null
+++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CreateUserTypeIntegrationTests.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2017 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
+ *
+ * http://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.mapping;
+
+import static org.assertj.core.api.Assertions.*;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.cassandra.convert.CustomConversions;
+import org.springframework.data.cassandra.test.integration.support.AbstractSpringDataEmbeddedCassandraIntegrationTest;
+import org.springframework.data.cassandra.test.integration.support.IntegrationTestConfig;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import com.datastax.driver.core.KeyspaceMetadata;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.UserType;
+
+/**
+ * Integration tests for creation of UDT types through {@link BasicCassandraMappingContext}.
+ *
+ * @author Mark Paluch
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+public class CreateUserTypeIntegrationTests extends AbstractSpringDataEmbeddedCassandraIntegrationTest {
+
+ @Configuration
+ public static class Config extends IntegrationTestConfig {
+
+ @Bean
+ public CassandraMappingContext cassandraMapping() throws ClassNotFoundException {
+
+ BasicCassandraMappingContext mappingContext = new BasicCassandraMappingContext();
+
+ mappingContext.setInitialEntitySet(new HashSet<>(Arrays.asList(Car.class, Engine.class, Manufacturer.class)));
+
+ CustomConversions customConversions = customConversions();
+
+ mappingContext.setCustomConversions(customConversions);
+ mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder());
+ mappingContext.setUserTypeResolver(new SimpleUserTypeResolver(cluster().getObject(), getKeyspaceName()));
+
+ return mappingContext;
+ }
+ }
+
+ @Autowired Session session;
+
+ @Test // DATACASS-424
+ public void shouldCreateUserTypes() {
+
+ KeyspaceMetadata keyspace = session.getCluster().getMetadata().getKeyspace(session.getLoggedKeyspace());
+
+ Collection userTypes = keyspace.getUserTypes();
+
+ assertThat(userTypes).extracting("typeName").contains("engine", "manufacturer");
+ }
+
+ @Table
+ @Getter
+ @AllArgsConstructor
+ private static class Car {
+
+ @Id String id;
+ Engine engine;
+ }
+
+ @UserDefinedType
+ @Getter
+ @AllArgsConstructor
+ private static class Engine {
+ Manufacturer manufacturer;
+ List alternative;
+ }
+
+ @UserDefinedType
+ @Getter
+ @AllArgsConstructor
+ private static class Manufacturer {
+ String name;
+ }
+
+}
diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java
index e8bee72e3..40581a42c 100755
--- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java
+++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java
@@ -23,6 +23,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.time.LocalDate;
import java.util.Arrays;
@@ -368,17 +369,36 @@ public class StringBasedCassandraQueryUnitTests {
}
}
+ @SuppressWarnings("unchecked")
private UserType createUserType(String typeName, Collection fields) {
try {
- Constructor constructor = UserType.class.getDeclaredConstructor(String.class, String.class,
- Collection.class, ProtocolVersion.class, CodecRegistry.class);
- constructor.setAccessible(true);
- return constructor.newInstance(typeName, typeName, fields, ProtocolVersion.NEWEST_SUPPORTED,
- CodecRegistry.DEFAULT_INSTANCE);
+
+ Constructor[] declaredConstructors = (Constructor[]) UserType.class.getDeclaredConstructors();
+ for (Constructor constructor : declaredConstructors) {
+
+ if (Modifier.isPrivate(constructor.getModifiers())) {
+ continue;
+ }
+
+ constructor.setAccessible(true);
+
+ if (constructor.getParameterCount() == 5) {
+ // Cassandra driver 3.0.x - 3.1.x
+ return constructor.newInstance(typeName, typeName, fields, ProtocolVersion.NEWEST_SUPPORTED,
+ CodecRegistry.DEFAULT_INSTANCE);
+ }
+
+ // Cassandra driver 3.2.x
+ return constructor.newInstance(typeName, typeName, false, fields, ProtocolVersion.NEWEST_SUPPORTED,
+ CodecRegistry.DEFAULT_INSTANCE);
+ }
+
} catch (Exception e) {
throw new IllegalStateException(e);
}
+
+ throw new IllegalStateException("No suitable constructor found");
}
private interface SampleRepository extends Repository {