Migrate Cassandra extension project (#3913)

* Migrate Cassandra extension project

* Add `spring-integration-cassandra` module based on the extension project
* Add Java DSL for Cassandra module
* Add documentation
* Add `CassandraContainerTest` based on a Testcontainers

* Fix language in docs

Co-authored-by: Gary Russell <grussell@vmware.com>

* * Fix `reactive-streams.adoc` for a proper link to
the new `spring-integration-cassandra` module

Co-authored-by: Gary Russell <grussell@vmware.com>
This commit is contained in:
Artem Bilan
2022-10-17 16:03:37 -04:00
committed by GitHub
parent 83f2a9c246
commit a08713fe87
34 changed files with 2357 additions and 3 deletions

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2022 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
*
* https://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.integration.cassandra;
import org.junit.jupiter.api.BeforeAll;
import org.testcontainers.containers.CassandraContainer;
import org.testcontainers.junit.jupiter.Testcontainers;
/**
* The base contract for JUnit tests based on the container for Apache Cassandra.
* The Testcontainers 'reuse' option must be disabled,so, Ryuk container is started
* and will clean all the containers up from this test suite after JVM exit.
* Since the MqSQL container instance is shared via static property, it is going to be
* started only once per JVM, therefore the target Docker container is reused automatically.
*
* @author Artem Bilan
*
* @since 6.0
*/
@Testcontainers(disabledWithoutDocker = true)
public interface CassandraContainerTest {
CassandraContainer<?> CASSANDRA_CONTAINER = new CassandraContainer<>("cassandra:4.1");
@BeforeAll
static void startContainer() {
CASSANDRA_CONTAINER.start();
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2022 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
*
* https://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.integration.cassandra;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.cassandra.config.AbstractReactiveCassandraConfiguration;
import org.springframework.data.cassandra.config.SchemaAction;
import org.springframework.data.cassandra.core.cql.keyspace.CreateKeyspaceSpecification;
import org.springframework.integration.cassandra.test.domain.Book;
/**
* Setup any spring configuration for unit tests.
* Must be used in combination with {@link CassandraContainerTest}.
*
* @author David Webb
* @author Matthew T. Adams
* @author Artem Bilan
*
* @since 6.0
*/
@Configuration
public class IntegrationTestConfig extends AbstractReactiveCassandraConfiguration {
public String keyspaceName = randomKeyspaceName();
public static String randomKeyspaceName() {
return "ks" + UUID.randomUUID().toString().replace("-", "");
}
@Override
protected String getContactPoints() {
return CassandraContainerTest.CASSANDRA_CONTAINER.getContactPoint().getHostName();
}
@Override
protected int getPort() {
return CassandraContainerTest.CASSANDRA_CONTAINER.getContactPoint().getPort();
}
@Override
public SchemaAction getSchemaAction() {
return SchemaAction.RECREATE;
}
@Override
protected String getKeyspaceName() {
return this.keyspaceName;
}
@Override
protected List<CreateKeyspaceSpecification> getKeyspaceCreations() {
return Collections.singletonList(
CreateKeyspaceSpecification.createKeyspace(getKeyspaceName())
.withSimpleReplication());
}
@Override
protected String getLocalDataCenter() {
return CassandraContainerTest.CASSANDRA_CONTAINER.getLocalDatacenter();
}
@Override
public String[] getEntityBasePackages() {
return new String[]{ Book.class.getPackage().getName() };
}
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-cassandra="http://www.springframework.org/schema/integration/cassandra"
xsi:schemaLocation="http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration/cassandra https://www.springframework.org/schema/integration/cassandra/spring-integration-cassandra.xsd">
<int-cassandra:outbound-channel-adapter id="cassandraMessageHandler1"
cassandra-template="reactiveCassandraTemplate"
async="false"/>
<int-cassandra:outbound-channel-adapter id="cassandraMessageHandler2"
cassandra-template="reactiveCassandraTemplate"
async="false"/>
<int-cassandra:outbound-channel-adapter id="cassandraMessageHandler3"
cassandra-template="reactiveCassandraTemplate"
ingest-query="insert into book (isbn, title, author, pages, saleDate, isInStock) values (?, ?, ?, ?, ?, ?)"
async="false"/>
<int-cassandra:outbound-channel-adapter id="cassandraMessageHandler4"
cassandra-template="reactiveCassandraTemplate"
statement-expression="T(QueryBuilder).truncate('book').build()"
async="false"/>
<int:channel id="inputChannel"/>
<bean id="resultChannel" class="org.springframework.integration.channel.FluxMessageChannel"/>
<int-cassandra:outbound-gateway id="cassandraMessageHandler5"
request-channel="inputChannel"
cassandra-template="reactiveCassandraTemplate"
mode="STATEMENT"
query="SELECT * FROM book limit :size"
reply-channel="resultChannel">
<int-cassandra:parameter-expression name="author" expression="payload"/>
<int-cassandra:parameter-expression name="size" expression="headers.limit"/>
</int-cassandra:outbound-gateway>
</beans>

View File

@@ -0,0 +1,156 @@
/*
* Copyright 2022 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
*
* https://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.integration.cassandra.config;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.data.cassandra.core.ReactiveCassandraTemplate;
import org.springframework.data.cassandra.core.WriteResult;
import org.springframework.integration.cassandra.CassandraContainerTest;
import org.springframework.integration.cassandra.IntegrationTestConfig;
import org.springframework.integration.cassandra.test.domain.Book;
import org.springframework.integration.cassandra.test.domain.BookSampler;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.FluxMessageChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.datastax.oss.driver.api.querybuilder.QueryBuilder;
import com.datastax.oss.driver.api.querybuilder.select.Select;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* @author Filippo Balicchia
* @author Artem Bilan
*
* @since 6.0
*/
@SpringJUnitConfig(CassandraOutboundAdapterIntegrationTests.Config.class)
@DirtiesContext
class CassandraOutboundAdapterIntegrationTests implements CassandraContainerTest {
@Autowired
private ReactiveCassandraTemplate cassandraTemplate;
@Autowired
private DirectChannel cassandraMessageHandler1;
@Autowired
private DirectChannel cassandraMessageHandler2;
@Autowired
private DirectChannel cassandraMessageHandler3;
@Autowired
private DirectChannel cassandraMessageHandler4;
@Autowired
private DirectChannel inputChannel;
@Autowired
private FluxMessageChannel resultChannel;
@Test
void testBasicCassandraInsert() {
Book b1 = BookSampler.getBook();
Message<Book> message = MessageBuilder.withPayload(b1).build();
this.cassandraMessageHandler1.send(message);
Select select = QueryBuilder.selectFrom("book").all();
List<Book> books = this.cassandraTemplate.select(select.build(), Book.class).collectList().block();
assertThat(books).hasSize(1);
this.cassandraTemplate.delete(b1);
}
@Test
void testCassandraBatchInsertAndSelectStatement() {
List<Book> books = BookSampler.getBookList(5);
this.cassandraMessageHandler2.send(new GenericMessage<>(books));
Message<?> message = MessageBuilder.withPayload("Cassandra Puppy Guru").setHeader("limit", 2).build();
this.inputChannel.send(message);
Mono<Integer> testMono =
Mono.from(this.resultChannel)
.map(Message::getPayload)
.cast(WriteResult.class)
.map(r -> r.getRows().size());
StepVerifier.create(testMono)
.expectNext(2)
.expectComplete()
.verify();
this.cassandraMessageHandler1.send(new GenericMessage<>(QueryBuilder.truncate("book").build()));
}
@Test
void testCassandraBatchIngest() {
List<Book> books = BookSampler.getBookList(5);
List<List<?>> ingestBooks = new ArrayList<>();
for (Book b : books) {
List<Object> l = new ArrayList<>();
l.add(b.isbn());
l.add(b.title());
l.add(b.author());
l.add(b.pages());
l.add(b.saleDate());
l.add(b.isInStock());
ingestBooks.add(l);
}
Message<List<List<?>>> message = MessageBuilder.withPayload(ingestBooks).build();
this.cassandraMessageHandler3.send(message);
Select select = QueryBuilder.selectFrom("book").all();
books = this.cassandraTemplate.select(select.build(), Book.class).collectList().block();
assertThat(books).hasSize(5);
this.cassandraTemplate.batchOps().delete(books);
}
@Test
void testExpressionTruncate() {
Message<Book> message = MessageBuilder.withPayload(BookSampler.getBook()).build();
this.cassandraMessageHandler1.send(message);
Select select = QueryBuilder.selectFrom("book").all();
List<Book> books = this.cassandraTemplate.select(select.build(), Book.class).collectList().block();
assertThat(books).hasSize(1);
this.cassandraMessageHandler4.send(MessageBuilder.withPayload("Empty").build());
books = this.cassandraTemplate.select(select.build(), Book.class).collectList().block();
assertThat(books).hasSize(0);
}
@Configuration
@EnableIntegration
@ImportResource("org/springframework/integration/cassandra/config/CassandraOutboundAdapterIntegrationTests-context.xml")
public static class Config extends IntegrationTestConfig {
}
}

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-cassandra="http://www.springframework.org/schema/integration/cassandra"
xsi:schemaLocation="http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration/cassandra https://www.springframework.org/schema/integration/cassandra/spring-integration-cassandra.xsd">
<int:poller default="true" fixed-delay="50"/>
<int:channel id="input">
<int:queue/>
</int:channel>
<int:channel id="resultChannel">
<int:queue/>
</int:channel>
<bean id="cassandraTemplate" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.data.cassandra.core.ReactiveCassandraOperations"/>
</bean>
<bean id="writeOptions" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.data.cassandra.core.InsertOptions"/>
</bean>
<int-cassandra:outbound-channel-adapter id="outbound1"
cassandra-template="cassandraTemplate"
write-options="writeOptions"
auto-startup="false"
async="false"/>
<int-cassandra:outbound-channel-adapter id="outbound2"
channel="input"
cassandra-template="cassandraTemplate"
ingest-query="insert into book (isbn, title, author, pages, saleDate, isInStock) values (?, ?, ?, ?, ?, ?)"/>
<int-cassandra:outbound-gateway id="outgateway"
request-channel="input"
cassandra-template="cassandraTemplate"
mode="STATEMENT"
write-options="writeOptions"
query="SELECT * FROM book limit :size"
reply-channel="resultChannel"
auto-startup="true">
<int-cassandra:parameter-expression name="author" expression="payload"/>
<int-cassandra:parameter-expression name="size" expression="headers.limit"/>
</int-cassandra:outbound-gateway>
<int-cassandra:outbound-channel-adapter id="outbound4"
cassandra-template="cassandraTemplate"
write-options="writeOptions"
statement-expression="T(QueryBuilder).truncate('book')"
auto-startup="false"/>
</beans>

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2022 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
*
* https://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.integration.cassandra.config;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.integration.cassandra.outbound.CassandraMessageHandler;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* @author Filippo Balicchia
* @author Artem Bilan
*
* @since 6.0
*/
@SpringJUnitConfig
class CassandraOutboundAdapterParserTests {
@Autowired
private ApplicationContext context;
@Test
void minimalConfig() {
CassandraMessageHandler handler =
TestUtils.getPropertyValue(this.context.getBean("outbound1.adapter"), "handler",
CassandraMessageHandler.class);
assertThat(TestUtils.getPropertyValue(handler, "componentName")).isEqualTo("outbound1.adapter");
assertThat(TestUtils.getPropertyValue(handler, "mode")).isEqualTo(CassandraMessageHandler.Type.INSERT);
assertThat(TestUtils.getPropertyValue(handler, "cassandraOperations"))
.isSameAs(this.context.getBean("cassandraTemplate"));
assertThat(TestUtils.getPropertyValue(handler, "writeOptions")).isSameAs(this.context.getBean("writeOptions"));
assertThat(TestUtils.getPropertyValue(handler, "async", Boolean.class)).isFalse();
}
@Test
void ingestConfig() {
CassandraMessageHandler handler =
TestUtils.getPropertyValue(this.context.getBean("outbound2"), "handler",
CassandraMessageHandler.class);
assertThat(TestUtils.getPropertyValue(handler, "ingestQuery"))
.isEqualTo("insert into book (isbn, title, author, pages, saleDate, isInStock) " +
"values (?, ?, ?, ?, ?, ?)");
assertThat(TestUtils.getPropertyValue(handler, "producesReply", Boolean.class)).isFalse();
}
@Test
void fullConfig() {
CassandraMessageHandler handler =
TestUtils.getPropertyValue(this.context.getBean("outgateway"), "handler",
CassandraMessageHandler.class);
assertThat(TestUtils.getPropertyValue(handler, "producesReply", Boolean.class)).isTrue();
assertThat(TestUtils.getPropertyValue(handler, "mode")).isEqualTo(CassandraMessageHandler.Type.STATEMENT);
assertThat(TestUtils.getPropertyValue(handler, "writeOptions")).isSameAs(this.context.getBean("writeOptions"));
}
@Test
void statementConfig() {
CassandraMessageHandler handler =
TestUtils.getPropertyValue(this.context.getBean("outbound4.adapter"), "handler",
CassandraMessageHandler.class);
assertThat(TestUtils.getPropertyValue(handler, "componentName")).isEqualTo("outbound4.adapter");
assertThat(TestUtils.getPropertyValue(handler, "mode")).isEqualTo(CassandraMessageHandler.Type.STATEMENT);
assertThat(TestUtils.getPropertyValue(handler, "cassandraOperations"))
.isSameAs(this.context.getBean("cassandraTemplate"));
assertThat(TestUtils.getPropertyValue(handler, "writeOptions")).isSameAs(this.context.getBean("writeOptions"));
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2022 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
*
* https://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.integration.cassandra.config;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.integration.cassandra.config.xml.CassandraParserUtils;
/**
* @author Filippo Balicchia
* @author Artem Bilan
*
* @since 6.0
*/
class CassandraParserUtilsTests {
@Test
void mutuallyExclusiveCase1() {
String query = "";
BeanDefinition statementExpressionDef = null;
String ingestQuery = "";
assertThat(CassandraParserUtils.areMutuallyExclusive(query, statementExpressionDef, ingestQuery)).isTrue();
}
@Test
void mutuallyExclusiveCase2() {
String query = "";
BeanDefinition statementExpressionDef = null;
String ingestQuery =
"insert into book (isbn, title, author, pages, saleDate, isInStock) values (?, ?, ?, ?, ?, ?)";
assertThat(CassandraParserUtils.areMutuallyExclusive(query, statementExpressionDef, ingestQuery)).isTrue();
}
@Test
void mutuallyExclusiveCase3() {
String query = "";
BeanDefinition statementExpressionDef = new RootBeanDefinition();
String ingestQuery = "";
assertThat(CassandraParserUtils.areMutuallyExclusive(query, statementExpressionDef, ingestQuery)).isTrue();
}
@Test
void mutuallyExclusiveCase4() {
String query = "";
BeanDefinition statementExpressionDef = new RootBeanDefinition();
String ingestQuery =
"insert into book (isbn, title, author, pages, saleDate, isInStock) values (?, ?, ?, ?, ?, ?)";
assertThat(CassandraParserUtils.areMutuallyExclusive(query, statementExpressionDef, ingestQuery)).isFalse();
}
@Test
void mutuallyExclusiveCase5() {
String query = "SELECT * FROM book limit :size";
BeanDefinition statementExpressionDef = new RootBeanDefinition();
String ingestQuery = "";
assertThat(CassandraParserUtils.areMutuallyExclusive(query, statementExpressionDef, ingestQuery)).isFalse();
}
@Test
void mutuallyExclusiveCase6() {
String query = "SELECT * FROM book limit :size";
BeanDefinition statementExpressionDef = new RootBeanDefinition();
String ingestQuery =
"insert into book (isbn, title, author, pages, saleDate, isInStock) values (?, ?, ?, ?, ?, ?)";
assertThat(CassandraParserUtils.areMutuallyExclusive(query, statementExpressionDef, ingestQuery)).isFalse();
}
@Test
void mutuallyExclusiveCase7() {
String query = "SELECT * FROM book limit :size";
BeanDefinition statementExpressionDef = new RootBeanDefinition();
String ingestQuery = "";
assertThat(CassandraParserUtils.areMutuallyExclusive(query, statementExpressionDef, ingestQuery)).isFalse();
}
@Test
void mutuallyExclusiveCase8() {
String query = "SELECT * FROM book limit :size";
BeanDefinition statementExpressionDef = new RootBeanDefinition();
String ingestQuery =
"insert into book (isbn, title, author, pages, saleDate, isInStock) values (?, ?, ?, ?, ?, ?)";
assertThat(CassandraParserUtils.areMutuallyExclusive(query, statementExpressionDef, ingestQuery)).isFalse();
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2022 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
*
* https://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.integration.cassandra.dsl;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.cassandra.core.InsertOptions;
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
import org.springframework.data.cassandra.core.WriteResult;
import org.springframework.integration.cassandra.CassandraContainerTest;
import org.springframework.integration.cassandra.IntegrationTestConfig;
import org.springframework.integration.cassandra.test.domain.BookSampler;
import org.springframework.integration.channel.FluxMessageChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.datastax.oss.driver.api.core.ConsistencyLevel;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* @author Artem Bilan
*
* @since 6.0
*/
@SpringJUnitConfig
@DirtiesContext
public class CassandraDslTests implements CassandraContainerTest {
@Autowired
@Qualifier("cassandraTruncateFlow.input")
MessageChannel cassandraTruncateFlowInput;
@Autowired
@Qualifier("cassandraInsertFlow.input")
MessageChannel cassandraInsertFlowInput;
@Autowired
@Qualifier("cassandraSelectFlow.input")
MessageChannel cassandraSelectFlowInput;
@Autowired
FluxMessageChannel resultChannel;
@Test
void testCassandraDslConfiguration() {
this.cassandraInsertFlowInput.send(new GenericMessage<>(BookSampler.getBookList(5)));
Mono<Integer> testMono =
Mono.from(this.resultChannel)
.map(Message::getPayload)
.cast(WriteResult.class)
.map(r -> r.getRows().size());
StepVerifier stepVerifier = StepVerifier.create(testMono)
.expectNext(1)
.expectComplete()
.verifyLater();
this.cassandraSelectFlowInput.send(MessageBuilder.withPayload("Cassandra Guru").setHeader("limit", 2).build());
stepVerifier.verify(Duration.ofSeconds(10));
this.cassandraTruncateFlowInput.send(new GenericMessage<>(""));
}
@Configuration
@EnableIntegration
public static class Config extends IntegrationTestConfig {
@Bean
IntegrationFlow cassandraTruncateFlow(ReactiveCassandraOperations cassandraOperations) {
return flow -> flow
.handle(Cassandra.outboundChannelAdapter(cassandraOperations)
.statementExpression("T(QueryBuilder).truncate('book').build()"),
e -> e.async(false));
}
@Bean
IntegrationFlow cassandraInsertFlow(ReactiveCassandraOperations cassandraOperations) {
return flow -> flow
.handle(Cassandra.outboundChannelAdapter(cassandraOperations)
.writeOptions(InsertOptions.builder()
.ttl(60)
.consistencyLevel(ConsistencyLevel.ONE)
.build()),
e -> e.async(false));
}
@Bean
IntegrationFlow cassandraSelectFlow(ReactiveCassandraOperations cassandraOperations) {
return flow -> flow
.handle(Cassandra.outboundGateway(cassandraOperations)
.query("SELECT * FROM book WHERE author = :author limit :size")
.parameter("author", "payload")
.parameter("size", m -> m.getHeaders().get("limit")))
.channel(c -> c.flux("resultChannel"));
}
}
}

View File

@@ -0,0 +1,217 @@
/*
* Copyright 2022 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
*
* https://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.integration.cassandra.outbound;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.data.cassandra.core.InsertOptions;
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
import org.springframework.data.cassandra.core.WriteResult;
import org.springframework.data.cassandra.core.cql.WriteOptions;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.cassandra.CassandraContainerTest;
import org.springframework.integration.cassandra.IntegrationTestConfig;
import org.springframework.integration.cassandra.test.domain.Book;
import org.springframework.integration.cassandra.test.domain.BookSampler;
import org.springframework.integration.channel.FluxMessageChannel;
import org.springframework.integration.channel.NullChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.datastax.oss.driver.api.core.ConsistencyLevel;
import com.datastax.oss.driver.api.querybuilder.QueryBuilder;
import com.datastax.oss.driver.api.querybuilder.select.Select;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* @author Soby Chacko
* @author Artem Bilan
*
* @since 6.0
*/
@SpringJUnitConfig
@DirtiesContext
public class CassandraMessageHandlerTests implements CassandraContainerTest {
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
@Autowired
public MessageHandler cassandraMessageHandler1;
@Autowired
public MessageHandler cassandraMessageHandler2;
@Autowired
public MessageHandler cassandraMessageHandler3;
@Autowired
public MessageHandler cassandraMessageHandler4;
@Autowired
public CassandraOperations template;
@Autowired
public FluxMessageChannel resultChannel;
@Test
void testBasicCassandraInsert() {
Book b1 = BookSampler.getBook();
Message<Book> message = MessageBuilder.withPayload(b1).build();
this.cassandraMessageHandler1.handleMessage(message);
Select select = QueryBuilder.selectFrom("book").all();
List<Book> books = this.template.select(select.build(), Book.class);
assertThat(books).hasSize(1);
this.template.delete(b1);
}
@Test
void testCassandraBatchInsertAndSelectStatement() {
List<Book> books = BookSampler.getBookList(5);
this.cassandraMessageHandler2.handleMessage(new GenericMessage<>(books));
Message<?> message = MessageBuilder.withPayload("Cassandra Guru").setHeader("limit", 2).build();
this.cassandraMessageHandler4.handleMessage(message);
Mono<Integer> testMono =
Mono.from(this.resultChannel)
.map(Message::getPayload)
.cast(WriteResult.class)
.map(r -> r.getRows().size());
StepVerifier.create(testMono)
.expectNext(1)
.expectComplete()
.verify();
this.cassandraMessageHandler1.handleMessage(new GenericMessage<>(QueryBuilder.truncate("book").build()));
}
@Test
void testCassandraBatchIngest() {
List<Book> books = BookSampler.getBookList(5);
List<List<Object>> ingestBooks =
books.stream()
.map(book ->
List.<Object>of(
book.isbn(),
book.title(),
book.author(),
book.pages(),
book.saleDate(),
book.isInStock()))
.toList();
this.cassandraMessageHandler3.handleMessage(MessageBuilder.withPayload(ingestBooks).build());
Select select = QueryBuilder.selectFrom("book").all();
books = this.template.select(select.build(), Book.class);
assertThat(books).hasSize(5);
this.template.batchOps().delete(books);
}
@Configuration
@EnableIntegration
public static class Config extends IntegrationTestConfig {
@Autowired
public ReactiveCassandraOperations template;
@Bean
public MessageHandler cassandraMessageHandler1() {
CassandraMessageHandler cassandraMessageHandler = new CassandraMessageHandler(this.template);
cassandraMessageHandler.setAsync(false);
return cassandraMessageHandler;
}
@Bean
public PollableChannel messageChannel() {
return new NullChannel();
}
@Bean
public MessageHandler cassandraMessageHandler2() {
CassandraMessageHandler cassandraMessageHandler = new CassandraMessageHandler(this.template);
WriteOptions options =
InsertOptions.builder()
.ttl(60)
.consistencyLevel(ConsistencyLevel.ONE)
.build();
cassandraMessageHandler.setWriteOptions(options);
cassandraMessageHandler.setOutputChannel(messageChannel());
cassandraMessageHandler.setAsync(false);
return cassandraMessageHandler;
}
@Bean
public MessageHandler cassandraMessageHandler3() {
CassandraMessageHandler cassandraMessageHandler = new CassandraMessageHandler(this.template);
String cqlIngest =
"insert into book (isbn, title, author, pages, saleDate, isInStock) values (?, ?, ?, ?, ?, ?)";
cassandraMessageHandler.setIngestQuery(cqlIngest);
cassandraMessageHandler.setAsync(false);
return cassandraMessageHandler;
}
@Bean
public FluxMessageChannel resultChannel() {
return new FluxMessageChannel();
}
@Bean
public MessageHandler cassandraMessageHandler4() {
CassandraMessageHandler cassandraMessageHandler = new CassandraMessageHandler(this.template);
cassandraMessageHandler.setQuery("SELECT * FROM book WHERE author = :author limit :size");
Map<String, Expression> params = new HashMap<>();
params.put("author", PARSER.parseExpression("payload"));
params.put("size", PARSER.parseExpression("headers.limit"));
cassandraMessageHandler.setParameterExpressions(params);
cassandraMessageHandler.setOutputChannel(resultChannel());
cassandraMessageHandler.setProducesReply(true);
return cassandraMessageHandler;
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2022 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
*
* https://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.integration.cassandra.test.domain;
import java.time.LocalDate;
import org.springframework.data.cassandra.core.mapping.Indexed;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.Table;
/**
* Test POJO
*
* @author David Webb
* @author Artem Bilan
*
* @since 6.0
*/
@Table("book")
public record Book(
@PrimaryKey String isbn,
String title,
@Indexed String author,
Integer pages,
LocalDate saleDate,
Boolean isInStock) {
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2022 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
*
* https://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.integration.cassandra.test.domain;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* @author Filippo Balicchia
* @author Artem Bilan
*
* @since 6.0
*/
public final class BookSampler {
public static List<Book> getBookList(int numBooks) {
List<Book> books = new ArrayList<>();
for (int i = 0; i < numBooks - 1; i++) {
books.add(new Book(UUID.randomUUID().toString(), "Spring Data Cassandra Guide", "Cassandra Guru puppy",
i * 10 + 5, LocalDate.now(), true));
}
books.add(getBook());
return books;
}
public static Book getBook() {
return new Book("123456-1", "Spring Integration Cassandra", "Cassandra Guru", 521, LocalDate.now(), true);
}
private BookSampler() {
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT">
<PatternLayout pattern="%d %p [%t] [%c] - %m%n" />
</Console>
</Appenders>
<Loggers>
<Logger name="org.springframework.integration" level="warn"/>
<Logger name="org.springframework.integration.cassandra" level="info"/>
<Root level="warn">
<AppenderRef ref="STDOUT" />
</Root>
</Loggers>
</Configuration>