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