diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractCassandraConfiguration.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractCassandraConfiguration.java index 0e8f637d0..a01c9c9e1 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractCassandraConfiguration.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractCassandraConfiguration.java @@ -33,6 +33,7 @@ import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; import org.springframework.data.cassandra.core.mapping.SimpleTupleTypeFactory; import org.springframework.data.cassandra.core.mapping.SimpleUserTypeResolver; import org.springframework.data.cassandra.core.mapping.Table; +import org.springframework.data.cassandra.core.mapping.UserTypeResolver; import org.springframework.data.convert.CustomConversions; import org.springframework.data.mapping.context.MappingContext; import org.springframework.lang.Nullable; @@ -146,8 +147,10 @@ public abstract class AbstractCassandraConfiguration extends AbstractClusterConf Cluster cluster = getRequiredCluster(); - CassandraMappingContext mappingContext = new CassandraMappingContext( - new SimpleUserTypeResolver(cluster, getKeyspaceName()), new SimpleTupleTypeFactory(cluster)); + UserTypeResolver userTypeResolver = new SimpleUserTypeResolver(cluster, getKeyspaceName()); + + CassandraMappingContext mappingContext = + new CassandraMappingContext(userTypeResolver, new SimpleTupleTypeFactory(cluster)); Optional.ofNullable(this.beanClassLoader).ifPresent(mappingContext::setBeanClassLoader); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractClusterConfiguration.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractClusterConfiguration.java index 8f886aad0..6e9083f62 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractClusterConfiguration.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractClusterConfiguration.java @@ -60,9 +60,12 @@ public abstract class AbstractClusterConfiguration { protected Cluster getRequiredCluster() { CassandraClusterFactoryBean factoryBean = cluster(); - Assert.state(factoryBean.getObject() != null, "Cluster factory not initialized"); - return factoryBean.getObject(); + Cluster cluster = factoryBean.getObject(); + + Assert.state(cluster != null, "Cluster not initialized"); + + return cluster; } /** diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraClusterFactoryBean.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraClusterFactoryBean.java index a48a02c89..126b817df 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraClusterFactoryBean.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraClusterFactoryBean.java @@ -41,15 +41,27 @@ import org.springframework.data.cassandra.core.cql.keyspace.AlterKeyspaceSpecifi import org.springframework.data.cassandra.core.cql.keyspace.CreateKeyspaceSpecification; import org.springframework.data.cassandra.core.cql.keyspace.DropKeyspaceSpecification; import org.springframework.data.cassandra.core.cql.keyspace.KeyspaceActionSpecification; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import com.datastax.driver.core.*; +import com.datastax.driver.core.AuthProvider; +import com.datastax.driver.core.Cluster; import com.datastax.driver.core.Cluster.Builder; +import com.datastax.driver.core.Host; +import com.datastax.driver.core.LatencyTracker; +import com.datastax.driver.core.NettyOptions; +import com.datastax.driver.core.PoolingOptions; import com.datastax.driver.core.ProtocolOptions.Compression; +import com.datastax.driver.core.ProtocolVersion; +import com.datastax.driver.core.QueryOptions; +import com.datastax.driver.core.SSLOptions; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.SocketOptions; +import com.datastax.driver.core.TimestampGenerator; import com.datastax.driver.core.policies.AddressTranslator; import com.datastax.driver.core.policies.LoadBalancingPolicy; import com.datastax.driver.core.policies.ReconnectionPolicy; @@ -71,20 +83,20 @@ import com.datastax.driver.core.policies.SpeculativeExecutionPolicy; * XML configuration * *
-     
-		
-		
-		
-		
-	
+ 
+ 
+ 
+ 
+ 
+ 
  * 
* * @author Alex Shvid @@ -132,16 +144,6 @@ public class CassandraClusterFactoryBean private @Nullable CompressionType compressionType; private @Nullable Host.StateListener hostStateListener; private @Nullable LatencyTracker latencyTracker; - - private List keyspaceCreations = new ArrayList<>(); - private List keyspaceAlterations = new ArrayList<>(); - private List keyspaceDrops = new ArrayList<>(); - private Set keyspaceSpecifications = new HashSet<>(); - private List keyspaceActions = new ArrayList<>(); - - private List startupScripts = new ArrayList<>(); - private List shutdownScripts = new ArrayList<>(); - private @Nullable LoadBalancingPolicy loadBalancingPolicy; private NettyOptions nettyOptions = NettyOptions.DEFAULT_INSTANCE; private @Nullable PoolingOptions poolingOptions; @@ -154,6 +156,15 @@ public class CassandraClusterFactoryBean private @Nullable SSLOptions sslOptions; private @Nullable TimestampGenerator timestampGenerator; + private List keyspaceAlterations = new ArrayList<>(); + private List keyspaceCreations = new ArrayList<>(); + private List keyspaceDrops = new ArrayList<>(); + private List keyspaceActions = new ArrayList<>(); + private List startupScripts = new ArrayList<>(); + private List shutdownScripts = new ArrayList<>(); + + private Set keyspaceSpecifications = new HashSet<>(); + private @Nullable String beanName; private @Nullable String clusterName; private String contactPoints = DEFAULT_CONTACT_POINTS; @@ -166,106 +177,181 @@ public class CassandraClusterFactoryBean */ @Override public void afterPropertiesSet() throws Exception { + this.cluster = initializeCluster(withRegistrations(buildCluster())); + } - Assert.hasText(contactPoints, "At least one server is required"); + private @NonNull Cluster buildCluster() { - Cluster.Builder clusterBuilder = newClusterBuilder(); + Assert.hasText(this.contactPoints, "At least one server is required"); - clusterBuilder.addContactPoints(StringUtils.commaDelimitedListToStringArray(contactPoints)).withPort(port); - clusterBuilder.withMaxSchemaAgreementWaitSeconds(maxSchemaAgreementWaitSeconds); + Builder clusterBuilder = newClusterBuilder() + .addContactPoints(StringUtils.commaDelimitedListToStringArray(this.contactPoints)) + .withMaxSchemaAgreementWaitSeconds(this.maxSchemaAgreementWaitSeconds) + .withPort(this.port); - Optional.ofNullable(compressionType).map(CassandraClusterFactoryBean::convertCompressionType) - .ifPresent(clusterBuilder::withCompression); + Optional.ofNullable(this.addressTranslator).ifPresent(clusterBuilder::withAddressTranslator); + Optional.ofNullable(this.loadBalancingPolicy).ifPresent(clusterBuilder::withLoadBalancingPolicy); + Optional.ofNullable(this.nettyOptions).ifPresent(clusterBuilder::withNettyOptions); + Optional.ofNullable(this.poolingOptions).ifPresent(clusterBuilder::withPoolingOptions); + Optional.ofNullable(this.protocolVersion).ifPresent(clusterBuilder::withProtocolVersion); + Optional.ofNullable(this.queryOptions).ifPresent(clusterBuilder::withQueryOptions); + Optional.ofNullable(this.reconnectionPolicy).ifPresent(clusterBuilder::withReconnectionPolicy); + Optional.ofNullable(this.retryPolicy).ifPresent(clusterBuilder::withRetryPolicy); + Optional.ofNullable(this.socketOptions).ifPresent(clusterBuilder::withSocketOptions); + Optional.ofNullable(this.speculativeExecutionPolicy).ifPresent(clusterBuilder::withSpeculativeExecutionPolicy); + Optional.ofNullable(this.timestampGenerator).ifPresent(clusterBuilder::withTimestampGenerator); - Optional.ofNullable(addressTranslator).ifPresent(clusterBuilder::withAddressTranslator); - Optional.ofNullable(loadBalancingPolicy).ifPresent(clusterBuilder::withLoadBalancingPolicy); - clusterBuilder.withNettyOptions(nettyOptions); - Optional.ofNullable(poolingOptions).ifPresent(clusterBuilder::withPoolingOptions); - Optional.ofNullable(protocolVersion).ifPresent(clusterBuilder::withProtocolVersion); - Optional.ofNullable(queryOptions).ifPresent(clusterBuilder::withQueryOptions); - Optional.ofNullable(reconnectionPolicy).ifPresent(clusterBuilder::withReconnectionPolicy); - Optional.ofNullable(retryPolicy).ifPresent(clusterBuilder::withRetryPolicy); - Optional.ofNullable(socketOptions).ifPresent(clusterBuilder::withSocketOptions); - Optional.ofNullable(speculativeExecutionPolicy).ifPresent(clusterBuilder::withSpeculativeExecutionPolicy); - Optional.ofNullable(timestampGenerator).ifPresent(clusterBuilder::withTimestampGenerator); + Optional.ofNullable(this.authProvider) + .map(clusterBuilder::withAuthProvider) + .orElseGet(() -> StringUtils.hasText(this.username) + ? clusterBuilder.withCredentials(this.username, this.password) + : clusterBuilder); - if (authProvider != null) { - clusterBuilder.withAuthProvider(authProvider); - } else if (username != null) { - clusterBuilder.withCredentials(username, password); - } + Optional.ofNullable(this.compressionType) + .map(CassandraClusterFactoryBean::convertCompressionType) + .ifPresent(clusterBuilder::withCompression); - if (!jmxReportingEnabled) { + if (!this.jmxReportingEnabled) { clusterBuilder.withoutJMXReporting(); } - if (!metricsEnabled) { + if (!this.metricsEnabled) { clusterBuilder.withoutMetrics(); } - if (sslEnabled) { - if (sslOptions != null) { - clusterBuilder.withSSL(sslOptions); - } else { - clusterBuilder.withSSL(); - } + if (this.sslEnabled) { + Optional.ofNullable(this.sslOptions) + .map(clusterBuilder::withSSL) + .orElseGet(clusterBuilder::withSSL); } - Optional.ofNullable(resolveClusterName()).filter(StringUtils::hasText).ifPresent(clusterBuilder::withClusterName); + Optional.ofNullable(resolveClusterName()) + .filter(StringUtils::hasText) + .ifPresent(clusterBuilder::withClusterName); - if (clusterBuilderConfigurer != null) { - clusterBuilderConfigurer.configure(clusterBuilder); + if (this.clusterBuilderConfigurer != null) { + this.clusterBuilderConfigurer.configure(clusterBuilder); } - cluster = clusterBuilder.build(); + return clusterBuilder.build(); + } - Optional.ofNullable(hostStateListener).ifPresent(cluster::register); - Optional.ofNullable(latencyTracker).ifPresent(cluster::register); + private static Compression convertCompressionType(CompressionType type) { - generateSpecificationsFromFactoryBeans(); + switch (type) { + case NONE: + return Compression.NONE; + case SNAPPY: + return Compression.SNAPPY; + case LZ4: + return Compression.LZ4; + } - List startup = new ArrayList<>(keyspaceCreations.size() + keyspaceAlterations.size()); - startup.addAll(keyspaceCreations); - startup.addAll(keyspaceAlterations); - - executeSpecsAndScripts(startup, startupScripts, cluster); + throw new IllegalArgumentException(String.format("Unknown compression type [%s]", type)); } /* * (non-Javadoc) * @see com.datastax.driver.core.Cluster#builder() */ - Cluster.Builder newClusterBuilder() { + @NonNull Cluster.Builder newClusterBuilder() { return Cluster.builder(); } - /* (non-Javadoc) */ - @Nullable - private String resolveClusterName() { - return StringUtils.hasText(clusterName) ? clusterName : beanName; + private @Nullable String resolveClusterName() { + return StringUtils.hasText(this.clusterName) ? this.clusterName : this.beanName; } - /* - * (non-Javadoc) - * @see org.springframework.beans.factory.DisposableBean#destroy() - */ - @Override - public void destroy() { + private @NonNull Cluster withRegistrations(@NonNull Cluster cluster) { - if (cluster != null) { + Optional.ofNullable(this.hostStateListener).ifPresent(cluster::register); + Optional.ofNullable(this.latencyTracker).ifPresent(cluster::register); - executeSpecsAndScripts(keyspaceDrops, shutdownScripts, cluster); - cluster.close(); + return cluster; + } + + private @NonNull Cluster initializeCluster(@NonNull Cluster cluster) { + + generateSpecificationsFromFactoryBeans(); + + List startupSpecifications = + new ArrayList<>(this.keyspaceCreations.size() + this.keyspaceAlterations.size()); + + startupSpecifications.addAll(this.keyspaceCreations); + startupSpecifications.addAll(this.keyspaceAlterations); + + executeSpecsAndScripts(startupSpecifications, this.startupScripts, cluster); + + return cluster; + } + + private void executeSpecsAndScripts(List keyspaceActionSpecifications, + List scripts, Cluster cluster) { + + if (!CollectionUtils.isEmpty(keyspaceActionSpecifications) || !CollectionUtils.isEmpty(scripts)) { + + try (Session session = cluster.connect()) { + + CqlTemplate template = new CqlTemplate(session); + + keyspaceActionSpecifications + .forEach(keyspaceActionSpecification -> template.execute(toCql(keyspaceActionSpecification))); + + scripts.forEach(template::execute); + } } } + /** + * Evaluates the contents of all the KeyspaceSpecificationFactoryBeans + * and generates the proper KeyspaceSpecification from them. + */ + private void generateSpecificationsFromFactoryBeans() { + + generateSpecifications(this.keyspaceSpecifications); + this.keyspaceActions.forEach(actions -> generateSpecifications(actions.getActions())); + } + + private void generateSpecifications(Collection specifications) { + + specifications.forEach(keyspaceActionSpecification -> { + + if (keyspaceActionSpecification instanceof AlterKeyspaceSpecification) { + this.keyspaceAlterations.add((AlterKeyspaceSpecification) keyspaceActionSpecification); + } + else if (keyspaceActionSpecification instanceof CreateKeyspaceSpecification) { + this.keyspaceCreations.add((CreateKeyspaceSpecification) keyspaceActionSpecification); + } + else if (keyspaceActionSpecification instanceof DropKeyspaceSpecification) { + this.keyspaceDrops.add((DropKeyspaceSpecification) keyspaceActionSpecification); + } + }); + } + + + private String toCql(KeyspaceActionSpecification specification) { + + if (specification instanceof AlterKeyspaceSpecification) { + return new AlterKeyspaceCqlGenerator((AlterKeyspaceSpecification) specification).toCql(); + } + else if (specification instanceof CreateKeyspaceSpecification) { + return new CreateKeyspaceCqlGenerator((CreateKeyspaceSpecification) specification).toCql(); + } + else if (specification instanceof DropKeyspaceSpecification) { + return new DropKeyspaceCqlGenerator((DropKeyspaceSpecification) specification).toCql(); + } + + throw new IllegalArgumentException("Unsupported specification type: " + + ClassUtils.getQualifiedName(specification.getClass())); + } + /* * (non-Javadoc) * @see org.springframework.beans.factory.FactoryBean#getObject() */ @Override public Cluster getObject() { - return cluster; + return this.cluster; } /* @@ -274,7 +360,7 @@ public class CassandraClusterFactoryBean */ @Override public Class getObjectType() { - return (cluster != null ? cluster.getClass() : Cluster.class); + return this.cluster != null ? this.cluster.getClass() : Cluster.class; } /* @@ -286,81 +372,26 @@ public class CassandraClusterFactoryBean return true; } + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.DisposableBean#destroy() + */ + @Override + public void destroy() { + + if (this.cluster != null) { + executeSpecsAndScripts(this.keyspaceDrops, this.shutdownScripts, this.cluster); + this.cluster.close(); + } + } + /* * (non-Javadoc) * @see org.springframework.dao.support.PersistenceExceptionTranslator#translateExceptionIfPossible(java.lang.RuntimeException) */ @Override - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { - return exceptionTranslator.translateExceptionIfPossible(ex); - } - - /** - * Examines the contents of all the KeyspaceSpecificationFactoryBeans and generates the proper KeyspaceSpecification - * from them. - */ - private void generateSpecificationsFromFactoryBeans() { - - generateSpecifications(keyspaceSpecifications); - keyspaceActions.forEach(actions -> generateSpecifications(actions.getActions())); - } - - private void generateSpecifications(Collection specifications) { - - specifications.forEach(keyspaceActionSpecification -> { - - if (keyspaceActionSpecification instanceof CreateKeyspaceSpecification) { - keyspaceCreations.add((CreateKeyspaceSpecification) keyspaceActionSpecification); - } - - if (keyspaceActionSpecification instanceof DropKeyspaceSpecification) { - keyspaceDrops.add((DropKeyspaceSpecification) keyspaceActionSpecification); - } - - if (keyspaceActionSpecification instanceof AlterKeyspaceSpecification) { - keyspaceAlterations.add((AlterKeyspaceSpecification) keyspaceActionSpecification); - } - }); - } - - private void executeSpecsAndScripts(List keyspaceActionSpecifications, - List scripts, Cluster cluster) { - - if (!CollectionUtils.isEmpty(keyspaceActionSpecifications) || !CollectionUtils.isEmpty(scripts)) { - - Session session = cluster.connect(); - - try { - CqlTemplate template = new CqlTemplate(session); - - keyspaceActionSpecifications - .forEach(keyspaceActionSpecification -> template.execute(toCql(keyspaceActionSpecification))); - - scripts.forEach(template::execute); - } finally { - if (session != null) { - session.close(); - } - } - } - } - - private String toCql(KeyspaceActionSpecification specification) { - - if (specification instanceof CreateKeyspaceSpecification) { - return new CreateKeyspaceCqlGenerator((CreateKeyspaceSpecification) specification).toCql(); - } - - if (specification instanceof DropKeyspaceSpecification) { - return new DropKeyspaceCqlGenerator((DropKeyspaceSpecification) specification).toCql(); - } - - if (specification instanceof AlterKeyspaceSpecification) { - return new AlterKeyspaceCqlGenerator((AlterKeyspaceSpecification) specification).toCql(); - } - - throw new IllegalArgumentException( - "Unsupported specification type: " + ClassUtils.getQualifiedName(specification.getClass())); + public DataAccessException translateExceptionIfPossible(RuntimeException cause) { + return exceptionTranslator.translateExceptionIfPossible(cause); } /* @@ -369,7 +400,7 @@ public class CassandraClusterFactoryBean * @since 1.5 */ @Override - public void setBeanName(String beanName) { + public void setBeanName(@Nullable String beanName) { this.beanName = beanName; } @@ -495,7 +526,7 @@ public class CassandraClusterFactoryBean * @return the {@link List} of {@link KeyspaceActions}. */ public List getKeyspaceActions() { - return Collections.unmodifiableList(keyspaceActions); + return Collections.unmodifiableList(this.keyspaceActions); } /** @@ -560,7 +591,7 @@ public class CassandraClusterFactoryBean * @return the startup scripts */ public List getStartupScripts() { - return Collections.unmodifiableList(startupScripts); + return Collections.unmodifiableList(this.startupScripts); } /** @@ -578,7 +609,7 @@ public class CassandraClusterFactoryBean * @return the shutdown scripts */ public List getShutdownScripts() { - return Collections.unmodifiableList(shutdownScripts); + return Collections.unmodifiableList(this.shutdownScripts); } /** @@ -592,7 +623,7 @@ public class CassandraClusterFactoryBean * @return the {@link KeyspaceActionSpecification} associated with this factory. */ public Set getKeyspaceSpecifications() { - return Collections.unmodifiableSet(keyspaceSpecifications); + return Collections.unmodifiableSet(this.keyspaceSpecifications); } /** @@ -646,7 +677,7 @@ public class CassandraClusterFactoryBean } /** - * @param latencyTracker The latencyTracker to set. + * @param latencyTracker {@link LatencyTracker} to set. */ public void setLatencyTracker(LatencyTracker latencyTracker) { this.latencyTracker = latencyTracker; @@ -667,13 +698,14 @@ public class CassandraClusterFactoryBean /** * Sets the {@link ClusterBuilderConfigurer} used to apply additional configuration logic to the - * {@link com.datastax.driver.core.Cluster.Builder}. {@link ClusterBuilderConfigurer} is invoked after all provided - * options are configured. The factory will {@link Builder#build()} the {@link Cluster} after applying - * {@link ClusterBuilderConfigurer}. + * {@link com.datastax.driver.core.Cluster.Builder} object. + * + * {@link ClusterBuilderConfigurer} is invoked after all provided options are configured. The factory will + * {@link Builder#build()} the {@link Cluster} after applying {@link ClusterBuilderConfigurer}. * * @param clusterBuilderConfigurer {@link ClusterBuilderConfigurer} used to configure the - * {@link com.datastax.driver.core.Cluster.Builder}. - * @see org.springframework.data.cql.config.ClusterBuilderConfigurer + * {@link com.datastax.driver.core.Cluster.Builder}. + * @see org.springframework.data.cassandra.config.ClusterBuilderConfigurer */ public void setClusterBuilderConfigurer(@Nullable ClusterBuilderConfigurer clusterBuilderConfigurer) { this.clusterBuilderConfigurer = clusterBuilderConfigurer; @@ -725,18 +757,4 @@ public class CassandraClusterFactoryBean public void setTimestampGenerator(@Nullable TimestampGenerator timestampGenerator) { this.timestampGenerator = timestampGenerator; } - - private static Compression convertCompressionType(CompressionType type) { - - switch (type) { - case NONE: - return Compression.NONE; - case SNAPPY: - return Compression.SNAPPY; - case LZ4: - return Compression.LZ4; - } - - throw new IllegalArgumentException(String.format("Unknown compression type [%s]", type)); - } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraCqlSessionFactoryBean.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraCqlSessionFactoryBean.java index f28619a3b..c1e4ca773 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraCqlSessionFactoryBean.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraCqlSessionFactoryBean.java @@ -86,7 +86,10 @@ public class CassandraCqlSessionFactoryBean /* (non-Javadoc) */ Session connect(@Nullable String keyspaceName) { - return (StringUtils.hasText(keyspaceName) ? getCluster().connect(keyspaceName) : getCluster().connect()); + + return StringUtils.hasText(keyspaceName) + ? getCluster().connect(keyspaceName) + : getCluster().connect(); } /* @@ -104,7 +107,7 @@ public class CassandraCqlSessionFactoryBean */ @Override public Class getObjectType() { - return (this.session != null ? this.session.getClass() : Session.class); + return this.session != null ? this.session.getClass() : Session.class; } /* @@ -121,6 +124,7 @@ public class CassandraCqlSessionFactoryBean */ @Override public void destroy() throws Exception { + executeScripts(getShutdownScripts()); getSession().close(); } @@ -253,7 +257,7 @@ public class CassandraCqlSessionFactoryBean * Sets CQL scripts to be executed immediately before the session is shutdown. */ public void setShutdownScripts(@Nullable List scripts) { - this.shutdownScripts = (scripts != null ? new ArrayList<>(scripts) : Collections.emptyList()); + this.shutdownScripts = scripts != null ? new ArrayList<>(scripts) : Collections.emptyList(); } /** 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 db822c1a9..f0c71810c 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 @@ -133,6 +133,7 @@ public class CassandraSessionFactoryBean extends CassandraCqlSessionFactoryBean public void setSchemaAction(SchemaAction schemaAction) { Assert.notNull(schemaAction, "SchemaAction must not be null"); + this.schemaAction = schemaAction; } @@ -158,13 +159,13 @@ public class CassandraSessionFactoryBean extends CassandraCqlSessionFactoryBean private void performSchemaActions(boolean drop, boolean dropUnused, boolean ifNotExists) { - CassandraPersistentEntitySchemaCreator schemaCreator = new CassandraPersistentEntitySchemaCreator( - getMappingContext(), getCassandraAdminOperations()); + CassandraPersistentEntitySchemaCreator schemaCreator = + new CassandraPersistentEntitySchemaCreator(getMappingContext(), getCassandraAdminOperations()); if (drop) { - CassandraPersistentEntitySchemaDropper schemaDropper = new CassandraPersistentEntitySchemaDropper( - getMappingContext(), getCassandraAdminOperations()); + CassandraPersistentEntitySchemaDropper schemaDropper = + new CassandraPersistentEntitySchemaDropper(getMappingContext(), getCassandraAdminOperations()); schemaDropper.dropTables(dropUnused); schemaDropper.dropUserTypes(dropUnused); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraClusterFactoryBeanUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraClusterFactoryBeanUnitTests.java index e528da7da..a71946f49 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraClusterFactoryBeanUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraClusterFactoryBeanUnitTests.java @@ -15,18 +15,35 @@ */ package org.springframework.data.cassandra.config; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; -import static org.mockito.Mockito.*; import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import org.junit.Test; + import org.springframework.data.cassandra.support.IntegrationTestNettyOptions; import org.springframework.test.util.ReflectionTestUtils; -import com.datastax.driver.core.*; +import com.datastax.driver.core.AuthProvider; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.Configuration; +import com.datastax.driver.core.PlainTextAuthProvider; +import com.datastax.driver.core.PoolingOptions; +import com.datastax.driver.core.ProtocolOptions; import com.datastax.driver.core.ProtocolOptions.Compression; +import com.datastax.driver.core.ProtocolVersion; +import com.datastax.driver.core.QueryOptions; +import com.datastax.driver.core.RemoteEndpointAwareJdkSSLOptions; +import com.datastax.driver.core.SSLOptions; +import com.datastax.driver.core.SocketOptions; +import com.datastax.driver.core.TimestampGenerator; import com.datastax.driver.core.policies.AddressTranslator; import com.datastax.driver.core.policies.ExponentialReconnectionPolicy; import com.datastax.driver.core.policies.LoadBalancingPolicy; @@ -47,6 +64,7 @@ public class CassandraClusterFactoryBeanUnitTests { public void shouldInitializeWithoutAnyOptions() throws Exception { CassandraClusterFactoryBean bean = new CassandraClusterFactoryBean(); + bean.afterPropertiesSet(); assertThat(bean.getObject()).isNotNull(); @@ -59,6 +77,7 @@ public class CassandraClusterFactoryBeanUnitTests { public void shouldShutdownClusterInstance() throws Exception { CassandraClusterFactoryBean bean = new CassandraClusterFactoryBean(); + bean.afterPropertiesSet(); bean.destroy(); @@ -273,6 +292,8 @@ public class CassandraClusterFactoryBeanUnitTests { Cluster mockCluster = mock(Cluster.class); when(mockClusterBuilder.addContactPoints(anyString())).thenReturn(mockClusterBuilder); + when(mockClusterBuilder.withMaxSchemaAgreementWaitSeconds(anyInt())).thenReturn(mockClusterBuilder); + when(mockClusterBuilder.withPort(anyInt())).thenReturn(mockClusterBuilder); when(mockClusterBuilder.build()).thenReturn(mockCluster); CassandraClusterFactoryBean bean = spy(new CassandraClusterFactoryBean()); @@ -294,6 +315,8 @@ public class CassandraClusterFactoryBeanUnitTests { Cluster mockCluster = mock(Cluster.class); when(mockClusterBuilder.addContactPoints(anyString())).thenReturn(mockClusterBuilder); + when(mockClusterBuilder.withMaxSchemaAgreementWaitSeconds(anyInt())).thenReturn(mockClusterBuilder); + when(mockClusterBuilder.withPort(anyInt())).thenReturn(mockClusterBuilder); when(mockClusterBuilder.build()).thenReturn(mockCluster); CassandraClusterFactoryBean bean = spy(new CassandraClusterFactoryBean()); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraNamespaceIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraNamespaceIntegrationTests.java index 5de55c0a2..40f82361d 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraNamespaceIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraNamespaceIntegrationTests.java @@ -15,10 +15,11 @@ */ package org.springframework.data.cassandra.config; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.Test; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.data.cassandra.core.CassandraTemplate; @@ -27,7 +28,7 @@ import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; import org.springframework.data.cassandra.core.mapping.SimpleUserTypeResolver; import org.springframework.data.cassandra.repository.support.AbstractSpringDataEmbeddedCassandraIntegrationTest; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.util.ReflectionTestUtils; import com.datastax.driver.core.Cluster; @@ -43,24 +44,29 @@ import com.datastax.driver.core.SocketOptions; * * @author Mark Paluch */ -@RunWith(SpringJUnit4ClassRunner.class) +@RunWith(SpringRunner.class) @ContextConfiguration +@SuppressWarnings("unused") public class CassandraNamespaceIntegrationTests extends AbstractSpringDataEmbeddedCassandraIntegrationTest { - @Autowired ApplicationContext applicationContext; + @Autowired + private ApplicationContext applicationContext; @Test // DATACASS-271 public void clusterShouldHaveCompressionSet() { - Cluster cluster = applicationContext.getBean(Cluster.class); + Cluster cluster = this.applicationContext.getBean(Cluster.class); + Configuration configuration = cluster.getConfiguration(); + assertThat(configuration.getProtocolOptions().getCompression()).isEqualTo(Compression.SNAPPY); } @Test // DATACASS-271 public void clusterShouldHavePoolingOptionsConfigured() { - Cluster cluster = applicationContext.getBean(Cluster.class); + Cluster cluster = this.applicationContext.getBean(Cluster.class); + PoolingOptions poolingOptions = cluster.getConfiguration().getPoolingOptions(); assertThat(poolingOptions.getMaxRequestsPerConnection(HostDistance.LOCAL)).isEqualTo(101); @@ -76,7 +82,8 @@ public class CassandraNamespaceIntegrationTests extends AbstractSpringDataEmbedd @Test // DATACASS-271 public void clusterShouldHaveSocketOptionsConfigured() { - Cluster cluster = applicationContext.getBean(Cluster.class); + Cluster cluster = this.applicationContext.getBean(Cluster.class); + SocketOptions socketOptions = cluster.getConfiguration().getSocketOptions(); assertThat(socketOptions.getConnectTimeoutMillis()).isEqualTo(5000); @@ -91,10 +98,10 @@ public class CassandraNamespaceIntegrationTests extends AbstractSpringDataEmbedd @Test // DATACASS-172 public void mappingContextShouldHaveUserTypeResolverConfigured() { - CassandraMappingContext mappingContext = applicationContext.getBean(CassandraMappingContext.class); + CassandraMappingContext mappingContext = this.applicationContext.getBean(CassandraMappingContext.class); - SimpleUserTypeResolver userTypeResolver = (SimpleUserTypeResolver) ReflectionTestUtils.getField(mappingContext, - "userTypeResolver"); + SimpleUserTypeResolver userTypeResolver = + (SimpleUserTypeResolver) ReflectionTestUtils.getField(mappingContext, "userTypeResolver"); assertThat(userTypeResolver).isNotNull(); } @@ -102,8 +109,9 @@ public class CassandraNamespaceIntegrationTests extends AbstractSpringDataEmbedd @Test // DATACASS-417 public void mappingContextShouldCassandraTemplateConfigured() { - CassandraTemplate cassandraTemplate = applicationContext.getBean(CassandraTemplate.class); - CqlTemplate cqlTemplate = applicationContext.getBean(CqlTemplate.class); + CassandraTemplate cassandraTemplate = this.applicationContext.getBean(CassandraTemplate.class); + + CqlTemplate cqlTemplate = this.applicationContext.getBean(CqlTemplate.class); assertThat(cassandraTemplate.getCqlOperations()).isSameAs(cqlTemplate); } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/SchemaActionIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/SchemaActionIntegrationTests.java index b8cd5657b..9a08e4e6a 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/SchemaActionIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/SchemaActionIntegrationTests.java @@ -16,15 +16,16 @@ package org.springframework.data.cassandra.config; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; import java.util.Collections; import java.util.List; import java.util.Set; -import org.junit.Rule; +import org.junit.Before; import org.junit.Test; -import org.junit.rules.ExpectedException; + import org.springframework.beans.factory.BeanCreationException; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -32,8 +33,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.cassandra.core.cql.SessionCallback; import org.springframework.data.cassandra.domain.Person; -import org.springframework.data.cassandra.test.util.AbstractEmbeddedCassandraIntegrationTest; -import org.springframework.data.cassandra.test.util.KeyspaceRule; +import org.springframework.data.cassandra.test.util.AbstractKeyspaceCreatingIntegrationTest; import com.datastax.driver.core.Cluster; import com.datastax.driver.core.KeyspaceMetadata; @@ -42,48 +42,44 @@ import com.datastax.driver.core.Session; import com.datastax.driver.core.TableMetadata; /** - * The SchemaActionIntegrationTests class is a test suite of test cases testing the contract and behavior of various - * {@link SchemaAction}s on startup of a Spring configured, Cassandra application client. + * Integration test testing various {@link SchemaAction SchemaActions} on startup of a Spring configured, + * Apache Cassandra application client. * * @author John Blum * @author Mark Paluch + * @see org.springframework.data.cassandra.test.util.AbstractKeyspaceCreatingIntegrationTest */ -public class SchemaActionIntegrationTests extends AbstractEmbeddedCassandraIntegrationTest { +public class SchemaActionIntegrationTests extends AbstractKeyspaceCreatingIntegrationTest { - protected static final String KEYSPACE_NAME = SchemaActionIntegrationTests.class.getSimpleName().toLowerCase(); + protected static final String CREATE_PERSON_TABLE_CQL = + "CREATE TABLE IF NOT EXISTS person (id int, firstName text, lastName text, PRIMARY KEY(id));"; - protected static final String PERSON_TABLE_DEFINITION_CQL = String - .format("CREATE TABLE %s.person (id int, firstName text, lastName text, PRIMARY KEY(id));", KEYSPACE_NAME); - - @Rule public ExpectedException exception = ExpectedException.none(); - - @Rule public KeyspaceRule KEYSPACE_RULE = new KeyspaceRule(cassandraEnvironment, KEYSPACE_NAME); + protected static final String DROP_ADDRESS_TYPE_CQL = "DROP TYPE IF EXISTS address"; + protected static final String DROP_PERSON_TABLE_CQL = "DROP TABLE IF EXISTS person"; protected ConfigurableApplicationContext newApplicationContext(Class... annotatedClasses) { - AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(annotatedClasses); + + AnnotationConfigApplicationContext applicationContext = + new AnnotationConfigApplicationContext(annotatedClasses); applicationContext.registerShutdownHook(); return applicationContext; } + @SuppressWarnings("all") protected T doInSessionWithConfiguration(Class annotatedClass, SessionCallback sessionCallback) { - ConfigurableApplicationContext applicationContext = null; - try { - applicationContext = newApplicationContext(annotatedClass); + try (ConfigurableApplicationContext applicationContext = newApplicationContext(annotatedClass)) { return sessionCallback.doInSession(applicationContext.getBean(Session.class)); - } finally { - if (applicationContext != null) { - applicationContext.close(); - } } } - protected void assertHasTableWithColumns(Session session, String tableName, String... columns) { + @SuppressWarnings("all") + protected void assertTableWithColumnsExists(Session session, String tableName, String... columns) { Metadata clusterMetadata = session.getCluster().getMetadata(); - KeyspaceMetadata keyspaceMetadata = clusterMetadata.getKeyspace(KEYSPACE_NAME); + KeyspaceMetadata keyspaceMetadata = clusterMetadata.getKeyspace(getKeyspace()); assertThat(keyspaceMetadata).isNotNull(); @@ -99,11 +95,24 @@ public class SchemaActionIntegrationTests extends AbstractEmbeddedCassandraInteg assertThat(tableMetadata.getColumns()).hasSize(columns.length); } + @Before + public void setup() { + + Session session = getSession(); + + session.execute(DROP_PERSON_TABLE_CQL); + session.execute(DROP_ADDRESS_TYPE_CQL); + } + @Test public void createWithNoExistingTableCreatesTableFromEntity() { - doInSessionWithConfiguration(CreateWithNoExistingTableConfiguration.class, (SessionCallback) session -> { - assertHasTableWithColumns(session, "person", "firstName", "lastName", "nickname", "birthDate", "numberOfChildren", - "cool", "createdDate", "zoneId", "mainAddress", "alternativeAddresses"); + + doInSessionWithConfiguration(CreateWithNoExistingTableConfiguration.class, session -> { + + assertTableWithColumnsExists(session, "person", "firstName", "lastName", "nickname", + "birthDate", "numberOfChildren", "cool", "createdDate", "zoneId", "mainAddress", + "alternativeAddresses"); + return null; }); } @@ -112,43 +121,54 @@ public class SchemaActionIntegrationTests extends AbstractEmbeddedCassandraInteg public void createWithExistingTableThrowsErrorWhenCreatingTableFromEntity() { try { - doInSessionWithConfiguration(CreateWithExistingTableConfiguration.class, s -> { - fail(String.format("%s should have failed!", CreateWithExistingTableConfiguration.class.getSimpleName())); + + doInSessionWithConfiguration(CreateWithExistingTableConfiguration.class, session -> { + fail(String.format("%s should have failed", CreateWithExistingTableConfiguration.class.getSimpleName())); return null; }); - fail("Missing BeanCreationException"); - } catch (BeanCreationException e) { - assertThat(e).hasMessageContaining(String.format("Table %s.person already exists", KEYSPACE_NAME)); + + fail("Expected BeanCreationException"); + + } catch (BeanCreationException cause) { + assertThat(cause).hasMessageContaining(String.format("Table %s.person already exists", getKeyspace())); } } @Test public void createIfNotExistsWithNoExistingTableCreatesTableFromEntity() { - doInSessionWithConfiguration(CreateIfNotExistsWithNoExistingTableConfiguration.class, - (SessionCallback) session -> { - assertHasTableWithColumns(session, "person", "firstName", "lastName", "nickname", "birthDate", - "numberOfChildren", "cool", "createdDate", "zoneId", "mainAddress", "alternativeAddresses"); - return null; - }); + + doInSessionWithConfiguration(CreateIfNotExistsWithNoExistingTableConfiguration.class, session -> { + + assertTableWithColumnsExists(session, "person", "firstName", "lastName", "nickname", + "birthDate", "numberOfChildren", "cool", "createdDate", "zoneId", "mainAddress", + "alternativeAddresses"); + + return null; + }); } @Test public void createIfNotExistsWithExistingTableUsesExistingTable() { - doInSessionWithConfiguration(CreateIfNotExistsWithExistingTableConfiguration.class, - (SessionCallback) session -> { - assertHasTableWithColumns(session, "person", "id", "firstName", "lastName"); - return null; - }); + + doInSessionWithConfiguration(CreateIfNotExistsWithExistingTableConfiguration.class, session -> { + + assertTableWithColumnsExists(session, "person", "id", "firstName", "lastName"); + + return null; + }); } @Test public void recreateTableFromEntityDropsExistingTable() { - doInSessionWithConfiguration(RecreateSchemaActionWithExistingTableConfiguration.class, - (SessionCallback) session -> { - assertHasTableWithColumns(session, "person", "firstName", "lastName", "nickname", "birthDate", - "numberOfChildren", "cool", "createdDate", "zoneId", "mainAddress", "alternativeAddresses"); - return null; - }); + + doInSessionWithConfiguration(RecreateSchemaActionWithExistingTableConfiguration.class, session -> { + + assertTableWithColumnsExists(session, "person", "firstName", "lastName", "nickname", + "birthDate", "numberOfChildren", "cool", "createdDate", "zoneId", "mainAddress", + "alternativeAddresses"); + + return null; + }); } @Configuration @@ -170,7 +190,7 @@ public class SchemaActionIntegrationTests extends AbstractEmbeddedCassandraInteg @Override protected List getStartupScripts() { - return Collections.singletonList(PERSON_TABLE_DEFINITION_CQL); + return Collections.singletonList(CREATE_PERSON_TABLE_CQL); } } @@ -193,7 +213,7 @@ public class SchemaActionIntegrationTests extends AbstractEmbeddedCassandraInteg @Override protected List getStartupScripts() { - return Collections.singletonList(PERSON_TABLE_DEFINITION_CQL); + return Collections.singletonList(CREATE_PERSON_TABLE_CQL); } } @@ -207,7 +227,7 @@ public class SchemaActionIntegrationTests extends AbstractEmbeddedCassandraInteg @Override protected List getStartupScripts() { - return Collections.singletonList(PERSON_TABLE_DEFINITION_CQL); + return Collections.singletonList(CREATE_PERSON_TABLE_CQL); } } @@ -217,7 +237,9 @@ public class SchemaActionIntegrationTests extends AbstractEmbeddedCassandraInteg @Bean @Override public CassandraClusterFactoryBean cluster() { + return new CassandraClusterFactoryBean() { + @Override public void afterPropertiesSet() throws Exception { // avoid Cassandra Cluster creation; use embedded @@ -231,13 +253,13 @@ public class SchemaActionIntegrationTests extends AbstractEmbeddedCassandraInteg } @Override - protected Set> getInitialEntitySet() throws ClassNotFoundException { + protected Set> getInitialEntitySet() { return Collections.singleton(Person.class); } @Override protected String getKeyspaceName() { - return KEYSPACE_NAME; + return keyspaceRule.getKeyspaceName(); } } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/AlterKeyspaceCqlGeneratorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/AlterKeyspaceCqlGeneratorUnitTests.java index 6484daea9..3aa5b9c16 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/AlterKeyspaceCqlGeneratorUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/AlterKeyspaceCqlGeneratorUnitTests.java @@ -25,7 +25,7 @@ import org.springframework.data.cassandra.core.cql.keyspace.AlterKeyspaceSpecifi import org.springframework.data.cassandra.core.cql.keyspace.DefaultOption; import org.springframework.data.cassandra.core.cql.keyspace.KeyspaceOption; import org.springframework.data.cassandra.core.cql.keyspace.Option; -import org.springframework.data.cassandra.support.RandomKeySpaceName; +import org.springframework.data.cassandra.support.RandomKeyspaceName; /** * Unit tests for {@link AlterKeyspaceCqlGenerator}. @@ -62,7 +62,7 @@ public class AlterKeyspaceCqlGeneratorUnitTests { public static class CompleteTest extends AlterKeyspaceTest { - public String name = RandomKeySpaceName.create(); + public String name = RandomKeyspaceName.create(); public Boolean durableWrites = true; public Map replicationMap = new HashMap<>(); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/CreateKeyspaceCqlGeneratorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/CreateKeyspaceCqlGeneratorUnitTests.java index a44781956..9ef2c0de6 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/CreateKeyspaceCqlGeneratorUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/CreateKeyspaceCqlGeneratorUnitTests.java @@ -26,7 +26,7 @@ import org.springframework.data.cassandra.core.cql.keyspace.DefaultOption; import org.springframework.data.cassandra.core.cql.keyspace.KeyspaceAttributes; import org.springframework.data.cassandra.core.cql.keyspace.KeyspaceOption; import org.springframework.data.cassandra.core.cql.keyspace.Option; -import org.springframework.data.cassandra.support.RandomKeySpaceName; +import org.springframework.data.cassandra.support.RandomKeyspaceName; /** * Unit tests for {@link CreateKeyspaceCqlGenerator}. @@ -72,7 +72,7 @@ public class CreateKeyspaceCqlGeneratorUnitTests { public static class BasicTest extends CreateKeyspaceTest { - public String name = RandomKeySpaceName.create(); + public String name = RandomKeyspaceName.create(); public Boolean durableWrites = true; public Map replicationMap = KeyspaceAttributes.newSimpleReplication(); @@ -97,7 +97,7 @@ public class CreateKeyspaceCqlGeneratorUnitTests { public static class NoOptionsBasicTest extends CreateKeyspaceTest { - public String name = RandomKeySpaceName.create(); + public String name = RandomKeyspaceName.create(); public Boolean durableWrites = true; public Map replicationMap = KeyspaceAttributes.newSimpleReplication(); @@ -121,7 +121,7 @@ public class CreateKeyspaceCqlGeneratorUnitTests { public static class NetworkTopologyTest extends CreateKeyspaceTest { - public String name = RandomKeySpaceName.create(); + public String name = RandomKeyspaceName.create(); public Boolean durableWrites = false; public Map replicationMap = new HashMap<>(); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/DropKeyspaceCqlGeneratorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/DropKeyspaceCqlGeneratorUnitTests.java index 5736cf10f..bfe56f628 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/DropKeyspaceCqlGeneratorUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/generator/DropKeyspaceCqlGeneratorUnitTests.java @@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.*; import org.junit.Test; import org.springframework.data.cassandra.core.cql.keyspace.DropKeyspaceSpecification; -import org.springframework.data.cassandra.support.RandomKeySpaceName; +import org.springframework.data.cassandra.support.RandomKeyspaceName; /** * Unit tests for {@link DropKeyspaceCqlGenerator}. @@ -45,7 +45,7 @@ public class DropKeyspaceCqlGeneratorUnitTests { public static class BasicTest extends DropTableTest { - public String name = RandomKeySpaceName.create(); + public String name = RandomKeyspaceName.create(); @Override public DropKeyspaceSpecification specification() { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/cdi/CassandraOperationsProducer.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/cdi/CassandraOperationsProducer.java index 01ff38ebf..4c467bd8b 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/cdi/CassandraOperationsProducer.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/cdi/CassandraOperationsProducer.java @@ -37,7 +37,7 @@ import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity import org.springframework.data.cassandra.core.mapping.SimpleUserTypeResolver; import org.springframework.data.cassandra.domain.User; import org.springframework.data.cassandra.support.CassandraConnectionProperties; -import org.springframework.data.cassandra.support.RandomKeySpaceName; +import org.springframework.data.cassandra.support.RandomKeyspaceName; import com.datastax.driver.core.Cluster; import com.google.common.collect.Sets; @@ -48,7 +48,7 @@ import com.google.common.util.concurrent.Service; */ class CassandraOperationsProducer { - public static final String KEYSPACE_NAME = RandomKeySpaceName.create(); + public static final String KEYSPACE_NAME = RandomKeyspaceName.create(); @Produces @Singleton diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/AbstractSpringDataEmbeddedCassandraIntegrationTest.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/AbstractSpringDataEmbeddedCassandraIntegrationTest.java index 0c4c6761c..0d7053bf0 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/AbstractSpringDataEmbeddedCassandraIntegrationTest.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/AbstractSpringDataEmbeddedCassandraIntegrationTest.java @@ -30,17 +30,19 @@ import org.springframework.data.cassandra.test.util.AbstractEmbeddedCassandraInt public abstract class AbstractSpringDataEmbeddedCassandraIntegrationTest extends AbstractEmbeddedCassandraIntegrationTest { - @Autowired private CassandraOperations template; + @Autowired @SuppressWarnings("unused") + private CassandraOperations template; /** - * Truncate table for all known {@link org.springframework.data.mapping.PersistentEntity entities}. + * Truncate tables for all known {@link org.springframework.data.mapping.PersistentEntity entities}. */ public void deleteAllEntities() { - Stream> stream = template.getConverter().getMappingContext() - .getTableEntities() - .stream(); + Stream> stream = + this.template.getConverter().getMappingContext().getTableEntities().stream(); - stream.map(CassandraPersistentEntity::getType).filter(type -> !type.isInterface()).forEach(template::truncate); + stream.map(CassandraPersistentEntity::getType) + .filter(type -> !type.isInterface()) + .forEach(this.template::truncate); } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/IntegrationTestConfig.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/IntegrationTestConfig.java index aab88ecef..a970bc51e 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/IntegrationTestConfig.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/IntegrationTestConfig.java @@ -27,7 +27,7 @@ import org.springframework.data.cassandra.core.cql.keyspace.CreateKeyspaceSpecif import org.springframework.data.cassandra.core.cql.keyspace.DropKeyspaceSpecification; import org.springframework.data.cassandra.support.CassandraConnectionProperties; import org.springframework.data.cassandra.support.IntegrationTestNettyOptions; -import org.springframework.data.cassandra.support.RandomKeySpaceName; +import org.springframework.data.cassandra.support.RandomKeyspaceName; import com.datastax.driver.core.NettyOptions; import com.datastax.driver.core.QueryOptions; @@ -45,7 +45,7 @@ public class IntegrationTestConfig extends AbstractReactiveCassandraConfiguratio public static final CassandraConnectionProperties PROPS = new CassandraConnectionProperties(); public static final int PORT = PROPS.getCassandraPort(); - public String keyspaceName = RandomKeySpaceName.create(); + public String keyspaceName = RandomKeyspaceName.create(); @Override protected int getPort() { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/CassandraConnectionProperties.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/CassandraConnectionProperties.java index 4ae69fa5d..2e0f42722 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/CassandraConnectionProperties.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/CassandraConnectionProperties.java @@ -30,7 +30,7 @@ import org.springframework.util.Assert; @SuppressWarnings("serial") public class CassandraConnectionProperties extends Properties { - protected String resourceName = null; + protected String resourceName; /** * Create a new {@link CassandraConnectionProperties} using properties from @@ -41,36 +41,45 @@ public class CassandraConnectionProperties extends Properties { } protected CassandraConnectionProperties(String resourceName) { + this.resourceName = resourceName; loadProperties(); } private void loadProperties() { - loadProperties(resourceName); + + loadProperties(this.resourceName); putAll(System.getProperties()); } private void loadProperties(String resourceName) { + InputStream in = null; + try { in = getClass().getResourceAsStream(resourceName); - if (in == null) { - return; + + if (in != null) { + load(in); } - load(in); - } catch (Exception x) { - throw new RuntimeException(x); + } catch (Exception cause) { + throw new RuntimeException(cause); } finally { if (in != null) { try { in.close(); - } catch (Exception e) { - // gulp - } + } catch (Exception ignore) { } } } } + /** + * @return the Cassandra hostname + */ + public String getCassandraHost() { + return getProperty("build.cassandra.host"); + } + /** * @return the Cassandra port (native). */ @@ -99,23 +108,16 @@ public class CassandraConnectionProperties extends Properties { return getInt("build.cassandra.ssl_storage_port"); } - /** - * @return the Cassandra hostname - */ - public String getCassandraHost() { - return getProperty("build.cassandra.host"); - } - /** * @return the Cassandra type (Embedded or External) */ public CassandraType getCassandraType() { - String property = getProperty("build.cassandra.mode"); - if (property != null && property.equalsIgnoreCase(CassandraType.EXTERNAL.name())) { - return CassandraType.EXTERNAL; - } - return CassandraType.EMBEDDED; + String cassandraType = getProperty("build.cassandra.mode"); + + return CassandraType.EXTERNAL.name().equalsIgnoreCase(cassandraType) + ? CassandraType.EXTERNAL + : CassandraType.EMBEDDED; } /** @@ -155,14 +157,16 @@ public class CassandraConnectionProperties extends Properties { String propertyValue = getProperty(propertyName); try { return converter.convert(propertyValue); - } catch (Exception e) { - throw new IllegalArgumentException(String.format("%1$s: cannot parse value [%2$s] of property [%3$s] as a [%4$s]", - resourceName, propertyValue, propertyName, type.getSimpleName()), e); + } catch (Exception cause) { + + String message = "%1$s: cannot parse value [%2$s] of property [%3$s] as a [%4$s]"; + + throw new IllegalArgumentException(String.format(message, this.resourceName, propertyValue, propertyName, + type.getSimpleName()), cause); } } public enum CassandraType { EMBEDDED, EXTERNAL - } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/RandomKeySpaceName.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/RandomKeyspaceName.java similarity index 76% rename from spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/RandomKeySpaceName.java rename to spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/RandomKeyspaceName.java index 9009ce679..c55c63a13 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/RandomKeySpaceName.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/RandomKeyspaceName.java @@ -22,16 +22,16 @@ import java.util.UUID; * * @author Matthew T. Adams */ -public class RandomKeySpaceName { +public abstract class RandomKeyspaceName { - private RandomKeySpaceName() { - - } + private RandomKeyspaceName() { } /** - * Creates a random key space name starting with {@code ks} based on a random {@link UUID}. + * Creates a random {@link String keyspace name} starting with {@code ks} based on a random {@link UUID}. * - * @return + * @return a random {@link String keyspace name}. + * @see java.lang.String + * @see java.util.UUID */ public static String create() { return "ks" + UUID.randomUUID().toString().replace("-", ""); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/AbstractEmbeddedCassandraIntegrationTest.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/AbstractEmbeddedCassandraIntegrationTest.java index fd9a31715..736dfe812 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/AbstractEmbeddedCassandraIntegrationTest.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/AbstractEmbeddedCassandraIntegrationTest.java @@ -19,6 +19,7 @@ import java.util.UUID; import org.junit.ClassRule; import org.junit.Rule; + import org.springframework.data.cassandra.support.CqlDataSet; import com.datastax.driver.core.Cluster; @@ -36,49 +37,55 @@ import com.datastax.driver.core.Cluster; public abstract class AbstractEmbeddedCassandraIntegrationTest { /** - * Initiate a Cassandra environment in test class scope. + * Initiate a Cassandra environment in this test scope. */ - @ClassRule public static final CassandraRule cassandraEnvironment = new CassandraRule("embedded-cassandra.yaml"); + @ClassRule + public static final CassandraRule cassandraEnvironment = + new CassandraRule("embedded-cassandra.yaml"); /** - * Initiate a Cassandra environment in test scope. - */ - @Rule public final CassandraRule cassandraRule = cassandraEnvironment.testInstance().before(session -> { - AbstractEmbeddedCassandraIntegrationTest.this.cluster = session.getCluster(); - return null; - }); - - /** - * The {@link Cluster} that's connected to Cassandra. - */ - protected Cluster cluster; - - /** - * Creates a random UUID. + * Create and return a random {@link UUID} as a {@link String}. * - * @return + * @return a random {@link UUID} as a {@link String}. + * @see java.util.UUID#randomUUID() */ public static String uuid() { return UUID.randomUUID().toString(); } /** - * Returns the {@link Cluster}. + * Initiate a Cassandra environment in test scope. + */ + @Rule + public final CassandraRule cassandraRule = cassandraEnvironment.testInstance().before(session -> { + AbstractEmbeddedCassandraIntegrationTest.this.cluster = session.getCluster(); + return null; + }); + + /** + * The {@link Cluster} connected to Cassandra. + */ + protected Cluster cluster; + + /** + * Returns the {@link Cluster} instance. * - * @return + * @return an instance of {@link Cluster} connected to Cassandra. */ public Cluster getCluster() { - return cluster; + return this.cluster; } /** - * Executes a CQL script from a classpath resource in given {@code keyspace}. + * Executes a CQL script from a classpath resource in the given {@code keyspace}. * - * @param cqlResourceName - * @param keyspace + * @param cqlResourceName {@link String resource name} of the CQL script to apply. + * @param keyspace {@link String name} of the Cassandra Keyspace in which to apply the CQL script. */ public void execute(String cqlResourceName, String keyspace) { - cassandraRule.execute(CqlDataSet.fromClassPath(cqlResourceName).executeIn(keyspace)); - } + CqlDataSet cqlDataSet = CqlDataSet.fromClassPath(cqlResourceName).executeIn(keyspace); + + this.cassandraRule.execute(cqlDataSet); + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/AbstractKeyspaceCreatingIntegrationTest.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/AbstractKeyspaceCreatingIntegrationTest.java index c3f661b00..4f97614b9 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/AbstractKeyspaceCreatingIntegrationTest.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/AbstractKeyspaceCreatingIntegrationTest.java @@ -16,6 +16,7 @@ package org.springframework.data.cassandra.test.util; import org.junit.ClassRule; + import org.springframework.util.Assert; import com.datastax.driver.core.Session; @@ -33,72 +34,83 @@ import com.datastax.driver.core.Session; * @author Matthew T. Adams * @author David Webb * @author Mark Paluch + * @author John Blum */ public abstract class AbstractKeyspaceCreatingIntegrationTest extends AbstractEmbeddedCassandraIntegrationTest { /** - * Class rule to prepare a keyspace to give tests a keyspace context. The keyspace name is random and changes per - * test. + * Class rule to prepare a Cassandra Keyspace giving tests a Keyspace context. + * The Keyspace name is random and changes per test. */ - @ClassRule public static final KeyspaceRule keyspaceRule = new KeyspaceRule(cassandraEnvironment); + @ClassRule + public static final KeyspaceRule keyspaceRule = new KeyspaceRule(cassandraEnvironment); /** - * The session that's connected to the keyspace used in the current instance's test. + * The Session that's connected to the Cassandra Keyspace used in tests. */ protected Session session; /** - * The name of the keyspace to use for this test instance. + * The name of the Cassanda Keyspace to use for this test. */ protected final String keyspace; /** - * Create a new {@link AbstractKeyspaceCreatingIntegrationTest}. + * Constructs a new instance of {@link AbstractKeyspaceCreatingIntegrationTest}. */ public AbstractKeyspaceCreatingIntegrationTest() { this(keyspaceRule.getKeyspaceName()); } - private AbstractKeyspaceCreatingIntegrationTest(final String keyspace) { + private AbstractKeyspaceCreatingIntegrationTest(String keyspace) { Assert.hasText(keyspace, "Keyspace must not be empty"); this.keyspace = keyspace; this.session = keyspaceRule.getSession(); - cassandraRule.before(session -> { + this.cassandraRule.before(session -> { if (!keyspace.equals(session.getLoggedKeyspace())) { - session.execute(String.format("USE %s;", keyspace)); + session.execute(String.format(KeyspaceRule.USE_KEYSPACE_CQL, keyspace)); } + return null; }); } /** - * Returns the {@link Session}. The session is logged into the {@link #getKeyspace()}. + * Returns the configured {@link String name} of the Cassandra Keyspace used for tests. * - * @return - */ - public Session getSession() { - return session; - } - - /** - * Returns the keyspace name. - * - * @return + * @return the confiured {@link String name} of the Cassandra Keyspace used for tests. */ public String getKeyspace() { - return keyspace; + return this.keyspace; } /** - * Drop a Keyspace if it exists. + * Returns the configured {@link Session}. * - * @param keyspace + * The {@link Session} is logged into the {@link #getKeyspace()}. + * + * @return the configured {@link Session}. + * @see com.datastax.driver.core.Session + */ + public Session getSession() { + return this.session; + } + + @SuppressWarnings("unused") + protected void dropKeyspace() { + dropKeyspace(getKeyspace()); + } + + /** + * Drops the given Keyspace by {@link String name} if it exists. + * + * @param keyspace {@link String name} of the Keyspace to drop. */ public void dropKeyspace(String keyspace) { - session.execute("DROP KEYSPACE IF EXISTS " + keyspace); + this.session.execute(String.format(KeyspaceRule.DROP_KEYSPACE_IF_EXISTS_CQL, keyspace)); } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/CassandraRule.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/CassandraRule.java index 337b1760b..f726b5f88 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/CassandraRule.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/CassandraRule.java @@ -15,21 +15,22 @@ */ package org.springframework.data.cassandra.test.util; -import static org.springframework.data.cassandra.test.util.CassandraRule.InvocationMode.*; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; import org.junit.rules.ExternalResource; + import org.springframework.data.cassandra.core.cql.SessionCallback; import org.springframework.data.cassandra.support.CassandraConnectionProperties; import org.springframework.data.cassandra.support.CqlDataSet; import org.springframework.data.cassandra.support.IntegrationTestNettyOptions; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; +import org.springframework.util.StringUtils; import com.datastax.driver.core.Cluster; import com.datastax.driver.core.QueryOptions; @@ -37,45 +38,47 @@ import com.datastax.driver.core.Session; import com.datastax.driver.core.SocketOptions; /** - * Rule to provide a Cassandra context for integration tests. This rule can use/spin up either an embedded Cassandra - * instance or use an external instance. Typical usage: + * JUnit Rule used to provide a Cassandra context for integration tests. + * + * This rule can use/spin up either an embedded Cassandra instance or use an external instance. + * + * Typical usage: * *
- * {
- * 	public class MyIntegrationTest {
- * 		@Rule public CassandraRule rule = new CassandraRule(CONFIG). //
- * 				before(new ClassPathCQLDataSet("CreateIndexCqlGeneratorIntegrationTests-BasicTest.cql", "keyspace"));
- * 	}
+ * public class MyIntegrationTest {
+ * 		@Rule public CassandraRule rule = new CassandraRule(CONFIG)
+ * 				.before(new ClassPathCQLDataSet("CreateIndexCqlGeneratorIntegrationTests-BasicTest.cql", "keyspace"));
  * }
  * 
* * @author Mark Paluch + * @author John Blum * @since 1.5 */ public class CassandraRule extends ExternalResource { private static ResourceHolder resourceHolder; + private final long startupTimeout; + + @SuppressWarnings("all") private final CassandraConnectionProperties properties = new CassandraConnectionProperties(); - private final String configurationFileName; - - private final long startUpTimeout; - - private List> before = new ArrayList<>(); - - private Map, InvocationMode> invocationModeMap = new HashMap<>(); - - private List> after = new ArrayList<>(); - - private Session session; - - private Cluster cluster; - private CassandraRule parent; + private Cluster cluster; + private Integer cassandraPort; + private final List> after = new ArrayList<>(); + private final List> before = new ArrayList<>(); + + private final Map, InvocationMode> invocationModeMap = new HashMap<>(); + + private Session session; + + private final String configurationFilename; + /** * Create a new {@link CassandraRule} and allows the use of a config file. * @@ -86,31 +89,78 @@ public class CassandraRule extends ExternalResource { } /** - * Create a new {@link CassandraRule}, allows the use of a config file and to provide a startup timeout. + * Constructs a new instance of {@link CassandraRule} initialized with the given YAML configuration resource, + * thereby allowing the use of a configuration file and to provide a startup timeout. * - * @param yamlConfigurationResource name of the configuration resource, must not be {@literal null} and not empty - * @param startUpTimeout the startup timeout + * @param yamlConfigurationResource {@link String name} of the configuration resource; + * must not be {@literal null} or empty. + * @param startupTimeout long value indicating the startup timeout in milliseconds. */ - public CassandraRule(String yamlConfigurationResource, long startUpTimeout) { + public CassandraRule(String yamlConfigurationResource, long startupTimeout) { - Assert.hasText(yamlConfigurationResource, "Configuration file name must not be empty!"); + Assert.hasText(yamlConfigurationResource, "YAML configuration resource must not be empty"); - this.configurationFileName = yamlConfigurationResource; - this.startUpTimeout = startUpTimeout; + this.configurationFilename = yamlConfigurationResource; + this.startupTimeout = startupTimeout; } /** - * Create a new {@link CassandraRule} using a parent {@link CassandraRule} to preserve cluster/connection facilities. + * Constructs a new instance of {@link CassandraRule} using the provided (parent) {@link CassandraRule} + * to preserve cluster/connection context. * - * @param parent the parent instance + * @param parent the {@link CassandraRule parent} instance. */ private CassandraRule(CassandraRule parent) { - this.configurationFileName = null; - this.startUpTimeout = -1; + this.configurationFilename = null; + this.startupTimeout = -1; this.parent = parent; } + /** + * Creates a {@link CassandraRule} to be used in an "owning" scope. + * + * The derived {@link CassandraRule} shares the connection of {@literal this} instance + * and starts with a fresh before/after configuration. + * + * @return a derived {@link CassandraRule} sharing the connection of {@literal this} instance. + * @see #CassandraRule(CassandraRule) + */ + public CassandraRule testInstance() { + return new CassandraRule(this); + } + + /** + * Returns the {@link Cluster}. + * + * @return the Cluster + */ + public Cluster getCluster() { + return this.cluster; + } + + /** + * Returns the Cassandra port. + * + * @return the Cassandra port + */ + public int getPort() { + + Assert.state(this.cassandraPort != null, "Cassandra port was not initialized"); + + return this.cassandraPort; + } + + /** + * Returns the {@link Session}. The session state can be initialized and pointing to a keyspace other than + * {@code system}. + * + * @return the Session + */ + public Session getSession() { + return this.session; + } + /** * Add a {@link CqlDataSet} to execute before each test run. * @@ -118,7 +168,7 @@ public class CassandraRule extends ExternalResource { * @return the rule */ public CassandraRule before(CqlDataSet cqlDataSet) { - return before(each(), cqlDataSet); + return before(InvocationMode.EACH, cqlDataSet); } /** @@ -128,9 +178,9 @@ public class CassandraRule extends ExternalResource { * @param cqlDataSet must not be {@literal null} * @return the rule */ - public CassandraRule before(InvocationMode invocationMode, final CqlDataSet cqlDataSet) { + public CassandraRule before(InvocationMode invocationMode, CqlDataSet cqlDataSet) { - Assert.notNull(cqlDataSet, "CQLDataSet must not be null"); + Assert.notNull(cqlDataSet, "CqlDataSet must not be null"); SessionCallback sessionCallback = session -> { load(session, cqlDataSet); @@ -138,6 +188,7 @@ public class CassandraRule extends ExternalResource { }; before(invocationMode, sessionCallback); + return this; } @@ -147,10 +198,8 @@ public class CassandraRule extends ExternalResource { * @param sessionCallback must not be {@literal null} * @return the rule */ - public CassandraRule before(final SessionCallback sessionCallback) { - - Assert.notNull(sessionCallback, "SessionCallback must not be null"); - return before(each(), sessionCallback); + public CassandraRule before(SessionCallback sessionCallback) { + return before(InvocationMode.EACH, sessionCallback); } /** @@ -161,12 +210,13 @@ public class CassandraRule extends ExternalResource { * @return the rule */ @SuppressWarnings("unchecked") - public CassandraRule before(InvocationMode invocationMode, final SessionCallback sessionCallback) { + public CassandraRule before(InvocationMode invocationMode, SessionCallback sessionCallback) { Assert.notNull(sessionCallback, "SessionCallback must not be null"); - before.add((SessionCallback) sessionCallback); - invocationModeMap.put(sessionCallback, invocationMode); + this.before.add((SessionCallback) sessionCallback); + this.invocationModeMap.put(sessionCallback, invocationMode); + return this; } @@ -176,11 +226,12 @@ public class CassandraRule extends ExternalResource { * @param cqlDataSet must not be {@literal null} * @return the rule */ - public CassandraRule after(final CqlDataSet cqlDataSet) { + @SuppressWarnings("unused") + public CassandraRule after(CqlDataSet cqlDataSet) { - Assert.notNull(cqlDataSet, "CQLDataSet must not be null"); + Assert.notNull(cqlDataSet, "CqlDataSet must not be null"); - after.add(session -> { + this.after.add(session -> { load(CassandraRule.this.session, cqlDataSet); return null; }); @@ -189,29 +240,125 @@ public class CassandraRule extends ExternalResource { } /** - * Execute a {@link CqlDataSet}. - * - * @param cqlDataSet the CQL data set, must not be {@literal null}. - */ - public void execute(CqlDataSet cqlDataSet) { - - Assert.notNull(cqlDataSet, "CQLDataSet must not be null"); - load(session, cqlDataSet); - } - - /** - * Execute the {@code before} sequence. - * - * @throws Exception + * Execute {@code before} sequence. */ @Override - public void before() throws Exception { + protected void before() throws Exception { startCassandraIfNeeded(); - setupConnection(); + initializeConnection(); executeBeforeHooks(); } + private void startCassandraIfNeeded() throws Exception { + + if (isStartNeeded()) { + configureRemoteJmxPort(); + runEmbeddedCassandra(); + } + } + + private boolean isStartNeeded() { + return isParent() && isEmbedded(); + } + + private void configureRemoteJmxPort() { + + if (!System.getProperties().containsKey("com.sun.management.jmxremote.port")) { + System.setProperty("com.sun.management.jmxremote.port", + String.valueOf(SocketUtils.findAvailableTcpPort(1024))); + } + } + + private void runEmbeddedCassandra() throws Exception { + + if (this.configurationFilename != null) { + EmbeddedCassandraServerHelper.startEmbeddedCassandra(this.configurationFilename, this.startupTimeout); + } + } + + private synchronized void initializeConnection() { + + if (isParent()) { + + this.cassandraPort = resolvePort(); + + if (resourceHolder == null) { + + this.cluster = buildCluster(this.cassandraPort); + + if (isClusterReuseEnabled()) { + resourceHolder = new ResourceHolder(this.cluster); + } + } else { + this.cluster = resourceHolder.cluster; + } + } else { + this.cassandraPort = this.parent.cassandraPort; + this.cluster = this.parent.cluster; + } + + this.session = resolveSession(); + } + + private Cluster buildCluster(int port) { + + QueryOptions queryOptions = new QueryOptions().setRefreshSchemaIntervalMillis(0); + + SocketOptions socketOptions = new SocketOptions() + .setConnectTimeoutMillis((int) TimeUnit.SECONDS.toMillis(15)) + .setReadTimeoutMillis((int) TimeUnit.SECONDS.toMillis(15)); + + String host = resolveHost(); + + return new Cluster.Builder() + .addContactPoints(host) + .withPort(port) + .withMaxSchemaAgreementWaitSeconds(3) + .withNettyOptions(IntegrationTestNettyOptions.INSTANCE) + .withQueryOptions(queryOptions) + .withSocketOptions(socketOptions) + .build(); + } + + private String resolveHost() { + + return isEmbedded() + ? EmbeddedCassandraServerHelper.getHost() + : this.properties.getCassandraHost(); + } + + private int resolvePort() { + + return isEmbedded() + ? EmbeddedCassandraServerHelper.getNativeTransportPort() + : this.properties.getCassandraPort(); + } + + private Session resolveSession() { + + return isNotParent() ? this.parent.getSession() + : resourceHolder != null ? resourceHolder.session + : this.cluster.connect(); + } + + private void executeBeforeHooks() { + + this.before.forEach(sessionCallback -> { + + InvocationMode invocationMode = this.invocationModeMap.get(sessionCallback); + + if (!InvocationMode.NEVER.equals(invocationMode)) { + + if (InvocationMode.ONCE.equals(invocationMode)) { + this.invocationModeMap.put(sessionCallback, InvocationMode.NEVER); + } + + sessionCallback.doInSession(this.session); + } + }); + } + /** * Execute the {@code after} sequence. */ @@ -220,161 +367,62 @@ public class CassandraRule extends ExternalResource { super.after(); executeAfterHooks(); - cleanupConnection(); - } - - /** - * Returns the {@link Cluster}. - * - * @return the Cluster - */ - public Cluster getCluster() { - return cluster; - } - - /** - * Returns the {@link Session}. The session state can be initialized and pointing to a keyspace other than - * {@code system}. - * - * @return the Session - */ - public Session getSession() { - return session; - } - - /** - * Returns the Cassandra port. - * - * @return the Cassandra port - */ - public int getPort() { - - Assert.state(cassandraPort != null, "Cassandra port is not initialized"); - return cassandraPort; - } - - /** - * Creates a {@link CassandraRule} to be used in a own scope. The derived {@link CassandraRule} shares the connection - * of this instance and starts with a fresh before/after configuration. - * - * @return a derived {@link CassandraRule} sharing the connection of this instance - */ - public CassandraRule testInstance() { - return new CassandraRule(this); - } - - private void startCassandraIfNeeded() throws Exception { - - if (parent == null && properties.getCassandraType() == CassandraConnectionProperties.CassandraType.EMBEDDED) { - - /* start an embedded Cassandra instance*/ - if (!System.getProperties().containsKey("com.sun.management.jmxremote.port")) { - System.setProperty("com.sun.management.jmxremote.port", "" + SocketUtils.findAvailableTcpPort(1024)); - } - - if (configurationFileName != null) { - EmbeddedCassandraServerHelper.startEmbeddedCassandra(configurationFileName, startUpTimeout); - } - } - } - - private void executeBeforeHooks() { - - for (SessionCallback sessionCallback : before) { - - InvocationMode invocationMode = invocationModeMap.get(sessionCallback); - if (invocationMode == never()) { - continue; - } - - if (invocationMode == firstTest()) { - invocationModeMap.put(sessionCallback, never()); - } - - sessionCallback.doInSession(session); - } + releaseConnection(); } private void executeAfterHooks() { - - for (SessionCallback sessionCallback : after) { - sessionCallback.doInSession(session); - } + this.after.forEach(sessionCallback -> sessionCallback.doInSession(this.session)); } - private void setupConnection() { - - if (parent == null) { - String hostIp; - int port; - - if (properties.getCassandraType() == CassandraConnectionProperties.CassandraType.EMBEDDED) { - hostIp = EmbeddedCassandraServerHelper.getHost(); - port = EmbeddedCassandraServerHelper.getNativeTransportPort(); - } else { - hostIp = properties.getCassandraHost(); - port = properties.getCassandraPort(); - } - cassandraPort = port; - - QueryOptions queryOptions = new QueryOptions(); - queryOptions.setRefreshSchemaIntervalMillis(0); - - SocketOptions socketOptions = new SocketOptions(); - socketOptions.setConnectTimeoutMillis((int) TimeUnit.SECONDS.toMillis(15)); - socketOptions.setReadTimeoutMillis((int) TimeUnit.SECONDS.toMillis(15)); - - if (resourceHolder == null) { - - cluster = new Cluster.Builder().addContactPoints(hostIp) // - .withPort(port) // - .withQueryOptions(queryOptions) // - .withMaxSchemaAgreementWaitSeconds(3) // - .withSocketOptions(socketOptions) // - .withNettyOptions(IntegrationTestNettyOptions.INSTANCE) // - .build(); - - if (properties.getBoolean("build.cassandra.reuse-cluster")) { - resourceHolder = new ResourceHolder(cluster, cluster.connect()); - } - } else { - cluster = resourceHolder.cluster; - } - - } else { - cluster = parent.cluster; - cassandraPort = parent.cassandraPort; - } - - if (parent != null) { - session = parent.getSession(); - } else if (resourceHolder == null) { - session = cluster.connect(); - } else { - session = resourceHolder.session; - } - } - - private void cleanupConnection() { + private synchronized void releaseConnection() { if (resourceHolder == null) { - if (parent == null) { - session.close(); - cluster.closeAsync(); - cluster = null; + if (isParent()) { + this.session.close(); + this.cluster.closeAsync(); + this.cluster = null; } else { - session.closeAsync(); + this.session.closeAsync(); } } - session = null; + this.session = null; } - private void load(Session session, final CqlDataSet cqlDataSet) { + private boolean isClusterReuseEnabled() { + return this.properties.getBoolean("build.cassandra.reuse-cluster"); + } - if (cqlDataSet.getKeyspaceName() != null && !cqlDataSet.getKeyspaceName().equals(session.getLoggedKeyspace())) { - session.execute(String.format("USE %s;", cqlDataSet.getKeyspaceName())); - } + private boolean isEmbedded() { + return CassandraConnectionProperties.CassandraType.EMBEDDED.equals(this.properties.getCassandraType()); + } + + private boolean isNotParent() { + return !isParent(); + } + + private boolean isParent() { + return this.parent == null; + } + + /** + * Execute a {@link CqlDataSet}. + * + * @param cqlDataSet the CQL data set, must not be {@literal null}. + */ + public void execute(CqlDataSet cqlDataSet) { + + Assert.notNull(cqlDataSet, "CqlDataSet must not be null"); + + load(this.session, cqlDataSet); + } + + private void load(Session session, CqlDataSet cqlDataSet) { + + Optional.of(cqlDataSet.getKeyspaceName()) + .filter(StringUtils::hasText) + .filter(keyspaceName -> !keyspaceName.equals(session.getLoggedKeyspace())) + .ifPresent(keyspaceName -> session.execute(String.format(KeyspaceRule.USE_KEYSPACE_CQL, keyspaceName))); cqlDataSet.getCqlStatements().forEach(session::execute); } @@ -382,42 +430,17 @@ public class CassandraRule extends ExternalResource { /** * Invocation mode for before calls. */ - public static class InvocationMode { + public enum InvocationMode { - private static final InvocationMode once = new InvocationMode(); - private static final InvocationMode each = new InvocationMode(); - private static final InvocationMode never = new InvocationMode(); + /** {@link InvocationMode} to invoke an action before each test. */ + EACH, - /** - * Invocation mode to invoke an action once at before the first test. - * - * @return the {@code on first test} invocation mode - */ - public static InvocationMode firstTest() { - return once; - } + /** {@link InvocationMode} to never invoke an action. */ + NEVER, - /** - * Invocation mode to invoke an action on each run. - * - * @return the {@code on each test} invocation mode - */ - public static InvocationMode each() { - return each; - } + /** {@link InvocationMode} to invoke an action once before the first test. */ + ONCE - /** - * Invocation mode to never invoke an action. - * - * @return the {@code never} invocation mode - */ - static InvocationMode never() { - return never; - } - - private InvocationMode() { - - } } private static class ResourceHolder { @@ -425,7 +448,12 @@ public class CassandraRule extends ExternalResource { private Cluster cluster; private Session session; - public ResourceHolder(final Cluster cluster, final Session session) { + private ResourceHolder(Cluster cluster) { + this(cluster, cluster.connect()); + } + + private ResourceHolder(Cluster cluster, Session session) { + this.cluster = cluster; this.session = session; diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/EmbeddedCassandraServerHelper.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/EmbeddedCassandraServerHelper.java index 7e64bbf67..4f15af273 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/EmbeddedCassandraServerHelper.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/EmbeddedCassandraServerHelper.java @@ -15,7 +15,7 @@ */ package org.springframework.data.cassandra.test.util; -import static java.util.concurrent.TimeUnit.*; +import static java.util.concurrent.TimeUnit.MILLISECONDS; import java.io.File; import java.io.FileOutputStream; @@ -73,15 +73,6 @@ class EmbeddedCassandraServerHelper { return DatabaseDescriptor.getRpcAddress().getHostName(); } - /** - * Get embedded cassandra RPC port. - * - * @return the cassandra RPC port - */ - public static int getRpcPort() { - return DatabaseDescriptor.getRpcPort(); - } - /** * Get embedded cassandra native transport port. * @@ -91,6 +82,15 @@ class EmbeddedCassandraServerHelper { return DatabaseDescriptor.getNativeTransportPort(); } + /** + * Get embedded cassandra RPC port. + * + * @return the cassandra RPC port + */ + public static int getRpcPort() { + return DatabaseDescriptor.getRpcPort(); + } + /** * Start an embedded Cassandra instance. * @@ -113,16 +113,17 @@ class EmbeddedCassandraServerHelper { public static void startEmbeddedCassandra(String yamlResource, String tmpDir, long timeout) throws Exception { if (cassandraRef.get() != null) { - /* nothing to do Cassandra is already started */ + /* Nothing to do; Cassandra is already started */ return; } if (!sync.compareAndSet(null, new Object())) { - /* A different Thread was faster, so nothing to do for us here */ + /* A different Thread was faster, so nothing to do this time */ return; } File yamlFile = new File(tmpDir, new File(yamlResource).getName()); + prepareCassandraDirectory(yamlResource, tmpDir, yamlFile); startEmbeddedCassandra(yamlFile, timeout); } @@ -160,8 +161,10 @@ class EmbeddedCassandraServerHelper { cleanupAndRecreateDirectories(); - final CassandraDaemon cassandraDaemon = new CassandraDaemon(); + CassandraDaemon cassandraDaemon = new CassandraDaemon(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(() -> { cassandraDaemon.activate(); cassandraRef.compareAndSet(null, cassandraDaemon); @@ -169,16 +172,17 @@ class EmbeddedCassandraServerHelper { try { future.get(timeout, MILLISECONDS); - } catch (ExecutionException e) { + } catch (ExecutionException cause) { log.error("Cassandra daemon did not start after " + timeout + " ms. Consider increasing the timeout"); - throw new IllegalStateException("Cassandra daemon did not start within timeout", e); - } catch (InterruptedException e) { - log.error("Interrupted waiting for Cassandra daemon to start:", e); + throw new IllegalStateException("Cassandra daemon did not start within timeout", cause); + } catch (InterruptedException cause) { + + log.error("Interrupted waiting for Cassandra daemon to start:", cause); Thread.currentThread().interrupt(); - throw new IllegalStateException(e); + throw new IllegalStateException(cause); } finally { executor.shutdown(); } @@ -192,13 +196,14 @@ class EmbeddedCassandraServerHelper { createCassandraDirectories(); CommitLog commitLog = CommitLog.instance; + commitLog.getCurrentPosition(); // wait for commit log allocator instantiation to avoid hanging on a race condition commitLog.resetUnsafe(true); // cleanup screws w/ CommitLog, this brings it back to safe state } private static void cleanup() throws IOException { - // clean up commitlog and data locations + // clean up commit log and data locations rmdirs(DatabaseDescriptor.getCommitLogLocation()); rmdirs(DatabaseDescriptor.getAllDataFileLocations()); } @@ -247,6 +252,9 @@ class EmbeddedCassandraServerHelper { } private static void rmdirs(File... fileOrDirectories) throws IOException { - Arrays.stream(fileOrDirectories).filter(File::exists).forEach(FileUtils::deleteRecursive); + + Arrays.stream(fileOrDirectories) + .filter(File::exists) + .forEach(FileUtils::deleteRecursive); } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/KeyspaceRule.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/KeyspaceRule.java index 983359d87..2f3042713 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/KeyspaceRule.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/KeyspaceRule.java @@ -16,7 +16,8 @@ package org.springframework.data.cassandra.test.util; import org.junit.rules.ExternalResource; -import org.springframework.data.cassandra.support.RandomKeySpaceName; + +import org.springframework.data.cassandra.support.RandomKeyspaceName; import org.springframework.util.Assert; import com.datastax.driver.core.Cluster; @@ -38,78 +39,104 @@ import com.datastax.driver.core.Session; */ public class KeyspaceRule extends ExternalResource { + static final String CREATE_KEYSPACE_CQL = + "CREATE KEYSPACE %s WITH durable_writes = false AND replication = {'class': 'SimpleStrategy', 'replication_factor' : 1};"; + + static final String DROP_KEYSPACE_CQL = "DROP KEYSPACE %s;"; + static final String DROP_KEYSPACE_IF_EXISTS_CQL = String.format(DROP_KEYSPACE_CQL, "IF EXISTS %s"); + static final String USE_KEYSPACE_CQL = "USE %s;"; + private final CassandraRule cassandraRule; + private Session session; + private final String keyspaceName; /** - * Create a {@link KeyspaceRule} initialized with a {@link CassandraRule} for creating a keyspace using a random name. + * Constructs a new instance of {@link KeyspaceRule} initialized with a {@link CassandraRule} + * to create a Cassandra Keyspace using a random name. * - * @param cassandraRule + * @param cassandraRule {@link CassandraRule} used to setup the Cassandra environment. + * @throws IllegalArgumentException if {@link CassandraRule} is {@literal null}. + * @see org.springframework.data.cassandra.support.RandomKeyspaceName + * @see org.springframework.data.cassandra.test.util.CassandraRule */ public KeyspaceRule(CassandraRule cassandraRule) { - this(cassandraRule, RandomKeySpaceName.create()); + this(cassandraRule, RandomKeyspaceName.create()); } /** - * Create a {@link KeyspaceRule} initialized with a {@link CassandraRule} for creating a keyspace using the given - * {@code keyspaceName}. + * Constructs a new instance of {@link KeyspaceRule} initialized with a {@link CassandraRule} + * to create a Cassandra Keyspace with the given {@code keyspaceName}. * - * @param cassandraRule - * @param keyspaceName + * @param cassandraRule {@link CassandraRule} used to setup the Cassandra environment. + * @param keyspaceName {@link String name} of the Cassandra Keyspace to use in tests. + * @throws IllegalArgumentException if {@link CassandraRule} is {@literal null} + * or the Keyspace name is not specified. + * @see org.springframework.data.cassandra.test.util.CassandraRule + * @see org.springframework.data.cassandra.test.util.CassandraRule */ public KeyspaceRule(CassandraRule cassandraRule, String keyspaceName) { - Assert.notNull(cassandraRule, "CassandraRule must not be null!"); - Assert.hasText(keyspaceName, "KeyspaceName must not be empty!"); + Assert.notNull(cassandraRule, "CassandraRule must not be null"); + Assert.hasText(keyspaceName, "KeyspaceName must not be empty"); - this.keyspaceName = keyspaceName; this.cassandraRule = cassandraRule; + this.keyspaceName = keyspaceName; + + this.cassandraRule.before(session -> { + KeyspaceRule.this.session = this.cassandraRule.getSession(); + return null; + }); + } + + /** + * Returns the {@link String name} of the Cassandra Keyspace. + * + * @return the {@link String name} of the Cassandra keyspace. + */ + public String getKeyspaceName() { + return this.keyspaceName; + } + + /** + * Returns the {@link Session}. + * + * The {@link Session} state can be initialized and pointing to a Keyspace other than {@code system}. + * + * @return the current Cassandr {@link Session}. + * @see com.datastax.driver.core.Session + */ + public Session getSession() { + return this.session; + } + + private Session resolveSession() { + + Session session = getSession(); + + this.session = session != null ? session : this.cassandraRule.getSession(); + + Assert.state(this.session != null, "Session was not initialized"); + + return this.session; } @Override - protected void before() throws Throwable { + protected void before() { - // Support initialized and initializing CassandraRule. - if (cassandraRule.getCluster() != null) { - this.session = cassandraRule.getSession(); - } else { - cassandraRule.before(session -> { - KeyspaceRule.this.session = cassandraRule.getSession(); - return null; - }); - } + Session session = resolveSession(); - Assert.state(session != null, "Session was not initialized"); - - session.execute(String.format("CREATE KEYSPACE %s WITH durable_writes = false AND " - + "replication = {'class': 'SimpleStrategy', 'replication_factor' : 1};", keyspaceName)); - session.execute(String.format("USE %s;", keyspaceName)); + session.execute(String.format(CREATE_KEYSPACE_CQL, this.keyspaceName)); + session.execute(String.format(USE_KEYSPACE_CQL, this.keyspaceName)); } @Override protected void after() { - session.execute("USE system;"); - session.execute(String.format("DROP KEYSPACE %s;", keyspaceName)); - } + Session session = getSession(); - /** - * Returns the {@link Session}. The session state can be initialized and pointing to a keyspace other than - * {@code system}. - * - * @return - */ - public Session getSession() { - return session; - } - - /** - * Returns the keyspace name. - * - * @return - */ - public String getKeyspaceName() { - return keyspaceName; + session.execute(String.format(USE_KEYSPACE_CQL, "system")); + session.execute(String.format(DROP_KEYSPACE_CQL, this.keyspaceName)); } } diff --git a/spring-data-cassandra/src/test/resources/config/spring-data-cassandra-basic.xml b/spring-data-cassandra/src/test/resources/config/spring-data-cassandra-basic.xml index d16c93295..94666c21e 100644 --- a/spring-data-cassandra/src/test/resources/config/spring-data-cassandra-basic.xml +++ b/spring-data-cassandra/src/test/resources/config/spring-data-cassandra-basic.xml @@ -10,7 +10,7 @@ "> - + diff --git a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/repository/forcequote/compositeprimarykey/ForceQuotedCompositePrimaryKeyRepositoryXmlConfigIntegrationTests-context.xml b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/repository/forcequote/compositeprimarykey/ForceQuotedCompositePrimaryKeyRepositoryXmlConfigIntegrationTests-context.xml index 140961593..1d4e79c40 100644 --- a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/repository/forcequote/compositeprimarykey/ForceQuotedCompositePrimaryKeyRepositoryXmlConfigIntegrationTests-context.xml +++ b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/repository/forcequote/compositeprimarykey/ForceQuotedCompositePrimaryKeyRepositoryXmlConfigIntegrationTests-context.xml @@ -1,12 +1,13 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:cass="http://www.springframework.org/schema/data/cassandra" + xsi:schemaLocation=" + 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.xsd +"> - + @@ -36,6 +37,6 @@ schema-action="RECREATE_DROP_UNUSED"> - + + diff --git a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/repository/forcequote/config/ForceQuotedRepositoryXmlConfigIntegrationTests-context.xml b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/repository/forcequote/config/ForceQuotedRepositoryXmlConfigIntegrationTests-context.xml index d2f72a402..b286f1518 100644 --- a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/repository/forcequote/config/ForceQuotedRepositoryXmlConfigIntegrationTests-context.xml +++ b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/repository/forcequote/config/ForceQuotedRepositoryXmlConfigIntegrationTests-context.xml @@ -1,12 +1,13 @@ + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd +"> - + @@ -46,6 +47,6 @@ schema-action="RECREATE_DROP_UNUSED"> - + +