From 33e356da2872dc27819c7889ad9e3fab23b46a62 Mon Sep 17 00:00:00 2001 From: Gunnar Hillert Date: Mon, 7 Nov 2011 16:01:26 -0500 Subject: [PATCH] INT-2228 - Content Enricher - request-channel should be optional see also: https://jira.springsource.org/browse/INT-2228 --- .../config/xml/EnricherParser.java | 7 +- .../transformer/ContentEnricher.java | 56 +++++- .../config/xml/spring-integration-2.1.xsd | 18 +- ...rserTestsWithoutRequestChannel-context.xml | 22 +++ ...icherParserTestsWithoutRequestChannel.java | 131 ++++++++++++++ .../transformer/ContentEnricherTests.java | 164 ++++++++++++++++++ 6 files changed, 385 insertions(+), 13 deletions(-) create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTestsWithoutRequestChannel-context.xml create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTestsWithoutRequestChannel.java 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..cc18017a47 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 @@ -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 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..1c95d20c1c 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 @@ -46,7 +46,7 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem private final Map propertyExpressions = new HashMap(); - 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 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; + } } 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..8ead98e0b5 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; @@ -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 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() { + + 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 propertyExpressions = new HashMap(); + 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 propertyExpressions = new HashMap(); + 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"); + } + } + } \ No newline at end of file