diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/config/CassandraClusterFactoryBean.java b/spring-cassandra/src/main/java/org/springframework/cassandra/config/CassandraClusterFactoryBean.java index ea21b424c..c0360cb0a 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/config/CassandraClusterFactoryBean.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/config/CassandraClusterFactoryBean.java @@ -15,9 +15,19 @@ */ package org.springframework.cassandra.config; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; +import org.springframework.cassandra.core.CassandraTemplate; +import org.springframework.cassandra.core.cql.generator.CreateKeyspaceCqlGenerator; +import org.springframework.cassandra.core.cql.generator.DropKeyspaceCqlGenerator; +import org.springframework.cassandra.core.keyspace.CreateKeyspaceSpecification; +import org.springframework.cassandra.core.keyspace.DropKeyspaceSpecification; import org.springframework.cassandra.support.CassandraExceptionTranslator; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; @@ -28,6 +38,7 @@ import com.datastax.driver.core.Cluster; import com.datastax.driver.core.HostDistance; import com.datastax.driver.core.PoolingOptions; import com.datastax.driver.core.ProtocolOptions.Compression; +import com.datastax.driver.core.Session; import com.datastax.driver.core.SocketOptions; import com.datastax.driver.core.policies.LoadBalancingPolicy; import com.datastax.driver.core.policies.ReconnectionPolicy; @@ -39,10 +50,11 @@ import com.datastax.driver.core.policies.RetryPolicy; * @author Alex Shvid * @author Matthew T. Adams */ - public class CassandraClusterFactoryBean implements FactoryBean, InitializingBean, DisposableBean, PersistenceExceptionTranslator { + protected static final Logger log = LoggerFactory.getLogger(CassandraClusterFactoryBean.class); + private static final int DEFAULT_PORT = 9042; private Cluster cluster; @@ -64,6 +76,12 @@ public class CassandraClusterFactoryBean implements FactoryBean, Initia private final PersistenceExceptionTranslator exceptionTranslator = new CassandraExceptionTranslator(); + private List keyspaceCreations = new ArrayList(); + private List keyspaceDrops = new ArrayList(); + + private List scripts = new ArrayList(); + + @Override public Cluster getObject() throws Exception { return cluster; } @@ -72,6 +90,7 @@ public class CassandraClusterFactoryBean implements FactoryBean, Initia * (non-Javadoc) * @see org.springframework.beans.factory.FactoryBean#getObjectType() */ + @Override public Class getObjectType() { return Cluster.class; } @@ -80,6 +99,7 @@ public class CassandraClusterFactoryBean implements FactoryBean, Initia * (non-Javadoc) * @see org.springframework.beans.factory.FactoryBean#isSingleton() */ + @Override public boolean isSingleton() { return true; } @@ -88,6 +108,7 @@ public class CassandraClusterFactoryBean implements FactoryBean, Initia * (non-Javadoc) * @see org.springframework.dao.support.PersistenceExceptionTranslator#translateExceptionIfPossible(java.lang.RuntimeException) */ + @Override public DataAccessException translateExceptionIfPossible(RuntimeException ex) { return exceptionTranslator.translateExceptionIfPossible(ex); } @@ -96,6 +117,7 @@ public class CassandraClusterFactoryBean implements FactoryBean, Initia * (non-Javadoc) * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ + @Override public void afterPropertiesSet() throws Exception { if (!StringUtils.hasText(contactPoints)) { @@ -146,13 +168,72 @@ public class CassandraClusterFactoryBean implements FactoryBean, Initia // initialize property this.cluster = cluster; + + processKeyspaceCreations(); + executeCqlScripts(); } - /* - * (non-Javadoc) - * @see org.springframework.beans.factory.DisposableBean#destroy() - */ + protected void processKeyspaceCreations() { + + Session system = null; + try { + system = keyspaceCreations.size() > 0 ? cluster.connect() : null; + CassandraTemplate template = system == null ? null : new CassandraTemplate(system); + + for (CreateKeyspaceSpecification spec : this.keyspaceCreations) { + + String cql = new CreateKeyspaceCqlGenerator(spec).toCql(); + + if (log.isDebugEnabled()) { + log.info("executing CQL [{}]", cql); + } + + template.execute(cql); + } + } finally { + if (system != null) { + system.shutdown(); + } + } + } + + protected void executeCqlScripts() { + + Session system = null; + try { + system = scripts.size() > 0 ? cluster.connect() : null; + CassandraTemplate template = system == null ? null : new CassandraTemplate(system); + + for (String cql : this.scripts) { + if (cql.trim().length() == 0) { + continue; + } + template.execute(cql); + } + } finally { + if (system != null) { + system.shutdown(); + } + } + } + + @Override public void destroy() throws Exception { + + Session system = null; + try { + system = keyspaceDrops.size() > 0 ? cluster.connect() : null; + CassandraTemplate template = new CassandraTemplate(system); + + for (DropKeyspaceSpecification spec : this.keyspaceDrops) { + template.execute(new DropKeyspaceCqlGenerator(spec).toCql()); + } + } finally { + if (system != null) { + system.shutdown(); + } + } + this.cluster.shutdown(); } @@ -200,6 +281,26 @@ public class CassandraClusterFactoryBean implements FactoryBean, Initia this.metricsEnabled = metricsEnabled; } + public void setKeyspaceCreations(List specifications) { + this.keyspaceCreations = specifications; + } + + public List getKeyspaceCreations() { + return keyspaceCreations; + } + + public void setKeyspaceDrops(List specifications) { + this.keyspaceDrops = specifications; + } + + public List getKeyspaceDrops() { + return keyspaceDrops; + } + + public void setScripts(List scripts) { + this.scripts = scripts; + } + private static Compression convertCompressionType(CompressionType type) { switch (type) { case NONE: diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/config/xml/CassandraClusterParser.java b/spring-cassandra/src/main/java/org/springframework/cassandra/config/xml/CassandraClusterParser.java index e454049c0..683fcdab8 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/config/xml/CassandraClusterParser.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/config/xml/CassandraClusterParser.java @@ -15,7 +15,10 @@ */ package org.springframework.cassandra.config.xml; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.config.BeanDefinition; @@ -27,9 +30,15 @@ import org.springframework.cassandra.config.CassandraClusterFactoryBean; import org.springframework.cassandra.config.CompressionType; import org.springframework.cassandra.config.PoolingOptionsConfig; import org.springframework.cassandra.config.SocketOptionsConfig; +import org.springframework.cassandra.core.keyspace.CreateKeyspaceSpecification; +import org.springframework.cassandra.core.keyspace.DefaultOption; +import org.springframework.cassandra.core.keyspace.DropKeyspaceSpecification; +import org.springframework.cassandra.core.keyspace.KeyspaceOption; +import org.springframework.cassandra.core.keyspace.Option; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; +import org.w3c.dom.NodeList; /** * Parser for <cluster;gt; definitions. @@ -45,10 +54,6 @@ public class CassandraClusterParser extends AbstractSimpleBeanDefinitionParser { return CassandraClusterFactoryBean.class; } - /* - * (non-Javadoc) - * @see org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#resolveId(org.w3c.dom.Element, org.springframework.beans.factory.support.AbstractBeanDefinition, org.springframework.beans.factory.xml.ParserContext) - */ @Override protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) throws BeanDefinitionStoreException { @@ -79,6 +84,11 @@ public class CassandraClusterParser extends AbstractSimpleBeanDefinitionParser { } protected void parseChildElements(BeanDefinitionBuilder builder, Element element) { + + List creates = new ArrayList(); + List drops = new ArrayList(); + List scripts = new ArrayList(); + List elements = DomUtils.getChildElements(element); // parse nested elements @@ -91,9 +101,101 @@ public class CassandraClusterParser extends AbstractSimpleBeanDefinitionParser { builder.addPropertyValue("remotePoolingOptions", parsePoolingOptions(subElement)); } else if ("socket-options".equals(name)) { builder.addPropertyValue("socketOptions", parseSocketOptions(subElement)); + } else if ("keyspace".equals(name)) { + + KeyspaceSpecifications specifications = parseKeyspace(subElement); + + if (specifications.create != null) { + creates.add(specifications.create); + } + if (specifications.drop != null) { + drops.add(specifications.drop); + } + } else if ("cql".equals(name)) { + scripts.add(parseScript(subElement)); } } + builder.addPropertyValue("keyspaceCreations", creates); + builder.addPropertyValue("keyspaceDrops", drops); + builder.addPropertyValue("scripts", scripts); + } + + private KeyspaceSpecifications parseKeyspace(Element element) { + + CreateKeyspaceSpecification create = null; + DropKeyspaceSpecification drop = null; + + String name = element.getAttribute("name"); + if (name == null || name.trim().length() == 0) { + name = BeanNames.CASSANDRA_KEYSPACE; + } + + boolean durableWrites = Boolean.valueOf(element.getAttribute("durable-writes")); + + String action = element.getAttribute("action"); + if (action == null || action.trim().length() == 0) { + throw new IllegalArgumentException("attribute action must be given"); + } + + if (action.startsWith("CREATE")) { + + create = CreateKeyspaceSpecification.createKeyspace().name(name) + .with(KeyspaceOption.DURABLE_WRITES, durableWrites); + + NodeList nodes = element.getElementsByTagName("replication"); + parseReplication((Element) (nodes.getLength() == 1 ? nodes.item(0) : null), create); + } + + if (action.equals("CREATE-DROP")) { + drop = DropKeyspaceSpecification.dropKeyspace().name(create.getName()); + } + + return new KeyspaceSpecifications(create, drop); + } + + protected void parseReplication(Element element, CreateKeyspaceSpecification create) { + + String strategyClass = null; + if (element != null) { + strategyClass = element.getAttribute("class"); + } + if (strategyClass == null || strategyClass.trim().length() == 0) { + strategyClass = "SimpleStrategy"; + } + + Long replicationFactor = null; + if (element != null) { + String s = element.getAttribute("replication-factor"); + replicationFactor = (s == null || s.trim().length() == 0) ? null : Long.parseLong(s); + } + if (replicationFactor == null) { + replicationFactor = 1L; + } + + Map replicationMap = new HashMap(); + replicationMap.put(new DefaultOption("class", String.class, false, false, true), strategyClass); + replicationMap.put(new DefaultOption("replication_factor", Long.class, true, false, false), replicationFactor); + + if (element != null) { + + NodeList dataCenters = element.getElementsByTagName("data-center"); + + int length = dataCenters.getLength(); + for (int i = 0; i < length; i++) { + + Element dataCenter = (Element) dataCenters.item(i); + + replicationMap.put(new DefaultOption(dataCenter.getAttribute("name"), Long.class, false, false, true), + dataCenter.getAttribute("replicas-per-node")); + } + } + + create.with(KeyspaceOption.REPLICATION, replicationMap); + } + + private String parseScript(Element element) { + return element.getTextContent(); } private BeanDefinition parsePoolingOptions(Element element) { @@ -121,4 +223,15 @@ public class CassandraClusterParser extends AbstractSimpleBeanDefinitionParser { return builder.getBeanDefinition(); } + private static class KeyspaceSpecifications { + + public KeyspaceSpecifications(CreateKeyspaceSpecification create, DropKeyspaceSpecification drop) { + this.create = create; + this.drop = drop; + } + + public CreateKeyspaceSpecification create; + public DropKeyspaceSpecification drop; + // TODO: public AlterKeyspaceSpecification alter; + } } diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/config/xml/CassandraNamespaceHandler.java b/spring-cassandra/src/main/java/org/springframework/cassandra/config/xml/CassandraNamespaceHandler.java index f09b3071c..9d743fa39 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/config/xml/CassandraNamespaceHandler.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/config/xml/CassandraNamespaceHandler.java @@ -26,10 +26,11 @@ import org.springframework.beans.factory.xml.NamespaceHandlerSupport; public class CassandraNamespaceHandler extends NamespaceHandlerSupport { + @Override public void init() { registerBeanDefinitionParser("cluster", new CassandraClusterParser()); registerBeanDefinitionParser("session", new CassandraSessionParser()); registerBeanDefinitionParser("template", new CassandraTemplateParser()); } -} +} \ No newline at end of file diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/CreateKeyspaceSpecification.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/CreateKeyspaceSpecification.java index be04dd195..f945093e4 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/CreateKeyspaceSpecification.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/CreateKeyspaceSpecification.java @@ -35,6 +35,11 @@ public class CreateKeyspaceSpecification extends KeyspaceSpecification @@ -67,6 +67,21 @@ Defines a Cassandra Cluster. + + + + + + + + + + + @@ -81,7 +96,7 @@ Defines a Cassandra Cluster. + ]]> + ]]> + ]]> @@ -104,14 +119,14 @@ The protocol compression option. Default is 'none'. + ]]> + ]]> @@ -121,7 +136,7 @@ Uses SNAPPY compression algorithm. + ]]> @@ -138,7 +153,7 @@ AuthInfoProvider implementation. + ]]> @@ -156,7 +171,7 @@ LoadBalancingPolicy implementation. + ]]> @@ -174,7 +189,7 @@ ReconnectionPolicy implementation. + ]]> @@ -217,28 +232,28 @@ RetryPolicy implementation. + ]]> + ]]> + ]]> + ]]> @@ -248,54 +263,64 @@ More connections are created up to a configurable maximum number of connections. + ]]> + ]]> + ]]> + ]]> + ]]> + ]]> + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/config/xml/FullySpecifiedKeyspaceCreatingXmlConfigTest.java b/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/config/xml/FullySpecifiedKeyspaceCreatingXmlConfigTest.java new file mode 100644 index 000000000..44ac32069 --- /dev/null +++ b/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/config/xml/FullySpecifiedKeyspaceCreatingXmlConfigTest.java @@ -0,0 +1,33 @@ +package org.springframework.cassandra.test.integration.config.xml; + +import javax.inject.Inject; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.cassandra.test.integration.AbstractEmbeddedCassandraIntegrationTest; +import org.springframework.cassandra.test.integration.config.IntegrationTestUtils; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.datastax.driver.core.Session; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class FullySpecifiedKeyspaceCreatingXmlConfigTest extends AbstractEmbeddedCassandraIntegrationTest { + + @Override + protected String keyspace() { + return null; + } + + @Inject + Session s; + + @Test + public void test() { + IntegrationTestUtils.assertKeyspaceExists("full1", s); + IntegrationTestUtils.assertKeyspaceExists("full2", s); + IntegrationTestUtils.assertKeyspaceExists("script1", s); + IntegrationTestUtils.assertKeyspaceExists("script2", s); + } +} diff --git a/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/config/xml/MinimalKeyspaceCreatingXmlConfigTest.java b/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/config/xml/MinimalKeyspaceCreatingXmlConfigTest.java new file mode 100644 index 000000000..492605316 --- /dev/null +++ b/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/config/xml/MinimalKeyspaceCreatingXmlConfigTest.java @@ -0,0 +1,30 @@ +package org.springframework.cassandra.test.integration.config.xml; + +import javax.inject.Inject; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.cassandra.test.integration.AbstractEmbeddedCassandraIntegrationTest; +import org.springframework.cassandra.test.integration.config.IntegrationTestUtils; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.datastax.driver.core.Session; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class MinimalKeyspaceCreatingXmlConfigTest extends AbstractEmbeddedCassandraIntegrationTest { + + @Override + protected String keyspace() { + return null; + } + + @Inject + Session s; + + @Test + public void test() { + IntegrationTestUtils.assertKeyspaceExists("minimal", s); + } +} diff --git a/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/AlterTableCqlGeneratorTests.java b/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/AlterTableCqlGeneratorTests.java index 89a6d5ec4..e281fdaa7 100644 --- a/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/AlterTableCqlGeneratorTests.java +++ b/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/AlterTableCqlGeneratorTests.java @@ -56,10 +56,12 @@ public class AlterTableCqlGeneratorTests { public String dropped = "dropped"; + @Override public AlterTableSpecification specification() { return AlterTableSpecification.alterTable().name(name).alter(altered, alteredType).add(added, addedType); } + @Override public AlterTableCqlGenerator generator() { return new AlterTableCqlGenerator(specification); } @@ -98,6 +100,7 @@ public class AlterTableCqlGeneratorTests { public Map compactionMap = new LinkedHashMap(); public Map compressionMap = new LinkedHashMap(); + @Override public AlterTableSpecification specification() { // Compaction @@ -108,7 +111,7 @@ public class AlterTableCqlGeneratorTests { compressionMap.put(CompressionOption.CHUNK_LENGTH_KB, 128); compressionMap.put(CompressionOption.CRC_CHECK_CHANCE, 0.75); - return (AlterTableSpecification) AlterTableSpecification + return AlterTableSpecification .alterTable() .name(name) // .with(TableOption.COMPACT_STORAGE) diff --git a/spring-cassandra/src/test/resources/org/springframework/cassandra/test/integration/config/xml/FullySpecifiedKeyspaceCreatingXmlConfigTest-context.xml b/spring-cassandra/src/test/resources/org/springframework/cassandra/test/integration/config/xml/FullySpecifiedKeyspaceCreatingXmlConfigTest-context.xml new file mode 100644 index 000000000..cea9714fc --- /dev/null +++ b/spring-cassandra/src/test/resources/org/springframework/cassandra/test/integration/config/xml/FullySpecifiedKeyspaceCreatingXmlConfigTest-context.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-cassandra/src/test/resources/org/springframework/cassandra/test/integration/config/xml/FullySpecifiedKeyspaceCreatingXmlConfigTest.properties b/spring-cassandra/src/test/resources/org/springframework/cassandra/test/integration/config/xml/FullySpecifiedKeyspaceCreatingXmlConfigTest.properties new file mode 100644 index 000000000..a5049296f --- /dev/null +++ b/spring-cassandra/src/test/resources/org/springframework/cassandra/test/integration/config/xml/FullySpecifiedKeyspaceCreatingXmlConfigTest.properties @@ -0,0 +1 @@ +script2=CREATE KEYSPACE script2 WITH durable_writes = true AND replication = { 'replication_factor' : 1, 'class' : 'SimpleStrategy' }; diff --git a/spring-cassandra/src/test/resources/org/springframework/cassandra/test/integration/config/xml/MinimalKeyspaceCreatingXmlConfigTest-context.xml b/spring-cassandra/src/test/resources/org/springframework/cassandra/test/integration/config/xml/MinimalKeyspaceCreatingXmlConfigTest-context.xml new file mode 100644 index 000000000..156cf8572 --- /dev/null +++ b/spring-cassandra/src/test/resources/org/springframework/cassandra/test/integration/config/xml/MinimalKeyspaceCreatingXmlConfigTest-context.xml @@ -0,0 +1,16 @@ + + + + + + + + +