From cfa8a6177fae1bdd0cff8129aba47eb8451ac067 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 7 Sep 2016 13:12:46 +0200 Subject: [PATCH] DATACASS-172 - Support for UDTs. We now support Cassandra User-defined types. UDTs can be created using CQL generators and used inside of mapped domain classes. User-defined types can be used either raw as UDTValue that is passed through or as mapped object. Mapped UDTs must be annotated with @UserDefinedType. Types are included into schema generation so known and defined UDTs are created before any tables are created. UDTs can be used with set and list collection types and in primary keys. UDTs can also be used in repository query methods as query predicates. Updating UDTs will update the whole UDT. @UserDefinedType public class Address { String city; String country; } @Table public class Person { @Id String id; Address address; UDTValue genericUdt; } The XML namespace support was extended with new schema versions to support provide a User Type resolver so UDTs can be resolved: --- .../generator/RenameColumnCqlGenerator.java | 11 +- .../keyspace/UserTypeNameSpecification.java | 2 +- .../main/resources/META-INF/spring.schemas | 3 +- .../cassandra/config/spring-cql-1.5.xsd | 730 ++++++++++++++ ...rUserTypeCqlGeneratorIntegrationTests.java | 1 - .../AlterUserTypeCqlGeneratorUnitTests.java | 11 +- ...eUserTypeCqlGeneratorIntegrationTests.java | 7 +- .../CreateUserTypeCqlGeneratorUnitTests.java | 9 +- .../DropUserTypeCqlGeneratorUnitTests.java | 17 +- .../config/xml/XmlConfigIntegrationTests.java | 2 +- ...atingXmlConfigIntegrationTests-context.xml | 2 +- ...atingXmlConfigIntegrationTests-context.xml | 2 +- ...nimalXmlConfigIntegrationTests-context.xml | 2 +- .../config/CassandraSessionFactoryBean.java | 60 +- .../cassandra/config/DefaultBeanNames.java | 1 + .../java/AbstractCassandraConfiguration.java | 6 +- .../xml/CassandraMappingContextParser.java | 45 +- .../BasicCassandraRowValueProvider.java | 25 +- .../convert/CassandraRowValueProvider.java | 16 +- .../convert/CassandraUDTValueProvider.java | 84 ++ .../convert/CassandraValueProvider.java | 38 + .../data/cassandra/convert/ColumnReader.java | 2 +- .../cassandra/convert/CustomConversions.java | 6 + .../convert/MappingCassandraConverter.java | 202 +++- .../core/CassandraAdminOperations.java | 22 +- .../core/CassandraAdminTemplate.java | 42 +- ...assandraPersistentEntitySchemaCreator.java | 213 ++++ .../mapping/BasicCassandraMappingContext.java | 202 +++- .../BasicCassandraPersistentEntity.java | 44 +- ...andraPersistentEntityMetadataVerifier.java | 27 +- .../BasicCassandraPersistentProperty.java | 77 +- .../mapping/CassandraMappingContext.java | 37 +- .../mapping/CassandraPersistentEntity.java | 22 +- .../mapping/CassandraSimpleTypeHolder.java | 2 + .../data/cassandra/mapping/CassandraType.java | 10 + .../CassandraUserTypePersistentEntity.java | 99 ++ ...andraPersistentEntityMetadataVerifier.java | 13 +- ...PrimaryKeyClassEntityMetadataVerifier.java | 9 - .../mapping/SimpleUserTypeResolver.java | 61 ++ .../cassandra/mapping/UserTypeResolver.java | 37 + .../query/ConvertingParameterAccessor.java | 68 +- .../query/StringBasedCassandraQuery.java | 2 +- .../main/resources/META-INF/spring.schemas | 3 +- .../cassandra/config/spring-cassandra-1.0.xsd | 29 + .../cassandra/config/spring-cassandra-1.5.xsd | 944 ++++++++++++++++++ .../CassandraSessionFactoryBeanUnitTests.java | 111 +- .../config/SchemaActionIntegrationTests.java | 34 +- .../CassandraNamespaceIntegrationTests.java | 16 + ...CassandraConverterUDTIntegrationTests.java | 556 +++++++++++ ...ersistentEntitySchemaCreatorUnitTests.java | 121 +++ ...BasicCassandraMappingContextUnitTests.java | 192 +++- ...istentEntityMetadataVerifierUnitTests.java | 35 - ...sicCassandraPersistentEntityUnitTests.java | 13 + ...ndraUserTypePersistentEntityUnitTests.java | 98 ++ ...istentEntityMetadataVerifierUnitTests.java | 2 +- ...BasicCassandraMappingContextUnitTests.java | 197 +++- ...yClassEntityMetadataVerifierUnitTests.java | 20 - .../conversion/Address.java | 2 +- .../repository/conversion/Contact.java | 51 + ...dsParameterConversionIntegrationTests.java | 139 +++ .../ParameterConversionTestSupport.java} | 175 ++-- .../repository/conversion/Phone.java | 27 + ...dsParameterConversionIntegrationTests.java | 147 +++ .../PartTreeCassandraQueryUnitTests.java | 63 +- ...> StringBasedCassandraQueryUnitTests.java} | 93 +- .../cdi/CassandraOperationsProducer.java | 19 +- .../Contact.java => declared/Address.java} | 27 +- .../querymethods/declared/Person.java | 6 + .../derived/PersonRepository.java | 6 + .../QueryDerivationIntegrationTests.java | 38 + .../repository/simple/AddressType.java | 31 + .../integration/repository/simple/User.java | 11 + .../repository/simple/UserRepository.java | 4 + .../UserRepositoryIntegrationTests.java | 37 +- ...erRepositoryIntegrationTestsDelegator.java | 5 + ...andraNamespaceIntegrationTests-context.xml | 8 +- ...itoryXmlConfigIntegrationTests-context.xml | 2 +- ...itoryXmlConfigIntegrationTests-context.xml | 2 +- ...itoryXmlConfigIntegrationTests-context.xml | 2 +- ...nimumXmlConfigIntegrationTests-context.xml | 2 +- ...itoryXmlConfigIntegrationTests-context.xml | 10 +- ...EntityScanningIntegrationTests-context.xml | 2 +- 82 files changed, 4924 insertions(+), 627 deletions(-) create mode 100644 spring-cql/src/main/resources/org/springframework/cassandra/config/spring-cql-1.5.xsd create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraUDTValueProvider.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraValueProvider.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreator.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraUserTypePersistentEntity.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/SimpleUserTypeResolver.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/UserTypeResolver.java create mode 100644 spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-1.5.xsd rename spring-data-cassandra/src/test/java/org/springframework/data/cassandra/{test/integration => }/config/SchemaActionIntegrationTests.java (88%) create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/MappingCassandraConverterUDTIntegrationTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreatorUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CassandraUserTypePersistentEntityUnitTests.java rename spring-data-cassandra/src/test/java/org/springframework/data/cassandra/{test/integration/repository/querymethods => repository}/conversion/Address.java (89%) create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/Contact.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/DerivedQueryMethodsParameterConversionIntegrationTests.java rename spring-data-cassandra/src/test/java/org/springframework/data/cassandra/{test/integration/repository/querymethods/conversion/ParameterConversionIntegrationTests.java => repository/conversion/ParameterConversionTestSupport.java} (55%) create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/Phone.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/StringQueryMethodsParameterConversionIntegrationTests.java rename spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/{StringBasedCassandraQueryIntegrationUnitTests.java => StringBasedCassandraQueryUnitTests.java} (78%) rename spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/{conversion/Contact.java => declared/Address.java} (73%) create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/AddressType.java diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/cql/generator/RenameColumnCqlGenerator.java b/spring-cql/src/main/java/org/springframework/cassandra/core/cql/generator/RenameColumnCqlGenerator.java index bed503f26..7389c0dd1 100644 --- a/spring-cql/src/main/java/org/springframework/cassandra/core/cql/generator/RenameColumnCqlGenerator.java +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/cql/generator/RenameColumnCqlGenerator.java @@ -33,18 +33,23 @@ public class RenameColumnCqlGenerator extends ColumnChangeCqlGenerator The subtype of the {@link UserTypeNameSpecification} + * @param Subtype of {@link UserTypeNameSpecification}. * @since 1.5 * @see CqlIdentifier */ diff --git a/spring-cql/src/main/resources/META-INF/spring.schemas b/spring-cql/src/main/resources/META-INF/spring.schemas index b002ec859..dd44454ef 100644 --- a/spring-cql/src/main/resources/META-INF/spring.schemas +++ b/spring-cql/src/main/resources/META-INF/spring.schemas @@ -1,2 +1,3 @@ http\://www.springframework.org/schema/cql/spring-cql-1.0.xsd=org/springframework/cassandra/config/spring-cql-1.0.xsd -http\://www.springframework.org/schema/cql/spring-cql.xsd=org/springframework/cassandra/config/spring-cql-1.0.xsd \ No newline at end of file +http\://www.springframework.org/schema/cql/spring-cql-1.5.xsd=org/springframework/cassandra/config/spring-cql-1.5.xsd +http\://www.springframework.org/schema/cql/spring-cql.xsd=org/springframework/cassandra/config/spring-cql-1.5.xsd diff --git a/spring-cql/src/main/resources/org/springframework/cassandra/config/spring-cql-1.5.xsd b/spring-cql/src/main/resources/org/springframework/cassandra/config/spring-cql-1.5.xsd new file mode 100644 index 000000000..9ff0bceb8 --- /dev/null +++ b/spring-cql/src/main/resources/org/springframework/cassandra/config/spring-cql-1.5.xsd @@ -0,0 +1,730 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/AlterUserTypeCqlGeneratorIntegrationTests.java b/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/AlterUserTypeCqlGeneratorIntegrationTests.java index 4d3c7f918..58e8652ee 100644 --- a/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/AlterUserTypeCqlGeneratorIntegrationTests.java +++ b/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/AlterUserTypeCqlGeneratorIntegrationTests.java @@ -102,5 +102,4 @@ public class AlterUserTypeCqlGeneratorIntegrationTests extends AbstractKeyspaceC public void generationFailsWithoutFields() { toCql(AlterUserTypeSpecification.alterType().name("hello")); } - } diff --git a/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/AlterUserTypeCqlGeneratorUnitTests.java b/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/AlterUserTypeCqlGeneratorUnitTests.java index 738da7306..8a239e758 100644 --- a/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/AlterUserTypeCqlGeneratorUnitTests.java +++ b/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/AlterUserTypeCqlGeneratorUnitTests.java @@ -15,8 +15,7 @@ */ package org.springframework.cassandra.core.cql.generator; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.*; import static org.springframework.cassandra.core.cql.generator.AlterUserTypeCqlGenerator.*; import org.junit.Test; @@ -40,7 +39,7 @@ public class AlterUserTypeCqlGeneratorUnitTests { AlterUserTypeSpecification spec = AlterUserTypeSpecification.alterType("address") // .add("zip", DataType.varchar()); - assertThat(toCql(spec), is(equalTo("ALTER TYPE address ADD zip varchar;"))); + assertThat(toCql(spec)).isEqualTo("ALTER TYPE address ADD zip varchar;"); } /** @@ -52,7 +51,7 @@ public class AlterUserTypeCqlGeneratorUnitTests { AlterUserTypeSpecification spec = AlterUserTypeSpecification.alterType("address") // .alter("zip", DataType.varchar()); - assertThat(toCql(spec), is(equalTo("ALTER TYPE address ALTER zip TYPE varchar;"))); + assertThat(toCql(spec)).isEqualTo("ALTER TYPE address ALTER zip TYPE varchar;"); } /** @@ -64,7 +63,7 @@ public class AlterUserTypeCqlGeneratorUnitTests { AlterUserTypeSpecification spec = AlterUserTypeSpecification.alterType("address") // .rename("zip", "zap"); - assertThat(toCql(spec), is(equalTo("ALTER TYPE address RENAME zip TO zap;"))); + assertThat(toCql(spec)).isEqualTo("ALTER TYPE address RENAME zip TO zap;"); } /** @@ -77,7 +76,7 @@ public class AlterUserTypeCqlGeneratorUnitTests { .rename("zip", "zap") // .rename("city", "county"); - assertThat(toCql(spec), is(equalTo("ALTER TYPE address RENAME zip TO zap AND city TO county;"))); + assertThat(toCql(spec)).isEqualTo("ALTER TYPE address RENAME zip TO zap AND city TO county;"); } /** 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 29214597d..f49a3084e 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 @@ -15,8 +15,7 @@ */ package org.springframework.cassandra.core.cql.generator; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.cassandra.core.cql.generator.CreateUserTypeCqlGenerator.*; import org.junit.Before; @@ -57,7 +56,7 @@ public class CreateUserTypeCqlGeneratorIntegrationTests extends AbstractKeyspace KeyspaceMetadata keyspace = session.getCluster().getMetadata().getKeyspace(session.getLoggedKeyspace()); UserType address = keyspace.getUserType("address"); - assertThat(address.getFieldNames(), contains("zip", "city")); + assertThat(address.getFieldNames()).contains("zip", "city"); } /** @@ -75,7 +74,7 @@ public class CreateUserTypeCqlGeneratorIntegrationTests extends AbstractKeyspace KeyspaceMetadata keyspace = session.getCluster().getMetadata().getKeyspace(session.getLoggedKeyspace()); UserType address = keyspace.getUserType("address"); - assertThat(address.getFieldNames(), contains("zip", "city")); + assertThat(address.getFieldNames()).contains("zip", "city"); } /** diff --git a/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/CreateUserTypeCqlGeneratorUnitTests.java b/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/CreateUserTypeCqlGeneratorUnitTests.java index 29652ab00..a05dbc970 100644 --- a/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/CreateUserTypeCqlGeneratorUnitTests.java +++ b/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/CreateUserTypeCqlGeneratorUnitTests.java @@ -15,8 +15,7 @@ */ package org.springframework.cassandra.core.cql.generator; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.*; import static org.springframework.cassandra.core.cql.generator.CreateUserTypeCqlGenerator.*; import org.junit.Test; @@ -41,7 +40,7 @@ public class CreateUserTypeCqlGeneratorUnitTests { .createType("address") // .field("city", DataType.varchar()); - assertThat(toCql(spec), is(equalTo("CREATE TYPE address (city varchar);"))); + assertThat(toCql(spec)).isEqualTo("CREATE TYPE address (city varchar);"); } /** @@ -55,7 +54,7 @@ public class CreateUserTypeCqlGeneratorUnitTests { .field("zip", DataType.ascii()) // .field("city", DataType.varchar()); - assertThat(toCql(spec), is(equalTo("CREATE TYPE address (zip ascii, city varchar);"))); + assertThat(toCql(spec)).isEqualTo("CREATE TYPE address (zip ascii, city varchar);"); } /** @@ -69,7 +68,7 @@ public class CreateUserTypeCqlGeneratorUnitTests { .name("address").ifNotExists().field("zip", DataType.ascii()) // .field("city", DataType.varchar()); - assertThat(toCql(spec), is(equalTo("CREATE TYPE IF NOT EXISTS address (zip ascii, city varchar);"))); + assertThat(toCql(spec)).isEqualTo("CREATE TYPE IF NOT EXISTS address (zip ascii, city varchar);"); } /** diff --git a/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/DropUserTypeCqlGeneratorUnitTests.java b/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/DropUserTypeCqlGeneratorUnitTests.java index 631d739ba..ff5568da9 100644 --- a/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/DropUserTypeCqlGeneratorUnitTests.java +++ b/spring-cql/src/test/java/org/springframework/cassandra/core/cql/generator/DropUserTypeCqlGeneratorUnitTests.java @@ -15,8 +15,7 @@ */ package org.springframework.cassandra.core.cql.generator; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.cassandra.core.cql.generator.DropUserTypeCqlGenerator.*; import org.junit.Test; @@ -29,19 +28,25 @@ import org.springframework.cassandra.core.keyspace.DropUserTypeSpecification; */ public class DropUserTypeCqlGeneratorUnitTests { + /** + * @see DATACASS-172 + */ @Test - public void shouldDropUserType() throws Exception { + public void shouldDropUserType() { DropUserTypeSpecification spec = DropUserTypeSpecification.dropType("address"); - assertThat(toCql(spec), is(equalTo("DROP TYPE address;"))); + assertThat(toCql(spec)).isEqualTo("DROP TYPE address;"); } + /** + * @see DATACASS-172 + */ @Test - public void shouldDropUserTypeIfExists() throws Exception { + public void shouldDropUserTypeIfExists() { DropUserTypeSpecification spec = DropUserTypeSpecification.dropType("address").ifExists(); - assertThat(toCql(spec), is(equalTo("DROP TYPE IF EXISTS address;"))); + assertThat(toCql(spec)).isEqualTo("DROP TYPE IF EXISTS address;"); } } diff --git a/spring-cql/src/test/java/org/springframework/cassandra/test/integration/config/xml/XmlConfigIntegrationTests.java b/spring-cql/src/test/java/org/springframework/cassandra/test/integration/config/xml/XmlConfigIntegrationTests.java index 568c7f221..06e95d945 100755 --- a/spring-cql/src/test/java/org/springframework/cassandra/test/integration/config/xml/XmlConfigIntegrationTests.java +++ b/spring-cql/src/test/java/org/springframework/cassandra/test/integration/config/xml/XmlConfigIntegrationTests.java @@ -41,7 +41,7 @@ import com.datastax.driver.core.policies.AddressTranslator; import com.datastax.driver.core.policies.SpeculativeExecutionPolicy; /** - * Test XML namespace configuration using the spring-cql-1.0.xsd. + * Test XML namespace configuration using the spring-cql XSD. * * @author Matthews T. Adams * @author Oliver Gierke diff --git a/spring-cql/src/test/resources/org/springframework/cassandra/test/integration/config/xml/FullySpecifiedKeyspaceCreatingXmlConfigIntegrationTests-context.xml b/spring-cql/src/test/resources/org/springframework/cassandra/test/integration/config/xml/FullySpecifiedKeyspaceCreatingXmlConfigIntegrationTests-context.xml index 982a0e793..434b9dfec 100644 --- a/spring-cql/src/test/resources/org/springframework/cassandra/test/integration/config/xml/FullySpecifiedKeyspaceCreatingXmlConfigIntegrationTests-context.xml +++ b/spring-cql/src/test/resources/org/springframework/cassandra/test/integration/config/xml/FullySpecifiedKeyspaceCreatingXmlConfigIntegrationTests-context.xml @@ -3,7 +3,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cass="http://www.springframework.org/schema/cql" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" - http://www.springframework.org/schema/cql http://www.springframework.org/schema/cql/spring-cql-1.0.xsd + http://www.springframework.org/schema/cql http://www.springframework.org/schema/cql/spring-cql.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> diff --git a/spring-cql/src/test/resources/org/springframework/cassandra/test/integration/config/xml/MinimalKeyspaceCreatingXmlConfigIntegrationTests-context.xml b/spring-cql/src/test/resources/org/springframework/cassandra/test/integration/config/xml/MinimalKeyspaceCreatingXmlConfigIntegrationTests-context.xml index 974d17a69..b9ab187c9 100644 --- a/spring-cql/src/test/resources/org/springframework/cassandra/test/integration/config/xml/MinimalKeyspaceCreatingXmlConfigIntegrationTests-context.xml +++ b/spring-cql/src/test/resources/org/springframework/cassandra/test/integration/config/xml/MinimalKeyspaceCreatingXmlConfigIntegrationTests-context.xml @@ -3,7 +3,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cass="http://www.springframework.org/schema/cql" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" - http://www.springframework.org/schema/cql http://www.springframework.org/schema/cql/spring-cql-1.0.xsd + http://www.springframework.org/schema/cql http://www.springframework.org/schema/cql/spring-cql.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> diff --git a/spring-cql/src/test/resources/org/springframework/cassandra/test/integration/config/xml/MinimalXmlConfigIntegrationTests-context.xml b/spring-cql/src/test/resources/org/springframework/cassandra/test/integration/config/xml/MinimalXmlConfigIntegrationTests-context.xml index 5878c2bfc..745a3f701 100644 --- a/spring-cql/src/test/resources/org/springframework/cassandra/test/integration/config/xml/MinimalXmlConfigIntegrationTests-context.xml +++ b/spring-cql/src/test/resources/org/springframework/cassandra/test/integration/config/xml/MinimalXmlConfigIntegrationTests-context.xml @@ -2,7 +2,7 @@ diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBean.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBean.java index e1414ee64..60c85472f 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBean.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBean.java @@ -16,30 +16,24 @@ package org.springframework.data.cassandra.config; -import static org.springframework.cassandra.core.cql.CqlIdentifier.*; - -import java.util.Collection; - import org.springframework.cassandra.config.CassandraCqlSessionFactoryBean; import org.springframework.data.cassandra.convert.CassandraConverter; import org.springframework.data.cassandra.core.CassandraAdminOperations; import org.springframework.data.cassandra.core.CassandraAdminTemplate; +import org.springframework.data.cassandra.core.CassandraPersistentEntitySchemaCreator; import org.springframework.data.cassandra.mapping.CassandraMappingContext; -import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.util.Assert; -import com.datastax.driver.core.KeyspaceMetadata; -import com.datastax.driver.core.Metadata; import com.datastax.driver.core.Session; -import com.datastax.driver.core.TableMetadata; /** - * Factory to create and configure a Cassandra {@link com.datastax.driver.core.Session} with support - * for executing CQL and initializing the database schema (a.k.a. keyspace). + * Factory to create and configure a Cassandra {@link com.datastax.driver.core.Session} with support for executing CQL + * and initializing the database schema (a.k.a. keyspace). * * @author Mathew Adams * @author David Webb * @author John Blum + * @author Mark Paluch * @see com.datastax.driver.core.KeyspaceMetadata * @see com.datastax.driver.core.TableMetadata */ @@ -74,61 +68,37 @@ public class CassandraSessionFactoryBean extends CassandraCqlSessionFactoryBean /* (non-Javadoc) */ protected void performSchemaAction() { - boolean dropTables = DEFAULT_DROP_TABLES; + boolean drop = DEFAULT_DROP_TABLES; boolean dropUnused = DEFAULT_DROP_UNUSED_TABLES; boolean ifNotExists = DEFAULT_CREATE_IF_NOT_EXISTS; + boolean create = false; switch (schemaAction) { case RECREATE_DROP_UNUSED: dropUnused = true; case RECREATE: - dropTables = true; + drop = true; case CREATE_IF_NOT_EXISTS: ifNotExists = SchemaAction.CREATE_IF_NOT_EXISTS.equals(schemaAction); case CREATE: - createTables(dropTables, dropUnused, ifNotExists); + create = true; case NONE: default: // do nothing } - } - /* (non-Javadoc) */ - protected void createTables(boolean dropTables, boolean dropUnused, boolean ifNotExists) { - - if (dropTables) { - dropTables(dropUnused); - } - - Collection> entities = - getConverter().getMappingContext().getNonPrimaryKeyEntities(); - - for (CassandraPersistentEntity entity : entities) { - // TODO: pass specification of user configurable table options - getCassandraAdminOperations().createTable(ifNotExists, entity.getTableName(), entity.getType(), null); + if (create) { + createTables(drop, dropUnused, ifNotExists); } } - /* (non-Javadoc) */ - @SuppressWarnings("all") - protected void dropTables(boolean dropUnused) { + protected void createTables(boolean drop, boolean dropUnused, boolean ifNotExists) { - String keyspaceName = getKeyspaceName(); + CassandraPersistentEntitySchemaCreator schemaCreator = new CassandraPersistentEntitySchemaCreator( + getMappingContext(), getCassandraAdminOperations()); - Metadata clusterMetadata = getSession().getCluster().getMetadata(); - KeyspaceMetadata keyspaceMetadata = clusterMetadata.getKeyspace(keyspaceName); - - // TODO: fix this with KeyspaceIdentifier - keyspaceMetadata = (keyspaceMetadata != null ? keyspaceMetadata - : clusterMetadata.getKeyspace(keyspaceName.toLowerCase())); - - Assert.state(keyspaceMetadata != null, String.format("keyspace [%s] does not exist", keyspaceName)); - - for (TableMetadata table : keyspaceMetadata.getTables()) { - if (dropUnused || getMappingContext().usesTable(table)) { - getCassandraAdminOperations().dropTable(cqlId(table.getName())); - } - } + schemaCreator.createUserTypes(drop, dropUnused, ifNotExists); + schemaCreator.createTables(drop, dropUnused, ifNotExists); } /* (non-Javadoc) */ diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/DefaultBeanNames.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/DefaultBeanNames.java index 286eaeebb..a295e37b7 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/DefaultBeanNames.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/DefaultBeanNames.java @@ -22,4 +22,5 @@ public interface DefaultBeanNames extends DefaultCqlBeanNames { public static final String DATA_TEMPLATE = "cassandraTemplate"; public static final String CONVERTER = "cassandraConverter"; public static final String CONTEXT = "cassandraMapping"; + public static final String USER_TYPE_RESOLVER = "userTypeResolver"; } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/java/AbstractCassandraConfiguration.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/java/AbstractCassandraConfiguration.java index 8d80f094a..7e6572bbb 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/java/AbstractCassandraConfiguration.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/java/AbstractCassandraConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.data.cassandra.core.CassandraAdminOperations; import org.springframework.data.cassandra.core.CassandraAdminTemplate; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.SimpleUserTypeResolver; import org.springframework.data.cassandra.mapping.Table; import org.springframework.data.mapping.context.MappingContext; @@ -50,7 +51,7 @@ public abstract class AbstractCassandraConfiguration extends AbstractClusterConf protected ClassLoader beanClassLoader; @Bean - public CassandraSessionFactoryBean session() throws Exception { + public CassandraSessionFactoryBean session() throws ClassNotFoundException { CassandraSessionFactoryBean session = new CassandraSessionFactoryBean(); @@ -74,7 +75,7 @@ public abstract class AbstractCassandraConfiguration extends AbstractClusterConf * @see #customConversions() */ @Bean - public CassandraConverter cassandraConverter() throws Exception { + public CassandraConverter cassandraConverter() throws ClassNotFoundException { MappingCassandraConverter mappingCassandraConverter = new MappingCassandraConverter(cassandraMapping()); @@ -115,6 +116,7 @@ public abstract class AbstractCassandraConfiguration extends AbstractClusterConf mappingContext.setCustomConversions(customConversions); mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); + mappingContext.setUserTypeResolver(new SimpleUserTypeResolver(cluster().getObject(), getKeyspaceName())); return mappingContext; } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/xml/CassandraMappingContextParser.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/xml/CassandraMappingContextParser.java index 8a46de0c9..e26d638d7 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/xml/CassandraMappingContextParser.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/xml/CassandraMappingContextParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors + * Copyright 2013-2016 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. @@ -17,20 +17,24 @@ package org.springframework.data.cassandra.config.xml; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.cassandra.config.xml.DefaultCqlBeanNames; import org.springframework.data.cassandra.config.CassandraEntityClassScanner; import org.springframework.data.cassandra.config.DefaultBeanNames; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; import org.springframework.data.cassandra.mapping.EntityMapping; import org.springframework.data.cassandra.mapping.Mapping; import org.springframework.data.cassandra.mapping.PropertyMapping; +import org.springframework.data.cassandra.mapping.SimpleUserTypeResolver; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; @@ -68,12 +72,12 @@ public class CassandraMappingContextParser extends AbstractSingleBeanDefinitionP String packages = element.getAttribute("entity-base-packages"); if (StringUtils.hasText(packages)) { try { - Set> entityClasses = CassandraEntityClassScanner.scan(StringUtils - .commaDelimitedListToStringArray(packages)); + Set> entityClasses = CassandraEntityClassScanner + .scan(StringUtils.commaDelimitedListToStringArray(packages)); builder.addPropertyValue("initialEntitySet", entityClasses); } catch (Exception x) { - throw new IllegalArgumentException(String.format( - "encountered exception while scanning for entity classes in package(s) [%s]", packages), x); + throw new IllegalArgumentException( + String.format("encountered exception while scanning for entity classes in package(s) [%s]", packages), x); } } @@ -88,6 +92,21 @@ public class CassandraMappingContextParser extends AbstractSingleBeanDefinitionP } } + List userTypeResolvers = DomUtils.getChildElementsByTagName(element, "user-type-resolver"); + String userTypeResolverRef = element.getAttribute("user-type-resolver-ref"); + if (StringUtils.hasText(userTypeResolverRef)) { + if (!userTypeResolvers.isEmpty()) { + throw new IllegalArgumentException("Must not define user-type-resolver and user-type-resolver-ref"); + } + builder.addPropertyReference("userTypeResolver", userTypeResolverRef); + } + + if(!userTypeResolvers.isEmpty()){ + + BeanDefinition userTypeResolver = parseUserTypeResolver(userTypeResolvers.get(0)); + builder.addPropertyValue("userTypeResolver", userTypeResolver); + } + Mapping mapping = new Mapping(); mapping.setEntityMappings(mappings); @@ -126,6 +145,22 @@ public class CassandraMappingContextParser extends AbstractSingleBeanDefinitionP return entityMapping; } + protected BeanDefinition parseUserTypeResolver(Element entity) { + + String keyspaceName = entity.getAttribute("keyspace-name"); + if (!StringUtils.hasText(keyspaceName)) { + throw new IllegalStateException("keyspace-name attribute must not be null or empty"); + } + + String clusterRef = entity.getAttribute("cluster-ref"); + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(SimpleUserTypeResolver.class); + builder.addConstructorArgReference(StringUtils.hasText(clusterRef) ? clusterRef : DefaultCqlBeanNames.CLUSTER); + builder.addConstructorArgValue(keyspaceName); + + return builder.getBeanDefinition(); + } + protected Map parsePropertyMappings(Element entity) { Map pms = new HashMap(); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/BasicCassandraRowValueProvider.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/BasicCassandraRowValueProvider.java index 0cef42732..47d28244d 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/BasicCassandraRowValueProvider.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/BasicCassandraRowValueProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors + * Copyright 2013-2016 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. @@ -15,8 +15,6 @@ */ package org.springframework.data.cassandra.convert; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; import org.springframework.data.mapping.model.PropertyValueProvider; @@ -26,11 +24,12 @@ import org.springframework.util.Assert; import com.datastax.driver.core.Row; /** - * {@link PropertyValueProvider} to read property values from a {@link Row}. + * {@link CassandraValueProvider} to read property values from a {@link Row}. * * @author Alex Shvid * @author Matthew T. Adams * @author David Webb + * @author Mark Paluch */ public class BasicCassandraRowValueProvider implements CassandraRowValueProvider { @@ -45,6 +44,7 @@ public class BasicCassandraRowValueProvider implements CassandraRowValueProvider * @param evaluator must not be {@literal null}. */ public BasicCassandraRowValueProvider(Row source, DefaultSpELExpressionEvaluator evaluator) { + Assert.notNull(source); Assert.notNull(evaluator); @@ -52,6 +52,9 @@ public class BasicCassandraRowValueProvider implements CassandraRowValueProvider this.evaluator = evaluator; } + /* (non-Javadoc) + * @see org.springframework.data.mapping.model.PropertyValueProvider#getPropertyValue(org.springframework.data.mapping.PersistentProperty) + */ @Override @SuppressWarnings("unchecked") public Object getPropertyValue(CassandraPersistentProperty property) { @@ -64,8 +67,22 @@ public class BasicCassandraRowValueProvider implements CassandraRowValueProvider return reader.get(property.getColumnName()); } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.convert.CassandraRowValueProvider#getRow() + */ @Override public Row getRow() { return reader.getRow(); } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.convert.CassandraValueProvider#hasProperty(org.springframework.data.cassandra.mapping.CassandraPersistentProperty) + */ + @Override + public boolean hasProperty(CassandraPersistentProperty property) { + + Assert.notNull(property, "CassandraPersistentProperty must not be null"); + + return getRow().getColumnDefinitions().contains(property.getColumnName().toCql()); + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraRowValueProvider.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraRowValueProvider.java index f35daec67..edeb65352 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraRowValueProvider.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraRowValueProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors + * Copyright 2013-2016 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. @@ -15,12 +15,18 @@ */ package org.springframework.data.cassandra.convert; -import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; -import org.springframework.data.mapping.model.PropertyValueProvider; - import com.datastax.driver.core.Row; -public interface CassandraRowValueProvider extends PropertyValueProvider { +/** + * {@link CassandraValueProvider} providing values based on a {@link Row}. + * + * @author Matthew T. Adams + * @author Mark Paluch + */ +public interface CassandraRowValueProvider extends CassandraValueProvider { + /** + * @return the underlying {@link Row}. + */ Row getRow(); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraUDTValueProvider.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraUDTValueProvider.java new file mode 100644 index 000000000..1dbacc69f --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraUDTValueProvider.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016 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.convert; + +import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; +import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; +import org.springframework.data.mapping.model.PropertyValueProvider; +import org.springframework.data.mapping.model.SpELExpressionEvaluator; +import org.springframework.util.Assert; + +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.UDTValue; + +/** + * {@link CassandraValueProvider} to read property values from a {@link UDTValue}. + * + * @author Mark Paluch + * @since 1.5 + */ +public class CassandraUDTValueProvider implements CassandraValueProvider { + + private final UDTValue udtValue; + private final CodecRegistry codecRegistry; + private final SpELExpressionEvaluator evaluator; + + /** + * Creates a new {@link CassandraUDTValueProvider} with the given {@link UDTValue} and + * {@link DefaultSpELExpressionEvaluator}. + * + * @param udtValue must not be {@literal null}. + * @param codecRegistry must not be {@literal null}. + * @param evaluator must not be {@literal null}. + */ + public CassandraUDTValueProvider(UDTValue udtValue, CodecRegistry codecRegistry, + DefaultSpELExpressionEvaluator evaluator) { + + Assert.notNull(udtValue, "UDTValue must not be null"); + Assert.notNull(codecRegistry, "CodecRegistry must not be null"); + Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null"); + + this.udtValue = udtValue; + this.codecRegistry = codecRegistry; + this.evaluator = evaluator; + } + + /* (non-Javadoc) + * @see org.springframework.data.mapping.model.PropertyValueProvider#getPropertyValue(org.springframework.data.mapping.PersistentProperty) + */ + @SuppressWarnings("unchecked") + public Object getPropertyValue(CassandraPersistentProperty property) { + + String expression = property.getSpelExpression(); + if (expression != null) { + return evaluator.evaluate(expression); + } + + String name = property.getColumnName().toCql(); + DataType fieldType = udtValue.getType().getFieldType(name); + + return udtValue.get(name, codecRegistry.codecFor(fieldType)); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.convert.CassandraValueProvider#hasProperty(org.springframework.data.cassandra.mapping.CassandraPersistentProperty) + */ + @Override + public boolean hasProperty(CassandraPersistentProperty property) { + return udtValue.getType().contains(property.getColumnName().toCql()); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraValueProvider.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraValueProvider.java new file mode 100644 index 000000000..3bd200901 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraValueProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016 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.convert; + +import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; +import org.springframework.data.mapping.model.PropertyValueProvider; + +/** + * {@link PropertyValueProvider} for {@link CassandraPersistentProperty}. This {@link PropertyValueProvider} allows + * querying whether the source contains a data source for {@link CassandraPersistentProperty} like a field or a column. + * + * @author Mark Paluch + * @since 1.5 + */ +public interface CassandraValueProvider extends PropertyValueProvider { + + /** + * Returns whether the underlying source contains a data source for the given {@link CassandraPersistentProperty}. + * + * @param property must not be {@literal null}. + * @return {@literal true} if the underlying source contains a data source for the given + * {@link CassandraPersistentProperty}. + */ + boolean hasProperty(CassandraPersistentProperty property); +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/ColumnReader.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/ColumnReader.java index 8698052b5..4e1fb439a 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/ColumnReader.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/ColumnReader.java @@ -119,7 +119,7 @@ public class ColumnReader { return row.getMap(i, keyTypeCodec.getJavaType().getRawType(), valueTypeCodec.getJavaType().getRawType()); } - throw new IllegalStateException("Unknown Collection type encountered. Valid collections are Set, List and Map."); + throw new IllegalStateException("Unknown Collection type encountered. Valid collections are Set, List and Map."); } public Row getRow() { diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CustomConversions.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CustomConversions.java index 1e46c1a97..c46f5d507 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CustomConversions.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CustomConversions.java @@ -135,6 +135,12 @@ public class CustomConversions { * @return */ public boolean isSimpleType(Class type) { + + // Enums have no native Cassandra support + if (type.isEnum()) { + return false; + } + return simpleTypeHolder.isSimpleType(type); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java index 2520976aa..f9a470893 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java @@ -44,6 +44,7 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.mapping.model.PropertyValueProvider; import org.springframework.data.mapping.model.SpELContext; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; @@ -52,8 +53,11 @@ import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.DataType; import com.datastax.driver.core.Row; import com.datastax.driver.core.TypeCodec; +import com.datastax.driver.core.UDTValue; +import com.datastax.driver.core.UserType; import com.datastax.driver.core.querybuilder.Clause; import com.datastax.driver.core.querybuilder.Delete; import com.datastax.driver.core.querybuilder.Insert; @@ -79,7 +83,7 @@ import com.datastax.driver.core.querybuilder.Update; * @see org.springframework.data.convert.EntityWriter */ public class MappingCassandraConverter extends AbstractCassandraConverter - implements CassandraConverter, ApplicationContextAware, BeanClassLoaderAware { + implements CassandraConverter, ApplicationContextAware, BeanClassLoaderAware { protected final CassandraMappingContext mappingContext; protected ApplicationContext applicationContext; @@ -129,8 +133,8 @@ public class MappingCassandraConverter extends AbstractCassandraConverter return conversionService.convert(row, type); } - CassandraPersistentEntity persistentEntity = - (CassandraPersistentEntity) mappingContext.getPersistentEntity(typeInfo); + CassandraPersistentEntity persistentEntity = (CassandraPersistentEntity) mappingContext + .getPersistentEntity(typeInfo); if (persistentEntity == null) { throw new MappingException("No mapping metadata found for " + rawType.getName()); @@ -153,8 +157,8 @@ public class MappingCassandraConverter extends AbstractCassandraConverter DefaultSpELExpressionEvaluator expressionEvaluator = new DefaultSpELExpressionEvaluator(row, spELContext); BasicCassandraRowValueProvider rowValueProvider = new BasicCassandraRowValueProvider(row, expressionEvaluator); - CassandraPersistentEntityParameterValueProvider parameterProvider = - new CassandraPersistentEntityParameterValueProvider(entity, rowValueProvider, null); + CassandraPersistentEntityParameterValueProvider parameterProvider = new CassandraPersistentEntityParameterValueProvider( + entity, rowValueProvider, null); EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); S instance = instantiator.createInstance(entity, parameterProvider); @@ -164,20 +168,56 @@ public class MappingCassandraConverter extends AbstractCassandraConverter return instance; } - protected void readPropertiesFromRow(final CassandraPersistentEntity entity, - final BasicCassandraRowValueProvider row, final PersistentPropertyAccessor propertyAccessor) { + protected S readEntityFromUdt(CassandraPersistentEntity entity, UDTValue udtValue) { + + DefaultSpELExpressionEvaluator expressionEvaluator = new DefaultSpELExpressionEvaluator(udtValue, spELContext); + CassandraUDTValueProvider valueProvider = new CassandraUDTValueProvider(udtValue, CodecRegistry.DEFAULT_INSTANCE, + expressionEvaluator); + + CassandraPersistentEntityParameterValueProvider parameterProvider = new CassandraPersistentEntityParameterValueProvider( + entity, valueProvider, null); + + EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); + S instance = instantiator.createInstance(entity, parameterProvider); + + readProperties(entity, valueProvider, getConvertingAccessor(instance, entity)); + + return instance; + } + + protected void readPropertiesFromRow(final CassandraPersistentEntity entity, final CassandraRowValueProvider row, + final PersistentPropertyAccessor propertyAccessor) { + readProperties(entity, row, propertyAccessor); + } + + protected void readProperties(final CassandraPersistentEntity entity, final CassandraValueProvider valueProvider, + final PersistentPropertyAccessor propertyAccessor) { entity.doWithProperties(new PropertyHandler() { @Override public void doWithPersistentProperty(CassandraPersistentProperty property) { - MappingCassandraConverter.this.readPropertyFromRow(entity, property, row, propertyAccessor); + MappingCassandraConverter.this.readProperty(entity, property, valueProvider, propertyAccessor); } }); } + /** + * @param entity + * @param property + * @param valueProvider + * @param propertyAccessor + * @deprecated Use + * {@link #readProperty(CassandraPersistentEntity, CassandraPersistentProperty, CassandraValueProvider, PersistentPropertyAccessor)} + */ + @Deprecated protected void readPropertyFromRow(CassandraPersistentEntity entity, CassandraPersistentProperty property, - BasicCassandraRowValueProvider row, PersistentPropertyAccessor propertyAccessor) { + CassandraRowValueProvider valueProvider, PersistentPropertyAccessor propertyAccessor) { + readProperty(entity, property, valueProvider, propertyAccessor); + } + + protected void readProperty(CassandraPersistentEntity entity, CassandraPersistentProperty property, + CassandraValueProvider valueProvider, PersistentPropertyAccessor propertyAccessor) { // if true then skip; property was set in constructor if (entity.isConstructorArgument(property)) { @@ -192,11 +232,11 @@ public class MappingCassandraConverter extends AbstractCassandraConverter Object key = propertyAccessor.getProperty(keyProperty); if (key == null) { - key = instantiatePrimaryKey(keyEntity, keyProperty, row); + key = instantiatePrimaryKey(keyEntity, keyProperty, valueProvider); } // now recurse on using the key this time - readPropertiesFromRow(property.getCompositePrimaryKeyEntity(), row, getConvertingAccessor(key, keyEntity)); + readProperties(property.getCompositePrimaryKeyEntity(), valueProvider, getConvertingAccessor(key, keyEntity)); // now that the key's properties have been populated, set the key property on the entity propertyAccessor.setProperty(keyProperty, key); @@ -204,18 +244,18 @@ public class MappingCassandraConverter extends AbstractCassandraConverter return; } - if (!row.getRow().getColumnDefinitions().contains(property.getColumnName().toCql())) { + if (!valueProvider.hasProperty(property)) { return; } - Object obj = getReadValue(property, row); + Object obj = getReadValue(property, valueProvider); propertyAccessor.setProperty(property, obj); } @SuppressWarnings("unused") protected Object instantiatePrimaryKey(CassandraPersistentEntity entity, CassandraPersistentProperty keyProperty, - BasicCassandraRowValueProvider propertyProvider) { + PropertyValueProvider propertyProvider) { return instantiators.getInstantiatorFor(entity).createInstance(entity, new CassandraPersistentEntityParameterValueProvider(entity, propertyProvider, null)); @@ -264,6 +304,8 @@ public class MappingCassandraConverter extends AbstractCassandraConverter writeSelectWhereFromObject(source, (Select.Where) sink, entity); } else if (sink instanceof Delete.Where) { writeDeleteWhereFromObject(source, (Delete.Where) sink, entity); + } else if (sink instanceof UDTValue) { + writeUDTValueWhereFromObject(getConvertingAccessor(source, entity), (UDTValue) sink, entity); } else { throw new MappingException("Unknown write target " + sink.getClass().getName()); } @@ -312,7 +354,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter } protected void writeUpdateFromWrapper(final ConvertingPropertyAccessor accessor, final Update update, - final CassandraPersistentEntity entity) { + final CassandraPersistentEntity entity) { entity.doWithProperties(new PropertyHandler() { @@ -356,6 +398,32 @@ public class MappingCassandraConverter extends AbstractCassandraConverter } } + protected void writeUDTValueWhereFromObject(final ConvertingPropertyAccessor accessor, final UDTValue udtValue, + CassandraPersistentEntity entity) { + + entity.doWithProperties(new PropertyHandler() { + + @Override + public void doWithPersistentProperty(CassandraPersistentProperty property) { + + Object value = getWriteValue(property, accessor); + + if (log.isDebugEnabled()) { + log.debug("writeUDTValueWhereFromObject Property.type {}, Property.value {}", property.getType().getName(), + value); + } + + if (log.isDebugEnabled()) { + log.debug("Adding udt.value [{}] - [{}]", property.getColumnName().toCql(), value); + } + + TypeCodec typeCodec = CodecRegistry.DEFAULT_INSTANCE.codecFor(mappingContext.getDataType(property)); + + udtValue.set(property.getColumnName().toCql(), value, typeCodec); + } + }); + } + private Collection getWhereClauses(Object source, CassandraPersistentEntity entity) { Assert.notNull(source, "Id source must not be null"); @@ -370,7 +438,8 @@ public class MappingCassandraConverter extends AbstractCassandraConverter } if (id instanceof MapId) { - return getWhereClauses((MapId) id, idProperty != null && idProperty.isCompositePrimaryKey() ? idProperty.getCompositePrimaryKeyEntity() : entity); + return getWhereClauses((MapId) id, idProperty != null && idProperty.isCompositePrimaryKey() + ? idProperty.getCompositePrimaryKeyEntity() : entity); } if (idProperty == null) { @@ -389,10 +458,10 @@ public class MappingCassandraConverter extends AbstractCassandraConverter } } - TypeCodec codec = getCodec(idProperty); - - if(conversionService.canConvert(id.getClass(), codec.getJavaType().getRawType())){ - return Collections.singleton(QueryBuilder.eq(idProperty.getColumnName().toCql(), conversionService.convert(id, codec.getJavaType().getRawType()))); + Class targetType = getTargetType(idProperty); + if (conversionService.canConvert(id.getClass(), targetType)) { + return Collections + .singleton(QueryBuilder.eq(idProperty.getColumnName().toCql(), conversionService.convert(id, targetType))); } return Collections.singleton(QueryBuilder.eq(idProperty.getColumnName().toCql(), id)); @@ -423,8 +492,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter public void doWithPersistentProperty(CassandraPersistentProperty property) { TypeCodec codec = getCodec(property); - Object value = accessor.getProperty(property, - codec.getJavaType().getRawType()); + Object value = accessor.getProperty(property, codec.getJavaType().getRawType()); clauses.add(QueryBuilder.eq(property.getColumnName().toCql(), value)); } }); @@ -441,16 +509,19 @@ public class MappingCassandraConverter extends AbstractCassandraConverter for (Entry entry : id.entrySet()) { CassandraPersistentProperty persistentProperty = entity.getPersistentProperty(entry.getKey()); if (persistentProperty == null) { - throw new IllegalArgumentException(String.format("MapId contains references [%s] that is an unknown property of [%s]", entry.getKey(), entity.getName())); + throw new IllegalArgumentException(String.format( + "MapId contains references [%s] that is an unknown property of [%s]", entry.getKey(), entity.getName())); } - clauses.add(QueryBuilder.eq(persistentProperty.getColumnName().toCql(), getWriteValue(persistentProperty, entry.getValue()))); + clauses.add(QueryBuilder.eq(persistentProperty.getColumnName().toCql(), + getWriteValue(persistentProperty, entry.getValue()))); } return clauses; } @Override + @SuppressWarnings("unchecked") public Object getId(Object object, CassandraPersistentEntity entity) { Assert.notNull(object, "Object instance must not be null"); @@ -471,8 +542,9 @@ public class MappingCassandraConverter extends AbstractCassandraConverter CassandraPersistentProperty idProperty = entity.getIdProperty(); if (idProperty != null) { - return accessor.getProperty(idProperty, (Class) (idProperty.isCompositePrimaryKey() ? idProperty.getType() - : getCodec(idProperty).getJavaType().getRawType())); + return accessor.getProperty(idProperty, + idProperty.isCompositePrimaryKey() ? (Class) idProperty.getType() + : (Class) getTargetType(idProperty)); } // if the class doesn't have an id property, then it's using MapId @@ -522,7 +594,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter private ConvertingPropertyAccessor getConvertingAccessor(Object source, CassandraPersistentEntity entity) { PersistentPropertyAccessor propertyAccessor = (source instanceof PersistentPropertyAccessor - ? (PersistentPropertyAccessor) source : entity.getPropertyAccessor(source)); + ? (PersistentPropertyAccessor) source : entity.getPropertyAccessor(source)); return new ConvertingPropertyAccessor(propertyAccessor, conversionService); } @@ -538,7 +610,30 @@ public class MappingCassandraConverter extends AbstractCassandraConverter } private Class getTargetType(CassandraPersistentProperty property) { - return (property.isCompositePrimaryKey() ? property.getType() : getCodec(property).getJavaType().getRawType()); + + if (conversions.hasCustomWriteTarget(property.getType())) { + return conversions.getCustomWriteTarget(property.getType()); + } + + if (conversions.isSimpleType(property.getType())) { + return property.getType(); + } + + if (property.isCompositePrimaryKey()) { + return property.getType(); + } + + if (property.isCollectionLike()) { + return property.getType(); + } + + DataType dataType = mappingContext.getDataType(property); + if (dataType instanceof UserType) { + return property.getType(); + } + + TypeCodec codec = CodecRegistry.DEFAULT_INSTANCE.codecFor(mappingContext.getDataType(property)); + return codec.getJavaType().getRawType(); } /** @@ -579,9 +674,33 @@ public class MappingCassandraConverter extends AbstractCassandraConverter converted.add(getConversionService().convert(o, customWriteTarget)); } - value = converted; + return converted; } } + + CassandraPersistentEntity persistentEntity = getMappingContext().getPersistentEntity(property.getActualType()); + if (persistentEntity != null && persistentEntity.isUserDefinedType()) { + + if (property.isCollectionLike() && value instanceof Collection) { + Collection original = (Collection) value; + + Collection converted = CollectionFactory.createCollection(property.getType(), original.size()); + + for (Object element : original) { + if (element instanceof UDTValue) { + converted.add(element); + } else { + converted.add(getWriteValue(property, element)); + } + } + return converted; + } + + UDTValue udtValue = persistentEntity.getUserType().newValue(); + write(value, udtValue, persistentEntity); + + return udtValue; + } } return value; @@ -596,7 +715,8 @@ public class MappingCassandraConverter extends AbstractCassandraConverter * @return the return value, may be {@literal null}. */ @SuppressWarnings("unchecked") - private Object getReadValue(CassandraPersistentProperty property, BasicCassandraRowValueProvider row) { + private Object getReadValue(CassandraPersistentProperty property, + PropertyValueProvider row) { Object obj = row.getPropertyValue(property); @@ -608,8 +728,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter Collection original = (Collection) obj; - Collection converted = CollectionFactory.createCollection( - property.getType(), original.size()); + Collection converted = CollectionFactory.createCollection(property.getType(), original.size()); for (Object element : original) { converted.add(getConversionService().convert(element, property.getActualType())); @@ -620,6 +739,25 @@ public class MappingCassandraConverter extends AbstractCassandraConverter } } + CassandraPersistentEntity persistentEntity = getMappingContext().getPersistentEntity(property.getActualType()); + if (persistentEntity != null && persistentEntity.isUserDefinedType()) { + + if (property.isCollectionLike() && obj instanceof Collection) { + Collection original = (Collection) obj; + + Collection converted = CollectionFactory.createCollection(property.getType(), original.size()); + + for (Object element : original) { + if (element instanceof UDTValue) { + converted.add(readEntityFromUdt(persistentEntity, (UDTValue) element)); + } + } + return converted; + } else if (obj instanceof UDTValue) { + return readEntityFromUdt(persistentEntity, (UDTValue) obj); + } + } + return obj; } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminOperations.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminOperations.java index 199e3e544..4115c0311 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminOperations.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2013-2016 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. @@ -19,8 +19,8 @@ import java.util.Map; import org.springframework.cassandra.core.cql.CqlIdentifier; +import com.datastax.driver.core.KeyspaceMetadata; import com.datastax.driver.core.TableMetadata; -import com.datastax.driver.core.UserType; /** * Operations for managing a Cassandra keyspace. @@ -45,7 +45,7 @@ public interface CassandraAdminOperations extends CassandraOperations { * @param optionsByName Table options, given by the string option name and the appropriate option value. */ void createTable(boolean ifNotExists, CqlIdentifier tableName, Class entityClass, - Map optionsByName); + Map optionsByName); /** * Add columns to the given table from the given class. If parameter dropRemovedAttributColumns is true, then this @@ -85,12 +85,18 @@ public interface CassandraAdminOperations extends CassandraOperations { TableMetadata getTableMetadata(String keyspace, CqlIdentifier tableName); /** - * Lookup {@link UserType} metadata. + * Returns {@link KeyspaceMetadata} for the current keyspace. * - * @param keyspace must not be empty or {@literal null}. - * @param userTypeName must not be {@literal null}. - * @return the {@link UserType} or {@literal null}. + * @return {@link KeyspaceMetadata} for the current keyspace. * @since 1.5 */ - UserType getUserTypeMetadata(String keyspace, CqlIdentifier userTypeName); + KeyspaceMetadata getKeyspaceMetadata(); + + /** + * Drops a user type. + * + * @param typeName must not be {@literal null}. + * @since 1.5 + */ + void dropUserType(CqlIdentifier typeName); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java index 8da134734..3be6de66d 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2013-2016 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. @@ -23,13 +23,16 @@ import org.slf4j.LoggerFactory; import org.springframework.cassandra.core.SessionCallback; import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.cassandra.core.cql.generator.CreateTableCqlGenerator; +import org.springframework.cassandra.core.cql.generator.DropUserTypeCqlGenerator; import org.springframework.cassandra.core.keyspace.DropTableSpecification; +import org.springframework.cassandra.core.keyspace.DropUserTypeSpecification; import org.springframework.dao.DataAccessException; import org.springframework.data.cassandra.convert.CassandraConverter; import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.util.CqlUtils; import org.springframework.util.Assert; +import com.datastax.driver.core.KeyspaceMetadata; import com.datastax.driver.core.Session; import com.datastax.driver.core.TableMetadata; import com.datastax.driver.core.UserType; @@ -133,18 +136,34 @@ public class CassandraAdminTemplate extends CassandraTemplate implements Cassand dropTable(getTableName(entityClass)); } - /* + /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.CassandraAdminOperations#dropTable(org.springframework.cassandra.core.cql.CqlIdentifier) */ @Override public void dropTable(CqlIdentifier tableName) { + Assert.notNull(tableName, "Type name must not be null"); + log.info("Dropping table => " + tableName); execute(DropTableSpecification.dropTable(tableName)); } + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraAdminOperations#dropUserType(org.springframework.cassandra.core.cql.CqlIdentifier) + */ + @Override + public void dropUserType(CqlIdentifier typeName) { + + Assert.notNull(typeName, "Type name must not be null"); + + log.info("Dropping user type => {}", typeName); + + execute(DropUserTypeCqlGenerator.toCql(DropUserTypeSpecification.dropType(typeName))); + } + /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.CassandraAdminOperations#getTableMetadata(java.lang.String, org.springframework.cassandra.core.cql.CqlIdentifier) @@ -163,20 +182,21 @@ public class CassandraAdminTemplate extends CassandraTemplate implements Cassand }); } - /* + /* * (non-Javadoc) - * @see org.springframework.data.cassandra.core.CassandraAdminOperations#getUserTypeMetadata(java.lang.String, org.springframework.cassandra.core.cql.CqlIdentifier) + * @see org.springframework.data.cassandra.core.CassandraAdminOperations#getKeyspaceMetadata() */ @Override - public UserType getUserTypeMetadata(final String keyspace, final CqlIdentifier userTypeName) { + public KeyspaceMetadata getKeyspaceMetadata() { - Assert.hasText(keyspace, "Keyspace name must not be empty"); - Assert.notNull(userTypeName, "User type name must not be null"); - - return execute(new SessionCallback() { + return execute(new SessionCallback() { @Override - public UserType doInSession(Session s) { - return s.getCluster().getMetadata().getKeyspace(keyspace).getUserType(userTypeName.toCql()); + public KeyspaceMetadata doInSession(Session s) throws DataAccessException { + + KeyspaceMetadata keyspaceMetadata = s.getCluster().getMetadata().getKeyspace(s.getLoggedKeyspace()); + Assert.state(keyspaceMetadata != null, + String.format("Metadata for keyspace [%s] not available", s.getLoggedKeyspace())); + return keyspaceMetadata; } }); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreator.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreator.java new file mode 100644 index 000000000..ebe1ac757 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreator.java @@ -0,0 +1,213 @@ +/* + * Copyright 2016 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.core; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.cassandra.core.cql.generator.CreateTableCqlGenerator; +import org.springframework.cassandra.core.cql.generator.CreateUserTypeCqlGenerator; +import org.springframework.cassandra.core.keyspace.CreateTableSpecification; +import org.springframework.cassandra.core.keyspace.CreateUserTypeSpecification; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.util.Assert; + +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.TableMetadata; +import com.datastax.driver.core.UserType; + +/** + * Schema creation support for Cassandra based on {@link CassandraMappingContext} and {@link CassandraPersistentEntity}. + * This class generates CQL to drop, recreate and create user types (UDT) and tables. + * + * @author Mark Paluch + * @since 1.5 + * @see org.springframework.data.cassandra.mapping.Table + * @see org.springframework.data.cassandra.mapping.UserDefinedType + * @see org.springframework.data.cassandra.mapping.CassandraType + */ +public class CassandraPersistentEntitySchemaCreator { + + private final CassandraMappingContext mappingContext; + private final CassandraAdminOperations cassandraAdminOperations; + + /** + * Creates a new {@link CassandraPersistentEntitySchemaCreator} for the given {@link CassandraMappingContext} and + * {@link CassandraAdminOperations}. + * + * @param mappingContext must not be {@literal null}. + * @param cassandraAdminOperations must not be {@literal null}. + */ + public CassandraPersistentEntitySchemaCreator(CassandraMappingContext mappingContext, + CassandraAdminOperations cassandraAdminOperations) { + + Assert.notNull(mappingContext, "CassandraMappingContext must not be null"); + Assert.notNull(cassandraAdminOperations, "CassandraAdminOperations must not be null"); + + this.mappingContext = mappingContext; + this.cassandraAdminOperations = cassandraAdminOperations; + } + + /** + * Create user types. Can drop types and drop unused types. + * + * @param dropUserTypes {@literal true} to drop types before creation. + * @param dropUnused {@literal true} to drop unused types before creation. Type usage is determined from existing + * mapped {@link org.springframework.data.cassandra.mapping.UserDefinedType}s and UDT names on field + * specifications. + * @param ifNotExists {@literal true} to create types using {@code IF NOT EXISTS}. + */ + public void createUserTypes(boolean dropUserTypes, boolean dropUnused, boolean ifNotExists) { + + if (dropUserTypes) { + dropUserTypes(dropUnused); + } + + List specifications = createUserTypeSpecifications(ifNotExists); + + for (CreateUserTypeSpecification specification : specifications) { + cassandraAdminOperations.execute(CreateUserTypeCqlGenerator.toCql(specification)); + } + } + + /** + * Create user types. Can drop types and drop unused types. + * + * @param dropTables {@literal true} to drop tables before creation. + * @param dropUnused {@literal true} to drop unused tables before creation. Table usage is determined by existing + * table mappings. + * @param ifNotExists {@literal true} to create tables using {@code IF NOT EXISTS}. + */ + public void createTables(boolean dropTables, boolean dropUnused, boolean ifNotExists) { + + if (dropTables) { + dropTables(dropUnused); + } + + List specifications = createTableSpecifications(ifNotExists); + + for (CreateTableSpecification specification : specifications) { + cassandraAdminOperations.execute(CreateTableCqlGenerator.toCql(specification)); + } + } + + protected List createUserTypeSpecifications(boolean ifNotExists) { + + Collection> entities = new ArrayList>( + mappingContext.getUserDefinedTypeEntities()); + + Map> byName = new HashMap>(); + + for (CassandraPersistentEntity entity : entities) { + byName.put(entity.getTableName(), entity); + } + + List specifications = new ArrayList(); + + Set created = new HashSet(); + for (CassandraPersistentEntity entity : entities) { + + Set seen = new LinkedHashSet(); + seen.add(entity.getTableName()); + visitUserTypes(entity, seen); + + List ordered = new ArrayList(seen); + Collections.reverse(ordered); + + for (CqlIdentifier identifier : ordered) { + + if (created.add(identifier)) { + specifications + .add(mappingContext.getCreateUserTypeSpecificationFor(byName.get(identifier)).ifNotExists(ifNotExists)); + } + } + } + return specifications; + } + + protected List createTableSpecifications(boolean ifNotExists) { + Collection> entities = new ArrayList>( + mappingContext.getNonPrimaryKeyEntities()); + + List specifications = new ArrayList(); + + for (CassandraPersistentEntity entity : entities) { + specifications.add(mappingContext.getCreateTableSpecificationFor(entity).ifNotExists(ifNotExists)); + } + return specifications; + } + + private void visitUserTypes(CassandraPersistentEntity entity, final Set seen) { + + entity.doWithProperties(new PropertyHandler() { + @Override + public void doWithPersistentProperty(CassandraPersistentProperty persistentProperty) { + + CassandraPersistentEntity persistentEntity = mappingContext.getPersistentEntity(persistentProperty); + if (persistentEntity != null && persistentEntity.isUserDefinedType()) { + if (seen.add(persistentEntity.getTableName())) { + visitUserTypes(persistentEntity, seen); + } + } + } + }); + } + + private void dropUserTypes(boolean dropUnused) { + + KeyspaceMetadata keyspaceMetadata = cassandraAdminOperations.getKeyspaceMetadata(); + + Collection> userDefinedTypeEntities = mappingContext.getUserDefinedTypeEntities(); + Set canRecreate = new HashSet(); + + for (CassandraPersistentEntity userDefinedTypeEntity : userDefinedTypeEntities) { + canRecreate.add(userDefinedTypeEntity.getTableName()); + } + + for (UserType userType : keyspaceMetadata.getUserTypes()) { + + CqlIdentifier identifier = CqlIdentifier.cqlId(userType.getTypeName()); + + if (canRecreate.contains(identifier)) { + cassandraAdminOperations.dropUserType(identifier); + } else if (dropUnused && !mappingContext.usesUserType(userType)) { + cassandraAdminOperations.dropUserType(identifier); + } + } + } + + private void dropTables(boolean dropUnused) { + + KeyspaceMetadata keyspaceMetadata = cassandraAdminOperations.getKeyspaceMetadata(); + + for (TableMetadata table : keyspaceMetadata.getTables()) { + if (dropUnused || mappingContext.usesTable(table)) { + cassandraAdminOperations.dropTable(CqlIdentifier.cqlId(table.getName())); + } + } + } +} 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 713ab5113..5f79d5882 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 @@ -13,7 +13,6 @@ * 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.*; @@ -29,12 +28,16 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import org.springframework.beans.BeansException; import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.cassandra.core.keyspace.CreateTableSpecification; +import org.springframework.cassandra.core.keyspace.CreateUserTypeSpecification; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.annotation.Persistent; import org.springframework.data.cassandra.convert.CustomConversions; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.context.AbstractMappingContext; @@ -48,6 +51,7 @@ import org.springframework.util.StringUtils; import com.datastax.driver.core.DataType; import com.datastax.driver.core.TableMetadata; +import com.datastax.driver.core.UserType; /** * Default implementation of a {@link MappingContext} for Cassandra using {@link CassandraPersistentEntity} and @@ -77,8 +81,10 @@ public class BasicCassandraMappingContext protected Set> nonPrimaryKeyEntities = new HashSet>(); protected Set> primaryKeyEntities = new HashSet>(); + protected Set> userDefinedTypes = new HashSet>(); private CustomConversions customConversions; + private UserTypeResolver userTypeResolver; /** * Creates a new {@link BasicCassandraMappingContext}. @@ -95,11 +101,25 @@ public class BasicCassandraMappingContext * @since 1.5 */ public void setCustomConversions(CustomConversions customConversions) { + Assert.notNull(customConversions, "CustomConversions must not be null"); this.customConversions = customConversions; } + /** + * Sets the {@link UserTypeResolver}. + * + * @param userTypeResolver must not be {@literal null}. + * @since 1.5 + */ + public void setUserTypeResolver(UserTypeResolver userTypeResolver) { + + Assert.notNull(userTypeResolver, "UserTypeResolver must not be null"); + + this.userTypeResolver = userTypeResolver; + } + @Override public void initialize() { super.initialize(); @@ -122,8 +142,18 @@ public class BasicCassandraMappingContext } @Override - public Collection> getPersistentEntities(boolean includePrimaryKeyTypes) { - if (includePrimaryKeyTypes) { + public Collection> getUserDefinedTypeEntities() { + return Collections.unmodifiableSet(userDefinedTypes); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getPersistentEntities(boolean) + */ + @Override + public Collection> getPersistentEntities(boolean includePrimaryKeyTypesAndUdts) { + + if (includePrimaryKeyTypesAndUdts) { return super.getPersistentEntities(); } @@ -140,13 +170,23 @@ public class BasicCassandraMappingContext public CassandraPersistentProperty createPersistentProperty(Field field, PropertyDescriptor descriptor, CassandraPersistentEntity owner, CassandraSimpleTypeHolder simpleTypeHolder) { - return new BasicCassandraPersistentProperty(field, descriptor, owner, simpleTypeHolder); + return new BasicCassandraPersistentProperty(field, descriptor, owner, simpleTypeHolder, userTypeResolver); } @Override protected CassandraPersistentEntity createPersistentEntity(TypeInformation typeInformation) { - CassandraPersistentEntity entity = new BasicCassandraPersistentEntity(typeInformation, this, verifier); + UserDefinedType userDefinedType = AnnotatedElementUtils.findMergedAnnotation(typeInformation.getType(), + UserDefinedType.class); + + CassandraPersistentEntity entity; + + if (userDefinedType != null) { + entity = new CassandraUserTypePersistentEntity(typeInformation, this, verifier, userTypeResolver); + userDefinedTypes.add(entity); + } else { + entity = new BasicCassandraPersistentEntity(typeInformation, this, verifier); + } if (context != null) { entity.setApplicationContext(context); @@ -163,10 +203,14 @@ public class BasicCassandraMappingContext entities.add(entity); - if (entity.isCompositePrimaryKey()) { - primaryKeyEntities.add(entity); - } else { - nonPrimaryKeyEntities.add(entity); + if (!entity.isUserDefinedType()) { + if (entity.isCompositePrimaryKey()) { + primaryKeyEntities.add(entity); + } else { + if (entity.findAnnotation(Persistent.class) != null) { + nonPrimaryKeyEntities.add(entity); + } + } } entitiesByType.put(entity.getType(), entity); @@ -174,20 +218,75 @@ public class BasicCassandraMappingContext return entity; } + /* (non-Javadoc) + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = applicationContext; } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.mapping.CassandraMappingContext#usesTable(com.datastax.driver.core.TableMetadata) + */ @Override public boolean usesTable(TableMetadata table) { return entitySetsByTableName.containsKey(cqlId(table.getName())); } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.mapping.CassandraMappingContext#usesUserType(com.datastax.driver.core.UserType) + */ + @Override + public boolean usesUserType(final UserType userType) { + + CqlIdentifier identifier = CqlIdentifier.cqlId(userType.getTypeName()); + + return hasMappedUserType(identifier) || hasReferencedUserType(identifier); + } + + private boolean hasReferencedUserType(final CqlIdentifier identifier) { + + final AtomicBoolean foundReference = new AtomicBoolean(); + + for (CassandraPersistentEntity entity : getPersistentEntities()) { + + entity.doWithProperties(new PropertyHandler() { + + @Override + public void doWithPersistentProperty(CassandraPersistentProperty persistentProperty) { + + CassandraType cassandraType = persistentProperty.findAnnotation(CassandraType.class); + if (cassandraType == null) { + return; + } + + if (StringUtils.hasText(cassandraType.userTypeName()) + && CqlIdentifier.cqlId(cassandraType.userTypeName()).equals(identifier)) { + foundReference.set(true); + } + } + }); + } + + return foundReference.get(); + } + + private boolean hasMappedUserType(CqlIdentifier identifier) { + + for (CassandraPersistentEntity userDefinedType : userDefinedTypes) { + if (userDefinedType.getTableName().equals(identifier)) { + return true; + } + } + + return false; + } + @Override public CreateTableSpecification getCreateTableSpecificationFor(CassandraPersistentEntity entity) { - Assert.notNull(entity); + Assert.notNull(entity, "CassandraPersistentEntity must not be null"); final CreateTableSpecification spec = createTable().name(entity.getTableName()); @@ -231,19 +330,44 @@ public class BasicCassandraMappingContext }); if (spec.getPartitionKeyColumns().isEmpty()) { - throw new MappingException("no partition key columns found in the entity " + entity.getType()); + throw new MappingException(String.format("No partition key columns found in entity [%s]", entity.getType())); } return spec; } /* (non-Javadoc) - * @see org.springframework.data.mapping.context.AbstractMappingContext#shouldCreatePersistentEntityFor(org.springframework.data.util.TypeInformation) + * @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getCreateUserTypeSpecificationFor(org.springframework.data.cassandra.mapping.CassandraPersistentEntity) */ @Override + public CreateUserTypeSpecification getCreateUserTypeSpecificationFor(CassandraPersistentEntity entity) { + + Assert.notNull(entity, "CassandraPersistentEntity must not be null"); + + final CreateUserTypeSpecification spec = CreateUserTypeSpecification.createType(entity.getTableName()); + + entity.doWithProperties(new PropertyHandler() { + + @Override + public void doWithPersistentProperty(CassandraPersistentProperty property) { + spec.field(property.getColumnName(), getDataType(property)); + } + }); + + if (spec.getFields().isEmpty()) { + throw new MappingException(String.format("No fields in user type [%s]", entity.getType())); + } + + return spec; + } + + /* (non-Javadoc) + * @see org.springframework.data.mapping.context.AbstractMappingContext#shouldCreatePersistentEntityFor(org.springframework.data.util.TypeInformation) + */ + @Override protected boolean shouldCreatePersistentEntityFor(TypeInformation typeInfo) { return (!customConversions.hasCustomWriteTarget(typeInfo.getType()) - && super.shouldCreatePersistentEntityFor(typeInfo)); + && super.shouldCreatePersistentEntityFor(typeInfo)); } /* (non-Javadoc) @@ -269,6 +393,12 @@ public class BasicCassandraMappingContext return property.getDataType(); } + CassandraPersistentEntity persistentEntity = getPersistentEntity(property.getType()); + + if (persistentEntity != null && persistentEntity.isUserDefinedType()) { + return persistentEntity.getUserType(); + } + if (customConversions.hasCustomWriteTarget(property.getType())) { return getDataTypeFor(customConversions.getCustomWriteTarget(property.getType())); } @@ -299,8 +429,8 @@ public class BasicCassandraMappingContext */ @Override public DataType getDataType(Class type) { - return (customConversions.hasCustomWriteTarget(type) - ? getDataTypeFor(customConversions.getCustomWriteTarget(type)) : getDataTypeFor(type)); + return customConversions.hasCustomWriteTarget(type) + ? getDataTypeFor(customConversions.getCustomWriteTarget(type)) : getDataTypeFor(type); } public void setMapping(Mapping mapping) { @@ -311,32 +441,36 @@ public class BasicCassandraMappingContext @SuppressWarnings("all") protected void processMappingOverrides() { - if (mapping != null) { - for (EntityMapping entityMapping : mapping.getEntityMappings()) { - if (entityMapping != null) { - String entityClassName = entityMapping.getEntityClassName(); - try { - Class entityClass = ClassUtils.forName(entityClassName, beanClassLoader); + if (mapping == null) { + return; + } - CassandraPersistentEntity entity = getPersistentEntity(entityClass); + for (EntityMapping entityMapping : mapping.getEntityMappings()) { - Assert.state(entity != null, String.format("Unknown persistent entity class name [%s]", - entityClassName)); + if (entityMapping == null) { + continue; + } + String entityClassName = entityMapping.getEntityClassName(); - String tableName = entityMapping.getTableName(); + try { + + Class entityClass = ClassUtils.forName(entityClassName, beanClassLoader); - if (StringUtils.hasText(tableName)) { - entity.setTableName(cqlId(tableName, Boolean.valueOf(entityMapping.getForceQuote()))); - } + CassandraPersistentEntity entity = getPersistentEntity(entityClass); - processMappingOverrides(entity, entityMapping); + Assert.state(entity != null, String.format("Unknown persistent entity class name [%s]", entityClassName)); - } catch (ClassNotFoundException e) { - throw new IllegalStateException(String.format( - "unknown persistent entity name [%s]", entityClassName), e); - } + String tableName = entityMapping.getTableName(); + + if (StringUtils.hasText(tableName)) { + entity.setTableName(cqlId(tableName, Boolean.valueOf(entityMapping.getForceQuote()))); } + + processMappingOverrides(entity, entityMapping); + + } catch (ClassNotFoundException e) { + throw new IllegalStateException(String.format("Unknown persistent entity name [%s]", entityClassName), e); } } } @@ -353,7 +487,7 @@ public class BasicCassandraMappingContext if (property == null) { throw new IllegalArgumentException(String.format("Entity class [%s] has no persistent property named [%s]", - entity.getType().getName(), mapping.getPropertyName())); + entity.getType().getName(), mapping.getPropertyName())); } boolean forceQuote = false; diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntity.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntity.java index f54a0f161..1590a882a 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntity.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors + * Copyright 2013-2016 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. @@ -15,7 +15,7 @@ */ package org.springframework.data.cassandra.mapping; -import static org.springframework.cassandra.core.cql.CqlIdentifier.cqlId; +import static org.springframework.cassandra.core.cql.CqlIdentifier.*; import java.util.ArrayList; import java.util.List; @@ -38,12 +38,15 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import com.datastax.driver.core.UserType; + /** * Cassandra specific {@link BasicPersistentEntity} implementation that adds Cassandra specific metadata. * * @author Alex Shvid * @author Matthew T. Adams * @author John Blum + * @author Mark Paluch */ public class BasicCassandraPersistentEntity extends BasicPersistentEntity implements CassandraPersistentEntity, ApplicationContextAware { @@ -75,7 +78,6 @@ public class BasicCassandraPersistentEntity extends BasicPersistentEntity typeInformation, CassandraMappingContext mappingContext) { this(typeInformation, mappingContext, DEFAULT_VERIFIER); - } /** @@ -98,12 +100,11 @@ public class BasicCassandraPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity idType = idProperty.getType(); - - if (!idType.isAnnotationPresent(PrimaryKeyClass.class) - && CassandraSimpleTypeHolder.getDataTypeFor(idType) == null) { - exceptions.add(new MappingException(String.format( - "Property [%s] annotated with @%s must be a simple CassandraType", - idProperty.getName(), Id.class.getSimpleName()))); - } } if (!idProperties.isEmpty() && !primaryKeyColumns.isEmpty()) { @@ -120,14 +113,6 @@ public class BasicCassandraPersistentEntityMetadataVerifier implements Cassandra PrimaryKeyColumn.class.getSimpleName()))); } - for (CassandraPersistentProperty property : primaryKeyColumns) { - if (CassandraSimpleTypeHolder.getDataTypeFor(property.getType()) == null) { - exceptions.add(new MappingException(String.format( - "Property [%s] annotated with @PrimaryKeyColumn must be a simple CassandraType", - property.getName()))); - } - } - // Determine whether or not to throw Exception based on errors found if (!exceptions.isEmpty()) { fail(entity, exceptions); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentProperty.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentProperty.java index e7e65c021..11795ecdb 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentProperty.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentProperty.java @@ -38,6 +38,7 @@ import org.springframework.data.cassandra.util.SpelUtils; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; +import org.springframework.data.mapping.model.MappingException; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -46,6 +47,8 @@ import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import com.datastax.driver.core.DataType; +import com.datastax.driver.core.DataType.Name; +import com.datastax.driver.core.UserType; /** * Cassandra specific {@link org.springframework.data.mapping.model.AnnotationBasedPersistentProperty} implementation. @@ -59,6 +62,8 @@ import com.datastax.driver.core.DataType; public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentProperty implements CassandraPersistentProperty, ApplicationContextAware { + private final UserTypeResolver userTypeResolver; + protected ApplicationContext context; /** @@ -89,7 +94,24 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP public BasicCassandraPersistentProperty(Field field, PropertyDescriptor propertyDescriptor, CassandraPersistentEntity owner, CassandraSimpleTypeHolder simpleTypeHolder) { + this(field, propertyDescriptor, owner, simpleTypeHolder, null); + } + + /** + * Creates a new {@link BasicCassandraPersistentProperty}. + * + * @param field the actual {@link Field} in the domain entity corresponding to this persistent entity. + * @param propertyDescriptor a {@link PropertyDescriptor} for the corresponding property in the domain entity. + * @param owner the containing object or {@link CassandraPersistentEntity} of this persistent property. + * @param simpleTypeHolder mapping of Java [simple|wrapper] types to Cassandra data types. + * @param userTypeResolver resolver for user-defined types. + */ + public BasicCassandraPersistentProperty(Field field, PropertyDescriptor propertyDescriptor, + CassandraPersistentEntity owner, CassandraSimpleTypeHolder simpleTypeHolder, + UserTypeResolver userTypeResolver) { + super(field, propertyDescriptor, owner, simpleTypeHolder); + this.userTypeResolver = userTypeResolver; if (owner.getApplicationContext() != null) { setApplicationContext(owner.getApplicationContext()); @@ -157,7 +179,7 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP if (dataType == null) { throw new InvalidDataAccessApiUsageException(String.format( - "Unknown type [%s] for property [%s] in entity [%s]; only primitive types and Collections or Maps of primitive types are allowed", + "Unknown type [%s] for property [%s] in entity [%s]; only primitive types and Collections or Maps of primitive types are allowed", getType(), getName(), getOwner().getName())); } @@ -208,15 +230,41 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP getDataTypeFor(annotation.typeArguments()[1])); case LIST: ensureTypeArguments(annotation.typeArguments().length, 1); + if (annotation.typeArguments()[0] == Name.UDT) { + return DataType.list(getUserType(annotation)); + } return DataType.list(getDataTypeFor(annotation.typeArguments()[0])); case SET: ensureTypeArguments(annotation.typeArguments().length, 1); + if (annotation.typeArguments()[0] == Name.UDT) { + return DataType.set(getUserType(annotation)); + } return DataType.set(getDataTypeFor(annotation.typeArguments()[0])); + case UDT: + return getUserType(annotation); default: return CassandraSimpleTypeHolder.getDataTypeFor(type); } } + private DataType getUserType(CassandraType annotation) { + + if (!StringUtils.hasText(annotation.userTypeName())) { + throw new InvalidDataAccessApiUsageException( + String.format("Expected user type name in property ['%s'] of type ['%s'] in entity [%s]", getName(), + getType(), getOwner().getName())); + } + + CqlIdentifier identifier = CqlIdentifier.cqlId(annotation.userTypeName()); + UserType userType = userTypeResolver.resolveType(identifier); + + if (userType == null) { + throw new MappingException(String.format("User type [%s] not found", identifier)); + } + + return userType; + } + @Override public boolean isIndexed() { return isAnnotationPresent(Indexed.class); @@ -249,7 +297,7 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP if (dataType == null) { throw new InvalidDataAccessApiUsageException(String.format( - "Only primitive types are allowed inside Collections for property [%1$s] of type [%2$s] in entity [%3$s]", + "Only primitive types are allowed inside Collections for property [%1$s] of type [%2$s] in entity [%3$s]", getName(), getType(), getOwner().getName())); } @@ -258,11 +306,16 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP protected DataType getDataTypeFor(Class javaType) { + CassandraPersistentEntity persistentEntity = getOwner().getMappingContext().getPersistentEntity(javaType); + if (persistentEntity != null && persistentEntity.isUserDefinedType()) { + return persistentEntity.getUserType(); + } + DataType dataType = CassandraSimpleTypeHolder.getDataTypeFor(javaType); if (dataType == null) { throw new InvalidDataAccessApiUsageException(String.format( - "Only primitive types are allowed inside Collections for property [%1$s] of type ['%2$s'] in entity [%3$s]", + "Only primitive types are allowed inside Collections for property [%1$s] of type ['%2$s'] in entity [%3$s]", getName(), getType(), getOwner().getName())); } @@ -272,8 +325,8 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP protected void ensureTypeArguments(int args, int expected) { if (args != expected) { throw new InvalidDataAccessApiUsageException( - String.format("Expected [%1$s] typed arguments for property ['%2$s'] of type ['%3$s'] in entity [%4$s]", - expected, getName(), getType(), getOwner().getName())); + String.format("Expected [%1$s] typed arguments for property ['%2$s'] of type ['%3$s'] in entity [%4$s]", + expected, getName(), getType(), getOwner().getName())); } } @@ -362,13 +415,13 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP if (this.columnNames.size() != columnNames.size()) { throw new IllegalStateException(String.format( - "Property [%s] of entity [%s] is mapped to [%s] column%s, but given column name list has size [%s]", - getName(), getOwner().getType().getName(), this.columnNames.size(), - this.columnNames.size() == 1 ? "" : "s", columnNames.size())); + "Property [%s] of entity [%s] is mapped to [%s] column%s, but given column name list has size [%s]", + getName(), getOwner().getType().getName(), this.columnNames.size(), this.columnNames.size() == 1 ? "" : "s", + columnNames.size())); } - this.columnNames = this.explicitColumnNames = - Collections.unmodifiableList(new ArrayList(columnNames)); + this.columnNames = this.explicitColumnNames = Collections + .unmodifiableList(new ArrayList(columnNames)); } @Override @@ -394,8 +447,8 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP public List getCompositePrimaryKeyProperties() { if (!isCompositePrimaryKey()) { - throw new IllegalStateException(String.format( - "[%s] does not represent a composite primary key property", getName())); + throw new IllegalStateException( + String.format("[%s] does not represent a composite primary key property", getName())); } return getCompositePrimaryKeyEntity().getCompositePrimaryKeyProperties(); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraMappingContext.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraMappingContext.java index 8e6404474..a89ab5b67 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraMappingContext.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraMappingContext.java @@ -18,11 +18,13 @@ package org.springframework.data.cassandra.mapping; import java.util.Collection; import org.springframework.cassandra.core.keyspace.CreateTableSpecification; +import org.springframework.cassandra.core.keyspace.CreateUserTypeSpecification; import org.springframework.data.cassandra.convert.CustomConversions; import org.springframework.data.mapping.context.MappingContext; import com.datastax.driver.core.DataType; import com.datastax.driver.core.TableMetadata; +import com.datastax.driver.core.UserType; /** * A {@link MappingContext} for Cassandra. @@ -44,10 +46,10 @@ public interface CassandraMappingContext /** * Returns all persistent entities or only non-primary-key entities. * - * @param includePrimaryKeyTypes If true, returns all entities, including entities that represent primary - * key types. If false, returns only entities that don't represent primary key types. + * @param includePrimaryKeyTypesAndUdts If {@literal true}, returns all entities, including entities that represent primary + * key types and user-defined types. If {@literal false}, returns only entities that don't represent primary key types and no user-defined types. */ - Collection> getPersistentEntities(boolean includePrimaryKeyTypes); + Collection> getPersistentEntities(boolean includePrimaryKeyTypesAndUdts); /** * Returns only those entities representing primary key types. @@ -61,20 +63,45 @@ public interface CassandraMappingContext */ Collection> getNonPrimaryKeyEntities(); + /** + * Returns only those entities representing a user defined type. + * + * @see #getPersistentEntities(boolean) + * @since 1.5 + */ + Collection> getUserDefinedTypeEntities(); + /** * Returns a {@link CreateTableSpecification} for the given entity, including all mapping information. * - * @param The entity. May not be null. + * @param entity must not be {@literal null}. */ CreateTableSpecification getCreateTableSpecificationFor(CassandraPersistentEntity entity); + /** + * Returns a {@link CreateUserTypeSpecification} for the given entity, including all mapping information. + * + * @param entity must not be {@literal null}. + */ + CreateUserTypeSpecification getCreateUserTypeSpecificationFor(CassandraPersistentEntity entity); + /** * Returns whether this mapping context has any entities mapped to the given table. * - * @param table May not be null. + * @param table must not be {@literal null}. + * @return @return {@literal true} is this {@literal TableMetadata} is used by a mapping. */ boolean usesTable(TableMetadata table); + /** + * Returns whether this mapping context has any entities using the given user type. + * + * @param userType must not be {@literal null}. + * @return {@literal true} is this {@literal UserType} is used. + * @since 1.5 + */ + boolean usesUserType(UserType userType); + /** * Returns the existing {@link CassandraPersistentEntity} for the given {@link Class}. If it is not yet known to this * {@link CassandraMappingContext}, an {@link IllegalArgumentException} is thrown. diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraPersistentEntity.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraPersistentEntity.java index 4c18350b9..23e9c09cb 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraPersistentEntity.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraPersistentEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors + * Copyright 2013-2016 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. @@ -23,14 +23,16 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.model.MutablePersistentEntity; +import com.datastax.driver.core.UserType; + /** * Cassandra specific {@link PersistentEntity} abstraction. * * @author Alex Shvid * @author Matthew T. Adams */ -public interface CassandraPersistentEntity extends MutablePersistentEntity, - ApplicationContextAware { +public interface CassandraPersistentEntity + extends MutablePersistentEntity, ApplicationContextAware { /** * Returns whether this entity represents a composite primary key. @@ -51,4 +53,18 @@ public interface CassandraPersistentEntity extends MutablePersistentEntity> simpleTypes = getCassandraPrimitiveTypes(codecRegistry); simpleTypes.add(Number.class); simpleTypes.add(Row.class); + simpleTypes.add(UDTValue.class); classToDataType = Collections.unmodifiableMap(classToDataType(primitiveWrappers, codecRegistry)); nameToDataType = Collections.unmodifiableMap(nameToDataType()); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraType.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraType.java index a873829cd..338ef40f9 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraType.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraType.java @@ -47,4 +47,14 @@ public @interface CassandraType { * If the property is neither collection-like or a map, then this attribute is ignored. */ DataType.Name[] typeArguments() default {}; + + /** + * If the property maps to a user-defined type then this attribute holds the user type name. For collection-like + * properties the user type name applies to the component type. The user type name is only required if the UDT does + * not map to a class annotated with {@link UserDefinedType}. + * + * @return name of the user type + * @since 1.5 + */ + String userTypeName() default ""; } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraUserTypePersistentEntity.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraUserTypePersistentEntity.java new file mode 100644 index 000000000..1f900b1bf --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraUserTypePersistentEntity.java @@ -0,0 +1,99 @@ +/* + * Copyright 2016 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 org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; + +import com.datastax.driver.core.UserType; + +/** + * {@link org.springframework.data.mapping.PersistentEntity} for a mapped user-defined type (UDT). A mapped UDT consists + * of a set of fields. Each field requires a data type that can be either a simple Cassandra type or an UDT. + * + * @author Mark Paluch + * @since 1.5 + * @see UserDefinedType + */ +public class CassandraUserTypePersistentEntity extends BasicCassandraPersistentEntity { + + private final UserTypeResolver resolver; + private final Object lock = new Object(); + + private volatile UserType userType; + + /** + * Creates a new {@link CassandraUserTypePersistentEntity}. + * + * @param typeInformation must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + * @param verifier must not be {@literal null}. + * @param resolver must not be {@literal null}. + */ + public CassandraUserTypePersistentEntity(TypeInformation typeInformation, CassandraMappingContext mappingContext, + CassandraPersistentEntityMetadataVerifier verifier, UserTypeResolver resolver) { + + super(typeInformation, mappingContext, verifier); + + Assert.notNull(resolver, "UserTypeResolver must not be null"); + + this.resolver = resolver; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.mapping.BasicCassandraPersistentEntity#determineTableName() + */ + @Override + protected CqlIdentifier determineTableName() { + UserDefinedType typeAnnotation = findAnnotation(UserDefinedType.class); + return determineName(typeAnnotation.value(), typeAnnotation.forceQuote()); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.mapping.BasicCassandraPersistentEntity#isUserDefinedType() + */ + @Override + public boolean isUserDefinedType() { + return true; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.mapping.BasicCassandraPersistentEntity#getUserType() + */ + @Override + public UserType getUserType() { + + if (userType == null) { + synchronized (lock) { + if (userType == null) { + + CqlIdentifier identifier = determineTableName(); + UserType userType = resolver.resolveType(identifier); + + if (userType == null) { + throw new MappingException(String.format("User type [%s] not found", identifier)); + } + + this.userType = userType; + } + } + } + + return userType; + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CompositeCassandraPersistentEntityMetadataVerifier.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CompositeCassandraPersistentEntityMetadataVerifier.java index 0b11ae287..3f9002f9d 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CompositeCassandraPersistentEntityMetadataVerifier.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CompositeCassandraPersistentEntityMetadataVerifier.java @@ -81,15 +81,18 @@ public class CompositeCassandraPersistentEntityMetadataVerifier implements Cassa @Override public void verify(CassandraPersistentEntity entity) throws MappingException { + if (entity.getType().isInterface()) { return; } - // Ensure entity is either a @Table/@Persistent or a @PrimaryKey - if (entity.findAnnotation(Persistent.class) == null) { - throw new VerifierMappingExceptions(entity, Collections.singletonList(new MappingException( - String.format("Cassandra entities must be annotated with either @%s, @%s, or @%s", - Persistent.class.getSimpleName(), Table.class.getSimpleName(), PrimaryKeyClass.class.getSimpleName())))); + // Ensure entity is either a @Table/@Persistent, @UserDefinedType or a @PrimaryKey + if (entity.findAnnotation(Persistent.class) == null && entity.findAnnotation(UserDefinedType.class) == null) { + throw new VerifierMappingExceptions(entity, + Collections.singletonList(new MappingException( + String.format("Cassandra entities must be annotated with either @%s, @%s, @%s or @%s", + Persistent.class.getSimpleName(), Table.class.getSimpleName(), + UserDefinedType.class.getSimpleName(), PrimaryKeyClass.class.getSimpleName())))); } } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/PrimaryKeyClassEntityMetadataVerifier.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/PrimaryKeyClassEntityMetadataVerifier.java index 67e2437ba..9af9cee4b 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/PrimaryKeyClassEntityMetadataVerifier.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/PrimaryKeyClassEntityMetadataVerifier.java @@ -107,15 +107,6 @@ public class PrimaryKeyClassEntityMetadataVerifier implements CassandraPersisten Id.class.getSimpleName(), PrimaryKey.class.getSimpleName(), PrimaryKeyClass.class.getSimpleName()))); } - // Ensure that PrimaryKeyColumn is a supported Type. - for (CassandraPersistentProperty property : primaryKeyColumns) { - if (CassandraSimpleTypeHolder.getDataTypeFor(property.getType()) == null) { - exceptions.add(new MappingException(String.format( - "Property [%1$s] annotated with @%2$s must be a simple CassandraType", property.getName(), - PrimaryKeyColumn.class.getSimpleName()))); - } - } - // Determine whether or not to throw Exception based on errors found if (!exceptions.isEmpty()) { fail(entity, exceptions); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/SimpleUserTypeResolver.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/SimpleUserTypeResolver.java new file mode 100644 index 000000000..f1de4fe80 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/SimpleUserTypeResolver.java @@ -0,0 +1,61 @@ +/* + * Copyright 2016 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 org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.util.Assert; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.UserType; + +/** + * Default implementation of {@link UserTypeResolver} that resolves {@link UserType} by their name from + * {@link Cluster#getMetadata()}. + * + * @author Mark Paluch + * @since 1.5 + */ +public class SimpleUserTypeResolver implements UserTypeResolver { + + private final String keyspaceName; + private final Cluster cluster; + + /** + * Creates a new {@link SimpleUserTypeResolver}. + * + * @param cluster must not be {@literal null}. + * @param keyspaceName must not be empty or {@literal null}. + */ + public SimpleUserTypeResolver(Cluster cluster, String keyspaceName) { + + Assert.notNull(cluster, "Cluster must not be null"); + Assert.hasText(keyspaceName, "Keyspace must not be null or empty"); + + this.keyspaceName = keyspaceName; + this.cluster = cluster; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.mapping.UserTypeResolver#resolveType(org.springframework.cassandra.core.cql.CqlIdentifier) + */ + @Override + public UserType resolveType(CqlIdentifier typeName) { + + KeyspaceMetadata keyspace = cluster.getMetadata().getKeyspace(keyspaceName); + return keyspace.getUserType(typeName.toCql()); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/UserTypeResolver.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/UserTypeResolver.java new file mode 100644 index 000000000..7891185bf --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/UserTypeResolver.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 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 org.springframework.cassandra.core.cql.CqlIdentifier; + +import com.datastax.driver.core.UserType; + +/** + * Strategy interface to resolve {@link UserType} by its name. + * + * @author Mark Paluch + * @since 1.5 + */ +public interface UserTypeResolver { + + /** + * Resolve a {@link UserType} by its name. + * + * @param typeName must not be {@literal null}. + * @return the type or {@literal null}, if not found. + */ + UserType resolveType(CqlIdentifier typeName); +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java index 18ac61f7e..f2e5460f2 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java @@ -24,6 +24,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.data.cassandra.convert.CassandraConverter; import org.springframework.data.cassandra.convert.CustomConversions; import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; import org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder; import org.springframework.data.cassandra.mapping.CassandraType; @@ -36,6 +37,7 @@ import com.datastax.driver.core.CodecRegistry; import com.datastax.driver.core.DataType; import com.datastax.driver.core.DataType.CollectionType; import com.datastax.driver.core.TypeCodec; +import com.datastax.driver.core.UDTValue; /** * Custom {@link org.springframework.data.repository.query.ParameterAccessor} that uses a {@link CassandraConverter} to @@ -146,34 +148,70 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { return bindableValue; } + if (property == null && getCustomConversions().hasCustomWriteTarget(bindableValue.getClass())) { + return converter.getConversionService().convert(bindableValue, + getCustomConversions().getCustomWriteTarget(bindableValue.getClass())); + } + + // TODO: Polishing necessary DataType parameterType = getDataType(index, property); - TypeCodec cassandraType = CodecRegistry.DEFAULT_INSTANCE.codecFor(parameterType); + if (parameterType != null) { - if (property != null && getCustomConversions().hasCustomWriteTarget(property.getActualType()) - && property.isCollectionLike()) { + if (property != null && getCustomConversions().hasCustomWriteTarget(property.getActualType()) + && property.isCollectionLike()) { - Class customWriteTarget = getCustomConversions().getCustomWriteTarget(property.getActualType()); + Class customWriteTarget = getCustomConversions().getCustomWriteTarget(property.getActualType()); - if (Collection.class.isAssignableFrom(property.getType()) && bindableValue instanceof Collection) { + if (Collection.class.isAssignableFrom(property.getType()) && bindableValue instanceof Collection) { - Collection original = (Collection) bindableValue; - Collection converted = CollectionFactory.createCollection(property.getType(), original.size()); + Collection original = (Collection) bindableValue; + Collection converted = CollectionFactory.createCollection(property.getType(), original.size()); - for (Object element : original) { - converted.add(getConversionService().convert(element, customWriteTarget)); + for (Object element : original) { + converted.add(getConversionService().convert(element, customWriteTarget)); + } + + return converted; } - - return converted; } - } - if (cassandraType.getJavaType().getRawType().isAssignableFrom(bindableValue.getClass())) { - return bindableValue; - } + if (property != null) { + CassandraPersistentEntity persistentEntity = converter.getMappingContext() + .getPersistentEntity(property.getActualType()); + if (persistentEntity != null && persistentEntity.isUserDefinedType()) { + return toUDTValue(bindableValue, persistentEntity); + } + } + + TypeCodec cassandraType = CodecRegistry.DEFAULT_INSTANCE.codecFor(parameterType); + if (cassandraType.getJavaType().getRawType().isAssignableFrom(bindableValue.getClass())) { + return bindableValue; + } return converter.getConversionService().convert(bindableValue, cassandraType.getJavaType().getRawType()); } + CassandraPersistentEntity persistentEntity = converter.getMappingContext() + .getPersistentEntity(bindableValue.getClass()); + if (persistentEntity != null && persistentEntity.isUserDefinedType()) { + return toUDTValue(bindableValue, persistentEntity); + } + + return bindableValue; + } + + private UDTValue toUDTValue(Object bindableValue, CassandraPersistentEntity persistentEntity) { + + if (bindableValue instanceof UDTValue) { + return (UDTValue) bindableValue; + } + + UDTValue udtValue = persistentEntity.getUserType().newValue(); + converter.write(bindableValue, udtValue, persistentEntity); + + return udtValue; + } + private CustomConversions getCustomConversions() { return converter.getCustomConversions(); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java index 86b7e3f1c..a540c4db9 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java @@ -117,7 +117,7 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { } return boundQuery; - } catch (RuntimeException e) { + } catch (RuntimeException e) { e.printStackTrace(); throw QueryCreationException.create(getQueryMethod(), e); } } diff --git a/spring-data-cassandra/src/main/resources/META-INF/spring.schemas b/spring-data-cassandra/src/main/resources/META-INF/spring.schemas index 7cd18a56c..ee8d937ab 100644 --- a/spring-data-cassandra/src/main/resources/META-INF/spring.schemas +++ b/spring-data-cassandra/src/main/resources/META-INF/spring.schemas @@ -1,2 +1,3 @@ http\://www.springframework.org/schema/data/cassandra/spring-cassandra-1.0.xsd=org/springframework/data/cassandra/config/spring-cassandra-1.0.xsd -http\://www.springframework.org/schema/data/cassandra/spring-cassandra.xsd=org/springframework/data/cassandra/config/spring-cassandra-1.0.xsd \ No newline at end of file +http\://www.springframework.org/schema/data/cassandra/spring-cassandra-1.5.xsd=org/springframework/data/cassandra/config/spring-cassandra-1.5.xsd +http\://www.springframework.org/schema/data/cassandra/spring-cassandra.xsd=org/springframework/data/cassandra/config/spring-cassandra-1.5.xsd diff --git a/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-1.0.xsd b/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-1.0.xsd index 0b6067393..2abc498bb 100644 --- a/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-1.0.xsd +++ b/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-1.0.xsd @@ -789,6 +789,7 @@ Defines a CassandraMappingContext for holding rich entity mapping information. + @@ -797,6 +798,17 @@ The comma-delimited base packages in which to scan for entities and their mappin ]]> + + + + + + + + @@ -811,6 +823,23 @@ The comma-delimited base packages in which to scan for entities and their mappin + + + + + + + + + + + + + diff --git a/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-1.5.xsd b/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-1.5.xsd new file mode 100644 index 000000000..2260f6e54 --- /dev/null +++ b/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-1.5.xsd @@ -0,0 +1,944 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBeanUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBeanUnitTests.java index b4108fd27..904068548 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBeanUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBeanUnitTests.java @@ -62,16 +62,15 @@ public class CassandraSessionFactoryBeanUnitTests { @Rule public ExpectedException exception = ExpectedException.none(); - @Mock private CassandraConverter mockConverter; + @Mock CassandraConverter mockConverter; + @Mock Cluster mockCluster; + @Mock Session mockSession; - @Mock private Cluster mockCluster; - - @Mock private Session mockSession; - - private CassandraSessionFactoryBean factoryBean; + CassandraSessionFactoryBean factoryBean; @Before public void setup() { + when(mockCluster.connect()).thenReturn(mockSession); when(mockSession.getCluster()).thenReturn(mockCluster); @@ -85,6 +84,7 @@ public class CassandraSessionFactoryBeanUnitTests { @Test public void afterPropertiesSetPerformsSchemaAction() throws Exception { + doAnswer(new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) throws Throwable { @@ -109,6 +109,7 @@ public class CassandraSessionFactoryBeanUnitTests { @Test public void afterPropertiesSetThrowsIllegalStateExceptionWhenConverterIsNull() throws Exception { + exception.expect(IllegalStateException.class); exception.expectMessage("Converter was not properly initialized"); @@ -164,6 +165,7 @@ public class CassandraSessionFactoryBeanUnitTests { @Test public void performsSchemaActionDoesNotCallCreateTablesWhenSchemaActionIsNone() { + doAnswer(new Answer() { @Override public Void answer(InvocationOnMock invocationOnMock) throws Throwable { @@ -181,101 +183,9 @@ public class CassandraSessionFactoryBeanUnitTests { verify(factoryBean, never()).createTables(anyBoolean(), anyBoolean(), anyBoolean()); } - @Test - @SuppressWarnings("unchecked") - public void createsTableForEntity() throws Exception { - Metadata mockMetadata = mock(Metadata.class); - KeyspaceMetadata mockKeyspaceMetadata = mock(KeyspaceMetadata.class); - CassandraMappingContext mockMappingContext = mock(CassandraMappingContext.class); - CassandraPersistentEntity mockPersistentEntity = mock(CassandraPersistentEntity.class); - CassandraAdminOperations mockCassandraAdminOperations = mock(CassandraAdminOperations.class); - - doReturn(mockCassandraAdminOperations).when(factoryBean).getCassandraAdminOperations(); - doReturn(mockSession).when(factoryBean).getObject(); - when(mockCluster.getMetadata()).thenReturn(mockMetadata); - when(mockMetadata.getKeyspace(eq("TestKeyspace"))).thenReturn(mockKeyspaceMetadata); - when(mockKeyspaceMetadata.getTables()).thenReturn(Collections. emptyList()); - when(mockConverter.getMappingContext()).thenReturn(mockMappingContext); - when(mockMappingContext.getNonPrimaryKeyEntities()) - .thenReturn(Collections.> singletonList(mockPersistentEntity)); - when(mockPersistentEntity.getTableName()).thenReturn(newCqlIdentifier("TestTable")); - when(mockPersistentEntity.getType()).thenReturn(Person.class); - - factoryBean.setConverter(mockConverter); - factoryBean.setKeyspaceName("TestKeyspace"); - - assertThat(factoryBean.getConverter()).isEqualTo(mockConverter); - - factoryBean.createTables(true, false, false); - - verify(mockSession, times(1)).getCluster(); - verify(mockCluster, times(1)).getMetadata(); - verify(mockMetadata, times(1)).getKeyspace(eq("TestKeyspace")); - verify(mockKeyspaceMetadata, times(1)).getTables(); - verify(mockConverter, times(1)).getMappingContext(); - verify(mockMappingContext, times(1)).getNonPrimaryKeyEntities(); - verify(mockPersistentEntity, times(1)).getTableName(); - verify(mockPersistentEntity, times(1)).getType(); - verify(mockCassandraAdminOperations, times(1)).createTable(eq(false), eq(newCqlIdentifier("TestTable")), - eq(Person.class), isNull(Map.class)); - } - - @Test - @SuppressWarnings("unchecked") - public void createTableForEntityIfNotExists() { - CassandraMappingContext mockMappingContext = mock(CassandraMappingContext.class); - CassandraPersistentEntity mockPersistentEntity = mock(CassandraPersistentEntity.class); - CassandraAdminOperations mockCassandraAdminOperations = mock(CassandraAdminOperations.class); - - doReturn(mockCassandraAdminOperations).when(factoryBean).getCassandraAdminOperations(); - doReturn(mockSession).when(factoryBean).getObject(); - when(mockConverter.getMappingContext()).thenReturn(mockMappingContext); - when(mockMappingContext.getNonPrimaryKeyEntities()) - .thenReturn(Collections.> singletonList(mockPersistentEntity)); - when(mockPersistentEntity.getTableName()).thenReturn(newCqlIdentifier("TestTable")); - when(mockPersistentEntity.getType()).thenReturn(Person.class); - - factoryBean.setConverter(mockConverter); - factoryBean.setKeyspaceName("TestKeyspace"); - - assertThat(factoryBean.getConverter()).isEqualTo(mockConverter); - - factoryBean.createTables(false, false, true); - - verify(mockSession, never()).getCluster(); - verify(mockCluster, never()).getMetadata(); - verify(mockConverter, times(1)).getMappingContext(); - verify(mockMappingContext, times(1)).getNonPrimaryKeyEntities(); - verify(mockPersistentEntity, times(1)).getTableName(); - verify(mockPersistentEntity, times(1)).getType(); - verify(mockCassandraAdminOperations, times(1)).createTable(eq(true), eq(newCqlIdentifier("TestTable")), - eq(Person.class), isNull(Map.class)); - } - - @Test - public void createTableThrowsIllegalStateExceptionWhenKeyspaceNotFound() { - Metadata mockMetadata = mock(Metadata.class); - - doReturn(mockSession).when(factoryBean).getObject(); - when(mockCluster.getMetadata()).thenReturn(mockMetadata); - when(mockMetadata.getKeyspace(anyString())).thenReturn(null); - - exception.expect(IllegalStateException.class); - exception.expectMessage("keyspace [TestKeyspace] does not exist"); - - factoryBean.setKeyspaceName("TestKeyspace"); - factoryBean.createTables(true, false, true); - - verify(mockSession, times(1)).getCluster(); - verify(mockCluster, times(1)).getMetadata(); - verify(mockMetadata, times(1)).getKeyspace(eq("TestKeyspace")); - verify(mockMetadata, times(1)).getKeyspace(eq("testkeyspace")); - } - - // TODO: add more createTable tests covering drop tables, etc - @Test public void setAndGetConverter() { + assertThat(factoryBean.getConverter()).isNull(); factoryBean.setConverter(mockConverter); assertThat(factoryBean.getConverter()).isEqualTo(mockConverter); @@ -284,6 +194,7 @@ public class CassandraSessionFactoryBeanUnitTests { @Test public void setConverterToNull() { + exception.expect(IllegalArgumentException.class); exception.expectMessage("CassandraConverter must not be null"); @@ -292,6 +203,7 @@ public class CassandraSessionFactoryBeanUnitTests { @Test public void setAndGetSchemaAction() { + assertThat(factoryBean.getSchemaAction()).isEqualTo(SchemaAction.NONE); factoryBean.setSchemaAction(SchemaAction.CREATE); assertThat(factoryBean.getSchemaAction()).isEqualTo(SchemaAction.CREATE); @@ -308,5 +220,4 @@ public class CassandraSessionFactoryBeanUnitTests { } static class Person {} - } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/config/SchemaActionIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/SchemaActionIntegrationTests.java similarity index 88% rename from spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/config/SchemaActionIntegrationTests.java rename to spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/SchemaActionIntegrationTests.java index 4e9673949..ae435c64f 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/config/SchemaActionIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/SchemaActionIntegrationTests.java @@ -1,20 +1,20 @@ /* - * Copyright 2013-2016 the original author or authors + * Copyright 2016 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 + * 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 + * 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. + * 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.test.integration.config; +package org.springframework.data.cassandra.config; import static org.assertj.core.api.Assertions.*; @@ -34,7 +34,6 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.dao.DataAccessException; -import org.springframework.data.cassandra.config.SchemaAction; import org.springframework.data.cassandra.config.java.AbstractCassandraConfiguration; import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person; @@ -49,6 +48,7 @@ import com.datastax.driver.core.TableMetadata; * {@link SchemaAction}s on startup of a Spring configured, Cassandra application client. * * @author John Blum + * @author Mark Paluch * @see );"); + session.execute("CREATE TABLE addressbook (id text PRIMARY KEY, currentaddress FROZEN
, " + + "alternate FROZEN
, previousaddresses FROZEN>);"); + + session.execute("DROP TABLE IF EXISTS bank;"); + session.execute("CREATE TYPE IF NOT EXISTS currency (currency text);"); + session.execute( + "CREATE TABLE bank (id text PRIMARY KEY, currency FROZEN, othercurrencies FROZEN>);"); + + session.execute("DROP TABLE IF EXISTS money;"); + session.execute("CREATE TYPE IF NOT EXISTS currency (currency text);"); + session.execute("CREATE TABLE money (currency FROZEN PRIMARY KEY);"); + + session.execute("DROP TABLE IF EXISTS car;"); + session.execute("CREATE TYPE IF NOT EXISTS manufacturer (name text);"); + session.execute("CREATE TYPE IF NOT EXISTS engine (manufacturer FROZEN);"); + session.execute("CREATE TABLE car (id text PRIMARY KEY, engine FROZEN);"); + + } else { + + session.execute("TRUNCATE addressbook;"); + session.execute("TRUNCATE bank;"); + session.execute("TRUNCATE money;"); + session.execute("TRUNCATE car;"); + } + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldReadMappedUdt() { + + session.execute("INSERT INTO addressbook (id, currentaddress) " + "VALUES ('1', " + + "{zip:'69469', city: 'Weinheim', streetlines: ['Heckenpfad', '14']});"); + + ResultSet resultSet = session.execute("SELECT * from addressbook"); + AddressBook addressBook = converter.read(AddressBook.class, resultSet.one()); + + assertThat(addressBook.getCurrentaddress()).isNotNull(); + + AddressUserType address = addressBook.getCurrentaddress(); + assertThat(address.getCity()).isEqualTo("Weinheim"); + assertThat(address.getZip()).isEqualTo("69469"); + assertThat(address.getStreetLines()).contains("Heckenpfad", "14"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldWriteMappedUdt() { + + AddressUserType addressUserType = new AddressUserType(); + addressUserType.setZip("69469"); + addressUserType.setCity("Weinheim"); + addressUserType.setStreetLines(Arrays.asList("Heckenpfad", "14")); + + AddressBook addressBook = new AddressBook(); + addressBook.setId("1"); + addressBook.setCurrentaddress(addressUserType); + + Insert insert = QueryBuilder.insertInto("addressbook"); + converter.write(addressBook, insert); + + assertThat(insert.toString()).isEqualTo("INSERT INTO addressbook (alternate,currentaddress,id,previousaddresses) " + + "VALUES (null,{zip:'69469',city:'Weinheim',streetlines:['Heckenpfad','14']},'1',null);"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldReadMappedUdtCollection() { + + session.execute("INSERT INTO addressbook (id, previousaddresses) " + "VALUES ('1', " + + " [{zip:'53773', city: 'Bonn'}, {zip:'12345', city: 'Bonn'}]);"); + + ResultSet resultSet = session.execute("SELECT * from addressbook"); + AddressBook addressBook = converter.read(AddressBook.class, resultSet.one()); + + assertThat(addressBook.getPreviousaddresses()).hasSize(2); + + AddressUserType address = addressBook.getPreviousaddresses().get(0); + + assertThat(address.getCity()).isEqualTo("Bonn"); + assertThat(address.getZip()).isEqualTo("53773"); + assertThat(address.getStreetLines()).isEmpty(); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldWriteMappedUdtCollection() { + + AddressUserType addressUserType = new AddressUserType(); + addressUserType.setZip("69469"); + addressUserType.setCity("Weinheim"); + addressUserType.setStreetLines(Arrays.asList("Heckenpfad", "14")); + + AddressBook addressBook = new AddressBook(); + addressBook.setId("1"); + addressBook.setPreviousaddresses(Collections.singletonList(addressUserType)); + + Insert insert = QueryBuilder.insertInto("addressbook"); + converter.write(addressBook, insert); + + assertThat(insert.toString()).isEqualTo("INSERT INTO addressbook (alternate,currentaddress,id,previousaddresses) " + + "VALUES (null,null,'1',[{zip:'69469',city:'Weinheim',streetlines:['Heckenpfad','14']}]);"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldReadUdt() { + + session.execute("INSERT INTO addressbook (id, alternate) " + "VALUES ('1', " + + "{zip:'69469', city: 'Weinheim', streetlines: ['Heckenpfad', '14']});"); + + ResultSet resultSet = session.execute("SELECT * from addressbook"); + AddressBook addressBook = converter.read(AddressBook.class, resultSet.one()); + + assertThat(addressBook.getAlternate()).isNotNull(); + assertThat(addressBook.getAlternate().getString("city")).isEqualTo("Weinheim"); + assertThat(addressBook.getAlternate().getString("zip")).isEqualTo("69469"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldWriteUdt() { + + CassandraPersistentEntity persistentEntity = converter.getMappingContext() + .getPersistentEntity(AddressUserType.class); + UDTValue udtValue = persistentEntity.getUserType().newValue(); + udtValue.setString("zip", "69469"); + udtValue.setString("city", "Weinheim"); + udtValue.setList("streetlines", Arrays.asList("Heckenpfad", "14")); + + AddressBook addressBook = new AddressBook(); + addressBook.setId("1"); + addressBook.setAlternate(udtValue); + + Insert insert = QueryBuilder.insertInto("addressbook"); + converter.write(addressBook, insert); + + assertThat(insert.toString()).isEqualTo("INSERT INTO addressbook (alternate,currentaddress,id,previousaddresses) " + + "VALUES ({zip:'69469',city:'Weinheim',streetlines:['Heckenpfad','14']},null,'1',null);"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldWriteUdtPk() { + + AddressUserType addressUserType = new AddressUserType(); + addressUserType.setZip("69469"); + addressUserType.setCity("Weinheim"); + addressUserType.setStreetLines(Arrays.asList("Heckenpfad", "14")); + + WithMappedUdtId withUdtId = new WithMappedUdtId(); + withUdtId.setId(addressUserType); + + Insert insert = QueryBuilder.insertInto("addressbook"); + converter.write(withUdtId, insert); + + assertThat(insert.toString()).isEqualTo( + "INSERT INTO addressbook (id) " + "VALUES ({zip:'69469',city:'Weinheim',streetlines:['Heckenpfad','14']});"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldWriteMappedUdtPk() { + + CassandraPersistentEntity persistentEntity = converter.getMappingContext() + .getPersistentEntity(AddressUserType.class); + UDTValue udtValue = persistentEntity.getUserType().newValue(); + udtValue.setString("zip", "69469"); + udtValue.setString("city", "Weinheim"); + udtValue.setList("streetlines", Arrays.asList("Heckenpfad", "14")); + + WithUdtId withUdtId = new WithUdtId(); + withUdtId.setId(udtValue); + + Insert insert = QueryBuilder.insertInto("addressbook"); + converter.write(withUdtId, insert); + + assertThat(insert.toString()).isEqualTo( + "INSERT INTO addressbook (id) " + "VALUES ({zip:'69469',city:'Weinheim',streetlines:['Heckenpfad','14']});"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldReadUdtWithCustomConversion() { + + session.execute("INSERT INTO bank (id, currency) " + "VALUES ('1', {currency:'EUR'});"); + + ResultSet resultSet = session.execute("SELECT * from bank"); + Bank addressBook = converter.read(Bank.class, resultSet.one()); + + assertThat(addressBook.getCurrency()).isNotNull(); + assertThat(addressBook.getCurrency().getCurrencyCode()).isEqualTo("EUR"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldReadUdtListWithCustomConversion() { + + session.execute("INSERT INTO bank (id, othercurrencies) " + "VALUES ('1', [{currency:'EUR'}]);"); + + ResultSet resultSet = session.execute("SELECT * from bank"); + Bank addressBook = converter.read(Bank.class, resultSet.one()); + + assertThat(addressBook.getOtherCurrencies()).hasSize(1).contains(Currency.getInstance("EUR")); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldWriteUdtWithCustomConversion() { + + Bank bank = new Bank(); + bank.setCurrency(Currency.getInstance("EUR")); + + Insert insert = QueryBuilder.insertInto("bank"); + converter.write(bank, insert); + + assertThat(insert.toString()).isEqualTo("INSERT INTO bank (currency,id,othercurrencies) VALUES ({currency:'EUR'},null,null);"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldWriteUdtUpdateWherePrimaryKeyWithCustomConversion() { + + Money money = new Money(); + money.setCurrency(Currency.getInstance("EUR")); + + Update update = QueryBuilder.update("money"); + converter.write(money, update); + + assertThat(update.toString()).isEqualTo("UPDATE money WHERE currency={currency:'EUR'};"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldWriteUdtUpdateAssignmentsWithCustomConversion() { + + MoneyTransfer money = new MoneyTransfer(); + money.setId("1"); + money.setCurrency(Currency.getInstance("EUR")); + + Update update = QueryBuilder.update("money"); + converter.write(money, update); + + assertThat(update.toString()).isEqualTo("UPDATE money SET currency={currency:'EUR'} WHERE id='1';"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldWriteUdtSelectWherePrimaryKeyWithCustomConversion() { + + Money money = new Money(); + money.setCurrency(Currency.getInstance("EUR")); + + Select select = QueryBuilder.select().from("money"); + converter.write(money, select.where()); + + assertThat(select.toString()).isEqualTo("SELECT * FROM money WHERE currency={currency:'EUR'};"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldWriteUdtDeleteWherePrimaryKeyWithCustomConversion() { + + Money money = new Money(); + money.setCurrency(Currency.getInstance("EUR")); + + Delete delete = QueryBuilder.delete().from("money"); + converter.write(money, delete.where()); + + assertThat(delete.toString()).isEqualTo("DELETE FROM money WHERE currency={currency:'EUR'};"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldWriteUdtListWithCustomConversion() { + + Bank bank = new Bank(); + bank.setOtherCurrencies(Collections.singletonList(Currency.getInstance("EUR"))); + + Insert insert = QueryBuilder.insertInto("bank"); + converter.write(bank, insert); + + assertThat(insert.toString()).isEqualTo("INSERT INTO bank (currency,id,othercurrencies) VALUES (null,null,[{currency:'EUR'}]);"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldReadNestedUdt() { + + session.execute("INSERT INTO car (id, engine) VALUES ('1', {manufacturer: {name:'a good one'}});"); + + ResultSet resultSet = session.execute("SELECT * from car"); + Car car = converter.read(Car.class, resultSet.one()); + + assertThat(car.getEngine()).isNotNull(); + assertThat(car.getEngine().getManufacturer()).isNotNull(); + assertThat(car.getEngine().getManufacturer().getName()).isEqualTo("a good one"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldWriteNestedUdt() { + + session.execute("INSERT INTO car (id, engine) VALUES ('1', {manufacturer: {name:'a good one'}});"); + + Manufacturer manufacturer = new Manufacturer(); + manufacturer.setName("a good one"); + + Engine engine = new Engine(); + engine.setManufacturer(manufacturer); + + Car car = new Car(); + car.setId("1"); + car.setEngine(engine); + + Insert insert = QueryBuilder.insertInto("car"); + converter.write(car, insert); + + assertThat(insert.toString()).isEqualTo("INSERT INTO car (engine,id) VALUES ({manufacturer:{name:'a good one'}},'1');"); + } + + @Table + @Data + private static class Bank { + + @Id String id; + Currency currency; + List otherCurrencies; + } + + @Data + @Table + public static class Money { + @Id private Currency currency; + } + + @Data + @Table + public static class MoneyTransfer { + + @Id String id; + + private Currency currency; + } + + @Table + @Data + private static class Car { + + @Id String id; + Engine engine; + } + + @UserDefinedType + @Data + private static class Engine { + Manufacturer manufacturer; + } + + @UserDefinedType + @Data + private static class Manufacturer { + String name; + } + + @Data + @Table + public static class AddressBook { + + @Id private String id; + + private AddressUserType currentaddress; + private List previousaddresses; + private UDTValue alternate; + } + + @Data + @Table + public static class WithUdtId { + @Id private UDTValue id; + } + + @Data + @Table + public static class WithMappedUdtId { + @Id private AddressUserType id; + } + + @UserDefinedType("address") + @Data + public static class AddressUserType { + + String zip; + String city; + + List streetLines; + } + + private static class UDTToCurrencyConverter implements Converter { + + @Override + public Currency convert(UDTValue source) { + return Currency.getInstance(source.getString("currency")); + } + } + + private static class CurrencyToUDTConverter implements Converter { + + final UserTypeResolver userTypeResolver; + + CurrencyToUDTConverter(UserTypeResolver userTypeResolver) { + this.userTypeResolver = userTypeResolver; + } + + @Override + public UDTValue convert(Currency source) { + UserType userType = userTypeResolver.resolveType(CqlIdentifier.cqlId("currency")); + UDTValue udtValue = userType.newValue(); + udtValue.setString("currency", source.getCurrencyCode()); + return udtValue; + } + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreatorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreatorUnitTests.java new file mode 100644 index 000000000..f2f93b045 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreatorUnitTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2016 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.core; + +import static org.mockito.Mockito.*; + +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.UserDefinedType; +import org.springframework.data.cassandra.mapping.UserTypeResolver; + +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.UserType; + +import lombok.Data; + +/** + * Unit tests for {@link CassandraPersistentEntitySchemaCreator}. + * + * @author Mark Paluch. + */ +@RunWith(MockitoJUnitRunner.class) +public class CassandraPersistentEntitySchemaCreatorUnitTests { + + @Mock CassandraAdminOperations operations; + @Mock KeyspaceMetadata metadata; + @Mock UserType universetype; + @Mock UserType moontype; + @Mock UserType manufacturertype; + @Mock UserType biketype; + @Mock UserType tiretype; + + BasicCassandraMappingContext context = new BasicCassandraMappingContext(); + + @Before + public void setUp() throws Exception { + + context.setUserTypeResolver(new UserTypeResolver() { + @Override + public UserType resolveType(CqlIdentifier typeName) { + return metadata.getUserType(typeName.toCql()); + } + }); + } + + @Test + public void shouldCreateTypesInOrder() throws Exception { + + context.getPersistentEntity(MoonType.class); + context.getPersistentEntity(PlanetType.class); + context.getPersistentEntity(UniverseType.class); + + when(metadata.getUserType("universetype")).thenReturn(universetype); + when(metadata.getUserType("moontype")).thenReturn(moontype); + + CassandraPersistentEntitySchemaCreator schemaCreator = new CassandraPersistentEntitySchemaCreator(context, + operations); + + schemaCreator.createUserTypes(false, false, false); + + verify(operations).execute(Mockito.contains("CREATE TYPE universetype")); + verify(operations).execute(Mockito.contains("CREATE TYPE moontype")); + verify(operations).execute(Mockito.contains("CREATE TYPE planettype")); + + InOrder inOrder = Mockito.inOrder(operations); + + inOrder.verify(operations).execute(Mockito.contains("CREATE TYPE universetype")); + inOrder.verify(operations).execute(Mockito.contains("CREATE TYPE moontype")); + inOrder.verify(operations).execute(Mockito.contains("CREATE TYPE planettype")); + } + + /** + * @author Mark Paluch + */ + @UserDefinedType + @Data + static class UniverseType { + String name; + } + + /** + * @author Mark Paluch + */ + @UserDefinedType + static class MoonType { + + UniverseType universeType; + } + + /** + * @author Mark Paluch + */ + @UserDefinedType + static class PlanetType { + + Set moons; + UniverseType universeType; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContextUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContextUnitTests.java index 5cabbe15c..e1809cef8 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContextUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContextUnitTests.java @@ -16,6 +16,7 @@ package org.springframework.data.cassandra.mapping; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import java.io.Serializable; import java.util.Collection; @@ -23,6 +24,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import org.junit.Before; import org.junit.Test; import org.springframework.cassandra.core.Ordering; import org.springframework.cassandra.core.PrimaryKeyType; @@ -30,6 +32,7 @@ import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.cassandra.core.keyspace.ColumnSpecification; import org.springframework.cassandra.core.keyspace.CreateTableSpecification; import org.springframework.core.convert.converter.Converter; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.annotation.Id; import org.springframework.data.cassandra.convert.CustomConversions; import org.springframework.data.convert.WritingConverter; @@ -38,6 +41,8 @@ import org.springframework.data.util.ClassTypeInformation; import com.datastax.driver.core.DataType; import com.datastax.driver.core.DataType.Name; +import com.datastax.driver.core.UDTValue; +import com.datastax.driver.core.UserType; /** * Unit tests for {@link BasicCassandraMappingContext}. @@ -49,6 +54,18 @@ public class BasicCassandraMappingContextUnitTests { BasicCassandraMappingContext mappingContext = new BasicCassandraMappingContext(); + @Before + public void before() { + + mappingContext.setUserTypeResolver(new UserTypeResolver() { + + @Override + public UserType resolveType(CqlIdentifier typeName) { + return null; + } + }); + } + @Test(expected = MappingException.class) public void testGetPersistentEntityOfTransientType() { mappingContext.getPersistentEntity(Transient.class); @@ -66,16 +83,6 @@ public class BasicCassandraMappingContextUnitTests { assertThat(mappingContext.contains(Y.class)).isFalse(); } - @Table - private static class X { - @PrimaryKey String key; - } - - @Table - private static class Y { - @PrimaryKey String key; - } - /** * @see DATACASS-248 */ @@ -366,8 +373,173 @@ public class BasicCassandraMappingContextUnitTests { .isEqualTo(DataType.list(DataType.varchar())); } + /** + * @see DATACASS-172 + */ + @Test + public void shouldRegisterUdtTypes() { + + CassandraPersistentEntity persistentEntity = mappingContext.getPersistentEntity(MappedUdt.class); + + assertThat(persistentEntity.isUserDefinedType()).isTrue(); + } + + /** + * @see DATACASS-172 + */ + @Test + public void getNonPrimaryKeyEntitiesShouldNotContainUdt() { + + CassandraPersistentEntity existingPersistentEntity = mappingContext.getPersistentEntity(MappedUdt.class); + + assertThat(mappingContext.getNonPrimaryKeyEntities()).doesNotContain(existingPersistentEntity); + } + + /** + * @see DATACASS-172 + */ + @Test + public void getPersistentEntitiesShouldContainUdt() { + + CassandraPersistentEntity existingPersistentEntity = mappingContext.getPersistentEntity(MappedUdt.class); + + assertThat(mappingContext.getPersistentEntities(true)).contains(existingPersistentEntity); + assertThat(mappingContext.getPersistentEntities(false)).doesNotContain(existingPersistentEntity); + } + + /** + * @see DATACASS-172 + */ + @Test + public void usesTypeShouldNotReportTypeUsage() { + + UserType myTypeMock = mock(UserType.class, "mappedudt"); + when(myTypeMock.getTypeName()).thenReturn("mappedudt"); + + assertThat(mappingContext.usesUserType(myTypeMock)).isFalse(); + } + + /** + * @see DATACASS-172 + */ + @Test + public void usesTypeShouldReportTypeUsageInMappedUdt() { + + final UserType myTypeMock = mock(UserType.class, "mappedudt"); + when(myTypeMock.getTypeName()).thenReturn("mappedudt"); + + mappingContext.setUserTypeResolver(new UserTypeResolver() { + + @Override + public UserType resolveType(CqlIdentifier typeName) { + return myTypeMock; + } + }); + + mappingContext.getPersistentEntity(WithUdt.class); + + assertThat(mappingContext.usesUserType(myTypeMock)).isTrue(); + } + + /** + * @see DATACASS-172 + */ + @Test + public void usesTypeShouldReportTypeUsageInColumn() { + + final UserType myTypeMock = mock(UserType.class, "mappedudt"); + when(myTypeMock.getTypeName()).thenReturn("mappedudt"); + + mappingContext.setUserTypeResolver(new UserTypeResolver() { + + @Override + public UserType resolveType(CqlIdentifier typeName) { + return myTypeMock; + } + }); + + mappingContext.getPersistentEntity(MappedUdt.class); + + assertThat(mappingContext.usesUserType(myTypeMock)).isTrue(); + } + + /** + * @see DATACASS-172 + */ + @Test + public void createTableForComplexPrimaryKeyShouldFail() { + + try { + mappingContext + .getCreateTableSpecificationFor(mappingContext.getPersistentEntity(EntityWithComplexPrimaryKeyColumn.class)); + fail("Missing InvalidDataAccessApiUsageException"); + } catch (InvalidDataAccessApiUsageException e) { + assertThat(e).hasMessageContaining("Unknown type [class java.lang.Object] for property [complexObject]"); + } + + try { + mappingContext.getCreateTableSpecificationFor(mappingContext.getPersistentEntity(EntityWithComplexId.class)); + fail("Missing InvalidDataAccessApiUsageException"); + } catch (InvalidDataAccessApiUsageException e) { + assertThat(e).hasMessageContaining("Unknown type [class java.lang.Object] for property [complexObject]"); + } + + try { + mappingContext.getCreateTableSpecificationFor( + mappingContext.getPersistentEntity(EntityWithPrimaryKeyClassWithComplexId.class)); + fail("Missing InvalidDataAccessApiUsageException"); + } catch (InvalidDataAccessApiUsageException e) { + assertThat(e).hasMessageContaining("Unknown type [class java.lang.Object] for property [complexObject]"); + } + } + + @Table + static class EntityWithComplexPrimaryKeyColumn { + + @PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED) Object complexObject; + } + + @Table + static class EntityWithComplexId { + + @Id Object complexObject; + } + + @PrimaryKeyClass + static class PrimaryKeyClassWithComplexId { + + @PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED) Object complexObject; + } + + @Table + static class EntityWithPrimaryKeyClassWithComplexId { + + @Id PrimaryKeyClassWithComplexId primaryKeyClassWithComplexId; + } + private static class Human {} + @Table + private static class X { + @PrimaryKey String key; + } + + @Table + private static class Y { + @PrimaryKey String key; + } + + @UserDefinedType + private static class MappedUdt {} + + @Table + private static class WithUdt { + + @Id String id; + + @CassandraType(type = DataType.Name.UDT, userTypeName = "mappedudt") UDTValue udtValue; + } + enum HumanToStringConverter implements Converter { INSTANCE; diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifierUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifierUnitTests.java index eb86233fe..161a1f0f9 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifierUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifierUnitTests.java @@ -87,35 +87,6 @@ public class BasicCassandraPersistentEntityMetadataVerifierUnitTests { } } - /** - * @see DATACASS-258 - */ - @Test - public void shouldFailWithComplexTypePrimaryKey() { - - try { - verifier.verify(getEntity(EntityWithComplexTypePrimaryKey.class)); - fail("Missing MappingException"); - } catch (MappingException e) { - assertThat(e) - .hasMessageContaining("Property [species] annotated with @PrimaryKeyColumn must be a simple CassandraType"); - } - } - - /** - * @see DATACASS-258 - */ - @Test - public void shouldFailWithComplexTypeId() { - - try { - verifier.verify(getEntity(EntityWithComplexTypeId.class)); - fail("Missing MappingException"); - } catch (MappingException e) { - assertThat(e).hasMessageContaining("Property [species] annotated with @Id must be a simple CassandraType"); - } - } - /** * @see DATACASS-258 */ @@ -203,12 +174,6 @@ public class BasicCassandraPersistentEntityMetadataVerifierUnitTests { @PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED) Object species; } - @Table - static class EntityWithComplexTypeId { - - @Id Object species; - } - @Table @PrimaryKeyClass static class TooManyAnnotations {} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityUnitTests.java index cf412ec11..a66f75e02 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityUnitTests.java @@ -32,6 +32,7 @@ import org.springframework.data.util.ClassTypeInformation; * @author Alex Shvid * @author Matthew T. Adams * @author John Blum + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class BasicCassandraPersistentEntityUnitTests { @@ -100,6 +101,18 @@ public class BasicCassandraPersistentEntityUnitTests { verify(entitySpy, never()).setTableName(isA(CqlIdentifier.class)); } + /** + * @see DATACASS-172 + */ + @Test + public void isUserDefinedTypeShouldReturnFalse() { + + BasicCassandraPersistentEntity entity = new BasicCassandraPersistentEntity( + ClassTypeInformation.from(UserLine.class)); + + assertThat(entity.isUserDefinedType()).isFalse(); + } + @Table("messages") static class Message {} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CassandraUserTypePersistentEntityUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CassandraUserTypePersistentEntityUnitTests.java new file mode 100644 index 000000000..648ae8cf5 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CassandraUserTypePersistentEntityUnitTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2016 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 org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.util.ClassTypeInformation; + +/** + * Unit tests for {@link CassandraUserTypePersistentEntity}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class CassandraUserTypePersistentEntityUnitTests { + + @Mock CassandraMappingContext mappingContextMock; + @Mock UserTypeResolver userTypeResolverMock; + + /** + * @see DATACASS-172 + */ + @Test + public void isUserDefinedTypeShouldReportTrue() { + + CassandraUserTypePersistentEntity type = getEntity(MappedUdt.class); + + assertThat(type.isUserDefinedType()).isTrue(); + } + + /** + * @see DATACASS-172 + */ + @Test + public void getTableNameShouldReturnDefaultName() { + + CassandraUserTypePersistentEntity type = getEntity(MappedUdt.class); + + assertThat(type.getTableName()).isEqualTo(CqlIdentifier.cqlId("mappedudt")); + assertThat(type.getTableName()).isEqualTo(CqlIdentifier.cqlId("Mappedudt")); + } + + /** + * @see DATACASS-172 + */ + @Test + public void getTableNameShouldReturnDefinedName() { + + CassandraUserTypePersistentEntity type = getEntity(WithName.class); + + assertThat(type.getTableName()).isEqualTo(CqlIdentifier.cqlId("withname")); + assertThat(type.getTableName()).isEqualTo(CqlIdentifier.cqlId("Withname")); + } + + /** + * @see DATACASS-172 + */ + @Test + public void getTableNameShouldReturnDefinedNameUsingForceQuote() { + + CassandraUserTypePersistentEntity type = getEntity(WithForceQuote.class); + + assertThat(type.getTableName()).isNotEqualTo(CqlIdentifier.cqlId("upperCase", true)); + assertThat(type.getTableName()).isEqualTo(CqlIdentifier.cqlId("UpperCase", true)); + } + + private CassandraUserTypePersistentEntity getEntity(Class entityClass) { + return new CassandraUserTypePersistentEntity(ClassTypeInformation.from(entityClass), mappingContextMock, null, + userTypeResolverMock); + } + + @UserDefinedType + static class MappedUdt {} + + @UserDefinedType("withname") + static class WithName {} + + @UserDefinedType(value = "UpperCase", forceQuote = true) + static class WithForceQuote {} +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CompositeCassandraPersistentEntityMetadataVerifierUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CompositeCassandraPersistentEntityMetadataVerifierUnitTests.java index f16745cb2..586d1875d 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CompositeCassandraPersistentEntityMetadataVerifierUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CompositeCassandraPersistentEntityMetadataVerifierUnitTests.java @@ -75,7 +75,7 @@ public class CompositeCassandraPersistentEntityMetadataVerifierUnitTests { fail("Missing MappingException"); } catch (MappingException e) { assertThat(e).hasMessageContaining( - "Cassandra entities must be annotated with either @Persistent, @Table, or @PrimaryKeyClass"); + "Cassandra entities must be annotated with either @Persistent, @Table, @UserDefinedType or @PrimaryKeyClass"); } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CreateTableSpecificationBasicCassandraMappingContextUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CreateTableSpecificationBasicCassandraMappingContextUnitTests.java index f74e861e7..53bccbf3a 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CreateTableSpecificationBasicCassandraMappingContextUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CreateTableSpecificationBasicCassandraMappingContextUnitTests.java @@ -16,11 +16,13 @@ package org.springframework.data.cassandra.mapping; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; import org.codehaus.jackson.map.ObjectMapper; @@ -38,6 +40,8 @@ import org.springframework.util.StringUtils; 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.UDTValue; +import com.datastax.driver.core.UserType; import lombok.AllArgsConstructor; import lombok.Data; @@ -51,7 +55,7 @@ import lombok.NoArgsConstructor; */ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { - BasicCassandraMappingContext ctx = new BasicCassandraMappingContext(); + private BasicCassandraMappingContext ctx = new BasicCassandraMappingContext(); @Before public void setUp() throws Exception { @@ -74,7 +78,7 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = ctx.getCreateTableSpecificationFor(persistentEntity); - assertThat(getColumn("human", specification).getType()).isEqualTo(DataType.varchar()); + assertThat(getColumnType("human", specification)).isEqualTo(DataType.varchar()); ColumnSpecification friends = getColumn("friends", specification); assertThat(friends.getType().isCollection()).isTrue(); @@ -103,7 +107,7 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = ctx.getCreateTableSpecificationFor(persistentEntity); - assertThat(getColumn("floater", specification).getType()).isEqualTo(DataType.cfloat()); + assertThat(getColumnType("floater", specification)).isEqualTo(DataType.cfloat()); ColumnSpecification enemies = getColumn("enemies", specification); assertThat(enemies.getType().isCollection()).isTrue(); @@ -122,10 +126,10 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); - assertThat(getColumn("id", specification).getType()).isEqualTo(DataType.varchar()); - assertThat(getColumn("zoneId", specification).getType()).isEqualTo(DataType.varchar()); - assertThat(getColumn("bpZoneId", specification).getType()).isEqualTo(DataType.varchar()); - assertThat(getColumn("anEnum", specification).getType()).isEqualTo(DataType.varchar()); + assertThat(getColumnType("id", specification)).isEqualTo(DataType.varchar()); + assertThat(getColumnType("zoneId", specification)).isEqualTo(DataType.varchar()); + assertThat(getColumnType("bpZoneId", specification)).isEqualTo(DataType.varchar()); + assertThat(getColumnType("anEnum", specification)).isEqualTo(DataType.varchar()); } /** @@ -136,8 +140,8 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); - assertThat(getColumn("boxedByte", specification).getType()).isEqualTo(DataType.tinyint()); - assertThat(getColumn("primitiveByte", specification).getType()).isEqualTo(DataType.tinyint()); + assertThat(getColumnType("boxedByte", specification)).isEqualTo(DataType.tinyint()); + assertThat(getColumnType("primitiveByte", specification)).isEqualTo(DataType.tinyint()); } /** @@ -148,8 +152,8 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); - assertThat(getColumn("boxedShort", specification).getType()).isEqualTo(DataType.smallint()); - assertThat(getColumn("primitiveShort", specification).getType()).isEqualTo(DataType.smallint()); + assertThat(getColumnType("boxedShort", specification)).isEqualTo(DataType.smallint()); + assertThat(getColumnType("primitiveShort", specification)).isEqualTo(DataType.smallint()); } /** @@ -160,8 +164,8 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); - assertThat(getColumn("boxedLong", specification).getType()).isEqualTo(DataType.bigint()); - assertThat(getColumn("primitiveLong", specification).getType()).isEqualTo(DataType.bigint()); + assertThat(getColumnType("boxedLong", specification)).isEqualTo(DataType.bigint()); + assertThat(getColumnType("primitiveLong", specification)).isEqualTo(DataType.bigint()); } /** @@ -172,7 +176,7 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); - assertThat(getColumn("bigInteger", specification).getType()).isEqualTo(DataType.varint()); + assertThat(getColumnType("bigInteger", specification)).isEqualTo(DataType.varint()); } /** @@ -183,7 +187,7 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); - assertThat(getColumn("bigDecimal", specification).getType()).isEqualTo(DataType.decimal()); + assertThat(getColumnType("bigDecimal", specification)).isEqualTo(DataType.decimal()); } /** @@ -194,8 +198,8 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); - assertThat(getColumn("boxedInteger", specification).getType()).isEqualTo(DataType.cint()); - assertThat(getColumn("primitiveInteger", specification).getType()).isEqualTo(DataType.cint()); + assertThat(getColumnType("boxedInteger", specification)).isEqualTo(DataType.cint()); + assertThat(getColumnType("primitiveInteger", specification)).isEqualTo(DataType.cint()); } /** @@ -206,8 +210,8 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); - assertThat(getColumn("boxedFloat", specification).getType()).isEqualTo(DataType.cfloat()); - assertThat(getColumn("primitiveFloat", specification).getType()).isEqualTo(DataType.cfloat()); + assertThat(getColumnType("boxedFloat", specification)).isEqualTo(DataType.cfloat()); + assertThat(getColumnType("primitiveFloat", specification)).isEqualTo(DataType.cfloat()); } /** @@ -218,8 +222,8 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); - assertThat(getColumn("boxedDouble", specification).getType()).isEqualTo(DataType.cdouble()); - assertThat(getColumn("primitiveDouble", specification).getType()).isEqualTo(DataType.cdouble()); + assertThat(getColumnType("boxedDouble", specification)).isEqualTo(DataType.cdouble()); + assertThat(getColumnType("primitiveDouble", specification)).isEqualTo(DataType.cdouble()); } /** @@ -230,8 +234,8 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); - assertThat(getColumn("boxedBoolean", specification).getType()).isEqualTo(DataType.cboolean()); - assertThat(getColumn("primitiveBoolean", specification).getType()).isEqualTo(DataType.cboolean()); + assertThat(getColumnType("boxedBoolean", specification)).isEqualTo(DataType.cboolean()); + assertThat(getColumnType("primitiveBoolean", specification)).isEqualTo(DataType.cboolean()); } /** @@ -242,11 +246,11 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); - assertThat(getColumn("date", specification).getType()).isEqualTo(DataType.date()); - assertThat(getColumn("localDate", specification).getType()).isEqualTo(DataType.date()); - assertThat(getColumn("jodaLocalDate", specification).getType()).isEqualTo(DataType.date()); - assertThat(getColumn("jodaDateMidnight", specification).getType()).isEqualTo(DataType.date()); - assertThat(getColumn("bpLocalDate", specification).getType()).isEqualTo(DataType.date()); + assertThat(getColumnType("date", specification)).isEqualTo(DataType.date()); + assertThat(getColumnType("localDate", specification)).isEqualTo(DataType.date()); + assertThat(getColumnType("jodaLocalDate", specification)).isEqualTo(DataType.date()); + assertThat(getColumnType("jodaDateMidnight", specification)).isEqualTo(DataType.date()); + assertThat(getColumnType("bpLocalDate", specification)).isEqualTo(DataType.date()); } /** @@ -257,13 +261,13 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); - assertThat(getColumn("timestamp", specification).getType()).isEqualTo(DataType.timestamp()); - assertThat(getColumn("localDateTime", specification).getType()).isEqualTo(DataType.timestamp()); - assertThat(getColumn("instant", specification).getType()).isEqualTo(DataType.timestamp()); - assertThat(getColumn("jodaLocalDateTime", specification).getType()).isEqualTo(DataType.timestamp()); - assertThat(getColumn("jodaDateTime", specification).getType()).isEqualTo(DataType.timestamp()); - assertThat(getColumn("bpLocalDateTime", specification).getType()).isEqualTo(DataType.timestamp()); - assertThat(getColumn("bpInstant", specification).getType()).isEqualTo(DataType.timestamp()); + assertThat(getColumnType("timestamp", specification)).isEqualTo(DataType.timestamp()); + assertThat(getColumnType("localDateTime", specification)).isEqualTo(DataType.timestamp()); + assertThat(getColumnType("instant", specification)).isEqualTo(DataType.timestamp()); + assertThat(getColumnType("jodaLocalDateTime", specification)).isEqualTo(DataType.timestamp()); + assertThat(getColumnType("jodaDateTime", specification)).isEqualTo(DataType.timestamp()); + assertThat(getColumnType("bpLocalDateTime", specification)).isEqualTo(DataType.timestamp()); + assertThat(getColumnType("bpInstant", specification)).isEqualTo(DataType.timestamp()); } /** @@ -274,8 +278,8 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = getCreateTableSpecificationFor(TypeWithOverrides.class); - assertThat(getColumn("localDate", specification).getType()).isEqualTo(DataType.timestamp()); - assertThat(getColumn("jodaLocalDate", specification).getType()).isEqualTo(DataType.timestamp()); + assertThat(getColumnType("localDate", specification)).isEqualTo(DataType.timestamp()); + assertThat(getColumnType("jodaLocalDate", specification)).isEqualTo(DataType.timestamp()); } /** @@ -286,10 +290,76 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); - assertThat(getColumn("blob", specification).getType()).isEqualTo(DataType.blob()); + assertThat(getColumnType("blob", specification)).isEqualTo(DataType.blob()); } - public CreateTableSpecification getCreateTableSpecificationFor(Class persistentEntityClass) { + /** + * @see DATACASS-172 + */ + @Test + public void columnsShouldMapToUdt() { + + final UserType human_udt = mock(UserType.class, "human_udt"); + final UserType species_udt = mock(UserType.class, "species_udt"); + final UserType peeps_udt = mock(UserType.class, "peeps_udt"); + + ctx.setUserTypeResolver(new UserTypeResolver() { + @Override + public UserType resolveType(CqlIdentifier typeName) { + + if (typeName.toCql().equals(human_udt.toString())) { + return human_udt; + } + + if (typeName.toCql().equals(species_udt.toString())) { + return species_udt; + } + + if (typeName.toCql().equals(peeps_udt.toString())) { + return peeps_udt; + } + return null; + } + }); + + CreateTableSpecification specification = getCreateTableSpecificationFor(WithUdtFields.class); + + assertThat(getColumnType("human", specification)).isEqualTo(human_udt); + assertThat(getColumnType("friends", specification)).isEqualTo(DataType.list(species_udt)); + assertThat(getColumnType("people", specification)).isEqualTo(DataType.set(peeps_udt)); + } + + /** + * @see DATACASS-172 + */ + @Test + public void columnsShouldMapToMapped() { + + final UserType mappedUdt = mock(UserType.class, "mappedudt"); + + ctx.setUserTypeResolver(new UserTypeResolver() { + @Override + public UserType resolveType(CqlIdentifier typeName) { + + if (typeName.toCql().equals(mappedUdt.toString())) { + return mappedUdt; + } + return null; + } + }); + + CreateTableSpecification specification = getCreateTableSpecificationFor(WithMappedUdtFields.class); + + assertThat(getColumnType("human", specification)).isEqualTo(mappedUdt); + assertThat(getColumnType("friends", specification)).isEqualTo(DataType.list(mappedUdt)); + assertThat(getColumnType("people", specification)).isEqualTo(DataType.set(mappedUdt)); + assertThat(getColumnType("stringToUdt", specification)) + .isEqualTo(DataType.map(DataType.varchar(), mappedUdt)); + assertThat(getColumnType("udtToString", specification)) + .isEqualTo(DataType.map(mappedUdt, DataType.varchar())); + } + + private CreateTableSpecification getCreateTableSpecificationFor(Class persistentEntityClass) { CustomConversions customConversions = new CustomConversions(Collections.EMPTY_LIST); ctx.setCustomConversions(customConversions); @@ -298,6 +368,10 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { return ctx.getCreateTableSpecificationFor(persistentEntity); } + private DataType getColumnType(String columnName, CreateTableSpecification specification) { + return getColumn(columnName, specification).getType(); + } + private ColumnSpecification getColumn(String columnName, CreateTableSpecification specification) { for (ColumnSpecification columnSpecification : specification.getColumns()) { @@ -310,12 +384,9 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { String.format("Cannot find column '%s' amongst '%s'", columnName, specification.getColumns())); } - /** - * @author Mark Paluch - */ @Data @Table - public static class Employee { + private static class Employee { @Id String id; @@ -327,9 +398,33 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { @CassandraType(type = Name.SET, typeArguments = Name.BIGINT) List enemies; } - /** - * @author Mark Paluch - */ + @Data + @Table + private static class WithUdtFields { + + @Id String id; + + @CassandraType(type = Name.UDT, userTypeName = "human_udt") UDTValue human; + @CassandraType(type = Name.LIST, typeArguments = Name.UDT, userTypeName = "species_udt") List friends; + @CassandraType(type = Name.SET, typeArguments = Name.UDT, userTypeName = "peeps_udt") Set people; + } + + @Data + @Table + private static class WithMappedUdtFields { + + @Id String id; + + MappedUdt human; + List friends; + Set people; + Map stringToUdt; + Map udtToString; + } + + @UserDefinedType + private static class MappedUdt {} + @Data @AllArgsConstructor @NoArgsConstructor @@ -339,21 +434,17 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { String lastname; } - /** - * @author Mark Paluch - */ @Data @Table - static class TypeWithOverrides { + private static class TypeWithOverrides { @Id String id; @CassandraType(type = Name.TIMESTAMP) java.time.LocalDate localDate; - @CassandraType(type = Name.TIMESTAMP) org.joda.time.LocalDate jodaLocalDate; } - static class PersonReadConverter implements Converter { + private static class PersonReadConverter implements Converter { public Human convert(String source) { @@ -369,7 +460,7 @@ public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { } } - static class PersonWriteConverter implements Converter { + private static class PersonWriteConverter implements Converter { public String convert(Human source) { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/PrimaryKeyClassEntityMetadataVerifierUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/PrimaryKeyClassEntityMetadataVerifierUnitTests.java index 6b2b7125d..2a602f5c1 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/PrimaryKeyClassEntityMetadataVerifierUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/PrimaryKeyClassEntityMetadataVerifierUnitTests.java @@ -142,20 +142,6 @@ public class PrimaryKeyClassEntityMetadataVerifierUnitTests { } } - /** - * @see DATACASS-258 - */ - @Test - public void shouldFailWithComplexType() { - - try { - verifier.verify(getEntity(PKWithComplexType.class)); - fail("Missing MappingException"); - } catch (MappingException e) { - assertThat(e).hasMessageContaining("Property [species] annotated with @PrimaryKeyColumn must be a simple CassandraType"); - } - } - /** * @see DATACASS-258 */ @@ -224,12 +210,6 @@ public class PrimaryKeyClassEntityMetadataVerifierUnitTests { @PrimaryKeyColumn(ordinal = 2, type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING) String color; } - @PrimaryKeyClass - static class PKWithComplexType { - - @PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED) Object species; - } - @Table @PrimaryKeyClass static class TooManyAnnotations { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Address.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/Address.java similarity index 89% rename from spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Address.java rename to spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/Address.java index 933d97db5..67f0208ac 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Address.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/Address.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.cassandra.test.integration.repository.querymethods.conversion; +package org.springframework.data.cassandra.repository.conversion; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/Contact.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/Contact.java new file mode 100644 index 000000000..b3f1334e3 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/Contact.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016 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.repository.conversion; + +import java.util.List; + +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.mapping.CassandraType; +import org.springframework.data.cassandra.mapping.Table; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import com.datastax.driver.core.DataType.Name; + +/** + * @author Mark Paluch + */ +@Table +@Data +@NoArgsConstructor +class Contact { + + @Id String id; + + Address address; + List
addresses; + + @CassandraType(type = Name.UDT, userTypeName = "phone") + Phone mainPhone; + + @CassandraType(type = Name.LIST,typeArguments = Name.UDT, userTypeName = "phone") + List alternativePhones; + + public Contact(String id) { + this.id = id; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/DerivedQueryMethodsParameterConversionIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/DerivedQueryMethodsParameterConversionIntegrationTests.java new file mode 100644 index 000000000..8ea5a3250 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/DerivedQueryMethodsParameterConversionIntegrationTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2016 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.repository.conversion; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.cassandra.repository.CassandraRepository; +import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.base.PersonRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.UDTValue; + +/** + * Integration tests for query argument conversion through {@link PersonRepository}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = ParameterConversionTestSupport.Config.class) +public class DerivedQueryMethodsParameterConversionIntegrationTests extends ParameterConversionTestSupport { + + @Autowired ContactRepository contactRepository; + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByConvertedParameter() { + + List contacts = contactRepository.findByAddress(walter.getAddress()); + + assertThat(contacts).contains(walter, flynn); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByStringParameter() { + + String parameter = AddressWriteConverter.INSTANCE.convert(walter.getAddress()); + List contacts = contactRepository.findByAddress(parameter); + + assertThat(contacts).contains(walter, flynn); + } + + /** + * @see DATACASS-7 + */ + @Test + public void findByAddressesIn() { + + assertThat(contactRepository.findByAddressesContains(flynn.address)).contains(flynn, walter); + assertThat(contactRepository.findByAddressesContains(walter.addresses.get(1))).contains(walter); + } + + /** + * @see DATACASS-172 + */ + @Test + public void findByMainPhone() { + assertThat(contactRepository.findByMainPhone(walter.getMainPhone())).contains(walter); + } + + /** + * @see DATACASS-172 + */ + @Test + public void findByMainPhoneUdtValue() { + + KeyspaceMetadata keyspace = adminOperations.getKeyspaceMetadata(); + UDTValue udtValue = keyspace.getUserType("phone").newValue(); + udtValue.setString("number", walter.getMainPhone().getNumber()); + + assertThat(contactRepository.findByMainPhone(udtValue)).contains(walter); + } + + /** + * @see DATACASS-172 + */ + @Test + public void findByAlternativePhones() { + + Phone phone = walter.getAlternativePhones().get(0); + assertThat(contactRepository.findByAlternativePhonesContains(phone)).contains(walter); + } + + /** + * @see DATACASS-172 + */ + @Test + public void findByAlternativePhonesUdtValue() { + + Phone phone = walter.getAlternativePhones().get(0); + + KeyspaceMetadata keyspace = adminOperations.getKeyspaceMetadata(); + UDTValue udtValue = keyspace.getUserType("phone").newValue(); + udtValue.setString("number", phone.getNumber()); + + assertThat(contactRepository.findByAlternativePhonesContains(udtValue)).contains(walter); + } + + interface ContactRepository extends CassandraRepository { + + List findByAddress(Address address); + + List findByAddress(String address); + + List findByAddressesContains(Address address); + + List findByMainPhone(Phone phone); + + List findByMainPhone(UDTValue udtValue); + + List findByAlternativePhonesContains(Phone phone); + + List findByAlternativePhonesContains(UDTValue udtValue); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/ParameterConversionIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/ParameterConversionTestSupport.java similarity index 55% rename from spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/ParameterConversionIntegrationTests.java rename to spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/ParameterConversionTestSupport.java index 7c62c33c7..6f98077ed 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/ParameterConversionIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/ParameterConversionTestSupport.java @@ -13,42 +13,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.cassandra.test.integration.repository.querymethods.conversion; - -import static org.assertj.core.api.Assertions.*; +package org.springframework.data.cassandra.repository.conversion; import java.io.IOException; import java.util.Arrays; import java.util.Collections; -import java.util.List; import org.codehaus.jackson.map.ObjectMapper; import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.cassandra.config.CassandraSessionFactoryBean; import org.springframework.data.cassandra.config.SchemaAction; import org.springframework.data.cassandra.convert.CustomConversions; +import org.springframework.data.cassandra.core.CassandraAdminOperations; import org.springframework.data.cassandra.core.CassandraOperations; -import org.springframework.data.cassandra.repository.CassandraRepository; +import org.springframework.data.cassandra.mapping.SimpleUserTypeResolver; +import org.springframework.data.cassandra.mapping.UserTypeResolver; import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; -import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.base.PersonRepository; 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 org.springframework.util.StringUtils; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.UDTValue; +import com.datastax.driver.core.UserType; + /** - * Integration tests for query derivation through {@link PersonRepository}. - * + * Test support for query method parameter type conversion. + * * @author Mark Paluch */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration -public class ParameterConversionIntegrationTests extends AbstractSpringDataEmbeddedCassandraIntegrationTest { +abstract class ParameterConversionTestSupport extends AbstractSpringDataEmbeddedCassandraIntegrationTest { @Configuration @EnableCassandraRepositories(considerNestedRepositories = true) @@ -64,14 +63,27 @@ public class ParameterConversionIntegrationTests extends AbstractSpringDataEmbed return SchemaAction.RECREATE_DROP_UNUSED; } + @Override + public CassandraSessionFactoryBean session() throws ClassNotFoundException { + + Cluster cluster = cluster().getObject(); + Session session = cluster.connect(getKeyspaceName()); + session.execute("CREATE TYPE IF NOT EXISTS phone (number text);"); + session.close(); + + return super.session(); + } + @Override public CustomConversions customConversions() { - return new CustomConversions(Arrays.asList(AddressReadConverter.INSTANCE, AddressWriteConverter.INSTANCE)); + return new CustomConversions( + Arrays.asList(AddressReadConverter.INSTANCE, AddressWriteConverter.INSTANCE, PhoneReadConverter.INSTANCE, + new PhoneWriteConverter(new SimpleUserTypeResolver(cluster().getObject(), getKeyspaceName())))); } } @Autowired CassandraOperations template; - @Autowired ContactRepository contactRepository; + @Autowired CassandraAdminOperations adminOperations; Contact walter, flynn; @@ -83,65 +95,88 @@ public class ParameterConversionIntegrationTests extends AbstractSpringDataEmbed template.execute("CREATE INDEX IF NOT EXISTS contact_address ON contact (address);"); template.execute("CREATE INDEX IF NOT EXISTS contact_addresses ON contact (addresses);"); + template.execute("CREATE INDEX IF NOT EXISTS contact_main_phones ON contact (mainphone);"); + template.execute("CREATE INDEX IF NOT EXISTS contact_alternative_phones ON contact (alternativephones);"); + walter = new Contact("Walter"); walter.setAddress(new Address("Albuquerque", "USA")); walter.setAddresses(Arrays.asList(new Address("Albuquerque", "USA"), new Address("New Hampshire", "USA"), new Address("Grocery Store", "Mexico"))); + Phone phone = new Phone(); + phone.setNumber("(505) 555-1258"); + + Phone alternative = new Phone(); + alternative.setNumber("505-842-4205"); + + walter.setMainPhone(phone); + walter.setAlternativePhones(Collections.singletonList(alternative)); + flynn = new Contact("Flynn"); flynn.setAddress(new Address("Albuquerque", "USA")); flynn.setAddresses(Collections.singletonList(new Address("Albuquerque", "USA"))); - walter = contactRepository.save(walter); - flynn = contactRepository.save(flynn); - } - - /** - * @see DATACASS-7 - */ - @Test - public void shouldFindByConvertedParameter() { - - List contacts = contactRepository.findByAddress(walter.getAddress()); - - assertThat(contacts).contains(walter, flynn); - } - - /** - * @see DATACASS-7 - */ - @Test - public void shouldFindByStringParameter() { - - String parameter = AddressWriteConverter.INSTANCE.convert(walter.getAddress()); - List contacts = contactRepository.findByAddress(parameter); - - assertThat(contacts).contains(walter, flynn); - } - - /** - * @see DATACASS-7 - */ - @Test - public void findByAddressesIn() { - - assertThat(contactRepository.findByAddressesContains(flynn.address)).contains(flynn, walter); - assertThat(contactRepository.findByAddressesContains(walter.addresses.get(1))).contains(walter); - } - - interface ContactRepository extends CassandraRepository { - - List findByAddress(Address address); - - List findByAddress(String address); - - List findByAddressesContains(Address address); + template.insert(walter); + template.insert(flynn); } /** * @author Mark Paluch */ - static enum AddressReadConverter implements Converter { + enum AddressWriteConverter implements Converter { + INSTANCE; + + public String convert(Address source) { + + try { + return new ObjectMapper().writeValueAsString(source); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + } + + /** + * @author Mark Paluch + */ + private enum PhoneReadConverter implements Converter { + + INSTANCE; + + public Phone convert(UDTValue source) { + + Phone phone = new Phone(); + phone.setNumber(source.getString("number")); + + return phone; + } + } + + /** + * @author Mark Paluch + */ + private static class PhoneWriteConverter implements Converter { + + private UserTypeResolver userTypeResolver; + + PhoneWriteConverter(UserTypeResolver userTypeResolver) { + this.userTypeResolver = userTypeResolver; + } + + public UDTValue convert(Phone source) { + + UserType userType = userTypeResolver.resolveType(CqlIdentifier.cqlId("phone")); + UDTValue udtValue = userType.newValue(); + udtValue.setString("number", source.getNumber()); + + return udtValue; + } + } + + /** + * @author Mark Paluch + */ + private enum AddressReadConverter implements Converter { INSTANCE; @@ -158,20 +193,4 @@ public class ParameterConversionIntegrationTests extends AbstractSpringDataEmbed return null; } } - - /** - * @author Mark Paluch - */ - static enum AddressWriteConverter implements Converter { - INSTANCE; - - public String convert(Address source) { - - try { - return new ObjectMapper().writeValueAsString(source); - } catch (IOException e) { - throw new IllegalStateException(e); - } - } - } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/Phone.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/Phone.java new file mode 100644 index 000000000..8c31a67a0 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/Phone.java @@ -0,0 +1,27 @@ +/* + * Copyright 2016 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.repository.conversion; + +import lombok.Data; + +/** + * @author Mark Paluch + */ +@Data +class Phone { + + String number; +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/StringQueryMethodsParameterConversionIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/StringQueryMethodsParameterConversionIntegrationTests.java new file mode 100644 index 000000000..e8f51da68 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/conversion/StringQueryMethodsParameterConversionIntegrationTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2016 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.repository.conversion; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.cassandra.repository.CassandraRepository; +import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.base.PersonRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.UDTValue; + +/** + * Integration tests for query argument conversion through {@link PersonRepository}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = ParameterConversionTestSupport.Config.class) +public class StringQueryMethodsParameterConversionIntegrationTests extends ParameterConversionTestSupport { + + @Autowired ContactStringQueryRepository contactRepository; + + /** + * @see DATACASS-172 + */ + @Test + public void shouldFindByConvertedParameter() { + + List contacts = contactRepository.findByAddress(walter.getAddress()); + + assertThat(contacts).contains(walter, flynn); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldFindByStringParameter() { + + String parameter = AddressWriteConverter.INSTANCE.convert(walter.getAddress()); + List contacts = contactRepository.findByAddress(parameter); + + assertThat(contacts).contains(walter, flynn); + } + + /** + * @see DATACASS-172 + */ + @Test + public void findByAddressesIn() { + + assertThat(contactRepository.findByAddressesContains(flynn.address)).contains(flynn, walter); + assertThat(contactRepository.findByAddressesContains(walter.addresses.get(1))).contains(walter); + } + + /** + * @see DATACASS-172 + */ + @Test + public void findByMainPhone() { + assertThat(contactRepository.findByMainPhone(walter.getMainPhone())).contains(walter); + } + + /** + * @see DATACASS-172 + */ + @Test + public void findByMainPhoneUdtValue() { + + KeyspaceMetadata keyspace = adminOperations.getKeyspaceMetadata(); + UDTValue udtValue = keyspace.getUserType("phone").newValue(); + udtValue.setString("number", walter.getMainPhone().getNumber()); + + assertThat(contactRepository.findByMainPhone(udtValue)).contains(walter); + } + + /** + * @see DATACASS-172 + */ + @Test + public void findByAlternativePhones() { + + Phone phone = walter.getAlternativePhones().get(0); + assertThat(contactRepository.findByAlternativePhonesContains(phone)).contains(walter); + } + + /** + * @see DATACASS-172 + */ + @Test + public void findByAlternativePhonesUdtValue() { + + Phone phone = walter.getAlternativePhones().get(0); + + KeyspaceMetadata keyspace = adminOperations.getKeyspaceMetadata(); + UDTValue udtValue = keyspace.getUserType("phone").newValue(); + udtValue.setString("number", phone.getNumber()); + + assertThat(contactRepository.findByAlternativePhonesContains(udtValue)).contains(walter); + } + + interface ContactStringQueryRepository extends CassandraRepository { + + @Query("SELECT * from contact where address = ?0;") + List findByAddress(Address address); + + @Query("SELECT * from contact where address = ?0;") + List findByAddress(String address); + + @Query("SELECT * from contact where addresses contains ?0;") + List findByAddressesContains(Address address); + + @Query("SELECT * from contact where mainphone = ?0;") + List findByMainPhone(Phone phone); + + @Query("SELECT * from contact where mainphone = ?0;") + List findByMainPhone(UDTValue udtValue); + + @Query("SELECT * from contact where alternativephones contains ?0;") + List findByAlternativePhonesContains(Phone phone); + + @Query("SELECT * from contact where alternativephones contains ?0;") + List findByAlternativePhonesContains(UDTValue udtValue); + } +} 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 6edc69a3d..452aa53d2 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 @@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.lang.reflect.Method; +import java.util.Collections; import org.junit.Before; import org.junit.Rule; @@ -27,17 +28,23 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.data.cassandra.convert.CassandraConverter; import org.springframework.data.cassandra.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.CassandraOperations; -import org.springframework.data.cassandra.domain.Person; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; -import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.UserTypeResolver; import org.springframework.data.cassandra.repository.CassandraRepository; import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Address; +import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.util.ClassUtils; + +import com.datastax.driver.core.UDTValue; +import com.datastax.driver.core.UserType; /** * Unit tests for {@link PartTreeCassandraQuery}. @@ -49,17 +56,25 @@ public class PartTreeCassandraQueryUnitTests { @Rule public ExpectedException exception = ExpectedException.none(); - @Mock private CassandraOperations mockCassandraOperations; + @Mock CassandraOperations mockCassandraOperations; + @Mock UserTypeResolver userTypeResolverMock; + @Mock UserType userTypeMock; + @Mock UDTValue udtValueMock; - private CassandraMappingContext mappingContext; - private CassandraConverter converter; + BasicCassandraMappingContext mappingContext; + CassandraConverter converter; @Before public void setUp() { - mappingContext = new BasicCassandraMappingContext(); - converter = new MappingCassandraConverter(mappingContext); + + this.mappingContext = new BasicCassandraMappingContext(); + this.mappingContext.setUserTypeResolver(userTypeResolverMock); + + this.converter = new MappingCassandraConverter(mappingContext); when(mockCassandraOperations.getConverter()).thenReturn(converter); + when(udtValueMock.getType()).thenReturn(userTypeMock); + when(userTypeMock.iterator()).thenReturn(Collections. emptyIterator()); } /** @@ -102,11 +117,39 @@ public class PartTreeCassandraQueryUnitTests { assertThat(query).isEqualTo("SELECT * FROM person;"); } + /** + * @see DATACASS-172 + */ + @Test + public void shouldDeriveSimpleQueryWithMappedUDT() { + + when(userTypeResolverMock.resolveType(CqlIdentifier.cqlId("address"))).thenReturn(userTypeMock); + when(userTypeMock.newValue()).thenReturn(udtValueMock); + + String query = deriveQueryFromMethod("findByMainAddress", new Address()); + + assertThat(query).isEqualTo("SELECT * FROM person WHERE mainaddress={};"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldDeriveSimpleQueryWithUDTValue() { + + when(userTypeResolverMock.resolveType(CqlIdentifier.cqlId("address"))).thenReturn(userTypeMock); + when(userTypeMock.newValue()).thenReturn(udtValueMock); + + String query = deriveQueryFromMethod("findByMainAddress", udtValueMock); + + assertThat(query).isEqualTo("SELECT * FROM person WHERE mainaddress={};"); + } + private String deriveQueryFromMethod(String method, Object... args) { Class[] types = new Class[args.length]; for (int i = 0; i < args.length; i++) { - types[i] = args[i].getClass(); + types[i] = ClassUtils.getUserClass(args[i].getClass()); } PartTreeCassandraQuery partTreeQuery = createQueryForMethod(method, types); @@ -146,6 +189,10 @@ public class PartTreeCassandraQueryUnitTests { Person findPersonBy(); + Person findByMainAddress(Address address); + + Person findByMainAddress(UDTValue udtValue); + PersonProjection findPersonProjectedBy(); T findDynamicallyProjectedBy(Class type); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryIntegrationUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java similarity index 78% rename from spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryIntegrationUnitTests.java rename to spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java index 2a2d456d9..baf13d3ab 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryIntegrationUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryUnitTests.java @@ -18,6 +18,7 @@ package org.springframework.data.cassandra.repository.query; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.time.LocalDate; @@ -30,10 +31,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.data.cassandra.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.UserTypeResolver; import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Address; import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; @@ -49,7 +53,12 @@ import org.springframework.util.ReflectionUtils; import com.datastax.driver.core.Cluster; import com.datastax.driver.core.CodecRegistry; import com.datastax.driver.core.Configuration; +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.ProtocolVersion; import com.datastax.driver.core.Session; +import com.datastax.driver.core.UDTValue; +import com.datastax.driver.core.UserType; +import com.datastax.driver.core.UserType.Field; import com.datastax.driver.core.querybuilder.QueryBuilder; import com.datastax.driver.core.querybuilder.Select; @@ -61,7 +70,7 @@ import com.datastax.driver.core.querybuilder.Select; * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) -public class StringBasedCassandraQueryIntegrationUnitTests { +public class StringBasedCassandraQueryUnitTests { SpelExpressionParser PARSER = new SpelExpressionParser(); @@ -69,6 +78,8 @@ public class StringBasedCassandraQueryIntegrationUnitTests { @Mock Session session; @Mock Cluster cluster; @Mock Configuration configuration; + @Mock UserTypeResolver userTypeResolver; + @Mock UDTValue udtValue; RepositoryMetadata metadata; MappingCassandraConverter converter; @@ -77,14 +88,18 @@ public class StringBasedCassandraQueryIntegrationUnitTests { @Before public void setUp() { + BasicCassandraMappingContext mappingContext = new BasicCassandraMappingContext(); + mappingContext.setUserTypeResolver(userTypeResolver); + when(operations.getConverter()).thenReturn(converter); when(operations.getSession()).thenReturn(session); + when(operations.getConverter()).thenReturn(converter); when(session.getCluster()).thenReturn(cluster); when(cluster.getConfiguration()).thenReturn(configuration); when(configuration.getCodecRegistry()).thenReturn(CodecRegistry.DEFAULT_INSTANCE); this.metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class); - this.converter = new MappingCassandraConverter(new BasicCassandraMappingContext()); + this.converter = new MappingCassandraConverter(mappingContext); this.factory = new SpelAwareProxyProjectionFactory(); this.converter.afterPropertiesSet(); @@ -322,6 +337,50 @@ public class StringBasedCassandraQueryIntegrationUnitTests { assertThat(actual).isEqualTo("SELECT * FROM person WHERE createdDate='2010-07-04';"); } + /** + * @see DATACASS-172 + */ + @Test + public void bindsMappedUdtPropertyCorrectly() throws Exception { + + Field city = createField("city", DataType.varchar()); + Field country = createField("country", DataType.varchar()); + UserType addressType = createUserType("address", Arrays.asList(city, country)); + + when(userTypeResolver.resolveType(CqlIdentifier.cqlId("address"))).thenReturn(addressType); + when(udtValue.getType()).thenReturn(addressType); + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByMainAddress", Address.class); + CassandraParameterAccessor accessor = new ConvertingParameterAccessor(converter, + new CassandraParametersParameterAccessor(cassandraQuery.getQueryMethod(), new Address())); + + String stringQuery = cassandraQuery.createQuery(accessor); + + // udtValueMock because that's the mock's UDTValue.toString() representation + assertThat(stringQuery).isEqualTo("SELECT * FROM person WHERE address={city:NULL,country:NULL};"); + } + + /** + * @see DATACASS-172 + */ + @Test + public void bindsUdtValuePropertyCorrectly() throws Exception { + + Field city = createField("city", DataType.varchar()); + Field country = createField("country", DataType.varchar()); + UserType addressType = createUserType("address", Arrays.asList(city, country)); + when(udtValue.getType()).thenReturn(addressType); + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByMainAddress", UDTValue.class); + CassandraParameterAccessor accessor = new ConvertingParameterAccessor(converter, + new CassandraParametersParameterAccessor(cassandraQuery.getQueryMethod(), udtValue)); + + String stringQuery = cassandraQuery.createQuery(accessor); + + // udtValueMock because that's the mock's UDTValue.toString() representation + assertThat(stringQuery).isEqualTo("SELECT * FROM person WHERE address={city:NULL,country:NULL};"); + } + private StringBasedCassandraQuery getQueryMethod(String name, Class... args) { Method method = ReflectionUtils.findMethod(SampleRepository.class, name, args); CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, metadata, factory, @@ -330,6 +389,30 @@ public class StringBasedCassandraQueryIntegrationUnitTests { new ExtensionAwareEvaluationContextProvider()); } + private Field createField(String fieldName, DataType dataType) { + + try { + Constructor constructor = Field.class.getDeclaredConstructor(String.class, DataType.class); + constructor.setAccessible(true); + return constructor.newInstance(fieldName, dataType); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + 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); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + private interface SampleRepository extends Repository { @Query("SELECT * FROM person WHERE lastname = ?0;") @@ -367,5 +450,11 @@ public class StringBasedCassandraQueryIntegrationUnitTests { @Query("SELECT * FROM person WHERE createdDate=?0;") Person findByCreatedDate(LocalDate createdDate); + + @Query("SELECT * FROM person WHERE address=?0;") + Person findByMainAddress(Address address); + + @Query("SELECT * FROM person WHERE address=?0;") + Person findByMainAddress(UDTValue udtValue); } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/cdi/CassandraOperationsProducer.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/cdi/CassandraOperationsProducer.java index 5c37681d7..793a82e8d 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/cdi/CassandraOperationsProducer.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/cdi/CassandraOperationsProducer.java @@ -15,7 +15,7 @@ */ package org.springframework.data.cassandra.test.integration.repository.cdi; -import java.util.HashMap; +import java.util.Collections; import java.util.Set; import javax.enterprise.context.ApplicationScoped; @@ -23,7 +23,6 @@ import javax.enterprise.inject.Disposes; import javax.enterprise.inject.Produces; import javax.inject.Singleton; -import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.cassandra.core.keyspace.CreateKeyspaceSpecification; import org.springframework.cassandra.core.keyspace.DropKeyspaceSpecification; import org.springframework.cassandra.support.RandomKeySpaceName; @@ -31,7 +30,10 @@ import org.springframework.cassandra.test.integration.support.CassandraConnectio import org.springframework.data.cassandra.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.CassandraAdminTemplate; import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.core.CassandraPersistentEntitySchemaCreator; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.mapping.SimpleUserTypeResolver; import org.springframework.data.cassandra.test.integration.repository.simple.User; import com.datastax.driver.core.Cluster; @@ -59,7 +61,12 @@ class CassandraOperationsProducer { @ApplicationScoped public CassandraOperations createCassandraOperations(Cluster cluster) throws Exception { - MappingCassandraConverter cassandraConverter = new MappingCassandraConverter(); + BasicCassandraMappingContext mappingContext = new BasicCassandraMappingContext(); + mappingContext.setUserTypeResolver(new SimpleUserTypeResolver(cluster, KEYSPACE_NAME)); + mappingContext.setInitialEntitySet(Collections.singleton(User.class)); + mappingContext.afterPropertiesSet(); + + MappingCassandraConverter cassandraConverter = new MappingCassandraConverter(mappingContext); CassandraAdminTemplate cassandraTemplate = new CassandraAdminTemplate(cluster.connect(), cassandraConverter); @@ -68,10 +75,12 @@ class CassandraOperationsProducer { cassandraTemplate.execute(createKeyspaceSpecification); cassandraTemplate.execute("USE " + KEYSPACE_NAME); - cassandraTemplate.createTable(true, CqlIdentifier.cqlId("users"), User.class, new HashMap()); + CassandraPersistentEntitySchemaCreator schemaCreator = new CassandraPersistentEntitySchemaCreator(mappingContext, cassandraTemplate); + schemaCreator.createUserTypes(false, false, true); + schemaCreator.createTables(false, false, true); for (CassandraPersistentEntity entity : cassandraTemplate.getConverter().getMappingContext() - .getPersistentEntities()) { + .getNonPrimaryKeyEntities()) { cassandraTemplate.truncate(entity.getTableName()); } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Contact.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Address.java similarity index 73% rename from spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Contact.java rename to spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Address.java index a646a8c0f..7c6478a1f 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Contact.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Address.java @@ -13,30 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.cassandra.test.integration.repository.querymethods.conversion; - -import java.util.List; - -import org.springframework.data.annotation.Id; -import org.springframework.data.cassandra.mapping.Table; +package org.springframework.data.cassandra.test.integration.repository.querymethods.declared; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.springframework.data.cassandra.mapping.UserDefinedType; + /** * @author Mark Paluch */ -@Table -@Data +@UserDefinedType +@AllArgsConstructor @NoArgsConstructor -class Contact { +@Data +public class Address { - @Id String id; - - Address address; - List
addresses; - - public Contact(String id) { - this.id = id; - } + String city; + String country; } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java index 73fba9e21..bf758f632 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java @@ -18,6 +18,7 @@ package org.springframework.data.cassandra.test.integration.repository.querymeth import java.time.LocalDate; import java.time.ZoneId; import java.util.Date; +import java.util.List; import org.springframework.cassandra.core.PrimaryKeyType; import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; @@ -28,6 +29,8 @@ import lombok.NoArgsConstructor; /** * Sample domain class. + * + * @author Mark Paluch */ @Table @Data @@ -46,6 +49,9 @@ public class Person { private LocalDate createdDate; private ZoneId zoneId; + private Address mainAddress; + private List
alternativeAddresses; + public Person(String firstname, String lastname) { this.firstname = firstname; diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java index b7603fcc1..d15748ab8 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java @@ -21,6 +21,7 @@ import java.util.List; import org.springframework.data.cassandra.repository.CassandraRepository; import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Address; import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person; import org.springframework.data.domain.Sort; @@ -37,6 +38,11 @@ interface PersonRepository extends CassandraRepository { Person findByFirstnameAndLastname(String firstname, String lastname); + Person findByMainAddress(Address address); + + @Query("select * from person where mainaddress = ?0") + Person findByAddress(Address address); + Person findByCreatedDate(LocalDate createdDate); Person findByNicknameStartsWith(String prefix); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java index 732f9d756..36b0d1ed0 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java @@ -20,6 +20,7 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assume.*; import java.time.LocalDate; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -32,6 +33,7 @@ import org.springframework.core.SpringVersion; import org.springframework.data.cassandra.config.SchemaAction; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; +import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Address; import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person; import org.springframework.data.cassandra.test.integration.repository.querymethods.derived.PersonRepository.NumberOfChildren; import org.springframework.data.cassandra.test.integration.repository.querymethods.derived.PersonRepository.PersonProjection; @@ -84,6 +86,10 @@ public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedC Person person = new Person("Walter", "White"); person.setNumberOfChildren(2); + person.setMainAddress(new Address("Albuquerque", "USA")); + person.setAlternativeAddresses(Arrays.asList(new Address("Albuquerque", "USA"), new Address("New Hampshire", "USA"), + new Address("Grocery Store", "Mexico"))); + walter = personRepository.save(person); skyler = personRepository.save(new Person("Skyler", "White")); flynn = personRepository.save(new Person("Flynn (Walter Jr.)", "White")); @@ -133,6 +139,38 @@ public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedC assertThat(result).isEqualTo(walter); } + /** + * @see DATACASS-172 + */ + @Test + public void shouldFindByMappedUdt() throws InterruptedException { + + template.execute("CREATE INDEX IF NOT EXISTS person_main_address ON person (mainaddress);"); + + // Give Cassandra some time to build the index + Thread.sleep(500); + + Person result = personRepository.findByMainAddress(walter.getMainAddress()); + + assertThat(result).isEqualTo(walter); + } + + /** + * @see DATACASS-172 + */ + @Test + public void shouldFindByMappedUdtStringQuery() throws InterruptedException { + + template.execute("CREATE INDEX IF NOT EXISTS person_main_address ON person (mainaddress);"); + + // Give Cassandra some time to build the index + Thread.sleep(500); + + Person result = personRepository.findByAddress(walter.getMainAddress()); + + assertThat(result).isEqualTo(walter); + } + /** * @see DATACASS-7 */ diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/AddressType.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/AddressType.java new file mode 100644 index 000000000..555c864d0 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/AddressType.java @@ -0,0 +1,31 @@ +/* + * Copyright 2016 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.test.integration.repository.simple; + +import lombok.Data; + +import org.springframework.data.cassandra.mapping.UserDefinedType; + +/** + * @author Mark Paluch + */ +@UserDefinedType("address") +@Data +public class AddressType { + + String street; + String city; +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/User.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/User.java index 2ef957966..5aaa1111d 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/User.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/User.java @@ -27,6 +27,7 @@ import org.springframework.data.cassandra.mapping.Table; * information, emails, following users, friends. * * @author Alex Shvid + * @author Mark Paluch */ @Table("users") public class User { @@ -73,6 +74,8 @@ public class User { */ private Set friends; + private AddressType address; + public String getUsername() { return username; } @@ -151,6 +154,14 @@ public class User { this.birthYear = birthYear; } + public AddressType getAddress() { + return address; + } + + public void setAddress(AddressType address) { + this.address = address; + } + @Override public int hashCode() { final int prime = 31; diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/UserRepository.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/UserRepository.java index 98aafc205..0d736d235 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/UserRepository.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/UserRepository.java @@ -17,6 +17,8 @@ package org.springframework.data.cassandra.test.integration.repository.simple; import org.springframework.data.cassandra.repository.TypedIdCassandraRepository; +import com.datastax.driver.core.UDTValue; + /** * Sample repository managing {@link User} entities. * @@ -26,4 +28,6 @@ import org.springframework.data.cassandra.repository.TypedIdCassandraRepository; public interface UserRepository extends TypedIdCassandraRepository { String findByNamedQuery(String username); + + User findByAddress(AddressType addressType); } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryIntegrationTests.java index 08f912072..a89e757ef 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryIntegrationTests.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.junit.Assert; import org.springframework.data.cassandra.core.CassandraOperations; import com.google.common.collect.Lists; @@ -50,6 +51,8 @@ public class UserRepositoryIntegrationTests { public void setUp() { + template.execute("CREATE INDEX IF NOT EXISTS users_address ON users (address);"); + repository.deleteAll(); tom = new User(); @@ -59,6 +62,12 @@ public class UserRepositoryIntegrationTests { tom.setPassword("123"); tom.setPlace("SF"); + AddressType address = new AddressType(); + address.setCity("San Francisco"); + address.setStreet("Golden Gate Way 1"); + + tom.setAddress(address); + bob = new User(); bob.setUsername("bob"); bob.setFirstName("Bob"); @@ -94,24 +103,33 @@ public class UserRepositoryIntegrationTests { assertThat(name).isEqualTo("Bob"); } + public void findByDerivedQuery() { + + User user = repository.findByAddress(tom.getAddress()); + + assertThat(user).isNotNull().isEqualTo(tom); + } + public void findsUserById() throws Exception { - User user = repository.findOne(bob.getUsername()); - assertThat(user).isNotNull(); - assertEquals(bob, user); + User user = repository.findOne(tom.getUsername()); + assertThat(user).isNotNull().isEqualTo(tom); + } public void findsAll() throws Exception { + List result = Lists.newArrayList(repository.findAll()); + assertThat(result).hasSize(all.size()); assertThat(result.containsAll(all)).isTrue(); - } public void findsAllWithGivenIds() { Iterable result = repository.findAll(Arrays.asList(bob.getUsername(), tom.getUsername())); + assertThat(result).contains(bob, tom); assertThat(result).doesNotContain(alice, scott); } @@ -144,8 +162,7 @@ public class UserRepositoryIntegrationTests { repository.delete(id); - assertThat(!repository.exists(id)).isTrue(); - + assertThat(repository.exists(id)).isFalse(); } /** @@ -163,12 +180,4 @@ public class UserRepositoryIntegrationTests { assertThat(loadedTom.getPassword()).isNull(); assertThat(loadedTom.getFriends()).isNull(); } - - private static void assertEquals(User user1, User user2) { - assertThat(user2.getUsername()).isEqualTo(user1.getUsername()); - assertThat(user2.getFirstName()).isEqualTo(user1.getFirstName()); - assertThat(user2.getLastName()).isEqualTo(user1.getLastName()); - assertThat(user2.getPlace()).isEqualTo(user1.getPlace()); - assertThat(user2.getPassword()).isEqualTo(user1.getPassword()); - } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryIntegrationTestsDelegator.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryIntegrationTestsDelegator.java index a128e43d4..366d3200f 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryIntegrationTestsDelegator.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryIntegrationTestsDelegator.java @@ -48,6 +48,11 @@ public abstract class UserRepositoryIntegrationTestsDelegator tests.findByNamedQuery(); } + @Test + public void findByDerivedQuery() { + tests.findByDerivedQuery(); + } + @Test public void findsUserById() throws Exception { tests.findsUserById(); diff --git a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/config/xml/CassandraNamespaceIntegrationTests-context.xml b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/config/xml/CassandraNamespaceIntegrationTests-context.xml index f2e4d9eb3..51c8bf2cd 100644 --- a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/config/xml/CassandraNamespaceIntegrationTests-context.xml +++ b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/config/xml/CassandraNamespaceIntegrationTests-context.xml @@ -3,7 +3,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cass="http://www.springframework.org/schema/data/cassandra" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" - http://www.springframework.org/schema/data/cassandra http://www.springframework.org/schema/data/cassandra/spring-cassandra-1.0.xsd + http://www.springframework.org/schema/data/cassandra http://www.springframework.org/schema/data/cassandra/spring-cassandra.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> @@ -27,8 +27,10 @@ durable-writes="true" /> - - + + + + diff --git a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/forcequote/compositeprimarykey/ForceQuotedCompositePrimaryKeyRepositoryXmlConfigIntegrationTests-context.xml b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/forcequote/compositeprimarykey/ForceQuotedCompositePrimaryKeyRepositoryXmlConfigIntegrationTests-context.xml index 620424ac5..6f6f38f25 100644 --- a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/forcequote/compositeprimarykey/ForceQuotedCompositePrimaryKeyRepositoryXmlConfigIntegrationTests-context.xml +++ b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/forcequote/compositeprimarykey/ForceQuotedCompositePrimaryKeyRepositoryXmlConfigIntegrationTests-context.xml @@ -2,7 +2,7 @@ diff --git a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/forcequote/config/ForceQuotedRepositoryXmlConfigIntegrationTests-context.xml b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/forcequote/config/ForceQuotedRepositoryXmlConfigIntegrationTests-context.xml index f13850338..5f11f092a 100644 --- a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/forcequote/config/ForceQuotedRepositoryXmlConfigIntegrationTests-context.xml +++ b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/forcequote/config/ForceQuotedRepositoryXmlConfigIntegrationTests-context.xml @@ -2,7 +2,7 @@ diff --git a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/minimal/config/AbsoluteMinimumXmlConfigIntegrationTests-context.xml b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/minimal/config/AbsoluteMinimumXmlConfigIntegrationTests-context.xml index 042007670..eeccc7749 100644 --- a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/minimal/config/AbsoluteMinimumXmlConfigIntegrationTests-context.xml +++ b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/minimal/config/AbsoluteMinimumXmlConfigIntegrationTests-context.xml @@ -4,7 +4,7 @@ xmlns:context="http://www.springframework.org/schema/context" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation=" - http://www.springframework.org/schema/data/cassandra http://www.springframework.org/schema/data/cassandra/spring-cassandra-1.0.xsd + http://www.springframework.org/schema/data/cassandra http://www.springframework.org/schema/data/cassandra/spring-cassandra.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> diff --git a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryXmlConfigIntegrationTests-context.xml b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryXmlConfigIntegrationTests-context.xml index c1cec4720..945160c7a 100644 --- a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryXmlConfigIntegrationTests-context.xml +++ b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryXmlConfigIntegrationTests-context.xml @@ -2,14 +2,20 @@ + + + + + + entity-base-packages="org.springframework.data.cassandra.test.integration.repository.simple" + user-type-resolver-ref="userTypeResolver"> diff --git a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/xmlentityscanning/XmlEntityScanningIntegrationTests-context.xml b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/xmlentityscanning/XmlEntityScanningIntegrationTests-context.xml index c936e7fb0..d06609e90 100644 --- a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/xmlentityscanning/XmlEntityScanningIntegrationTests-context.xml +++ b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/xmlentityscanning/XmlEntityScanningIntegrationTests-context.xml @@ -2,7 +2,7 @@