Add MongoDbMessageSource UPDATE option (#3493)

* Add MongoDbMessageSource UPDATE option

* Extract `AbstractMongoDbMessageSource` with common options and methods
for both `MongoDbMessageSource` and `ReactiveMongoDbMessageSource`
* Add an `updateExpression` option into MongoDb source implementations
* Implement respective `update` logic after fetching the data from the collection
* Cover both reactive and blocking updates with tests
* Add `MongoDbMessageSourceSpec` into Java DSL for MongoDb channel adapters
* Expose an `update` XML attribute for the `<int-mongo:inbound-channel-adapter>`
* Upgrade MongoDb driver for latest Spring Data compatibility
* Document a new feature
* Upgrade `mongodb.adoc` for code block switch whenever it is appropriate

* * Add a Kotlin sample for `MongoDb.outboundGateway()` DSL

* Apply suggestions from code review

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

Co-authored-by: Gary Russell <grussell@vmware.com>
This commit is contained in:
Artem Bilan
2021-02-16 16:46:45 -05:00
committed by GitHub
parent b4290dd2a5
commit b1cb9069fe
17 changed files with 846 additions and 455 deletions

View File

@@ -29,6 +29,7 @@
<int-mongodb:inbound-channel-adapter id="fullConfigWithQueryExpression"
collection-name-expression="'foo'"
query-expression="new BasicQuery('{''address.state'' : ''PA''}').limit(2)"
update="{ $set: {'address.state' : 'NJ'} }"
mongo-converter="mongoConverter"
mongodb-factory="mongoDbFactory"
auto-startup="false">

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -17,9 +17,9 @@
package org.springframework.integration.mongodb.config;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -34,8 +34,7 @@ import org.springframework.integration.endpoint.SourcePollingChannelAdapter;
import org.springframework.integration.mongodb.inbound.MongoDbMessageSource;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* @author Oleg Zhurakousky
@@ -43,8 +42,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
* @author Artem Bilan
* @author Yaron Yamin
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringJUnitConfig
@DirtiesContext
public class MongoDbInboundChannelAdapterParserTests {
@@ -112,6 +110,8 @@ public class MongoDbInboundChannelAdapterParserTests {
assertThat(TestUtils.getPropertyValue(source, "queryExpression") instanceof SpelExpression).isTrue();
assertThat(TestUtils.getPropertyValue(source, "queryExpression.expression"))
.isEqualTo("new BasicQuery('{''address.state'' : ''PA''}').limit(2)");
assertThat(TestUtils.getPropertyValue(source, "updateExpression.literalValue"))
.isEqualTo("{ $set: {'address.state' : 'NJ'} }");
}
@Test
@@ -152,16 +152,20 @@ public class MongoDbInboundChannelAdapterParserTests {
assertThat(TestUtils.getPropertyValue(source, "collectionNameExpression.literalValue")).isEqualTo("foo");
}
@Test(expected = BeanDefinitionParsingException.class)
@Test
public void templateAndFactoryFail() {
new ClassPathXmlApplicationContext("inbound-adapter-parser-fail-template-factory-config.xml", this.getClass())
.close();
assertThatExceptionOfType(BeanDefinitionParsingException.class)
.isThrownBy(() ->
new ClassPathXmlApplicationContext("inbound-adapter-parser-fail-template-factory-config.xml",
getClass()));
}
@Test(expected = BeanDefinitionParsingException.class)
@Test
public void templateAndConverterFail() {
new ClassPathXmlApplicationContext("inbound-adapter-parser-fail-template-converter-config.xml", this.getClass())
.close();
assertThatExceptionOfType(BeanDefinitionParsingException.class)
.isThrownBy(() ->
new ClassPathXmlApplicationContext("inbound-adapter-parser-fail-template-converter-config.xml",
getClass()));
}
private MongoDbMessageSource assertMongoDbMessageSource(Object testedBean) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2007-2020 the original author or authors.
* Copyright 2007-2021 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.
@@ -35,6 +35,7 @@ import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -241,10 +242,8 @@ public class MongoDbMessageSourceTests extends MongoDbAvailableTests {
@SuppressWarnings("unchecked")
@Test
@MongoDbAvailable
public void validateSuccessfulQueryWithMongoTemplate() {
MongoDatabaseFactory mongoDbFactory = this.prepareMongoFactory();
public void validateSuccessfulQueryWithMongoTemplateAndUpdate() {
MongoDatabaseFactory mongoDbFactory = prepareMongoFactory();
MappingMongoConverter converter = new TestMongoConverter(mongoDbFactory, new MongoMappingContext());
converter.afterPropertiesSet();
converter = spy(converter);
@@ -253,16 +252,22 @@ public class MongoDbMessageSourceTests extends MongoDbAvailableTests {
Expression queryExpression = new LiteralExpression("{'address.state' : 'PA'}");
MongoDbMessageSource messageSource = new MongoDbMessageSource(template, queryExpression);
messageSource.setBeanFactory(mock(BeanFactory.class));
messageSource.setUpdateExpression(new LiteralExpression("{ $set: {'address.state' : 'NJ'} }"));
messageSource.afterPropertiesSet();
MongoTemplate writingTemplate = new MongoTemplate(mongoDbFactory, converter);
writingTemplate.save(this.createPerson("Manny"), "data");
writingTemplate.save(this.createPerson("Moe"), "data");
writingTemplate.save(this.createPerson("Jack"), "data");
writingTemplate.save(createPerson("Manny"), "data");
writingTemplate.save(createPerson("Moe"), "data");
writingTemplate.save(createPerson("Jack"), "data");
List<Person> persons = (List<Person>) messageSource.receive().getPayload();
assertThat(persons.size()).isEqualTo(3);
assertThat(persons).hasSize(3);
verify(converter, times(3)).read((Class<Person>) Mockito.any(), Mockito.any(Bson.class));
assertThat(messageSource.receive()).isNull();
assertThat(template.find(new BasicQuery("{'address.state' : 'NJ'}"), Object.class, "data"))
.hasSize(3);
}
@Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 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.
@@ -42,6 +42,7 @@ import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.channel.FluxMessageChannel;
@@ -91,7 +92,7 @@ public class ReactiveMongoDbMessageSourceTests extends MongoDbAvailableTests {
@MongoDbAvailable
@SuppressWarnings("unchecked")
public void validateSuccessfulQueryWithSingleElementFluxOfDbObject() {
ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory = this.prepareReactiveMongoFactory();
ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory = prepareReactiveMongoFactory();
ReactiveMongoTemplate template = new ReactiveMongoTemplate(reactiveMongoDatabaseFactory);
waitFor(template.save(createPerson(), "data"));
@@ -111,7 +112,7 @@ public class ReactiveMongoDbMessageSourceTests extends MongoDbAvailableTests {
@MongoDbAvailable
@SuppressWarnings("unchecked")
public void validateSuccessfulQueryWithSingleElementFluxOfPerson() {
ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory = this.prepareReactiveMongoFactory();
ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory = prepareReactiveMongoFactory();
ReactiveMongoTemplate template = new ReactiveMongoTemplate(reactiveMongoDatabaseFactory);
waitFor(template.save(createPerson(), "data"));
@@ -183,6 +184,7 @@ public class ReactiveMongoDbMessageSourceTests extends MongoDbAvailableTests {
StepVerifier.create(output)
.assertNext(
message -> assertThat(((Person) message.getPayload()).getName()).isEqualTo("Oleg"))
.expectNoEvent(Duration.ofMillis(100))
.thenCancel()
.verify(Duration.ofSeconds(10));
@@ -247,9 +249,10 @@ public class ReactiveMongoDbMessageSourceTests extends MongoDbAvailableTests {
public IntegrationFlow pollingFlow() {
return IntegrationFlows
.from(MongoDb.reactiveInboundChannelAdapter(
MongoDbAvailableTests.REACTIVE_MONGO_DATABASE_FACTORY, "{'name' : 'Oleg'}")
REACTIVE_MONGO_DATABASE_FACTORY, "{'name' : 'Oleg'}")
.update(Update.update("name", "DONE"))
.entityClass(Person.class),
c -> c.poller(Pollers.fixedDelay(100).maxMessagesPerPoll(1)))
c -> c.poller(Pollers.fixedDelay(100)))
.split()
.channel(c -> c.flux("output"))
.get();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -24,6 +24,7 @@ import org.bson.conversions.Bson;
import org.junit.Rule;
import org.springframework.dao.DataAccessException;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
@@ -130,6 +131,9 @@ public abstract class MongoDbAvailableTests {
public static class Person {
@Id
private String id;
private Address address;
private String name;