GH-2765: Add discardChannel for splitter (#2883)

* GH-2765: Add discardChannel for splitter

Fixes https://github.com/spring-projects/spring-integration/issues/2765

When encountering empty collections, splitter should be able to send
the result to a discard channel.
Currently, when encountering an empty collection,
the splitter ends the flow.
Some use-cases may rely on a custom split function which may returns
empty collections.
These use-cases should be able to define a discard channel
so they can proceed with a possible compensation flow.

* Add `discardChannel` option to the `AbstractMessageSplitter`
* Delegate `discardChannel` population from everywhere it is possible:
DSL, XML, `AbstractMessageSplitter` extension like `FileSplitter` etc.
* Fix `FileSplitterTests` for broken charset
* Document new feature; fix some typos and out-dated code sample

* * Fix `SplitterFactoryBean` for NPE on the `discardChannelName`
propagation

* * Check `this.discardChannel` first
* `Assert.state()` in `doInit()` for mutual exclusiveness
This commit is contained in:
Artem Bilan
2019-04-08 16:42:57 -04:00
committed by Gary Russell
parent 7fa1161f5b
commit 818be4cbe8
18 changed files with 263 additions and 61 deletions

View File

@@ -42,7 +42,8 @@ public class XPathMessageSplitterParser extends AbstractConsumerEndpointParser {
Assert.isTrue(xPathExpressionNodes.getLength() <= 1, "At most one xpath-expression child may be specified.");
boolean hasChild = xPathExpressionNodes.getLength() == 1;
boolean hasReference = StringUtils.hasText(xPathExpressionRef);
Assert.isTrue(hasChild ^ hasReference, "Exactly one of 'xpath-expression' or 'xpath-expression-ref' is required.");
Assert.isTrue(hasChild ^ hasReference,
"Exactly one of 'xpath-expression' or 'xpath-expression-ref' is required.");
if (hasChild) {
Element xpathExpressionElement = (Element) xPathExpressionNodes.item(0);
builder.addConstructorArgValue(xpathExpressionElement.getAttribute("expression"));
@@ -51,12 +52,14 @@ public class XPathMessageSplitterParser extends AbstractConsumerEndpointParser {
else {
builder.addConstructorArgReference(xPathExpressionRef);
}
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "doc-builder-factory", "documentBuilder");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "doc-builder-factory",
"documentBuilder");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "create-documents");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "apply-sequence");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "iterator");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "output-properties");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "send-timeout");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "discard-channel", "discardChannelName");
return builder;
}

View File

@@ -820,6 +820,19 @@
<xsd:union memberTypes="xsd:boolean xsd:string" />
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="discard-channel" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.messaging.MessageChannel" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
The channel where the splitter will send the messages that return an empty container from
split function.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>

View File

@@ -30,7 +30,8 @@
apply-sequence="false"
create-documents="true"
output-properties="outputProperties"
iterator="false">
iterator="false"
discard-channel="nullChannel">
<xpath-expression expression="/orders/order"/>
</xpath-splitter>

View File

@@ -45,13 +45,16 @@ import org.springframework.util.MultiValueMap;
@DirtiesContext
public class XPathSplitterParserTests {
@Autowired @Qualifier("xpathSplitter.handler")
@Autowired
@Qualifier("xpathSplitter.handler")
private MessageHandler xpathSplitter;
@Autowired @Qualifier("xpathSplitter")
@Autowired
@Qualifier("xpathSplitter")
private EventDrivenConsumer consumer;
@Autowired @Qualifier("outputProperties")
@Autowired
@Qualifier("outputProperties")
private Properties outputProperties;
@Autowired
@@ -64,10 +67,11 @@ public class XPathSplitterParserTests {
assertThat(TestUtils.getPropertyValue(this.xpathSplitter, "returnIterator", Boolean.class)).isFalse();
assertThat(TestUtils.getPropertyValue(this.xpathSplitter, "outputProperties")).isSameAs(this.outputProperties);
assertThat(TestUtils.getPropertyValue(this.xpathSplitter,
"xpathExpression.xpathExpression.xpath.m_patternString",
String.class)).isEqualTo("/orders/order");
"xpathExpression.xpathExpression.xpath.m_patternString", String.class))
.isEqualTo("/orders/order");
assertThat(TestUtils.getPropertyValue(xpathSplitter, "order")).isEqualTo(2);
assertThat(TestUtils.getPropertyValue(xpathSplitter, "messagingTemplate.sendTimeout")).isEqualTo(123L);
assertThat(TestUtils.getPropertyValue(this.xpathSplitter, "discardChannelName")).isEqualTo("nullChannel");
assertThat(TestUtils.getPropertyValue(consumer, "phase")).isEqualTo(-1);
assertThat(TestUtils.getPropertyValue(consumer, "autoStartup", Boolean.class)).isFalse();
@SuppressWarnings("unchecked")