Skip to content

Commit 30b5883

Browse files
krisjacynaartembilan
authored andcommitted
INT-3500: Consider to add "error-channel" to the <enricher>
JIRA: https://jira.spring.io/browse/INT-3500 * add 'error-channel' to the <enricher> component * delegate errorChannel to the internal gateway in ContentEnricher * added tests for xml based and java based enrichers with error channels INT-3500: Consider to add "error-channel" to the <enricher> * Fixed typos and comments * Reworked xml integration test to use the outputChannel and increased timeout * Added overview in whats-new doc INT-3500: Consider to add "error-channel" to the <enricher> * Ensure requestChannel is set if an errorChannel is set * Increased timeout to fix unit test * Added details to content-enricher ref doc Polishing
1 parent 17b9159 commit 30b5883

File tree

8 files changed

+291
-42
lines changed

8 files changed

+291
-42
lines changed

spring-integration-core/src/main/java/org/springframework/integration/config/xml/EnricherParser.java

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
* @author Mark Fisher
4242
* @author Artem Bilan
4343
* @author Liujiong
44+
* @author Kris Jacyna
4445
* @since 2.1
4546
*/
4647
public class EnricherParser extends AbstractConsumerEndpointParser {
@@ -51,6 +52,7 @@ protected BeanDefinitionBuilder parseHandler(Element element, ParserContext pars
5152

5253
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "request-channel");
5354
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "reply-channel");
55+
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "error-channel");
5456
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "request-timeout");
5557
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-timeout");
5658
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "requires-reply");
@@ -69,19 +71,21 @@ protected BeanDefinitionBuilder parseHandler(Element element, ParserContext pars
6971
boolean hasAttributeExpression = StringUtils.hasText(expression);
7072
boolean hasAttributeNullResultExpression = StringUtils.hasText(nullResultExpression);
7173

72-
if (hasAttributeValue && hasAttributeExpression){
74+
if (hasAttributeValue && hasAttributeExpression) {
7375
parserContext.getReaderContext().error("Only one of 'value' or 'expression' is allowed", element);
7476
}
7577

76-
if (!hasAttributeValue && !hasAttributeExpression && !hasAttributeNullResultExpression){
77-
parserContext.getReaderContext().error("One of 'value' or 'expression' or 'null-result-expression' is required", element);
78+
if (!hasAttributeValue && !hasAttributeExpression && !hasAttributeNullResultExpression) {
79+
parserContext.getReaderContext()
80+
.error("One of 'value' or 'expression' or 'null-result-expression' is required", element);
7881
}
7982

8083
BeanDefinition expressionDef = null;
8184
BeanDefinition nullResultExpressionExpressionDef;
8285

8386
if (hasAttributeValue) {
84-
BeanDefinitionBuilder expressionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ValueExpression.class);
87+
BeanDefinitionBuilder expressionBuilder =
88+
BeanDefinitionBuilder.genericBeanDefinition(ValueExpression.class);
8589
if (StringUtils.hasText(type)) {
8690
expressionBuilder.addConstructorArgValue(new TypedStringValue(value, type));
8791
}
@@ -93,19 +97,20 @@ protected BeanDefinitionBuilder parseHandler(Element element, ParserContext pars
9397
else if (hasAttributeExpression) {
9498
if (StringUtils.hasText(type)) {
9599
parserContext.getReaderContext().error("The 'type' attribute for '<property>' of '<enricher>' " +
96-
"is not allowed with an 'expression' attribute.", element);
100+
"is not allowed with an 'expression' attribute.", element);
97101
}
98102
expressionDef = BeanDefinitionBuilder
99103
.genericBeanDefinition(ExpressionFactoryBean.class)
100104
.addConstructorArgValue(expression)
101105
.getBeanDefinition();
102106
}
103-
if (expressionDef != null){
107+
if (expressionDef != null) {
104108
expressions.put(name, expressionDef);
105109
}
106110
if (hasAttributeNullResultExpression) {
107-
nullResultExpressionExpressionDef = BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class)
108-
.addConstructorArgValue(nullResultExpression).getBeanDefinition();
111+
nullResultExpressionExpressionDef =
112+
BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class)
113+
.addConstructorArgValue(nullResultExpression).getBeanDefinition();
109114
nullResultExpressions.put(name, nullResultExpressionExpressionDef);
110115
}
111116
}
@@ -129,21 +134,23 @@ else if (hasAttributeExpression) {
129134
boolean hasAttributeValue = StringUtils.hasText(valueElementValue);
130135
boolean hasAttributeExpression = StringUtils.hasText(expressionElementValue);
131136
boolean hasAttributeNullResultExpression = StringUtils.hasText(nullResultHeaderExpression);
132-
if (hasAttributeValue && hasAttributeExpression){
137+
if (hasAttributeValue && hasAttributeExpression) {
133138
parserContext.getReaderContext().error("Only one of '" + "value" + "' or '"
134-
+ "expression" + "' is allowed", subElement);
139+
+ "expression" + "' is allowed", subElement);
135140
}
136141

137-
if (!hasAttributeValue && !hasAttributeExpression && !hasAttributeNullResultExpression){
138-
parserContext.getReaderContext().error("One of 'value' or 'expression' or 'null-result-expression' is required", subElement);
142+
if (!hasAttributeValue && !hasAttributeExpression && !hasAttributeNullResultExpression) {
143+
parserContext.getReaderContext()
144+
.error("One of 'value' or 'expression' or 'null-result-expression' is required", subElement);
139145
}
140146
BeanDefinition expressionDef = null;
141147
if (hasAttributeValue) {
142148
expressionDef = new RootBeanDefinition(LiteralExpression.class);
143149
expressionDef.getConstructorArgumentValues().addGenericArgumentValue(valueElementValue);
144150
}
145151
else if (hasAttributeExpression) {
146-
expressionDef = IntegrationNamespaceUtils.createExpressionDefIfAttributeDefined("expression", subElement);
152+
expressionDef =
153+
IntegrationNamespaceUtils.createExpressionDefIfAttributeDefined("expression", subElement);
147154
}
148155

149156
if (StringUtils.hasText(subElement.getAttribute("expression"))
@@ -167,7 +174,8 @@ else if (hasAttributeExpression) {
167174
.genericBeanDefinition(ExpressionEvaluatingHeaderValueMessageProcessor.class)
168175
.addConstructorArgValue(nullResultExpressionDefinition)
169176
.addConstructorArgValue(subElement.getAttribute("type"));
170-
IntegrationNamespaceUtils.setValueIfAttributeDefined(nullResultValueProcessorBuilder, subElement, "overwrite");
177+
IntegrationNamespaceUtils.setValueIfAttributeDefined(nullResultValueProcessorBuilder, subElement,
178+
"overwrite");
171179
nullResultHeaderExpressions.put(name, nullResultValueProcessorBuilder.getBeanDefinition());
172180
}
173181
}
@@ -184,8 +192,9 @@ else if (hasAttributeExpression) {
184192
String requestPayloadExpression = element.getAttribute("request-payload-expression");
185193

186194
if (StringUtils.hasText(requestPayloadExpression)) {
187-
BeanDefinitionBuilder expressionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class);
188-
expressionBuilder.addConstructorArgValue(requestPayloadExpression);
195+
BeanDefinitionBuilder expressionBuilder =
196+
BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class)
197+
.addConstructorArgValue(requestPayloadExpression);
189198
builder.addPropertyValue("requestPayloadExpression", expressionBuilder.getBeanDefinition());
190199
}
191200

spring-integration-core/src/main/java/org/springframework/integration/transformer/ContentEnricher.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
* @author Gary Russell
5050
* @author Artem Bilan
5151
* @author Liujiong
52+
* @author Kris Jacyna
5253
* @since 2.1
5354
*/
5455
public class ContentEnricher extends AbstractReplyProducingMessageHandler
@@ -82,6 +83,10 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler
8283

8384
private volatile String replyChannelName;
8485

86+
private volatile MessageChannel errorChannel;
87+
88+
private volatile String errorChannelName;
89+
8590
private volatile Gateway gateway = null;
8691

8792
private volatile Long requestTimeout;
@@ -167,6 +172,22 @@ public void setReplyChannelName(String replyChannelName) {
167172
this.replyChannelName = replyChannelName;
168173
}
169174

175+
/**
176+
* Set the content enricher's error channel to allow the error handling flow to return
177+
* of an alternative object to use for enrichment if exceptions occur in the
178+
* downstream flow.
179+
* @param errorChannel The error channel.
180+
* @since 4.1
181+
*/
182+
public void setErrorChannel(MessageChannel errorChannel) {
183+
this.errorChannel = errorChannel;
184+
}
185+
186+
public void setErrorChannelName(String errorChannelName) {
187+
Assert.hasText(errorChannelName, "'errorChannelName' must not be empty");
188+
this.errorChannelName = errorChannelName;
189+
}
190+
170191
/**
171192
* Set the timeout value for sending request messages. If not explicitly configured,
172193
* the default is one second.
@@ -246,10 +267,17 @@ protected void doInit() {
246267
Assert.state(!(this.replyChannelName != null && this.replyChannel != null),
247268
"'replyChannelName' and 'replyChannel' are mutually exclusive.");
248269

270+
Assert.state(!(this.errorChannelName != null && this.errorChannel != null),
271+
"'errorChannelName' and 'errorChannel' are mutually exclusive.");
272+
249273
if (this.replyChannel != null || this.replyChannelName != null) {
250274
Assert.state(this.requestChannel != null || this.requestChannelName != null,
251275
"If the replyChannel is set, then the requestChannel must not be null");
252276
}
277+
if (this.errorChannel != null || this.errorChannelName != null) {
278+
Assert.state(this.requestChannel != null || this.requestChannelName != null,
279+
"If the errorChannel is set, then the requestChannel must not be null");
280+
}
253281
if (this.requestChannel != null || this.requestChannelName != null) {
254282
this.gateway = new Gateway();
255283
this.gateway.setRequestChannel(this.requestChannel);
@@ -269,6 +297,11 @@ protected void doInit() {
269297
this.gateway.setReplyChannelName(this.replyChannelName);
270298
}
271299

300+
this.gateway.setErrorChannel(errorChannel);
301+
if (this.errorChannelName != null) {
302+
this.gateway.setErrorChannelName(this.errorChannelName);
303+
}
304+
272305
if (this.getBeanFactory() != null) {
273306
this.gateway.setBeanFactory(this.getBeanFactory());
274307
}

spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-4.1.xsd

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,6 +1387,21 @@
13871387
</xsd:appinfo>
13881388
</xsd:annotation>
13891389
</xsd:attribute>
1390+
<xsd:attribute name="error-channel" type="xsd:string" use="optional">
1391+
<xsd:annotation>
1392+
<xsd:documentation>
1393+
Used when exceptions occur in a downstream flow and allows the error
1394+
handling flow to return an alternative object to use for enrichment.
1395+
If no "error-channel" reference is provided,
1396+
this enricher will propagate Exceptions to the caller.
1397+
</xsd:documentation>
1398+
<xsd:appinfo>
1399+
<tool:annotation kind="ref">
1400+
<tool:expected-type type="org.springframework.messaging.MessageChannel"/>
1401+
</tool:annotation>
1402+
</xsd:appinfo>
1403+
</xsd:annotation>
1404+
</xsd:attribute>
13901405
<xsd:attribute name="request-timeout" type="xsd:string">
13911406
<xsd:annotation>
13921407
<xsd:documentation><![CDATA[
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<beans:beans xmlns="http://www.springframework.org/schema/integration"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xmlns:beans="http://www.springframework.org/schema/beans"
5+
xsi:schemaLocation="http://www.springframework.org/schema/beans
6+
http://www.springframework.org/schema/beans/spring-beans.xsd
7+
http://www.springframework.org/schema/integration
8+
http://www.springframework.org/schema/integration/spring-integration.xsd">
9+
10+
<channel id="inputChannel"/>
11+
12+
<channel id="requestChannel"/>
13+
14+
<channel id="errChannel"/>
15+
16+
<channel id="outputChannel">
17+
<queue />
18+
</channel>
19+
20+
<enricher id="enricher"
21+
input-channel="inputChannel" request-channel="requestChannel"
22+
output-channel="outputChannel" error-channel="errChannel">
23+
<property name="name" expression="'Mr. ' + payload.name"/>
24+
</enricher>
25+
26+
</beans:beans>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.config.xml;
18+
19+
import static org.junit.Assert.assertEquals;
20+
21+
import org.junit.Test;
22+
import org.junit.runner.RunWith;
23+
24+
import org.springframework.beans.factory.annotation.Autowired;
25+
import org.springframework.context.ApplicationContext;
26+
import org.springframework.integration.channel.DirectChannel;
27+
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
28+
import org.springframework.integration.support.MessageBuilder;
29+
import org.springframework.messaging.Message;
30+
import org.springframework.messaging.PollableChannel;
31+
import org.springframework.test.context.ContextConfiguration;
32+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
33+
34+
/**
35+
* Tests the error-channel in an enricher to produce
36+
* a default object in case of downstream failure.
37+
*
38+
* @author Kris Jacyna
39+
* @since 4.1
40+
*/
41+
@RunWith(SpringJUnit4ClassRunner.class)
42+
@ContextConfiguration
43+
public class EnricherParserTests5 {
44+
45+
@Autowired
46+
private ApplicationContext context;
47+
48+
49+
@Test
50+
public void errorChannelTest() {
51+
52+
class ErrorThrower extends AbstractReplyProducingMessageHandler {
53+
@Override
54+
protected Object handleRequestMessage(Message<?> requestMessage) {
55+
throw new RuntimeException();
56+
}
57+
}
58+
59+
class DefaultTargetProducer extends AbstractReplyProducingMessageHandler {
60+
@Override
61+
protected Object handleRequestMessage(Message<?> requestMessage) {
62+
final Target defaultTarget = new Target();
63+
defaultTarget.setName("Default");
64+
return defaultTarget;
65+
}
66+
}
67+
68+
context.getBean("requestChannel", DirectChannel.class).subscribe(new ErrorThrower());
69+
context.getBean("errChannel", DirectChannel.class).subscribe(new DefaultTargetProducer());
70+
71+
Target original = new Target();
72+
original.setName("John");
73+
Message<?> request = MessageBuilder.withPayload(original).build();
74+
75+
context.getBean("inputChannel", DirectChannel.class).send(request);
76+
77+
Message<?> reply = context.getBean("outputChannel", PollableChannel.class).receive(10000);
78+
Target enriched = (Target) reply.getPayload();
79+
assertEquals("Mr. Default", enriched.getName());
80+
}
81+
82+
83+
public static class Target {
84+
85+
private volatile String name;
86+
87+
public String getName() {
88+
return name;
89+
}
90+
91+
public void setName(String name) {
92+
this.name = name;
93+
}
94+
95+
}
96+
97+
}

0 commit comments

Comments
 (0)