diff --git a/.travis.yml b/.travis.yml index 68e0c01fc74..4abb8a807d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ jdk: services: - mongodb - redis + - cassandra sudo: false install: true before_script: diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java index a2d04ec74bf..ddcab2bc74e 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; * * @author Julien Dubois * @author Phillip Webb + * @author Mark Paluch * @since 1.3.0 */ @ConfigurationProperties(prefix = "spring.data.cassandra") @@ -112,6 +113,11 @@ public class CassandraProperties { */ private int readTimeoutMillis = SocketOptions.DEFAULT_READ_TIMEOUT_MILLIS; + /** + * Schema action to take at startup. + */ + private String schemaAction = "none"; + /** * Enable SSL support. */ @@ -247,4 +253,12 @@ public class CassandraProperties { this.ssl = ssl; } + public String getSchemaAction() { + return this.schemaAction; + } + + public void setSchemaAction(String schemaAction) { + this.schemaAction = schemaAction; + } + } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfiguration.java index a086da5a020..72772348ccf 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfiguration.java @@ -30,9 +30,12 @@ import org.springframework.boot.autoconfigure.cassandra.CassandraProperties; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.domain.EntityScanPackages; +import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertyResolver; import org.springframework.data.cassandra.config.CassandraEntityClassScanner; import org.springframework.data.cassandra.config.CassandraSessionFactoryBean; import org.springframework.data.cassandra.config.SchemaAction; @@ -48,6 +51,7 @@ import org.springframework.data.cassandra.mapping.CassandraMappingContext; * * @author Julien Dubois * @author Eddú Meléndez + * @author Mark Paluch * @since 1.3.0 */ @Configuration @@ -62,11 +66,14 @@ public class CassandraDataAutoConfiguration { private final Cluster cluster; + private final PropertyResolver propertyResolver; + public CassandraDataAutoConfiguration(BeanFactory beanFactory, - CassandraProperties properties, Cluster cluster) { + CassandraProperties properties, Cluster cluster, Environment environment) { this.beanFactory = beanFactory; this.properties = properties; this.cluster = cluster; + this.propertyResolver = new RelaxedPropertyResolver(environment, "spring.data.cassandra."); } @Bean @@ -98,7 +105,9 @@ public class CassandraDataAutoConfiguration { session.setCluster(this.cluster); session.setConverter(converter); session.setKeyspaceName(this.properties.getKeyspaceName()); - session.setSchemaAction(SchemaAction.NONE); + SchemaAction schemaAction = this.propertyResolver + .getProperty("schemaAction", SchemaAction.class, SchemaAction.NONE); + session.setSchemaAction(schemaAction); return session; } diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 549db95ebc1..808b1358306 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -325,6 +325,17 @@ } ] }, + { + "name": "spring.data.cassandra.schema-action", + "providers": [ + { + "name": "handle-as", + "parameters": { + "target": "org.springframework.data.cassandra.config.SchemaAction" + } + } + ] + }, { "name": "spring.data.mongodb.field-naming-strategy", "providers": [ diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationIntegrationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationIntegrationTests.java new file mode 100644 index 00000000000..2c98e6f6f1e --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationIntegrationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.cassandra; + +import com.datastax.driver.core.Session; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurationPackages; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; +import org.springframework.boot.autoconfigure.data.cassandra.city.City; +import org.springframework.boot.test.util.EnvironmentTestUtils; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.data.cassandra.config.CassandraSessionFactoryBean; +import org.springframework.data.cassandra.config.SchemaAction; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CassandraDataAutoConfiguration} that require a Cassandra instance. + * + * @author Mark Paluch + * @author Stephane Nicoll + */ +public class CassandraDataAutoConfigurationIntegrationTests { + + @Rule + public final CassandraTestServer cassandra = new CassandraTestServer(); + + private AnnotationConfigApplicationContext context; + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void hasDefaultSchemaActionSet() { + this.context = new AnnotationConfigApplicationContext(); + String cityPackage = City.class.getPackage().getName(); + AutoConfigurationPackages.register(this.context, cityPackage); + this.context.register(CassandraAutoConfiguration.class, + CassandraDataAutoConfiguration.class); + this.context.refresh(); + + CassandraSessionFactoryBean bean = this.context + .getBean(CassandraSessionFactoryBean.class); + assertThat(bean.getSchemaAction()).isEqualTo(SchemaAction.NONE); + } + + @Test + public void hasRecreateSchemaActionSet() { + createTestKeyspaceIfNotExists(); + this.context = new AnnotationConfigApplicationContext(); + String cityPackage = City.class.getPackage().getName(); + AutoConfigurationPackages.register(this.context, cityPackage); + + EnvironmentTestUtils.addEnvironment(this.context, + "spring.data.cassandra.schemaAction:RECREATE_DROP_UNUSED", "spring.data.cassandra.keyspaceName:boot_test"); + + this.context.register(CassandraAutoConfiguration.class, + CassandraDataAutoConfiguration.class); + this.context.refresh(); + + CassandraSessionFactoryBean bean = this.context + .getBean(CassandraSessionFactoryBean.class); + assertThat(bean.getSchemaAction()).isEqualTo(SchemaAction.RECREATE_DROP_UNUSED); + } + + private void createTestKeyspaceIfNotExists() { + Session session = this.cassandra.getCluster().connect(); + try { + session.execute("CREATE KEYSPACE IF NOT EXISTS boot_test" + + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); + } + finally { + session.close(); + } + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationTests.java index bd9b15fd7b6..faf46b1815f 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.data.cassandra; import java.util.Set; import com.datastax.driver.core.Session; + import org.junit.After; import org.junit.Test; diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraRepositoriesAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraRepositoriesAutoConfigurationTests.java index 8f21ed54211..9db24e6b188 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraRepositoriesAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraRepositoriesAutoConfigurationTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.data.cassandra; +import java.util.Set; + import com.datastax.driver.core.Cluster; import com.datastax.driver.core.Session; import org.junit.After; @@ -34,7 +36,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -43,6 +47,8 @@ import static org.mockito.Mockito.mock; * Tests for {@link CassandraRepositoriesAutoConfiguration}. * * @author Eddú Meléndez + * @author Mark Paluch + * @author Stephane Nicoll */ public class CassandraRepositoriesAutoConfigurationTests { @@ -63,18 +69,29 @@ public class CassandraRepositoriesAutoConfigurationTests { addConfigurations(TestConfiguration.class); assertThat(this.context.getBean(CityRepository.class)).isNotNull(); assertThat(this.context.getBean(Cluster.class)).isNotNull(); + assertThat(getInitialEntitySet()).hasSize(1); } @Test public void testNoRepositoryConfiguration() { addConfigurations(TestExcludeConfiguration.class, EmptyConfiguration.class); assertThat(this.context.getBean(Cluster.class)).isNotNull(); + assertThat(getInitialEntitySet()).hasSize(1).containsOnly(City.class); } @Test public void doesNotTriggerDefaultRepositoryDetectionIfCustomized() { addConfigurations(TestExcludeConfiguration.class, CustomizedConfiguration.class); assertThat(this.context.getBean(CityCassandraRepository.class)).isNotNull(); + assertThat(getInitialEntitySet()).hasSize(1).containsOnly(City.class); + } + + @SuppressWarnings("unchecked") + private Set> getInitialEntitySet() { + BasicCassandraMappingContext mappingContext = this.context + .getBean(BasicCassandraMappingContext.class); + return (Set>) ReflectionTestUtils + .getField(mappingContext, "initialEntitySet"); } private void addConfigurations(Class... configurations) { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraTestServer.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraTestServer.java new file mode 100644 index 00000000000..5fcb6a0b331 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraTestServer.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.cassandra; + +import com.datastax.driver.core.Cluster; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Assume; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * {@link TestRule} for working with an optional Cassandra server. + * + * @author Stephane Nicoll + */ +public class CassandraTestServer implements TestRule { + + private static final Log logger = LogFactory.getLog(CassandraTestServer.class); + + private Cluster cluster; + + @Override + public Statement apply(Statement base, Description description) { + try { + this.cluster = newCluster(); + return new CassandraStatement(base, this.cluster); + } + catch (Exception ex) { + logger.error("No Cassandra server available", ex); + return new SkipStatement(); + } + } + + private Cluster newCluster() { + Cluster cluster = Cluster.builder().addContactPoint("localhost").build(); + testCluster(cluster); + return cluster; + } + + private void testCluster(Cluster cluster) { + cluster.connect().close(); + } + + /** + * @return the cluster if any + */ + public Cluster getCluster() { + return this.cluster; + } + + private static class CassandraStatement extends Statement { + + private final Statement base; + + private final Cluster cluster; + + CassandraStatement(Statement base, Cluster cluster) { + this.base = base; + this.cluster = cluster; + } + + @Override + public void evaluate() throws Throwable { + try { + this.base.evaluate(); + } + finally { + this.cluster.closeAsync(); + } + } + + } + + private static class SkipStatement extends Statement { + + @Override + public void evaluate() throws Throwable { + Assume.assumeTrue("Skipping test due to Cassandra not being available", false); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/city/City.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/city/City.java index b785a4ec012..2ddbe1c7b6b 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/city/City.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/city/City.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.boot.autoconfigure.data.cassandra.city; +import com.datastax.driver.core.DataType.Name; + +import org.springframework.data.cassandra.mapping.CassandraType; import org.springframework.data.cassandra.mapping.Column; import org.springframework.data.cassandra.mapping.PrimaryKey; import org.springframework.data.cassandra.mapping.Table; @@ -24,6 +27,7 @@ import org.springframework.data.cassandra.mapping.Table; public class City { @PrimaryKey + @CassandraType(type = Name.BIGINT) private Long id; @Column diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 3341165842d..b1f34a86b0a 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -548,6 +548,7 @@ content into your application; rather pick only the properties that you need. spring.data.cassandra.reconnection-policy= # Reconnection policy class. spring.data.cassandra.retry-policy= # Class name of the retry policy. spring.data.cassandra.serial-consistency-level= # Queries serial consistency level. + spring.data.cassandra.schema-action= # Schema action to take at startup. spring.data.cassandra.ssl=false # Enable SSL support. spring.data.cassandra.username= # Login user of the server.