GH-3241: MetadataStoreSelector - compare old/new (#3242)

Resolves: https://github.com/spring-projects/spring-integration/issues/3241

 * Docs and XML namespace support.
This commit is contained in:
Gary Russell
2020-04-06 18:12:36 -04:00
committed by GitHub
parent 1f2a33437d
commit 7d7c273515
10 changed files with 244 additions and 20 deletions

View File

@@ -59,6 +59,8 @@ public class IdempotentReceiverInterceptorParser extends AbstractBeanDefinitionP
boolean hasValueStrategy = StringUtils.hasText(valueStrategy);
String valueExpression = element.getAttribute("value-expression");
boolean hasValueExpression = StringUtils.hasText(valueExpression);
String compareValues = element.getAttribute("compare-values");
boolean hasCompareValues = StringUtils.hasText(compareValues);
String endpoints = element.getAttribute("endpoint");
@@ -68,10 +70,10 @@ public class IdempotentReceiverInterceptorParser extends AbstractBeanDefinitionP
}
if (hasSelector && (hasStore || hasKeyStrategy || hasKeyExpression || hasValueStrategy // NOSONAR complexity
|| hasValueExpression)) {
|| hasValueExpression || hasCompareValues)) {
parserContext.getReaderContext().error("The 'selector' attribute is mutually exclusive with " +
"'metadata-store', 'key-strategy', 'key-expression', 'value-strategy' " +
"or 'value-expression'", source);
"'metadata-store', 'key-strategy', 'key-expression', 'value-strategy', " +
"'value-expression', and 'compare-values'", source);
}
if (hasKeyStrategy && hasKeyExpression) {
@@ -133,6 +135,7 @@ public class IdempotentReceiverInterceptorParser extends AbstractBeanDefinitionP
else {
selectorBuilder.addConstructorArgValue(new RootBeanDefinition(SimpleMetadataStore.class));
}
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(selectorBuilder, element, "compare-values");
selectorBeanDefinition = selectorBuilder.getBeanDefinition();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2020 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.
@@ -16,10 +16,13 @@
package org.springframework.integration.selector;
import java.util.function.BiPredicate;
import org.springframework.integration.core.MessageSelector;
import org.springframework.integration.handler.MessageProcessor;
import org.springframework.integration.metadata.ConcurrentMetadataStore;
import org.springframework.integration.metadata.SimpleMetadataStore;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
@@ -58,6 +61,9 @@ public class MetadataStoreSelector implements MessageSelector {
private final MessageProcessor<String> valueStrategy;
@Nullable
private BiPredicate<String, String> compareValues;
public MetadataStoreSelector(MessageProcessor<String> keyStrategy) {
this(keyStrategy, (MessageProcessor<String>) null);
}
@@ -79,6 +85,26 @@ public class MetadataStoreSelector implements MessageSelector {
this.valueStrategy = valueStrategy;
}
/**
* Set a {@link BiPredicate} to compare old and new values in the metadata store for
* the key. The first parameter is the old value; return true if we should accept this
* message and replace the old value with the new value.
* @param compareValues the {@link BiPredicate}.
* @since 5.3
*/
public void setCompareValues(@Nullable BiPredicate<String, String> compareValues) {
this.compareValues = compareValues;
}
/**
* Fluent version of {@link #setCompareValues(BiPredicate)}.
* @param compareValues the {@link BiPredicate}.
* @return this.
*/
public MetadataStoreSelector compareValues(@Nullable BiPredicate<String, String> compareValues) {
setCompareValues(compareValues);
return this;
}
@Override
public boolean accept(Message<?> message) {
@@ -88,7 +114,21 @@ public class MetadataStoreSelector implements MessageSelector {
? this.valueStrategy.processMessage(message)
: (timestamp == null ? "0" : Long.toString(timestamp));
return this.metadataStore.putIfAbsent(key, value) == null;
if (this.compareValues == null) {
return this.metadataStore.putIfAbsent(key, value) == null;
}
else {
synchronized (this) {
String oldValue = this.metadataStore.get(key);
if (oldValue == null) {
return this.metadataStore.putIfAbsent(key, value) == null;
}
if (this.compareValues.test(oldValue, value)) {
return this.metadataStore.replace(key, oldValue, value);
}
return false;
}
}
}
}

View File

@@ -4853,6 +4853,19 @@ The list of component name patterns you want to track (e.g., tracked-components
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="compare-values">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="java.util.function.BiPredicate"/>
</tool:annotation>
</xsd:appinfo>
<xsd:documentation><![CDATA[
A 'BiPredicate<String, String>' which is called if a value exists to determine whether the message
should be accepted and the old value replaced with the new value in the metadata store.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="discard-channel" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>

View File

@@ -27,9 +27,13 @@
endpoint="foo"
key-strategy="keyStrategy"
value-strategy="valueStrategy"
compare-values="valueComparator"
discard-channel="nullChannel"
throw-exception-on-rejection="true"/>
<beans:bean id="valueComparator"
class="org.springframework.integration.config.xml.IdempotentReceiverParserTests$AlwaysAccept"/>
<beans:bean id="store" class="org.springframework.integration.metadata.SimpleMetadataStore"/>
<util:properties id="properties">

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2020 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 java.io.ByteArrayInputStream;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.function.BiPredicate;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -77,6 +78,9 @@ public class IdempotentReceiverParserTests {
@Autowired
private MessageProcessor<String> valueStrategy;
@Autowired
private AlwaysAccept alwaysAccept;
@Autowired
@Qualifier("nullChannel")
private MessageChannel nullChannel;
@@ -94,8 +98,8 @@ public class IdempotentReceiverParserTests {
assertThat(getPropertyValue(this.selectorInterceptor, "throwExceptionOnRejection", Boolean.class)).isFalse();
@SuppressWarnings("unchecked")
Map<String, List<String>> idempotentEndpoints =
(Map<String, List<String>>) getPropertyValue(this.idempotentReceiverAutoProxyCreator,
"idempotentEndpoints", Map.class);
getPropertyValue(this.idempotentReceiverAutoProxyCreator,
"idempotentEndpoints", Map.class);
List<String> endpoints = idempotentEndpoints.get("selectorInterceptor");
assertThat(endpoints).isNotNull();
assertThat(endpoints.isEmpty()).isFalse();
@@ -110,10 +114,11 @@ public class IdempotentReceiverParserTests {
assertThat(messageSelector).isInstanceOf(MetadataStoreSelector.class);
assertThat(getPropertyValue(messageSelector, "keyStrategy")).isSameAs(this.keyStrategy);
assertThat(getPropertyValue(messageSelector, "valueStrategy")).isSameAs(this.valueStrategy);
assertThat(getPropertyValue(messageSelector, "compareValues")).isSameAs(this.alwaysAccept);
@SuppressWarnings("unchecked")
Map<String, List<String>> idempotentEndpoints =
(Map<String, List<String>>) getPropertyValue(this.idempotentReceiverAutoProxyCreator,
"idempotentEndpoints", Map.class);
getPropertyValue(this.idempotentReceiverAutoProxyCreator,
"idempotentEndpoints", Map.class);
List<String> endpoints = idempotentEndpoints.get("strategyInterceptor");
assertThat(endpoints).isNotNull();
assertThat(endpoints.isEmpty()).isFalse();
@@ -130,8 +135,8 @@ public class IdempotentReceiverParserTests {
assertThat(keyStrategy.toString()).contains("headers.foo");
@SuppressWarnings("unchecked")
Map<String, List<String>> idempotentEndpoints =
(Map<String, List<String>>) getPropertyValue(this.idempotentReceiverAutoProxyCreator,
"idempotentEndpoints", Map.class);
getPropertyValue(this.idempotentReceiverAutoProxyCreator,
"idempotentEndpoints", Map.class);
List<String> endpoints = idempotentEndpoints.get("expressionInterceptor");
assertThat(endpoints).isNotNull();
assertThat(endpoints.isEmpty()).isFalse();
@@ -172,7 +177,8 @@ public class IdempotentReceiverParserTests {
catch (BeanDefinitionParsingException e) {
assertThat(e.getMessage())
.contains("The 'selector' attribute is mutually exclusive with 'metadata-store', " +
"'key-strategy', 'key-expression', 'value-strategy' or 'value-expression'");
"'key-strategy', 'key-expression', 'value-strategy', 'value-expression', and "
+ "'compare-values'");
}
}
@@ -185,7 +191,8 @@ public class IdempotentReceiverParserTests {
catch (BeanDefinitionParsingException e) {
assertThat(e.getMessage())
.contains("The 'selector' attribute is mutually exclusive with 'metadata-store', " +
"'key-strategy', 'key-expression', 'value-strategy' or 'value-expression'");
"'key-strategy', 'key-expression', 'value-strategy', 'value-expression', and "
+ "'compare-values'");
}
}
@@ -198,7 +205,8 @@ public class IdempotentReceiverParserTests {
catch (BeanDefinitionParsingException e) {
assertThat(e.getMessage())
.contains("The 'selector' attribute is mutually exclusive with 'metadata-store', " +
"'key-strategy', 'key-expression', 'value-strategy' or 'value-expression'");
"'key-strategy', 'key-expression', 'value-strategy', 'value-expression', and "
+ "'compare-values'");
}
}
@@ -211,7 +219,8 @@ public class IdempotentReceiverParserTests {
catch (BeanDefinitionParsingException e) {
assertThat(e.getMessage())
.contains("The 'selector' attribute is mutually exclusive with 'metadata-store', " +
"'key-strategy', 'key-expression', 'value-strategy' or 'value-expression'");
"'key-strategy', 'key-expression', 'value-strategy', 'value-expression', and "
+ "'compare-values'");
}
}
@@ -224,7 +233,8 @@ public class IdempotentReceiverParserTests {
catch (BeanDefinitionParsingException e) {
assertThat(e.getMessage())
.contains("The 'selector' attribute is mutually exclusive with 'metadata-store', " +
"'key-strategy', 'key-expression', 'value-strategy' or 'value-expression'");
"'key-strategy', 'key-expression', 'value-strategy', 'value-expression', and "
+ "'compare-values'");
}
}
@@ -268,4 +278,13 @@ public class IdempotentReceiverParserTests {
return ac;
}
public static class AlwaysAccept implements BiPredicate<String, String> {
@Override
public boolean test(String t, String u) {
return true;
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2020 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.selector;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.integration.metadata.SimpleMetadataStore;
import org.springframework.messaging.support.MessageBuilder;
/**
* @author Gary Russell
* @since 5.3
*
*/
public class MetadataStoreSelectorTests {
@Test
void lineNumbers() {
SimpleMetadataStore store = new SimpleMetadataStore();
store.put("file", "5");
MetadataStoreSelector selector = new MetadataStoreSelector(
msg -> "file", msg -> msg.getHeaders().get("lineNum").toString(), store);
selector.setCompareValues((oldValue, newValue) -> Integer.parseInt(oldValue) < Integer.parseInt(newValue));
for (int i = 0; i < 6; i++) {
assertThat(selector.accept(MessageBuilder.withPayload("foo").setHeader("lineNum", i).build()))
.isEqualTo(Boolean.FALSE);
assertThat(store.get("file")).isEqualTo("5");
}
assertThat(selector.accept(MessageBuilder.withPayload("foo").setHeader("lineNum", 6).build()))
.isEqualTo(Boolean.TRUE);
assertThat(store.get("file")).isEqualTo("6");
}
}