INT-2228 - Content Enricher - request-channel should be optional

see also: https://jira.springsource.org/browse/INT-2228
This commit is contained in:
Gunnar Hillert
2011-11-07 16:01:26 -05:00
committed by Mark Fisher
parent 7e30f7df9e
commit 33e356da28
6 changed files with 385 additions and 13 deletions

View File

@@ -43,10 +43,15 @@ public class EnricherParser extends AbstractConsumerEndpointParser {
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(requestChannel)) {
builder.addConstructorArgReference(requestChannel);
}
if (StringUtils.hasText(replyChannel)) {
builder.addConstructorArgReference(replyChannel);
}
List<Element> propertyElements = DomUtils.getChildElementsByTagName(element, "property");
if (!CollectionUtils.isEmpty(propertyElements)) {
ManagedMap<String, Object> propertyExpressions = new ManagedMap<String, Object>();

View File

@@ -46,7 +46,7 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem
private final Map<Expression, Expression> propertyExpressions = new HashMap<Expression, Expression>();
private final Gateway gateway = new Gateway();
private final Gateway gateway;
private final SpelExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true));
@@ -56,6 +56,15 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem
private Expression requestPayloadExpression;
/**
* Create a Content Enricher without providing a request channel. This is
* useful when only static values shall be enriched.
*/
public ContentEnricher() {
this.evaluationContext.addPropertyAccessor(new MapAccessor());
this.gateway = null;
}
/**
* Create a Content Enricher with the given request channel. An anonymous reply channel
* will be created for each request.
@@ -69,6 +78,9 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem
*/
public ContentEnricher(MessageChannel requestChannel, MessageChannel replyChannel) {
Assert.notNull(requestChannel, "requestChannel must not be null");
this.gateway = new Gateway();
this.gateway.setRequestChannel(requestChannel);
if (replyChannel != null) {
this.gateway.setReplyChannel(replyChannel);
@@ -135,7 +147,11 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem
@Override
public void onInit() {
super.onInit();
this.gateway.afterPropertiesSet();
if (this.gateway != null) {
this.gateway.afterPropertiesSet();
}
}
@Override
@@ -171,7 +187,13 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem
.build();
}
final Message<?> replyMessage = this.gateway.sendAndReceiveMessage(actualRequestMessage);
final Message<?> replyMessage;
if (this.gateway == null) {
replyMessage = actualRequestMessage;
} else {
replyMessage = this.gateway.sendAndReceiveMessage(actualRequestMessage);
}
for (Map.Entry<Expression, Expression> entry : this.propertyExpressions.entrySet()) {
Expression propertyExpression = entry.getKey();
@@ -184,20 +206,36 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem
}
/*
* 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;
@@ -59,6 +64,34 @@ 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() {
try {
new ContentEnricher(null);
} catch (IllegalArgumentException e) {
assertEquals("requestChannel must not be null", e.getMessage());
return;
}
fail("Expected an IllegalArgumentException to be thrown.");
}
@Test
public void nestedProperty() {
QueueChannel replyChannel = new QueueChannel();
@@ -108,6 +141,98 @@ 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(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);
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(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);
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(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 +291,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");
}
}
}