Merge pull request #175 from ghillert/INT-2228

Enricher fixes based on code review

  Content Enricher - request-channel should be optional

  see also: https://jira.springsource.org/browse/INT-2228
This commit is contained in:
Mark Fisher
2011-11-10 15:32:43 -05:00
6 changed files with 447 additions and 59 deletions

View File

@@ -41,12 +41,10 @@ public class EnricherParser extends AbstractConsumerEndpointParser {
@Override
protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) {
final BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(ContentEnricher.class);
String requestChannel = element.getAttribute("request-channel");
String replyChannel = element.getAttribute("reply-channel");
builder.addConstructorArgReference(requestChannel);
if (StringUtils.hasText(replyChannel)) {
builder.addConstructorArgReference(replyChannel);
}
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "request-channel");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "reply-channel");
List<Element> propertyElements = DomUtils.getChildElementsByTagName(element, "property");
if (!CollectionUtils.isEmpty(propertyElements)) {
ManagedMap<String, Object> propertyExpressions = new ManagedMap<String, Object>();

View File

@@ -36,18 +36,19 @@ import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Content Enricher is a Message Transformer that invokes any downstream message flow via
* its request channel and then applies values from the reply Message to the original payload.
* Content Enricher is a Message Transformer that can augment a message's payload
* with either static values or by optionally invoking a downstream message flow
* via its request channel and then applying values from the reply Message to the
* original payload.
*
* @author Mark Fisher
* @author Gunnar Hillert
* @since 2.1
*/
public class ContentEnricher extends AbstractReplyProducingMessageHandler implements Lifecycle {
private final Map<Expression, Expression> propertyExpressions = new HashMap<Expression, Expression>();
private final Gateway gateway = new Gateway();
private final SpelExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true));
private final StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
@@ -56,25 +57,11 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem
private Expression requestPayloadExpression;
/**
* Create a Content Enricher with the given request channel. An anonymous reply channel
* will be created for each request.
*/
public ContentEnricher(MessageChannel requestChannel) {
this(requestChannel, null);
}
private volatile MessageChannel requestChannel;
/**
* Create a Content Enricher with the given request and reply channels.
*/
public ContentEnricher(MessageChannel requestChannel, MessageChannel replyChannel) {
Assert.notNull(requestChannel, "requestChannel must not be null");
this.gateway.setRequestChannel(requestChannel);
if (replyChannel != null) {
this.gateway.setReplyChannel(replyChannel);
}
this.evaluationContext.addPropertyAccessor(new MapAccessor());
}
private volatile MessageChannel replyChannel;
private volatile Gateway gateway = null;
/**
@@ -96,6 +83,25 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem
}
}
/**
* Sets the content enricher's request channel. If specified, then an internal
* {@link Gateway} will be initialized. Setting a request channel is optional.
* Not setting a request channel is useful in situations where
* message payloads shall be enriched with static values only.
*/
public void setRequestChannel(MessageChannel requestChannel) {
this.requestChannel = requestChannel;
}
/**
* Sets the content enricher's reply channel. If not specified, yet the request
* channel is set, an anonymous reply channel will automatically created
* for each request.
*/
public void setReplyChannel(MessageChannel replyChannel) {
this.replyChannel = replyChannel;
}
/**
* By default the original message's payload will be used as the actual payload
* that will be send to the request-channel.
@@ -132,19 +138,32 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem
this.shouldClonePayload = shouldClonePayload;
}
/**
* Initializes the Content Enricher. Will instantiate an internal Gateway if
* the requestChannel is set.
*/
@Override
public void onInit() {
super.onInit();
this.gateway.afterPropertiesSet();
if (this.replyChannel != null) {
Assert.notNull(this.requestChannel, "If the replyChannel is set, then the requestChannel must not be null");
}
if (this.requestChannel != null) {
this.gateway = new Gateway();
this.gateway.setRequestChannel(requestChannel);
if (replyChannel != null) {
this.gateway.setReplyChannel(replyChannel);
}
this.gateway.afterPropertiesSet();
}
this.evaluationContext.addPropertyAccessor(new MapAccessor());
}
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
final Object requestPayload = requestMessage.getPayload();
final Object targetPayload;
if (requestPayload instanceof Cloneable && this.shouldClonePayload) {
try {
Method cloneMethod = requestPayload.getClass().getMethod("clone", new Class<?>[0]);
@@ -153,51 +172,67 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem
catch (Exception e) {
throw new MessageHandlingException(requestMessage, "Failed to clone payload object", e);
}
} else {
}
else {
targetPayload = requestPayload;
}
final Message<?> actualRequestMessage;
if (this.requestPayloadExpression==null) {
actualRequestMessage = requestMessage;
} else {
if (this.requestPayloadExpression == null) {
actualRequestMessage = requestMessage;
}
else {
final Object requestMessagePayload = this.requestPayloadExpression.getValue(this.evaluationContext, requestMessage);
actualRequestMessage = MessageBuilder.withPayload(requestMessagePayload)
.copyHeaders(requestMessage.getHeaders())
.build();
.copyHeaders(requestMessage.getHeaders()).build();
}
final Message<?> replyMessage;
if (this.gateway == null) {
replyMessage = actualRequestMessage;
}
else {
replyMessage = this.gateway.sendAndReceiveMessage(actualRequestMessage);
}
final Message<?> replyMessage = this.gateway.sendAndReceiveMessage(actualRequestMessage);
for (Map.Entry<Expression, Expression> entry : this.propertyExpressions.entrySet()) {
Expression propertyExpression = entry.getKey();
Expression valueExpression = entry.getValue();
Object value = valueExpression.getValue(this.evaluationContext, replyMessage);
propertyExpression.setValue(this.evaluationContext, targetPayload, value);
}
return targetPayload;
}
/*
* Lifecycle implementation
/**
* Lifecycle implementation. If no requestChannel is defined, this method
* has no effect as in that case no Gateway is initialized.
*/
public void start() {
this.gateway.start();
if (this.gateway != null) {
this.gateway.start();
}
}
/**
* Lifecycle implementation. If no requestChannel is defined, this method
* has no effect as in that case no Gateway is initialized.
*/
public void stop() {
this.gateway.stop();
if (this.gateway != null) {
this.gateway.stop();
}
}
/**
* Lifecycle implementation. If no requestChannel is defined, this method
* will return always return true as no Gateway is initialized.
*/
public boolean isRunning() {
return this.gateway.isRunning();
if (this.gateway != null) {
return this.gateway.isRunning();
}
else {
return true;
}
}

View File

@@ -1014,10 +1014,14 @@ endpoint itself is a Polling Consumer for a channel with a queue.
</xsd:annotation>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="request-channel" type="xsd:string" use="required">
<xsd:attribute name="request-channel" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation>
Channel to which a Message will be sent to get the data to use for enrichment.
Channel to which a Message will be sent to get the data to use
for enrichment. This attribute is optional. Not specifying a
'request-channel' is useful in situations, where only static
values shall be used for enrichment using the 'property'
sub-element.
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
@@ -1045,6 +1049,10 @@ endpoint itself is a Polling Consumer for a channel with a queue.
Boolean value indicating whether any payload that implements Cloneable should be cloned
prior to sending the Message to the request chanenl for acquiring the enriching data.
The cloned version would be used as the target payload for the ultimate reply.
If the payload does NOT implement 'Cloneable', then setting this
attribute to 'true' has NO effect.
Default is false.
</xsd:documentation>
</xsd:annotation>
@@ -1089,7 +1097,11 @@ endpoint itself is a Polling Consumer for a channel with a queue.
<xsd:attribute name="name" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation><![CDATA[
The name of the property on the target payload.
The name of the property on the target payload. Please be aware
that this value is a SpEL expression, also. For example, if
your payload is represented by a 'java.util.Map', you can add new
Map entries using the 'name' attribute, e.g. name='foo' would add a new
Map entry with key 'foo'.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/integration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration
http://www.springframework.org/schema/integration/spring-integration.xsd">
<channel id="input"/>
<channel id="output">
<queue />
</channel>
<enricher id="enricher" input-channel="input" order="99"
output-channel="output">
<property name="name" expression="payload.name"/>
<property name="age" value="42"/>
</enricher>
</beans:beans>

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2002-2011 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
*
* http://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.config.xml;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.integration.Message;
import org.springframework.integration.MessageChannel;
import org.springframework.integration.core.PollableChannel;
import org.springframework.integration.endpoint.EventDrivenConsumer;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.integration.transformer.ContentEnricher;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Mark Fisher
* @since 2.1
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class EnricherParserTestsWithoutRequestChannel {
@Autowired
private ApplicationContext context;
@Test
@SuppressWarnings("unchecked")
public void configurationCheck() {
Object endpoint = context.getBean("enricher");
assertEquals(EventDrivenConsumer.class, endpoint.getClass());
Object handler = TestUtils.getPropertyValue(endpoint, "handler");
assertEquals(ContentEnricher.class, handler.getClass());
ContentEnricher enricher = (ContentEnricher) handler;
assertEquals(99, enricher.getOrder());
DirectFieldAccessor accessor = new DirectFieldAccessor(enricher);
assertNull(accessor.getPropertyValue("gateway"));
assertEquals(context.getBean("output"), accessor.getPropertyValue("outputChannel"));
assertEquals(false, accessor.getPropertyValue("shouldClonePayload"));
assertNull(accessor.getPropertyValue("requestPayloadExpression"));
Map<Expression, Expression> propertyExpressions = (Map<Expression, Expression>) accessor.getPropertyValue("propertyExpressions");
for (Map.Entry<Expression, Expression> e : propertyExpressions.entrySet()) {
if ("name".equals(e.getKey().getExpressionString())) {
assertEquals("payload.name", e.getValue().getExpressionString());
}
else if ("age".equals(e.getKey().getExpressionString())) {
assertEquals("42", e.getValue().getExpressionString());
}
else {
throw new IllegalStateException("expected 'name' and 'age' only, not: " + e.getKey().getExpressionString());
}
}
}
@Test
public void integrationTest() {
Target original = new Target();
original.setAge(100);
original.setName("original name");
Message<?> request = MessageBuilder.withPayload(original).build();
context.getBean("input", MessageChannel.class).send(request);
Message<?> reply = context.getBean("output", PollableChannel.class).receive(0);
Target enriched = (Target) reply.getPayload();
assertEquals("original name", enriched.getName());
assertEquals(42, enriched.getAge());
assertSame(original, enriched);
}
public static class Target implements Cloneable {
private volatile String name;
private volatile int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Object clone() {
Target copy = new Target();
copy.setName(this.name);
copy.setAge(this.age);
return copy;
}
}
}

View File

@@ -18,6 +18,10 @@ package org.springframework.integration.transformer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import java.util.HashMap;
import java.util.Map;
@@ -26,6 +30,7 @@ import org.junit.Test;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.Message;
import org.springframework.integration.MessageHandlingException;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
@@ -47,11 +52,16 @@ public class ContentEnricherTests {
return new Source("John", "Doe");
}
});
ContentEnricher enricher = new ContentEnricher(requestChannel);
ContentEnricher enricher = new ContentEnricher();
enricher.setRequestChannel(requestChannel);
SpelExpressionParser parser = new SpelExpressionParser();
Map<String, Expression> propertyExpressions = new HashMap<String, Expression>();
propertyExpressions.put("name", parser.parseExpression("payload.lastName + ', ' + payload.firstName"));
enricher.setPropertyExpressions(propertyExpressions);
enricher.afterPropertiesSet();
Target target = new Target("replace me");
Message<?> requestMessage = MessageBuilder.withPayload(target).setReplyChannel(replyChannel).build();
enricher.handleMessage(requestMessage);
@@ -59,6 +69,37 @@ public class ContentEnricherTests {
assertEquals("Doe, John", ((Target) reply.getPayload()).getName());
}
@Test
public void testSimplePropertyWithoutUsingRequestChannel() {
QueueChannel replyChannel = new QueueChannel();
ContentEnricher enricher = new ContentEnricher();
SpelExpressionParser parser = new SpelExpressionParser();
Map<String, Expression> propertyExpressions = new HashMap<String, Expression>();
propertyExpressions.put("name", parser.parseExpression("'just a static string'"));
enricher.setPropertyExpressions(propertyExpressions);
Target target = new Target("replace me");
Message<?> requestMessage = MessageBuilder.withPayload(target).setReplyChannel(replyChannel).build();
enricher.handleMessage(requestMessage);
Message<?> reply = replyChannel.receive(0);
assertEquals("just a static string", ((Target) reply.getPayload()).getName());
}
@Test
public void testContentEnricherWithNullRequestChannel() {
ContentEnricher enricher = new ContentEnricher();
enricher.setReplyChannel(new QueueChannel());
try {
enricher.afterPropertiesSet();
} catch (IllegalArgumentException e) {
assertEquals("If the replyChannel is set, then the requestChannel must not be null", e.getMessage());
return;
}
fail("Expected an IllegalArgumentException to be thrown.");
}
@Test
public void nestedProperty() {
QueueChannel replyChannel = new QueueChannel();
@@ -69,11 +110,15 @@ public class ContentEnricherTests {
return new Source("John", "Doe");
}
});
ContentEnricher enricher = new ContentEnricher(requestChannel);
ContentEnricher enricher = new ContentEnricher();
enricher.setRequestChannel(requestChannel);
SpelExpressionParser parser = new SpelExpressionParser();
Map<String, Expression> propertyExpressions = new HashMap<String, Expression>();
propertyExpressions.put("child.name", parser.parseExpression("payload.lastName + ', ' + payload.firstName"));
enricher.setPropertyExpressions(propertyExpressions);
enricher.afterPropertiesSet();
Target target = new Target("test");
Message<?> requestMessage = MessageBuilder.withPayload(target).setReplyChannel(replyChannel).build();
enricher.handleMessage(requestMessage);
@@ -93,12 +138,16 @@ public class ContentEnricherTests {
return new Source("John", "Doe");
}
});
ContentEnricher enricher = new ContentEnricher(requestChannel);
ContentEnricher enricher = new ContentEnricher();
enricher.setRequestChannel(requestChannel);
enricher.setShouldClonePayload(true);
SpelExpressionParser parser = new SpelExpressionParser();
Map<String, Expression> propertyExpressions = new HashMap<String, Expression>();
propertyExpressions.put("name", parser.parseExpression("payload.lastName + ', ' + payload.firstName"));
enricher.setPropertyExpressions(propertyExpressions);
enricher.afterPropertiesSet();
Target target = new Target("replace me");
Message<?> requestMessage = MessageBuilder.withPayload(target).setReplyChannel(replyChannel).build();
enricher.handleMessage(requestMessage);
@@ -108,6 +157,108 @@ public class ContentEnricherTests {
assertNotSame(target, result);
}
@Test
public void clonePayloadIgnored() {
QueueChannel replyChannel = new QueueChannel();
DirectChannel requestChannel = new DirectChannel();
requestChannel.subscribe(new AbstractReplyProducingMessageHandler() {
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
return new Source("John", "Doe");
}
});
ContentEnricher enricher = new ContentEnricher();
enricher.setRequestChannel(requestChannel);
enricher.setShouldClonePayload(true);
SpelExpressionParser parser = new SpelExpressionParser();
Map<String, Expression> propertyExpressions = new HashMap<String, Expression>();
propertyExpressions.put("name", parser.parseExpression("payload.lastName + ', ' + payload.firstName"));
enricher.setPropertyExpressions(propertyExpressions);
enricher.afterPropertiesSet();
TargetUser target = new TargetUser();
target.setName("replace me");
Message<?> requestMessage = MessageBuilder.withPayload(target).setReplyChannel(replyChannel).build();
enricher.handleMessage(requestMessage);
Message<?> reply = replyChannel.receive(0);
TargetUser result = (TargetUser) reply.getPayload();
assertEquals("Doe, John", result.getName());
assertSame(target, result);
}
@Test
public void clonePayloadWithFailure() {
QueueChannel replyChannel = new QueueChannel();
DirectChannel requestChannel = new DirectChannel();
requestChannel.subscribe(new AbstractReplyProducingMessageHandler() {
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
return new Source("John", "Doe");
}
});
ContentEnricher enricher = new ContentEnricher();
enricher.setRequestChannel(requestChannel);
enricher.setShouldClonePayload(true);
SpelExpressionParser parser = new SpelExpressionParser();
Map<String, Expression> propertyExpressions = new HashMap<String, Expression>();
propertyExpressions.put("name", parser.parseExpression("payload.lastName + ', ' + payload.firstName"));
enricher.setPropertyExpressions(propertyExpressions);
enricher.afterPropertiesSet();
UncloneableTargetUser target = new UncloneableTargetUser();
target.setName("replace me");
Message<?> requestMessage = MessageBuilder.withPayload(target).setReplyChannel(replyChannel).build();
try {
enricher.handleMessage(requestMessage);
} catch (MessageHandlingException e) {
assertEquals("Failed to clone payload object", e.getMessage());
return;
}
fail("Expected a MessageHandlingException to be thrown.");
}
@Test
public void testLifeCycleMethodsWithoutRequestChannel() {
ContentEnricher enricher = new ContentEnricher();
enricher.afterPropertiesSet();
assertTrue(enricher.isRunning());
enricher.stop();
assertTrue(enricher.isRunning());
}
@Test
public void testLifeCycleMethodsWithRequestChannel() {
DirectChannel requestChannel = new DirectChannel();
requestChannel.subscribe(new AbstractReplyProducingMessageHandler() {
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
return new Source("John", "Doe");
}
});
ContentEnricher enricher = new ContentEnricher();
enricher.setRequestChannel(requestChannel);
enricher.afterPropertiesSet();
enricher.start();
assertTrue(enricher.isRunning());
enricher.stop();
assertFalse(enricher.isRunning());
enricher.start();
assertTrue(enricher.isRunning());
}
@SuppressWarnings("unused")
private static final class Source {
@@ -166,4 +317,43 @@ public class ContentEnricherTests {
}
}
public static final class TargetUser {
private volatile String name;
public TargetUser() {
this.name = "default";
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
public static final class UncloneableTargetUser implements Cloneable {
private volatile String name;
public UncloneableTargetUser() {
this.name = "default";
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Object clone() {
throw new IllegalStateException("Cloning not possible");
}
}
}