diff --git a/pom.xml b/pom.xml index a53fbcc04..a27fe0919 100644 --- a/pom.xml +++ b/pom.xml @@ -1,8 +1,17 @@ - + 4.0.0 + + org.springframework.data.build + spring-data-parent + 1.8.2.BUILD-SNAPSHOT + ../spring-data-build/parent/pom.xml + + org.springframework.data spring-data-cassandra-parent 1.4.2.BUILD-SNAPSHOT @@ -12,13 +21,6 @@ Spring Data Cassandra http://www.springsource.org/spring-data/cassandra - - org.springframework.data.build - spring-data-parent - 1.8.2.BUILD-SNAPSHOT - ../spring-data-build/parent/pom.xml - - spring-cql spring-data-cassandra @@ -75,6 +77,20 @@ + + + spring-libs-snapshot + https://repo.spring.io/libs-snapshot + + + + + + spring-plugins-release + https://repo.spring.io/plugins-release + + + @@ -210,20 +226,6 @@ - - - spring-libs-snapshot - https://repo.spring.io/libs-snapshot - - - - - - spring-plugins-release - https://repo.spring.io/plugins-release - - - com.datastax.cassandra @@ -353,6 +355,7 @@ release + diff --git a/spring-cql/pom.xml b/spring-cql/pom.xml index b08d369ef..22691c73e 100644 --- a/spring-cql/pom.xml +++ b/spring-cql/pom.xml @@ -1,14 +1,10 @@ - + 4.0.0 - spring-cql - - Spring CQL - Raw Cassandra CQL Support for Spring Core - org.springframework.data spring-data-cassandra-parent @@ -16,6 +12,12 @@ ../pom.xml + spring-cql + + Spring CQL + Raw Cassandra CQL Support for Spring Core + https://github.com/spring-projects/spring-data-cassandra/tree/master/spring-cql + 1.0.0.GA diff --git a/spring-cql/src/main/java/org/springframework/cassandra/config/CassandraCqlSessionFactoryBean.java b/spring-cql/src/main/java/org/springframework/cassandra/config/CassandraCqlSessionFactoryBean.java index 1500de3ce..b41f0c135 100644 --- a/spring-cql/src/main/java/org/springframework/cassandra/config/CassandraCqlSessionFactoryBean.java +++ b/spring-cql/src/main/java/org/springframework/cassandra/config/CassandraCqlSessionFactoryBean.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.cassandra.config; import java.util.ArrayList; @@ -24,136 +25,210 @@ 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.CqlOperations; import org.springframework.cassandra.core.CqlTemplate; import org.springframework.cassandra.support.CassandraExceptionTranslator; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import com.datastax.driver.core.Cluster; import com.datastax.driver.core.Session; /** - * Factory for configuring a Cassandra {@link Session}, which is a thread-safe singleton. As such, it is sufficient to - * have one {@link Session} per application and keyspace. + * Factory for creating and configuring a Cassandra {@link Session}, which is a thread-safe singleton. + * As such, it is sufficient to have one {@link Session} per application and keyspace. * * @author Alex Shvid * @author Matthew T. Adams + * @author John Blum + * @see org.springframework.beans.factory.DisposableBean + * @see org.springframework.beans.factory.FactoryBean + * @see org.springframework.beans.factory.InitializingBean + * @see org.springframework.dao.support.PersistenceExceptionTranslator + * @see org.springframework.cassandra.core.CqlTemplate + * @see org.springframework.cassandra.support.CassandraExceptionTranslator + * @see com.datastax.driver.core.Cluster + * @see com.datastax.driver.core.Session */ - +@SuppressWarnings("unused") public class CassandraCqlSessionFactoryBean implements FactoryBean, InitializingBean, DisposableBean, PersistenceExceptionTranslator { - private static final Logger log = LoggerFactory.getLogger(CassandraCqlSessionFactoryBean.class); + private Cluster cluster; + + private List startupScripts = Collections.emptyList(); + private List shutdownScripts = Collections.emptyList(); + + protected final Logger logger = LoggerFactory.getLogger(getClass()); - protected Cluster cluster; - protected Session session; - protected String keyspaceName; - protected List startupScripts = new ArrayList(); - protected List shutdownScripts = new ArrayList(); protected final PersistenceExceptionTranslator exceptionTranslator = new CassandraExceptionTranslator(); + private Session session; + + private String keyspaceName; + + /* (non-Javadoc) */ @Override public Session getObject() { - return session; + return this.session; } + /* (non-Javadoc) */ @Override public Class getObjectType() { - return Session.class; + return (this.session != null ? this.session.getClass() : Session.class); } + /* (non-Javadoc) */ @Override public boolean isSingleton() { return true; } - @Override - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { - return exceptionTranslator.translateExceptionIfPossible(ex); - } - + /* (non-Javadoc) */ @Override public void afterPropertiesSet() throws Exception { - - Assert.notNull(cluster); - - session = StringUtils.hasText(keyspaceName) ? cluster.connect(keyspaceName) : cluster.connect(); - executeScripts(startupScripts); + this.session = connect(getKeyspaceName()); + executeScripts(getStartupScripts()); } - /** - * Executes given scripts. Session must be connected when this method is called. - */ - protected void executeScripts(List scripts) { - - if (scripts == null || scripts.size() == 0) { - return; - } - - CqlTemplate template = new CqlTemplate(session); - - for (String script : scripts) { - - if (log.isInfoEnabled()) { - log.info("executing raw CQL [{}]", script); - } - - template.execute(script); - } + /* (non-Javadoc) */ + Session connect(String keyspaceName) { + return (StringUtils.hasText(keyspaceName) ? getCluster().connect(keyspaceName) : getCluster().connect()); } + /* (non-Javadoc) */ @Override public void destroy() throws Exception { - - executeScripts(shutdownScripts); - session.close(); + executeScripts(getShutdownScripts()); + getSession().close(); } /** - * Sets the keyspace name to connect to. Using null, empty string, or only whitespace will cause the - * system keyspace to be used. + * Executes the given Cassandra CQL scripts. The {@link Session} must be connected when this method is called. + */ + protected void executeScripts(List scripts) { + if (!CollectionUtils.isEmpty(scripts)) { + CqlOperations template = newCqlOperations(getSession()); + + for (String script : scripts) { + logger.info("executing raw CQL [{}]", script); + template.execute(script); + } + } + } + + /* (non-Javadoc) */ + CqlOperations newCqlOperations(Session session) { + return new CqlTemplate(session); + } + + /* (non-Javadoc) */ + @Override + public DataAccessException translateExceptionIfPossible(RuntimeException e) { + return this.exceptionTranslator.translateExceptionIfPossible(e); + } + + /** + * Null-safe operation to determine whether the Cassandra {@link Session} is connected or not. + * + * @return a boolean value indicating whether the Cassandra {@link Session} is connected. + * @see com.datastax.driver.core.Session#isClosed() + * @see #getObject() + */ + public boolean isConnected() { + Session session = getObject(); + return !(session == null || session.isClosed()); + } + + /** + * Sets a reference to the Cassandra {@link Cluster} to use. + * + * @param cluster a reference to the Cassandra {@link Cluster} used by this application. + * @throws IllegalArgumentException if the {@link Cluster} reference is null. + * @see com.datastax.driver.core.Cluster + * @see #getCluster() + */ + public void setCluster(Cluster cluster) { + Assert.notNull(cluster, "Cluster must not be null"); + this.cluster = cluster; + } + + /** + * Returns a reference to the configured Cassandra {@link Cluster} used by this application. + * + * @return a reference to the configured Cassandra {@link Cluster}. + * @throws IllegalStateException if the reference to the {@link Cluster} was not properly initialized. + * @see com.datastax.driver.core.Cluster + * @see #setCluster(Cluster) + */ + protected Cluster getCluster() { + Assert.state(this.cluster != null, "Cluster was not properly initialized"); + return this.cluster; + } + + /** + * Sets the name of the Cassandra Keyspace to connect to. Passing null, an empty String, + * or whitespace will cause the Cassandra System Keyspace to be used. + * + * @param keyspaceName a String indicating the name of the Keyspace in which to connect. + * @see #getKeyspaceName() */ public void setKeyspaceName(String keyspaceName) { this.keyspaceName = keyspaceName; } /** - * Sets the cluster to use. Must not be null. + * Gets the name of the Cassandra Keyspace to connect to. + * + * @return the name of the Cassandra Keyspace to connect to as a String. + * @see #setKeyspaceName(String) */ - public void setCluster(Cluster cluster) { - if (cluster == null) { - throw new IllegalArgumentException("cluster must not be null"); - } - this.cluster = cluster; + protected String getKeyspaceName() { + return this.keyspaceName; + } + + /** + * Returns a reference to the connected Cassandra {@link Session}. + * + * @return a reference to the connected Cassandra {@link Session}. + * @throws IllegalStateException if the Cassandra {@link Session} was not properly initialized. + * @see com.datastax.driver.core.Session + */ + protected Session getSession() { + Session session = getObject(); + Assert.state(session != null, "Session was not properly initialized"); + return session; } /** * Sets CQL scripts to be executed immediately after the session is connected. */ public void setStartupScripts(List scripts) { - this.startupScripts = scripts == null ? new ArrayList() : new ArrayList(scripts); + this.startupScripts = (scripts != null ? new ArrayList(scripts) : Collections.emptyList()); } /** * Returns an unmodifiable list of startup scripts. */ public List getStartupScripts() { - return Collections.unmodifiableList(startupScripts); + return Collections.unmodifiableList(this.startupScripts); } /** * Sets CQL scripts to be executed immediately before the session is shutdown. */ public void setShutdownScripts(List scripts) { - this.shutdownScripts = scripts == null ? new ArrayList() : new ArrayList(scripts); + this.shutdownScripts = (scripts != null ? new ArrayList(scripts) : Collections.emptyList()); } /** * Returns an unmodifiable list of shutdown scripts. */ public List getShutdownScripts() { - return Collections.unmodifiableList(shutdownScripts); + return Collections.unmodifiableList(this.shutdownScripts); } } diff --git a/spring-cql/src/main/java/org/springframework/cassandra/config/java/AbstractSessionConfiguration.java b/spring-cql/src/main/java/org/springframework/cassandra/config/java/AbstractSessionConfiguration.java index f1e65672b..1af3e6284 100644 --- a/spring-cql/src/main/java/org/springframework/cassandra/config/java/AbstractSessionConfiguration.java +++ b/spring-cql/src/main/java/org/springframework/cassandra/config/java/AbstractSessionConfiguration.java @@ -19,28 +19,32 @@ import org.springframework.cassandra.config.CassandraCqlSessionFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.datastax.driver.core.Cluster; - /** - * Base class for Spring Cassandra configuration that can handle creating namespaces, execute arbitrary CQL on startup & - * shutdown, and optionally drop namespaces. - * + * Spring {@link @Configuration} class used to configure a Cassandra client application + * {@link com.datastax.driver.core.Session} connected to a Cassandra {@link com.datastax.driver.core.Cluster}. + * + * Enables a Cassandra Keyspace to be specified along with the ability to execute arbitrary CQL on startup + * as well as shutdown. + * * @author Matthew T. Adams + * @author John Blum + * @see org.springframework.cassandra.config.java.AbstractClusterConfiguration + * @see org.springframework.context.annotation.Configuration */ @Configuration public abstract class AbstractSessionConfiguration extends AbstractClusterConfiguration { - protected abstract String getKeyspaceName(); - @Bean public CassandraCqlSessionFactoryBean session() throws Exception { - Cluster cluster = cluster().getObject(); - CassandraCqlSessionFactoryBean bean = new CassandraCqlSessionFactoryBean(); - bean.setCluster(cluster); + + bean.setCluster(cluster().getObject()); bean.setKeyspaceName(getKeyspaceName()); return bean; } + + protected abstract String getKeyspaceName(); + } diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/util/CollectionUtils.java b/spring-cql/src/main/java/org/springframework/cassandra/core/util/CollectionUtils.java index ff9d4cf64..301e48a57 100644 --- a/spring-cql/src/main/java/org/springframework/cassandra/core/util/CollectionUtils.java +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/util/CollectionUtils.java @@ -13,64 +13,44 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.cassandra.core.util; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -public class CollectionUtils { +public abstract class CollectionUtils extends org.springframework.util.CollectionUtils { - @SuppressWarnings("unchecked") - public static T[] toArray(Iterable i) { - return (T[]) toList(i).toArray(); + public static Object[] toArray(Iterable iterable) { + return toList(iterable).toArray(); } - public static List toList(Iterable i) { + public static List toList(T... elements) { + List list = Collections.emptyList(); - List list = null; - if (i instanceof List) { - list = (List) i; - } else { - list = new ArrayList(); - for (T t : i) { - list.add(t); - } + if (elements != null) { + list = new ArrayList(elements.length); + Collections.addAll(list, elements); } return list; } - public static List toList(Object thing) { - List list = new ArrayList(); - list.add(thing); - return list; - } + public static List toList(Iterable iterable) { + if (!(iterable instanceof List)) { + List list = new ArrayList(); - public static List toList(Object thing1, Object thing2) { - List list = new ArrayList(); - list.add(thing1); - list.add(thing2); - return list; - } - - public static List toList(Object thing1, Object thing2, Object thing3) { - List list = new ArrayList(); - list.add(thing1); - list.add(thing2); - list.add(thing3); - return list; - } - - public static List toList(Object thing1, Object thing2, Object thing3, Object... rest) { - List list = new ArrayList(); - list.add(thing1); - list.add(thing2); - list.add(thing3); - if (rest != null) { - for (Object thing : rest) { - list.add(thing); + if (iterable != null) { + for (T element : iterable) { + list.add(element); + } } + + iterable = list; } - return list; + + return (List) iterable; } + } diff --git a/spring-cql/src/test/java/org/springframework/cassandra/config/CassandraCqlSessionFactoryBeanUnitTests.java b/spring-cql/src/test/java/org/springframework/cassandra/config/CassandraCqlSessionFactoryBeanUnitTests.java new file mode 100644 index 000000000..b1b66b360 --- /dev/null +++ b/spring-cql/src/test/java/org/springframework/cassandra/config/CassandraCqlSessionFactoryBeanUnitTests.java @@ -0,0 +1,349 @@ +/* + * Copyright 2013-2016 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.cassandra.config; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +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 java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.cassandra.core.CqlOperations; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.Session; + +/** + * The CassandraCqlSessionFactoryBeanUnitTests class is a test suite of test cases testing the contract + * and functionality of the {@link CassandraCqlSessionFactoryBean} class. + * + * @author John Blum + * @see org.junit.Rule + * @see org.junit.Test + * @see org.junit.rules.ExpectedException + * @see org.junit.runner.RunWith + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.runners.MockitoJUnitRunner + * @see org.springframework.cassandra.config.CassandraCqlSessionFactoryBean + * @since 1.5.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class CassandraCqlSessionFactoryBeanUnitTests { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Mock + private Cluster mockCluster; + + @Mock + private Session mockSession; + + private CassandraCqlSessionFactoryBean factoryBean; + + protected List asList(T... array) { + return new ArrayList(Arrays.asList(array)); + } + + protected void assertNonNullEmptyCollection(Collection collection) { + assertThat(collection, is(notNullValue())); + assertThat(collection.isEmpty(), is(true)); + } + + @Before + public void setup() { + factoryBean = spy(new CassandraCqlSessionFactoryBean()); + } + + @Test + public void cassandraCqlSessionFactoryBeanIsSingleton() { + assertThat(factoryBean.isSingleton(), is(true)); + } + + @Test + public void objectTypeWhenSessionHasNotBeenInitializedIsSessionClass() { + assertThat(factoryBean.getObject(), is(nullValue())); + assertEquals(Session.class, factoryBean.getObjectType()); + } + + @Test + @SuppressWarnings("unchecked") + public void afterPropertiesSetInitializesSessionWithKeyspaceAndExecutesStartupScripts() throws Exception { + List expectedStartupScripts = asList("/path/to/schema.cql", "/path/to/data.cql"); + + CqlOperations mockCqlOperations = mock(CqlOperations.class); + + doReturn(mockSession).when(factoryBean).connect(eq("TestKeyspace")); + doReturn(mockCqlOperations).when(factoryBean).newCqlOperations(eq(mockSession)); + + factoryBean.setKeyspaceName("TestKeyspace"); + factoryBean.setStartupScripts(expectedStartupScripts); + + assertThat(factoryBean.getKeyspaceName(), is(equalTo("TestKeyspace"))); + assertThat(factoryBean.getStartupScripts(), is(equalTo(expectedStartupScripts))); + + factoryBean.afterPropertiesSet(); + + assertEquals(mockSession.getClass(), factoryBean.getObjectType()); + assertThat(factoryBean.getObject(), is(equalTo(mockSession))); + assertThat(factoryBean.getSession(), is(equalTo(mockSession))); + + InOrder inOrder = inOrder(factoryBean); + + inOrder.verify(factoryBean, times(1)).connect(eq("TestKeyspace")); + inOrder.verify(factoryBean, times(1)).executeScripts(eq(expectedStartupScripts)); + inOrder.verify(factoryBean, times(1)).newCqlOperations(eq(mockSession)); + verify(mockCqlOperations, times(1)).execute(eq(expectedStartupScripts.get(0))); + verify(mockCqlOperations, times(1)).execute(eq(expectedStartupScripts.get(1))); + } + + @Test + public void connectToSystemKeyspace() { + when(mockCluster.connect()).thenReturn(mockSession); + + factoryBean.setCluster(mockCluster); + + assertThat(factoryBean.connect(null), is(equalTo(mockSession))); + + verify(mockCluster, times(1)).connect(); + verify(mockCluster, never()).connect(anyString()); + } + + @Test + public void connectToTargetKeyspace() { + when(mockCluster.connect(eq("TestKeyspace"))).thenReturn(mockSession); + + factoryBean.setCluster(mockCluster); + + assertThat(factoryBean.connect("TestKeyspace"), is(equalTo(mockSession))); + + verify(mockCluster, never()).connect(); + verify(mockCluster, times(1)).connect(eq("TestKeyspace")); + } + + @Test + @SuppressWarnings("unchecked") + public void destroySessionAndExecutesShutdownScripts() throws Exception { + List expectedShutdownScripts = asList("/path/to/shutdown.cql"); + + CqlOperations mockCqlOperations = mock(CqlOperations.class); + + doReturn(mockSession).when(factoryBean).getSession(); + doReturn(mockCqlOperations).when(factoryBean).newCqlOperations(eq(mockSession)); + + factoryBean.setShutdownScripts(expectedShutdownScripts); + factoryBean.destroy(); + + InOrder inOrder = inOrder(factoryBean, mockSession); + + inOrder.verify(factoryBean, times(1)).executeScripts(eq(expectedShutdownScripts)); + verify(mockCqlOperations, times(1)).execute(eq(expectedShutdownScripts.get(0))); + inOrder.verify(mockSession, times(1)).close(); + } + + @Test + public void isConnectedWithNullSessionIsFalse() { + assertThat(factoryBean.getObject(), is(nullValue())); + assertThat(factoryBean.isConnected(), is(false)); + } + + @Test + public void isConnectedWithClosedSessionIsFalse() { + doReturn(mockSession).when(factoryBean).getObject(); + when(mockSession.isClosed()).thenReturn(true); + assertThat(factoryBean.isConnected(), is(false)); + verify(mockSession, times(1)).isClosed(); + } + + @Test + public void isConnectedWithOpenSessionIsTrue() { + doReturn(mockSession).when(factoryBean).getObject(); + when(mockSession.isClosed()).thenReturn(false); + assertThat(factoryBean.isConnected(), is(true)); + verify(mockSession, times(1)).isClosed(); + } + + @Test + public void setAndGetCluster() { + factoryBean.setCluster(mockCluster); + assertThat(factoryBean.getCluster(), is(equalTo(mockCluster))); + } + + @Test + public void setClusterToNullThrowsIllegalArgumentException() { + exception.expect(IllegalArgumentException.class); + exception.expectCause(is(nullValue(Throwable.class))); + exception.expectMessage("Cluster must not be null"); + + factoryBean.setCluster(null); + } + + @Test + public void getClusterWhenUninitializedThrowsIllegalStateException() { + exception.expect(IllegalStateException.class); + exception.expectCause(is(nullValue(Throwable.class))); + exception.expectMessage("Cluster was not properly initialized"); + + factoryBean.getCluster(); + } + + @Test + public void setAndGetKeyspaceName() { + assertThat(factoryBean.getKeyspaceName(), is(nullValue())); + + factoryBean.setKeyspaceName("TEST"); + + assertThat(factoryBean.getKeyspaceName(), is(equalTo("TEST"))); + + factoryBean.setKeyspaceName(null); + + assertThat(factoryBean.getKeyspaceName(), is(nullValue())); + } + + @Test + public void getSessionWhenUninitializedThrowsIllegalStateException() { + exception.expect(IllegalStateException.class); + exception.expectCause(is(nullValue(Throwable.class))); + exception.expectMessage(is(equalTo("Session was not properly initialized"))); + + assertThat(factoryBean.getObject(), is(nullValue())); + + factoryBean.getSession(); + } + + @Test + public void setAndGetStartupScripts() { + assertNonNullEmptyCollection(factoryBean.getStartupScripts()); + + List expectedStartupScripts = asList("/path/to/schema.cql", "/path/to/data.cql"); + + factoryBean.setStartupScripts(expectedStartupScripts); + + List actualStartupScripts = factoryBean.getStartupScripts(); + + assertThat(actualStartupScripts, is(not(sameInstance(expectedStartupScripts)))); + assertThat(actualStartupScripts, is(equalTo(expectedStartupScripts))); + + factoryBean.setStartupScripts(null); + + assertNonNullEmptyCollection(factoryBean.getStartupScripts()); + } + + @Test + public void startupScriptsAreImmutable() { + List startupScripts = asList("/path/to/startup.cql"); + + factoryBean.setStartupScripts(startupScripts); + + List actualStartupScripts = factoryBean.getStartupScripts(); + + assertThat(actualStartupScripts, is(notNullValue())); + assertThat(actualStartupScripts, is(not(sameInstance(startupScripts)))); + assertThat(actualStartupScripts, is(equalTo(startupScripts))); + + startupScripts.add("/path/to/another.cql"); + + actualStartupScripts = factoryBean.getStartupScripts(); + + assertThat(actualStartupScripts, is(not(equalTo(startupScripts)))); + assertThat(actualStartupScripts.size(), is(equalTo(1))); + assertThat(actualStartupScripts.get(0), is(equalTo(startupScripts.get(0)))); + + try { + exception.expect(UnsupportedOperationException.class); + actualStartupScripts.add("/path/to/yetAnother.cql"); + } + finally { + assertThat(actualStartupScripts.size(), is(equalTo(1))); + } + } + + @Test + public void setAndGetShutdownScripts() { + assertNonNullEmptyCollection(factoryBean.getShutdownScripts()); + + List expectedShutdownScripts = asList("/path/to/backup.cql", "/path/to/dropTables.cql"); + + factoryBean.setShutdownScripts(expectedShutdownScripts); + + List actualShutdownScripts = factoryBean.getShutdownScripts(); + + assertThat(actualShutdownScripts, is(not(sameInstance(expectedShutdownScripts)))); + assertThat(actualShutdownScripts, is(equalTo(expectedShutdownScripts))); + + factoryBean.setShutdownScripts(null); + + assertNonNullEmptyCollection(factoryBean.getShutdownScripts()); + } + + @Test + public void shutdownScriptsAreImmutable() { + List shutdownScripts = asList("/path/to/shutdown.cql"); + + factoryBean.setShutdownScripts(shutdownScripts); + + List actualShutdownScripts = factoryBean.getShutdownScripts(); + + assertThat(actualShutdownScripts, is(notNullValue())); + assertThat(actualShutdownScripts, is(not(sameInstance(shutdownScripts)))); + assertThat(actualShutdownScripts, is(equalTo(shutdownScripts))); + + shutdownScripts.add("/path/to/corruptSession.cql"); + + actualShutdownScripts = factoryBean.getShutdownScripts(); + + assertThat(actualShutdownScripts, is(not(sameInstance(shutdownScripts)))); + assertThat(actualShutdownScripts, is(not(equalTo(shutdownScripts)))); + assertThat(actualShutdownScripts.size(), is(equalTo(1))); + + try { + exception.expect(UnsupportedOperationException.class); + actualShutdownScripts.add("/path/to/blowUpCluster.cql"); + } + finally { + assertThat(actualShutdownScripts.size(), is(equalTo(1))); + } + } + +} diff --git a/spring-cql/src/test/java/org/springframework/cassandra/core/util/CollectionUtilsUnitTests.java b/spring-cql/src/test/java/org/springframework/cassandra/core/util/CollectionUtilsUnitTests.java new file mode 100644 index 000000000..cb294abdd --- /dev/null +++ b/spring-cql/src/test/java/org/springframework/cassandra/core/util/CollectionUtilsUnitTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2013-2016 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.cassandra.core.util; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.junit.Test; + +/** + * The CollectionUtilsUnitTests class is a test suite of test cases testing the contract and functionality + * of the {@link CollectionUtils} class. + * + * @author John Blum + * @see org.junit.Test + * @see org.springframework.cassandra.core.util.CollectionUtils + * @since 1.5.0 + */ +public class CollectionUtilsUnitTests { + + List asList(T... array) { + return Arrays.asList(array); + } + + void assertNonNullEmptyArray(Object[] array) { + assertThat(array, is(notNullValue())); + assertThat(array.length, is(equalTo(0))); + } + + void assertNonNullEmptyCollection(Collection collection) { + assertThat(collection, is(notNullValue())); + assertThat(collection.isEmpty(), is(true)); + } + + Iterable newIterable(final T... elements) { + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + int index = 0; + + @Override + public boolean hasNext() { + return (index < elements.length); + } + + @Override + public T next() { + return elements[index++]; + } + }; + } + }; + } + + @Test + public void toArrayWithIterable() { + Object[] array = CollectionUtils.toArray(newIterable(1, 2, 3)); + + assertThat(array, is(notNullValue())); + assertThat(array.length, is(equalTo(3))); + + for (int index = 0; index < array.length; index++) { + Object valueAtIndex = (index + 1); + assertThat(array[index], is(equalTo(valueAtIndex))); + } + } + + @Test + public void toArrayWithEmptyIterable() { + assertNonNullEmptyArray(CollectionUtils.toArray(newIterable())); + } + + @Test + public void toArrayWithNull() { + assertNonNullEmptyArray(CollectionUtils.toArray(null)); + } + + @Test + public void toListWithArray() { + List list = CollectionUtils.toList("test", "testing", "tested"); + + assertThat(list, is(notNullValue())); + assertThat(list.size(), is(equalTo(3))); + assertThat(list.containsAll(asList("test", "testing", "tested")), is(true)); + } + + @Test + public void toListWithEmptyArray() { + assertNonNullEmptyCollection(CollectionUtils.toList()); + } + + @Test + public void toListWithNullArray() { + assertNonNullEmptyCollection(CollectionUtils.toList((Object[]) null)); + } + + @Test + @SuppressWarnings("unchecked") + public void toListWithIterable() { + List list = CollectionUtils.toList(newIterable(1, 2, 3)); + + assertThat(list, is(notNullValue())); + assertThat(list.size(), is(equalTo(3))); + assertThat(list.containsAll(asList(1, 2, 3)), is(true)); + } + + @Test + public void toListWithList() { + List expected = asList(1, 2, 3); + List actual = CollectionUtils.toList(expected); + + assertThat(actual, is(sameInstance(expected))); + } + + @Test + public void toListWithNullIterable() { + assertNonNullEmptyCollection(CollectionUtils.toList((Iterable) null)); + } + +} diff --git a/spring-data-cassandra/pom.xml b/spring-data-cassandra/pom.xml index db6e85cbb..0847ed826 100644 --- a/spring-data-cassandra/pom.xml +++ b/spring-data-cassandra/pom.xml @@ -1,14 +1,10 @@ - + 4.0.0 - spring-data-cassandra - - Spring Data Cassandra - Core - Cassandra Support for Spring Data - org.springframework.data spring-data-cassandra-parent @@ -16,6 +12,12 @@ ../pom.xml + spring-data-cassandra + + Spring Data Cassandra Core + Cassandra support for Spring Data + https://github.com/spring-projects/spring-data-cassandra/tree/master/spring-data-cassandra + 1.0.0.GA 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 2957ca25a..31316e456 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors + * Copyright 2013-2016 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,12 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.data.cassandra.config; +import static org.springframework.cassandra.core.cql.CqlIdentifier.cqlId; + import java.util.Collection; import org.springframework.cassandra.config.CassandraCqlSessionFactoryBean; import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.core.CassandraAdminOperations; import org.springframework.data.cassandra.core.CassandraAdminTemplate; import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; @@ -26,96 +30,136 @@ import org.springframework.util.Assert; import com.datastax.driver.core.KeyspaceMetadata; import com.datastax.driver.core.Metadata; +import com.datastax.driver.core.Session; import com.datastax.driver.core.TableMetadata; -import static org.springframework.cassandra.core.cql.CqlIdentifier.cqlId; - +/** + * Factory to create and configure a Cassandra {@link com.datastax.driver.core.Session} with support + * for executing CQL and initializing the database schema (a.k.a. keyspace). + * + * @author Mathew Adams + * @author David Webb + * @author John Blum + * @see com.datastax.driver.core.KeyspaceMetadata + * @see com.datastax.driver.core.TableMetadata + */ public class CassandraSessionFactoryBean extends CassandraCqlSessionFactoryBean { - protected SchemaAction schemaAction = SchemaAction.NONE; - protected CassandraAdminTemplate admin; - protected CassandraConverter converter; - protected CassandraMappingContext mappingContext; + protected static final boolean DEFAULT_CREATE_IF_NOT_EXISTS = false; + protected static final boolean DEFAULT_DROP_TABLES = false; + protected static final boolean DEFAULT_DROP_UNUSED_TABLES = false; + + private CassandraAdminOperations admin; + + private CassandraConverter converter; + + private SchemaAction schemaAction = SchemaAction.NONE; @Override public void afterPropertiesSet() throws Exception { - super.afterPropertiesSet(); - Assert.notNull(converter); + Assert.state(converter != null, "Converter must not be null"); - admin = new CassandraAdminTemplate(session, converter); + admin = newCassandraAdminOperations(getObject(), converter); performSchemaAction(); } + /* (non-Javadoc) */ + CassandraAdminOperations newCassandraAdminOperations(Session session, CassandraConverter converter) { + return new CassandraAdminTemplate(session, converter); + } + + /* (non-Javadoc) */ protected void performSchemaAction() { - boolean dropTables = false; - boolean dropUnused = false; + boolean dropTables = DEFAULT_DROP_TABLES; + boolean dropUnused = DEFAULT_DROP_UNUSED_TABLES; + boolean ifNotExists = DEFAULT_CREATE_IF_NOT_EXISTS; switch (schemaAction) { - - case NONE: - return; - case RECREATE_DROP_UNUSED: dropUnused = true; - // don't break! case RECREATE: dropTables = true; - // don't break! + case CREATE_IF_NOT_EXISTS: + ifNotExists = SchemaAction.CREATE_IF_NOT_EXISTS.equals(schemaAction); case CREATE: - createTables(dropTables, dropUnused); + createTables(dropTables, dropUnused, ifNotExists); + case NONE: + default: + // do nothing } } - protected void createTables(boolean dropTables, boolean dropUnused) { + /* (non-Javadoc) */ + protected void createTables(boolean dropTables, boolean dropUnused, boolean ifNotExists) { - Metadata md = session.getCluster().getMetadata(); - KeyspaceMetadata kmd = md.getKeyspace(keyspaceName); - - // TODO: fix this with KeyspaceIdentifier - if (kmd == null) { // try lower-cased keyspace name - kmd = md.getKeyspace(keyspaceName.toLowerCase()); + if (dropTables) { + dropTables(dropUnused); } - if (kmd == null) { - throw new IllegalStateException(String.format("keyspace [%s] does not exist", keyspaceName)); - } - - for (TableMetadata table : kmd.getTables()) { - if (dropTables) { - if (dropUnused || mappingContext.usesTable(table)) { - admin.dropTable(cqlId(table.getName())); - } - } - } - - Collection> entities = converter.getMappingContext() - .getNonPrimaryKeyEntities(); + Collection> entities = + getConverter().getMappingContext().getNonPrimaryKeyEntities(); for (CassandraPersistentEntity entity : entities) { - admin.createTable(false, entity.getTableName(), entity.getType(), null); // TODO: allow spec of table options + // TODO: pass specification of user configurable table options + getCassandraAdminOperations().createTable(ifNotExists, entity.getTableName(), entity.getType(), null); } } - public SchemaAction getSchemaAction() { - return schemaAction; + /* (non-Javadoc) */ + @SuppressWarnings("all") + protected void dropTables(boolean dropUnused) { + + String keyspaceName = getKeyspaceName(); + + Metadata clusterMetadata = getSession().getCluster().getMetadata(); + KeyspaceMetadata keyspaceMetadata = clusterMetadata.getKeyspace(keyspaceName); + + // TODO: fix this with KeyspaceIdentifier + keyspaceMetadata = (keyspaceMetadata != null ? keyspaceMetadata + : clusterMetadata.getKeyspace(keyspaceName.toLowerCase())); + + Assert.state(keyspaceMetadata != null, String.format("keyspace [%s] does not exist", keyspaceName)); + + for (TableMetadata table : keyspaceMetadata.getTables()) { + if (dropUnused || getMappingContext().usesTable(table)) { + getCassandraAdminOperations().dropTable(cqlId(table.getName())); + } + } } + /* (non-Javadoc) */ + protected CassandraAdminOperations getCassandraAdminOperations() { + return this.admin; + } + + /* (non-Javadoc) */ + public void setConverter(CassandraConverter converter) { + Assert.notNull(converter, "CassandraConverter must not be null"); + this.converter = converter; + } + + /* (non-Javadoc) */ + public CassandraConverter getConverter() { + return this.converter; + } + + /* (non-Javadoc) */ + protected CassandraMappingContext getMappingContext() { + return getConverter().getMappingContext(); + } + + /* (non-Javadoc) */ public void setSchemaAction(SchemaAction schemaAction) { - Assert.notNull(schemaAction); + Assert.notNull(schemaAction, "SchemaAction must not be null"); this.schemaAction = schemaAction; } - public CassandraConverter getConverter() { - return converter; - } - - public void setConverter(CassandraConverter converter) { - Assert.notNull(converter); - this.converter = converter; - this.mappingContext = converter.getMappingContext(); + /* (non-Javadoc) */ + public SchemaAction getSchemaAction() { + return schemaAction; } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/SchemaAction.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/SchemaAction.java index 719918b68..5305b2d22 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/SchemaAction.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/SchemaAction.java @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.data.cassandra.config; /** * Enum identifying any schema actions to take at startup. * * @author Matthew T. Adams + * @author John Blum */ public enum SchemaAction { @@ -32,6 +34,11 @@ public enum SchemaAction { */ CREATE, + /** + * Create each table as necessary. Avoid table creation if the table already exists. + */ + CREATE_IF_NOT_EXISTS, + /** * Create each table as necessary, dropping the table first if it exists. */ @@ -44,11 +51,6 @@ public enum SchemaAction { // TODO: // /** - // * Validate that each required table and column exists. Fail if any required table or column does not exists. - // */ - // VALIDATE, - // - // /** // * Alter or create each table and column as necessary, leaving unused tables and columns untouched. // */ // UPDATE, @@ -56,5 +58,11 @@ public enum SchemaAction { // /** // * Alter or create each table and column as necessary, removing unused tables and columns. // */ - // UPDATE_DROP_UNUNSED; + // UPDATE_DROP_UNUNSED, + // + // /** + // * Validate that each required table and column exists. Fail if any required table or column does not exists. + // */ + // VALIDATE + } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/java/AbstractCassandraConfiguration.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/java/AbstractCassandraConfiguration.java index c971fe6e9..f13d79713 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/java/AbstractCassandraConfiguration.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/java/AbstractCassandraConfiguration.java @@ -19,15 +19,15 @@ import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.cassandra.config.java.AbstractClusterConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.cassandra.config.CassandraSessionFactoryBean; import org.springframework.data.cassandra.config.CassandraEntityClassScanner; +import org.springframework.data.cassandra.config.CassandraSessionFactoryBean; import org.springframework.data.cassandra.config.SchemaAction; import org.springframework.data.cassandra.convert.CassandraConverter; import org.springframework.data.cassandra.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.CassandraAdminOperations; import org.springframework.data.cassandra.core.CassandraAdminTemplate; -import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.data.cassandra.mapping.Table; import org.springframework.data.mapping.context.MappingContext; @@ -36,68 +36,43 @@ import org.springframework.data.mapping.context.MappingContext; * * @author Alex Shvid * @author Matthew T. Adams + * @author John Blum */ @Configuration -public abstract class AbstractCassandraConfiguration extends AbstractClusterConfiguration implements - BeanClassLoaderAware { - - protected abstract String getKeyspaceName(); +public abstract class AbstractCassandraConfiguration extends AbstractClusterConfiguration + implements BeanClassLoaderAware { protected ClassLoader beanClassLoader; - /** - * The {@link SchemaAction} to perform. Defaults to {@link SchemaAction#NONE}. - */ - public SchemaAction getSchemaAction() { - return SchemaAction.NONE; - } - - /** - * The base packages to scan for entities annotated with {@link Table} annotations. By default, returns the package - * name of {@literal this} (this.getClass().getPackage().getName()). This method must never return null. - */ - public String[] getEntityBasePackages() { - return new String[] { getClass().getPackage().getName() }; - } - @Bean public CassandraSessionFactoryBean session() throws Exception { - CassandraSessionFactoryBean bean = new CassandraSessionFactoryBean(); + CassandraSessionFactoryBean session = new CassandraSessionFactoryBean(); - bean.setCluster(cluster().getObject()); - bean.setConverter(cassandraConverter()); - bean.setSchemaAction(getSchemaAction()); - bean.setKeyspaceName(getKeyspaceName()); - bean.setStartupScripts(getStartupScripts()); - bean.setShutdownScripts(getShutdownScripts()); + session.setCluster(cluster().getObject()); + session.setConverter(cassandraConverter()); + session.setKeyspaceName(getKeyspaceName()); + session.setSchemaAction(getSchemaAction()); + session.setStartupScripts(getStartupScripts()); + session.setShutdownScripts(getShutdownScripts()); - return bean; - } - - /** - * Creates a {@link CassandraAdminTemplate}. - * - * @throws Exception - */ - @Bean - public CassandraAdminOperations cassandraTemplate() throws Exception { - return new CassandraAdminTemplate(session().getObject(), cassandraConverter()); + return session; } /** * Return the {@link MappingContext} instance to map Entities to properties. - * + * * @throws ClassNotFoundException */ @Bean public CassandraMappingContext cassandraMapping() throws ClassNotFoundException { - BasicCassandraMappingContext bean = new BasicCassandraMappingContext(); - bean.setInitialEntitySet(CassandraEntityClassScanner.scan(getEntityBasePackages())); - bean.setBeanClassLoader(beanClassLoader); + BasicCassandraMappingContext mappingContext = new BasicCassandraMappingContext(); - return bean; + mappingContext.setBeanClassLoader(beanClassLoader); + mappingContext.setInitialEntitySet(CassandraEntityClassScanner.scan(getEntityBasePackages())); + + return mappingContext; } /** @@ -108,8 +83,37 @@ public abstract class AbstractCassandraConfiguration extends AbstractClusterConf return new MappingCassandraConverter(cassandraMapping()); } + /** + * Creates a {@link CassandraAdminTemplate}. + * + * @throws Exception if the {@link com.datastax.driver.core.Session} could not be obtained. + */ + @Bean + public CassandraAdminOperations cassandraTemplate() throws Exception { + return new CassandraAdminTemplate(session().getObject(), cassandraConverter()); + } + @Override public void setBeanClassLoader(ClassLoader classLoader) { this.beanClassLoader = classLoader; } + + /** + * Base packages to scan for entities annotated with {@link Table} annotations. By default, returns the package + * name of {@literal this} (this.getClass().getPackage().getName()). + * + * This method must never return null. + */ + public String[] getEntityBasePackages() { + return new String[] { getClass().getPackage().getName() }; + } + + protected abstract String getKeyspaceName(); + + /** + * The {@link SchemaAction} to perform at startup. Defaults to {@link SchemaAction#NONE}. + */ + public SchemaAction getSchemaAction() { + return SchemaAction.NONE; + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/xml/CassandraNamespaceHandler.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/xml/CassandraNamespaceHandler.java index 5e3d39860..dc33fe7e2 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/xml/CassandraNamespaceHandler.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/xml/CassandraNamespaceHandler.java @@ -29,14 +29,12 @@ public class CassandraNamespaceHandler extends NamespaceHandlerSupport { @Override public void init() { - - registerBeanDefinitionParser("repositories", new RepositoryBeanDefinitionParser( - new CassandraRepositoryConfigurationExtension())); - registerBeanDefinitionParser("cluster", new CassandraClusterParser()); registerBeanDefinitionParser("session", new CassandraSessionParser()); registerBeanDefinitionParser("template", new CassandraTemplateParser()); registerBeanDefinitionParser("converter", new CassandraMappingConverterParser()); registerBeanDefinitionParser("mapping", new CassandraMappingContextParser()); + registerBeanDefinitionParser("repositories", new RepositoryBeanDefinitionParser( + new CassandraRepositoryConfigurationExtension())); } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java index 396544509..bec72349d 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java @@ -94,12 +94,25 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation setConverter(converter); } + private static CassandraConverter getDefaultCassandraConverter() { + + MappingCassandraConverter mappingCassandraConverter = new MappingCassandraConverter(); + mappingCassandraConverter.afterPropertiesSet(); + return mappingCassandraConverter; + } + + /** + * Set the {@link CassandraConverter} used by this template to perform conversions. + * + * @param cassandraConverter Converter used to perform conversion of Cassandra data types to entity types. + * Must not be {@literal null}. + */ public void setConverter(CassandraConverter cassandraConverter) { - Assert.notNull(cassandraConverter); + Assert.notNull(cassandraConverter, "CassandraConverter must not be null"); this.cassandraConverter = cassandraConverter; - mappingContext = cassandraConverter.getMappingContext(); + this.mappingContext = cassandraConverter.getMappingContext(); } @Override @@ -838,10 +851,6 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation /** * Generates a Query Object for an insert * - * @param tableName - * @param objectToSave - * @param entity - * @param optionsByName * @return The Query object to run with session.execute(); */ public static Insert createInsertQuery(String tableName, Object objectToSave, WriteOptions options, @@ -866,10 +875,6 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation /** * Generates a Query Object for an Update * - * @param tableName - * @param objectToSave - * @param entity - * @param optionsByName * @return The Query object to run with session.execute(); */ public static Update createUpdateQuery(String tableName, Object objectToSave, WriteOptions options, @@ -894,10 +899,6 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation /** * Generates a Batch Object for multiple Updates * - * @param tableName - * @param objectsToSave - * @param entity - * @param optionsByName * @return The Query object to run with session.execute(); */ public static Batch createUpdateBatchQuery(String tableName, List objectsToSave, WriteOptions options, @@ -917,10 +918,6 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation /** * Generates a Batch Object for multiple inserts * - * @param tableName - * @param entities - * @param entity - * @param optionsByName * @return The Query object to run with session.execute(); */ public static Batch createInsertBatchQuery(String tableName, List entities, WriteOptions options, @@ -939,12 +936,6 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation /** * Create a Delete Query Object from an annotated POJO - * - * @param tableName - * @param object - * @param entity - * @param optionsByName - * @return */ public static Delete createDeleteQuery(String tableName, Object object, QueryOptions options, EntityWriter entityWriter) { @@ -959,12 +950,6 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation /** * Create a Batch Query object for multiple deletes. - * - * @param tableName - * @param entities - * @param entity - * @param optionsByName - * @return */ public static Batch createDeleteBatchQuery(String tableName, List entities, QueryOptions options, EntityWriter entityWriter) { diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java index cd871f283..da52a1530 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java @@ -153,7 +153,7 @@ public class BasicCassandraMappingContext extends @Override public boolean usesTable(TableMetadata table) { - return entitySetsByTableName.containsKey(table.getName()); + return entitySetsByTableName.containsKey(cqlId(table.getName())); } @Override diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBeanUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBeanUnitTests.java new file mode 100644 index 000000000..91bab047f --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBeanUnitTests.java @@ -0,0 +1,342 @@ +/* + * Copyright 2013-2016 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.cassandra.config; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.data.cassandra.config.CassandraSessionFactoryBean.DEFAULT_CREATE_IF_NOT_EXISTS; +import static org.springframework.data.cassandra.config.CassandraSessionFactoryBean.DEFAULT_DROP_TABLES; +import static org.springframework.data.cassandra.config.CassandraSessionFactoryBean.DEFAULT_DROP_UNUSED_TABLES; + +import java.util.Collections; +import java.util.Map; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.core.CassandraAdminOperations; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.Metadata; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.TableMetadata; + +/** + * The CassandraSessionFactoryBeanUnitTests class is a test suite of test cases testing the contract and functionality + * of the {@link CassandraSessionFactoryBean} class. + * + * @author John Blum + * @see org.junit.Rule + * @see org.junit.Test + * @see org.junit.rules.ExpectedException + * @see org.junit.runner.RunWith + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.runners.MockitoJUnitRunner + * @see org.springframework.data.cassandra.config.CassandraSessionFactoryBean + * @since 1.5.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class CassandraSessionFactoryBeanUnitTests { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Mock + private CassandraConverter mockConverter; + + @Mock + private Cluster mockCluster; + + @Mock + private Session mockSession; + + private CassandraSessionFactoryBean factoryBean; + + @Before + public void setup() { + when(mockCluster.connect()).thenReturn(mockSession); + when(mockSession.getCluster()).thenReturn(mockCluster); + + factoryBean = spy(new CassandraSessionFactoryBean()); + factoryBean.setCluster(mockCluster); + } + + protected CqlIdentifier newCqlIdentifier(String id) { + return new CqlIdentifier(id, false); + } + + @Test + public void afterPropertiesSetPerformsSchemaAction() throws Exception { + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocationOnMock) throws Throwable { + assertThat(factoryBean.getSchemaAction(), is(equalTo(SchemaAction.RECREATE))); + return null; + } + }).when(factoryBean).performSchemaAction(); + + factoryBean.setConverter(mockConverter); + factoryBean.setSchemaAction(SchemaAction.RECREATE); + + assertThat(factoryBean.getConverter(), is(equalTo(mockConverter))); + assertThat(factoryBean.getSchemaAction(), is(equalTo(SchemaAction.RECREATE))); + + factoryBean.afterPropertiesSet(); + + assertThat(factoryBean.getCassandraAdminOperations(), is(notNullValue(CassandraAdminOperations.class))); + assertThat(factoryBean.getObject(), is(equalTo(mockSession))); + + verify(factoryBean, times(1)).performSchemaAction(); + } + + @Test + public void afterPropertiesSetThrowsIllegalStateExceptionWhenConverterIsNull() throws Exception { + exception.expect(IllegalStateException.class); + exception.expectCause(is(nullValue(Throwable.class))); + exception.expectMessage("Converter must not be null"); + + factoryBean.setCluster(mockCluster); + factoryBean.afterPropertiesSet(); + } + + protected void performSchemaActionCallsCreateTableWithArgumentsMatchingTheSchemaAction(SchemaAction schemaAction, + final boolean dropTables, final boolean dropUnused, final boolean ifNotExists) { + + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocationOnMock) throws Throwable { + assertThat(invocationOnMock.getArgumentAt(0, Boolean.class), is(equalTo(dropTables))); + assertThat(invocationOnMock.getArgumentAt(1, Boolean.class), is(equalTo(dropUnused))); + assertThat(invocationOnMock.getArgumentAt(2, Boolean.class), is(equalTo(ifNotExists))); + return null; + } + }).when(factoryBean).createTables(anyBoolean(), anyBoolean(), anyBoolean()); + + factoryBean.setSchemaAction(schemaAction); + + assertThat(factoryBean.getSchemaAction(), is(equalTo(schemaAction))); + + factoryBean.performSchemaAction(); + + verify(factoryBean, times(1)).createTables(eq(dropTables), eq(dropUnused), eq(ifNotExists)); + } + + @Test + public void performsSchemaActionCreatesTablesWithDefaults() { + performSchemaActionCallsCreateTableWithArgumentsMatchingTheSchemaAction(SchemaAction.CREATE, + DEFAULT_DROP_TABLES, DEFAULT_DROP_UNUSED_TABLES, DEFAULT_CREATE_IF_NOT_EXISTS); + } + + @Test + public void performsSchemaActionCreatesTablesIfNotExists() { + performSchemaActionCallsCreateTableWithArgumentsMatchingTheSchemaAction(SchemaAction.CREATE_IF_NOT_EXISTS, + DEFAULT_DROP_TABLES, DEFAULT_DROP_UNUSED_TABLES, true); + } + + @Test + public void performsSchemaActionRecreatesTables() { + performSchemaActionCallsCreateTableWithArgumentsMatchingTheSchemaAction(SchemaAction.RECREATE, true, + DEFAULT_DROP_UNUSED_TABLES, DEFAULT_CREATE_IF_NOT_EXISTS); + } + + @Test + public void performsSchemaActionRecreatesAndDropsUnusedTables() { + performSchemaActionCallsCreateTableWithArgumentsMatchingTheSchemaAction(SchemaAction.RECREATE_DROP_UNUSED, + true, true, DEFAULT_CREATE_IF_NOT_EXISTS); + } + + @Test + public void performsSchemaActionDoesNotCallCreateTablesWhenSchemaActionIsNone() { + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocationOnMock) throws Throwable { + fail("'createTables(..)' should not have been called"); + return null; + } + }).when(factoryBean).createTables(anyBoolean(), anyBoolean(), anyBoolean()); + + factoryBean.setSchemaAction(SchemaAction.NONE); + + assertThat(factoryBean.getSchemaAction(), is(equalTo(SchemaAction.NONE))); + + factoryBean.performSchemaAction(); + + verify(factoryBean, never()).createTables(anyBoolean(), anyBoolean(), anyBoolean()); + } + + @Test + @SuppressWarnings("unchecked") + public void createsTableForEntity() throws Exception { + Metadata mockMetadata = mock(Metadata.class); + KeyspaceMetadata mockKeyspaceMetadata = mock(KeyspaceMetadata.class); + CassandraMappingContext mockMappingContext = mock(CassandraMappingContext.class); + CassandraPersistentEntity mockPersistentEntity = mock(CassandraPersistentEntity.class); + CassandraAdminOperations mockCassandraAdminOperations = mock(CassandraAdminOperations.class); + + doReturn(mockCassandraAdminOperations).when(factoryBean).getCassandraAdminOperations(); + doReturn(mockSession).when(factoryBean).getObject(); + when(mockCluster.getMetadata()).thenReturn(mockMetadata); + when(mockMetadata.getKeyspace(eq("TestKeyspace"))).thenReturn(mockKeyspaceMetadata); + when(mockKeyspaceMetadata.getTables()).thenReturn(Collections.emptyList()); + when(mockConverter.getMappingContext()).thenReturn(mockMappingContext); + when(mockMappingContext.getNonPrimaryKeyEntities()).thenReturn( + Collections.>singletonList(mockPersistentEntity)); + when(mockPersistentEntity.getTableName()).thenReturn(newCqlIdentifier("TestTable")); + when(mockPersistentEntity.getType()).thenReturn(Person.class); + + factoryBean.setConverter(mockConverter); + factoryBean.setKeyspaceName("TestKeyspace"); + + assertThat(factoryBean.getConverter(), is(equalTo(mockConverter))); + + factoryBean.createTables(true, false, false); + + verify(mockSession, times(1)).getCluster(); + verify(mockCluster, times(1)).getMetadata(); + verify(mockMetadata, times(1)).getKeyspace(eq("TestKeyspace")); + verify(mockKeyspaceMetadata, times(1)).getTables(); + verify(mockConverter, times(1)).getMappingContext(); + verify(mockMappingContext, times(1)).getNonPrimaryKeyEntities(); + verify(mockPersistentEntity, times(1)).getTableName(); + verify(mockPersistentEntity, times(1)).getType(); + verify(mockCassandraAdminOperations, times(1)).createTable(eq(false), eq(newCqlIdentifier("TestTable")), + eq(Person.class), isNull(Map.class)); + } + + @Test + @SuppressWarnings("unchecked") + public void createTableForEntityIfNotExists() { + CassandraMappingContext mockMappingContext = mock(CassandraMappingContext.class); + CassandraPersistentEntity mockPersistentEntity = mock(CassandraPersistentEntity.class); + CassandraAdminOperations mockCassandraAdminOperations = mock(CassandraAdminOperations.class); + + doReturn(mockCassandraAdminOperations).when(factoryBean).getCassandraAdminOperations(); + doReturn(mockSession).when(factoryBean).getObject(); + when(mockConverter.getMappingContext()).thenReturn(mockMappingContext); + when(mockMappingContext.getNonPrimaryKeyEntities()).thenReturn( + Collections.>singletonList(mockPersistentEntity)); + when(mockPersistentEntity.getTableName()).thenReturn(newCqlIdentifier("TestTable")); + when(mockPersistentEntity.getType()).thenReturn(Person.class); + + factoryBean.setConverter(mockConverter); + factoryBean.setKeyspaceName("TestKeyspace"); + + assertThat(factoryBean.getConverter(), is(equalTo(mockConverter))); + + factoryBean.createTables(false, false, true); + + verify(mockSession, never()).getCluster(); + verify(mockCluster, never()).getMetadata(); + verify(mockConverter, times(1)).getMappingContext(); + verify(mockMappingContext, times(1)).getNonPrimaryKeyEntities(); + verify(mockPersistentEntity, times(1)).getTableName(); + verify(mockPersistentEntity, times(1)).getType(); + verify(mockCassandraAdminOperations, times(1)).createTable(eq(true), eq(newCqlIdentifier("TestTable")), + eq(Person.class), isNull(Map.class)); + } + + @Test + public void createTableThrowsIllegalStateExceptionWhenKeyspaceNotFound() { + Metadata mockMetadata = mock(Metadata.class); + + doReturn(mockSession).when(factoryBean).getObject(); + when(mockCluster.getMetadata()).thenReturn(mockMetadata); + when(mockMetadata.getKeyspace(anyString())).thenReturn(null); + + exception.expect(IllegalStateException.class); + exception.expectCause(is(nullValue(Throwable.class))); + exception.expectMessage("keyspace [TestKeyspace] does not exist"); + + factoryBean.setKeyspaceName("TestKeyspace"); + factoryBean.createTables(true, false, true); + + verify(mockSession, times(1)).getCluster(); + verify(mockCluster, times(1)).getMetadata(); + verify(mockMetadata, times(1)).getKeyspace(eq("TestKeyspace")); + verify(mockMetadata, times(1)).getKeyspace(eq("testkeyspace")); + } + + // TODO: add more createTable tests covering drop tables, etc + + @Test + public void setAndGetConverter() { + assertThat(factoryBean.getConverter(), is(nullValue())); + factoryBean.setConverter(mockConverter); + assertThat(factoryBean.getConverter(), is(equalTo(mockConverter))); + verifyZeroInteractions(mockConverter); + } + + @Test + public void setConverterToNull() { + exception.expect(IllegalArgumentException.class); + exception.expectCause(is(nullValue(Throwable.class))); + exception.expectMessage("CassandraConverter must not be null"); + + factoryBean.setConverter(null); + } + + @Test + public void setAndGetSchemaAction() { + assertThat(factoryBean.getSchemaAction(), is(equalTo(SchemaAction.NONE))); + factoryBean.setSchemaAction(SchemaAction.CREATE); + assertThat(factoryBean.getSchemaAction(), is(equalTo(SchemaAction.CREATE))); + factoryBean.setSchemaAction(SchemaAction.NONE); + assertThat(factoryBean.getSchemaAction(), is(equalTo(SchemaAction.NONE))); + } + + @Test + public void setSchemaActionToNullThrowsIllegalArgumentException() { + exception.expect(IllegalArgumentException.class); + exception.expectCause(is(nullValue(Throwable.class))); + exception.expectMessage("SchemaAction must not be null"); + + factoryBean.setSchemaAction(null); + } + + static class Person { + } + +} diff --git a/spring-data-cassandra/src/test/resources/spring-data-cassandra-basic.xml b/spring-data-cassandra/src/test/resources/spring-data-cassandra-basic.xml index 672f4dc0e..ac2921e2c 100644 --- a/spring-data-cassandra/src/test/resources/spring-data-cassandra-basic.xml +++ b/spring-data-cassandra/src/test/resources/spring-data-cassandra-basic.xml @@ -1,11 +1,13 @@ + xmlns:cass="http://www.springframework.org/schema/data/cassandra" + xmlns:context="http://www.springframework.org/schema/context" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + 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 + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd +"> @@ -16,6 +18,6 @@ - +