diff --git a/spring-geode-project/spring-geode/spring-geode.gradle b/spring-geode-project/spring-geode/spring-geode.gradle index 3b6ff0c5..09c6530a 100644 --- a/spring-geode-project/spring-geode/spring-geode.gradle +++ b/spring-geode-project/spring-geode/spring-geode.gradle @@ -32,6 +32,7 @@ dependencies { testImplementation "org.springframework.boot:spring-boot-starter-data-cassandra" testImplementation "org.springframework.data:spring-data-geode-test" testImplementation "org.testcontainers:testcontainers" + testImplementation "org.testcontainers:cassandra" testRuntimeOnly "org.hsqldb:hsqldb" diff --git a/spring-geode-project/spring-geode/src/test/java/example/app/crm/config/TestCassandraConfiguration.java b/spring-geode-project/spring-geode/src/test/java/example/app/crm/config/TestCassandraConfiguration.java index 66dd6073..7168fde0 100644 --- a/spring-geode-project/spring-geode/src/test/java/example/app/crm/config/TestCassandraConfiguration.java +++ b/spring-geode-project/spring-geode/src/test/java/example/app/crm/config/TestCassandraConfiguration.java @@ -15,6 +15,7 @@ */ package example.app.crm.config; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newRuntimeException; import java.io.BufferedReader; @@ -22,89 +23,196 @@ import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.stream.Collectors; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.session.Session; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Bean; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.data.cassandra.config.AbstractCassandraConfiguration; +import org.springframework.data.cassandra.SessionFactory; import org.springframework.data.cassandra.config.CqlSessionFactoryBean; -import org.springframework.data.gemfire.tests.util.IOUtils; +import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.data.cassandra.core.cql.CqlTemplate; +import org.springframework.data.cassandra.core.cql.RowMapper; +import org.springframework.data.cassandra.core.cql.session.init.KeyspacePopulator; +import org.springframework.data.cassandra.core.cql.session.init.SessionFactoryInitializer; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; +import example.app.crm.model.Customer; + /** * Base test configuration used to configure and bootstrap an Apache Cassandra database with a schema and data. * * @author John Blum + * @see com.datastax.oss.driver.api.core.session.Session + * @see org.springframework.beans.factory.config.BeanPostProcessor + * @see org.springframework.context.annotation.Bean * @see org.springframework.core.io.Resource - * @see org.springframework.data.cassandra.config.AbstractCassandraConfiguration + * @see org.springframework.data.cassandra.SessionFactory * @see org.springframework.data.cassandra.config.CqlSessionFactoryBean + * @see org.springframework.data.cassandra.core.CassandraTemplate + * @see org.springframework.data.cassandra.core.cql.CqlTemplate + * @see org.springframework.data.cassandra.core.cql.session.init.KeyspacePopulator + * @see org.springframework.data.cassandra.core.cql.session.init.SessionFactoryInitializer * @since 1.1.0 */ -public abstract class TestCassandraConfiguration extends AbstractCassandraConfiguration { +@SuppressWarnings("unused") +public abstract class TestCassandraConfiguration { protected static final int CASSANDRA_DEFAULT_PORT = CqlSessionFactoryBean.DEFAULT_PORT; - private static final String CASSANDRA_DATA_CQL = "cassandra-data.cql"; - private static final String CASSANDRA_SCHEMA_CQL = "cassandra-schema.cql"; - private static final String LOCAL_DATA_CENTER = "datacenter1"; - private static final String KEYSPACE_NAME = "CustomerService"; - private static final String SESSION_NAME = "CustomerServiceCluster"; + protected static final String CASSANDRA_DATA_CQL = "cassandra-data.cql"; + protected static final String CASSANDRA_SCHEMA_CQL = "cassandra-schema.cql"; - @NonNull - @Override - protected String getKeyspaceName() { - return KEYSPACE_NAME; + private static final String COMMENT_LINE_PREFIX = "--"; + + protected static final String LOCAL_DATA_CENTER = "datacenter1"; + protected static final String KEYSPACE_NAME = "CustomerService"; + + @Bean + SessionFactoryInitializer sessionFactoryInitializer(SessionFactory sessionFactory) { + + SessionFactoryInitializer sessionFactoryInitializer = new SessionFactoryInitializer(); + + KeyspacePopulator keyspacePopulator = + // cqlSession -> loadCassandraCqlScripts().forEach(cqlSession::execute); + cqlSession -> loadCassandraDataCqlScript().forEach(cqlSession::execute); + + sessionFactoryInitializer.setKeyspacePopulator(keyspacePopulator); + sessionFactoryInitializer.setSessionFactory(sessionFactory); + + return sessionFactoryInitializer; } - @Override - protected String getLocalDataCenter() { - return LOCAL_DATA_CENTER; + protected List loadCassandraCqlScripts() { + + List cassandraCqlStatements = new ArrayList<>(); + + cassandraCqlStatements.addAll(loadCassandraSchemaCqlScript()); + cassandraCqlStatements.addAll(loadCassandraDataCqlScript()); + + return cassandraCqlStatements; } - @Nullable - @Override - protected String getSessionName() { - return SESSION_NAME; + protected List loadCassandraDataCqlScript() { + return readLines(new ClassPathResource(CASSANDRA_DATA_CQL)); } - /* - @Nullable @Override - protected KeyspacePopulator keyspacePopulator() { - return cqlSession -> loadCassandraCqlScripts().forEach(cqlSession::execute); - } - */ - - // TODO: Remove use of deprecation after Spring Data for Apache Cassandra issues are resolved! - @Override - protected List getStartupScripts() { - - List startupScripts = new ArrayList<>(super.getStartupScripts()); - - startupScripts.addAll(readLines(new ClassPathResource(CASSANDRA_SCHEMA_CQL))); - startupScripts.addAll(readLines(new ClassPathResource(CASSANDRA_DATA_CQL))); - - return startupScripts; + protected List loadCassandraSchemaCqlScript() { + return readLines(new ClassPathResource(CASSANDRA_SCHEMA_CQL)); } private @NonNull List readLines(@NonNull Resource resource) { - BufferedReader resourceReader = null; - - try { - - resourceReader = new BufferedReader(new InputStreamReader(resource.getInputStream())); - + try (BufferedReader resourceReader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) { return resourceReader.lines() - .filter(StringUtils::hasText) + .filter(cqlPredicate()) .collect(Collectors.toList()); } catch (IOException cause) { throw newRuntimeException(cause, "Failed to read from Resource [%s]", resource); } - finally { - IOUtils.close(resourceReader); - } + } + + private @NonNull Predicate cqlPredicate() { + + Predicate cqlPredicate = StringUtils::hasText; + + cqlPredicate.and(this::isNotCommentLine); + + return cqlPredicate; + } + + private boolean isCommentLine(@Nullable String line) { + return String.valueOf(line).trim().startsWith(COMMENT_LINE_PREFIX); + } + + private boolean isNotCommentLine(@Nullable String line) { + return !isCommentLine(line); + } + + @Bean + BeanPostProcessor cassandraTemplatePostProcessor() { + + return new BeanPostProcessor() { + + @org.jetbrains.annotations.Nullable @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + + if (bean instanceof CassandraTemplate cassandraTemplate) { + + Consumer cassandraTemplateConsumer = noopCassandraTemplateConsumer() + .andThen(entityObjectInsertingCassandraTemplateConsumer()) + .andThen(entityObjectAssertingCassandraTemplateConsumer()); + //.andThen(entityTableNameAssertingCassandraTemplateConsumer()) + //.andThen(keyspaceNameAssertingCassandraTemplateConsumer()); + + cassandraTemplateConsumer.accept(cassandraTemplate); + } + + return bean; + } + }; + } + + private Consumer entityCountAssertingCassandraTemplateConsumer() { + return cassandraTemplate -> assertThat(cassandraTemplate.count(Customer.class)).isOne(); + } + + private Consumer entityObjectAssertingCassandraTemplateConsumer() { + return cassandraTemplate -> { + + String cql = "SELECT id, name FROM Customers"; + + RowMapper customerRowMapper = (row, rowNumber) -> + Customer.newCustomer(row.getLong("id"), row.getString("name")); + + Customer actualCustomer = cassandraTemplate.getCqlOperations().queryForObject(cql, customerRowMapper); + Customer expectedCustomer = Customer.newCustomer(16L, "Pie Doe"); + + assertThat(actualCustomer).isEqualTo(expectedCustomer); + }; + } + + // TODO: Why does this work and the CQL data script not work! + private Consumer entityObjectInsertingCassandraTemplateConsumer() { + return cassandraTemplate -> cassandraTemplate.insert(Customer.newCustomer(16L, "Pie Doe")); + } + + private Consumer entityTableNameAssertingCassandraTemplateConsumer() { + + return cassandraTemplate -> + assertThat(cassandraTemplate.getTableName(Customer.class).toString()).endsWithIgnoringCase("Customers"); + } + + private Consumer keyspaceNameAssertingCassandraTemplateConsumer() { + + return cassandraTemplate -> { + + String resolvedKeyspaceName = Optional.of(cassandraTemplate) + .map(CassandraTemplate::getCqlOperations) + .filter(CqlTemplate.class::isInstance) + .map(CqlTemplate.class::cast) + .map(CqlTemplate::getSessionFactory) + .map(SessionFactory::getSession) + .flatMap(Session::getKeyspace) + .map(CqlIdentifier::toString) + .orElse(null); + + assertThat(resolvedKeyspaceName).isEqualToIgnoringCase(KEYSPACE_NAME); + }; + } + + private Consumer noopCassandraTemplateConsumer() { + return cassandraTemplate -> {}; } } diff --git a/spring-geode-project/spring-geode/src/test/java/example/app/crm/config/TestcontainersCassandraConfiguration.java b/spring-geode-project/spring-geode/src/test/java/example/app/crm/config/TestcontainersCassandraConfiguration.java index 1a928981..9fbadc30 100644 --- a/spring-geode-project/spring-geode/src/test/java/example/app/crm/config/TestcontainersCassandraConfiguration.java +++ b/spring-geode-project/spring-geode/src/test/java/example/app/crm/config/TestcontainersCassandraConfiguration.java @@ -15,30 +15,46 @@ */ package example.app.crm.config; +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.InetSocketAddress; + +import com.datastax.oss.driver.api.core.CqlSession; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.cassandra.CqlSessionBuilderCustomizer; +import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import org.springframework.lang.NonNull; +import org.testcontainers.containers.CassandraContainer; import org.testcontainers.containers.GenericContainer; +import example.app.crm.model.Customer; + /** * Spring {@link @Configuration} for Apache Cassandra using Testcontainers. * * @author John Blum + * @see com.datastax.oss.driver.api.core.CqlSession * @see org.springframework.context.annotation.Bean * @see org.springframework.context.annotation.Configuration * @see org.springframework.context.annotation.Profile + * @see org.testcontainers.containers.CassandraContainer * @see org.testcontainers.containers.GenericContainer * @since 1.1.0 */ @Configuration @Profile("inline-caching-cassandra") +@EntityScan(basePackageClasses = Customer.class) @SuppressWarnings("unused") public class TestcontainersCassandraConfiguration extends TestCassandraConfiguration { private static final String CASSANDRA_DOCKER_IMAGE_NAME = "cassandra:latest"; - @Bean + @Bean("CassandraContainer") @SuppressWarnings("rawtypes") GenericContainer cassandraContainer() { @@ -46,32 +62,89 @@ public class TestcontainersCassandraConfiguration extends TestCassandraConfigura cassandraContainer.start(); + return withCassandraServer(cassandraContainer); + } + + @SuppressWarnings("rawtypes") + private @NonNull GenericContainer newCassandraContainer() { + return new CassandraContainer(CASSANDRA_DOCKER_IMAGE_NAME) + .withInitScript(CASSANDRA_SCHEMA_CQL) + .withExposedPorts(CASSANDRA_DEFAULT_PORT) + .withReuse(true); + } + + @SuppressWarnings("rawtypes") + private @NonNull GenericContainer newCustomCassandraContainer() { + + return newCassandraContainer() + .withEnv("CASSANDRA_SNITCH", "GossipingPropertyFileSnitch") + .withEnv("HEAP_NEWSIZE", "128M") + .withEnv("MAX_HEAP_SIZE", "1024M") + .withEnv("JVM_OPTS", "-Dcassandra.skip_wait_for_gossip_to_settle=0 -Dcassandra.initial_token=0"); + } + + private @NonNull CqlSession newCqlSession(@NonNull GenericContainer cassandraContainer) { + + return CqlSession.builder() + .addContactPoint(resolveContactPoint(cassandraContainer)) + .withLocalDatacenter(LOCAL_DATA_CENTER) + .build(); + } + + private @NonNull GenericContainer withCassandraServer(@NonNull GenericContainer cassandraContainer) { + + cassandraContainer = initializeCassandraServer(cassandraContainer); + //cassandraContainer = assertCassandraServerSetup(cassandraContainer); + return cassandraContainer; } - @SuppressWarnings("rawtypes") - private GenericContainer newCassandraContainer() { - return new GenericContainer(CASSANDRA_DOCKER_IMAGE_NAME) - .withExposedPorts(CASSANDRA_DEFAULT_PORT); + private GenericContainer initializeCassandraServer(GenericContainer cassandraContainer) { + + try (CqlSession session = newCqlSession(cassandraContainer)) { + //loadCassandraCqlScripts().forEach(session::execute); + loadCassandraSchemaCqlScript().forEach(session::execute); + } + + return cassandraContainer; } - @SuppressWarnings("rawtypes") - private GenericContainer newCustomCassandraContainer() { + private GenericContainer assertCassandraServerSetup(GenericContainer cassandraContainer) { - return newCassandraContainer() - .withEnv("HEAP_NEWSIZE", "128M") - .withEnv("MAX_HEAP_SIZE", "1024M") - .withEnv("JVM_OPTS", "-Dcassandra.skip_wait_for_gossip_to_settle=0 -Dcassandra.initial_token=0") - .withEnv("CASSANDRA_SNITCH", "GossipingPropertyFileSnitch"); + try (CqlSession session = newCqlSession(cassandraContainer)) { + + session.getMetadata().getKeyspace(KEYSPACE_NAME) + .map(keyspaceMetadata -> { + + assertThat(keyspaceMetadata.getName().toString()).isEqualToIgnoringCase(KEYSPACE_NAME); + + keyspaceMetadata.getTable("Customers") + .map(tableMetadata -> { + assertThat(tableMetadata.getName().toString()).isEqualToIgnoringCase("Customers"); + assertThat(tableMetadata.getKeyspace().toString()).isEqualToIgnoringCase(KEYSPACE_NAME); + return tableMetadata; + }) + .orElseThrow(() -> new IllegalStateException("Table [Customers] not found")); + + return keyspaceMetadata; + }) + .orElseThrow(() -> new IllegalStateException(String.format("Keyspace [%s] not found", KEYSPACE_NAME))); + } + + return cassandraContainer; } - @Override - protected String getContactPoints() { - return cassandraContainer().getContainerIpAddress(); + @Bean + CqlSessionBuilderCustomizer cqlSessionBuilderCustomizer( + @Qualifier("CassandraContainer") GenericContainer cassandraContainer) { + + return cqlSessionBuilder -> cqlSessionBuilder.addContactPoint(resolveContactPoint(cassandraContainer)) + .withLocalDatacenter(LOCAL_DATA_CENTER) + .withKeyspace(KEYSPACE_NAME); } - @Override - protected int getPort() { - return cassandraContainer().getFirstMappedPort(); + private InetSocketAddress resolveContactPoint(GenericContainer cassandraContainer) { + return new InetSocketAddress(cassandraContainer.getHost(), + cassandraContainer.getMappedPort(CASSANDRA_DEFAULT_PORT)); } } diff --git a/spring-geode-project/spring-geode/src/test/java/example/app/crm/model/Customer.java b/spring-geode-project/spring-geode/src/test/java/example/app/crm/model/Customer.java index 8ea185ba..249bc0bc 100644 --- a/spring-geode-project/spring-geode/src/test/java/example/app/crm/model/Customer.java +++ b/spring-geode-project/spring-geode/src/test/java/example/app/crm/model/Customer.java @@ -32,8 +32,10 @@ import lombok.NoArgsConstructor; * The {@link Customer} class is an Abstract Data Type (ADT) modeling a customer. * * @author John Blum - * @see lombok + * @see jakarta.persistence.Entity + * @see jakarta.persistence.Table * @see org.springframework.data.annotation.Id + * @see org.springframework.data.cassandra.core.mapping.Table * @see org.springframework.data.gemfire.mapping.annotation.Region * @since 1.1.0 */ diff --git a/spring-geode-project/spring-geode/src/test/java/org/springframework/geode/cache/inline/AbstractInlineCachingWithExternalDataSourceIntegrationTests.java b/spring-geode-project/spring-geode/src/test/java/org/springframework/geode/cache/inline/AbstractInlineCachingWithExternalDataSourceIntegrationTests.java index 4a6daddc..6e9ab8f5 100644 --- a/spring-geode-project/spring-geode/src/test/java/org/springframework/geode/cache/inline/AbstractInlineCachingWithExternalDataSourceIntegrationTests.java +++ b/spring-geode-project/spring-geode/src/test/java/org/springframework/geode/cache/inline/AbstractInlineCachingWithExternalDataSourceIntegrationTests.java @@ -41,8 +41,6 @@ import example.app.crm.repo.CustomerRepository; @SuppressWarnings("unused") public abstract class AbstractInlineCachingWithExternalDataSourceIntegrationTests extends IntegrationTestsSupport { - public static final String GEMFIRE_LOG_LEVEL = "off"; - @Autowired private CustomerRepository customerRepository; diff --git a/spring-geode-project/spring-geode/src/test/java/org/springframework/geode/cache/inline/cassandra/InlineCachingWithCassandraIntegrationTests.java b/spring-geode-project/spring-geode/src/test/java/org/springframework/geode/cache/inline/cassandra/InlineCachingWithCassandraIntegrationTests.java index 3e34a0a3..d9ed1121 100644 --- a/spring-geode-project/spring-geode/src/test/java/org/springframework/geode/cache/inline/cassandra/InlineCachingWithCassandraIntegrationTests.java +++ b/spring-geode-project/spring-geode/src/test/java/org/springframework/geode/cache/inline/cassandra/InlineCachingWithCassandraIntegrationTests.java @@ -23,7 +23,6 @@ import org.apache.geode.cache.GemFireCache; import org.apache.geode.cache.client.ClientRegionShortcut; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; @@ -69,8 +68,8 @@ import example.app.crm.repo.CustomerRepository; public class InlineCachingWithCassandraIntegrationTests extends AbstractInlineCachingWithExternalDataSourceIntegrationTests { - @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, CassandraDataAutoConfiguration.class }) - @ClientCacheApplication(logLevel = GEMFIRE_LOG_LEVEL) + @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) + @ClientCacheApplication @EnableCassandraRepositories(basePackageClasses = CustomerRepository.class) @EnableEntityDefinedRegions(basePackageClasses = Customer.class, clientRegionShortcut = ClientRegionShortcut.LOCAL) @Import(TestcontainersCassandraConfiguration.class) diff --git a/spring-geode-project/spring-geode/src/test/java/org/springframework/geode/cache/inline/database/InlineCachingWithDatabaseIntegrationTests.java b/spring-geode-project/spring-geode/src/test/java/org/springframework/geode/cache/inline/database/InlineCachingWithDatabaseIntegrationTests.java index b250d049..d821f879 100644 --- a/spring-geode-project/spring-geode/src/test/java/org/springframework/geode/cache/inline/database/InlineCachingWithDatabaseIntegrationTests.java +++ b/spring-geode-project/spring-geode/src/test/java/org/springframework/geode/cache/inline/database/InlineCachingWithDatabaseIntegrationTests.java @@ -68,7 +68,7 @@ public class InlineCachingWithDatabaseIntegrationTests extends AbstractInlineCachingWithExternalDataSourceIntegrationTests { @SpringBootApplication(exclude = { CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class }) - @ClientCacheApplication(logLevel = GEMFIRE_LOG_LEVEL) + @ClientCacheApplication @EntityScan(basePackageClasses = Customer.class) @EnableEntityDefinedRegions(basePackageClasses = Customer.class, clientRegionShortcut = ClientRegionShortcut.LOCAL) @EnableJpaRepositories(basePackageClasses = CustomerRepository.class) diff --git a/spring-geode-project/spring-geode/src/test/resources/application-inline-caching-cassandra.properties b/spring-geode-project/spring-geode/src/test/resources/application-inline-caching-cassandra.properties new file mode 100644 index 00000000..6f7ca387 --- /dev/null +++ b/spring-geode-project/spring-geode/src/test/resources/application-inline-caching-cassandra.properties @@ -0,0 +1,5 @@ +# Spring Boot application.properties for Apache Cassandra configuration of Inline Caching + +spring.data.cassandra.keyspace-name=CustomerService +spring.data.cassandra.schema-action=recreate +spring.data.cassandra.local-datacenter=datacenter1 diff --git a/spring-geode-project/spring-geode/src/test/resources/application-inline-caching-database.properties b/spring-geode-project/spring-geode/src/test/resources/application-inline-caching-database.properties index 3516ffb2..06254c81 100644 --- a/spring-geode-project/spring-geode/src/test/resources/application-inline-caching-database.properties +++ b/spring-geode-project/spring-geode/src/test/resources/application-inline-caching-database.properties @@ -1,4 +1,4 @@ -# RBMS (Database) configuration properties +# Spring Boot application.properties for RDBMS (Database) configuration of Inline Caching spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=none diff --git a/spring-geode-project/spring-geode/src/test/resources/cassandra-data.cql b/spring-geode-project/spring-geode/src/test/resources/cassandra-data.cql index 9c64b2f2..732dfb2f 100644 --- a/spring-geode-project/spring-geode/src/test/resources/cassandra-data.cql +++ b/spring-geode-project/spring-geode/src/test/resources/cassandra-data.cql @@ -1 +1 @@ -INSERT INTO customers (id, name) VALUES (16, 'Pie Doe'); +INSERT INTO Customers (id, name) VALUES (16, 'Pie Doe'); diff --git a/spring-geode-project/spring-geode/src/test/resources/cassandra-schema.cql b/spring-geode-project/spring-geode/src/test/resources/cassandra-schema.cql index 89ddcf17..f8dfd91c 100644 --- a/spring-geode-project/spring-geode/src/test/resources/cassandra-schema.cql +++ b/spring-geode-project/spring-geode/src/test/resources/cassandra-schema.cql @@ -1,4 +1,4 @@ CREATE KEYSPACE IF NOT EXISTS CustomerService WITH replication = { 'class':'SimpleStrategy', 'replication_factor':1 }; USE CustomerService; -CREATE TABLE IF NOT EXISTS customers (id BIGINT PRIMARY KEY, name TEXT); +CREATE TABLE IF NOT EXISTS Customers (id BIGINT PRIMARY KEY, name TEXT); CREATE INDEX IF NOT EXISTS CustomerNameIdx ON customers(name);