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:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() };
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user