diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/EnricherParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/EnricherParser.java index c56e999fab..55f084912c 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/EnricherParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/EnricherParser.java @@ -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 propertyElements = DomUtils.getChildElementsByTagName(element, "property"); if (!CollectionUtils.isEmpty(propertyElements)) { ManagedMap propertyExpressions = new ManagedMap(); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/transformer/ContentEnricher.java b/spring-integration-core/src/main/java/org/springframework/integration/transformer/ContentEnricher.java index fe693d3bd6..02b0aba300 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/transformer/ContentEnricher.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/transformer/ContentEnricher.java @@ -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 propertyExpressions = new HashMap(); - 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 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; + } } diff --git a/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.1.xsd b/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.1.xsd index 07ec3d65a5..eaf4bbae3b 100644 --- a/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.1.xsd +++ b/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.1.xsd @@ -1014,10 +1014,14 @@ endpoint itself is a Polling Consumer for a channel with a queue. - + - 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. @@ -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. @@ -1089,7 +1097,11 @@ endpoint itself is a Polling Consumer for a channel with a queue. diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTestsWithoutRequestChannel-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTestsWithoutRequestChannel-context.xml new file mode 100644 index 0000000000..1fd7e54d8f --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTestsWithoutRequestChannel-context.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTestsWithoutRequestChannel.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTestsWithoutRequestChannel.java new file mode 100644 index 0000000000..380fe4dce9 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTestsWithoutRequestChannel.java @@ -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 propertyExpressions = (Map) accessor.getPropertyValue("propertyExpressions"); + for (Map.Entry 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; + } + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/ContentEnricherTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/ContentEnricherTests.java index 99f319a767..159ce46d82 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/ContentEnricherTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/ContentEnricherTests.java @@ -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 propertyExpressions = new HashMap(); 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 propertyExpressions = new HashMap(); + 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 propertyExpressions = new HashMap(); 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 propertyExpressions = new HashMap(); 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 propertyExpressions = new HashMap(); + 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 propertyExpressions = new HashMap(); + 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"); + } + } + } \ No newline at end of file