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 {