diff --git a/docs/src/reference/docbook/content-enrichment.xml b/docs/src/reference/docbook/content-enrichment.xml index 1bebdd58fe..0d35e186f5 100644 --- a/docs/src/reference/docbook/content-enrichment.xml +++ b/docs/src/reference/docbook/content-enrichment.xml @@ -1,115 +1,168 @@
- Content Enricher + Content Enricher + +
+ Introduction + + At times you may have a requirement to enhance a request with more + information than was provided by the target system. The + Content Enricher + pattern describes various scenarios as well as the component + (Enricher), which allows you to address such requirements. + + + The Spring Integration Core module includes 2 enrichers: + + + Header Enricher + (Generic) Enricher + + + Furthermore, several Adapter specific Header Enrichers + are included as well: + + + XPath Header Enricher (XML Module) + Email Header Enricher (Mail Module) + XMPP Header Enricher (XMPP Module) + + + Please go to the adapter specific sections of this reference manual + to learn more about those adapters. + +
+ +
+ Header Enricher + + + If you only need to add headers to a Message, and they are not + dynamically determined by the Message content, then referencing a + custom implementation of a Transformer may be overkill. For that reason, + Spring Integration provides support for the Header Enricher + pattern. It is exposed via the <header-enricher> element. + -
- Introduction - - At times you may have a requirement to enhance a request with more information than was - provided by the target system. The Content Enricher pattern - describes various scenarios as well as the component (Enricher), which allows you to address such requirements. - -
- -
- Header Enricher - - - If you only need to add headers to a Message, and they are not dynamically determined by the Message content, - then referencing a custom implementation of a Transformer may be overkill. For that reason, - Spring Integration provides support for the Header Enricher pattern. It is exposed via - the <header-enricher> element. - ]]> - - - The Header Enricher also provides helpful sub-elements to set well-known header names. - + + The Header Enricher also provides helpful sub-elements + to set well-known header names. + + + ]]> - - In the above configuration you can clearly see that for well-known headers such as errorChannel, - correlationId, priority, replyChanneletc., instead of using generic - <header> sub-elements where you would have to provide both header 'name' and 'value', - you can use convenient sub-elements to set those values directly. - - - POJO Support - - - Often a header value cannot be defined statically and has to be determined dynamically based on some content in the Message. That is why - Header Enricher allows you to also specify a bean 'ref' and 'method' that will calculate the - header value. Let's look at the following configuration: + + In the above configuration you can clearly see that for well-known + headers such as errorChannel, correlationId, + priority, replyChanneletc., instead of + using generic <header> sub-elements where + you would have to provide both header 'name' and 'value', you can use + convenient sub-elements to set those values directly. + - + + POJO Support + + + + Often a header value cannot be defined statically and has to be + determined dynamically based on some content in the Message. That is why + Header Enricher allows you to also specify a bean + reference using the ref and method attribute. + The specified method will calculate the header value. Let's look at + the following configuration: + + + ]]> - - - - You can also configure your POJO as inner bean + + + You can also configure your POJO as inner bean: + ]]> + + as well as point to a Groovy script: + -as well as point to a Groovy script - - + ]]> - + + SpEL Support + + + In Spring Integration 2.0 we have introduced the convenience of the + Spring Expression Language (SpEL) + to help configure many different components. The Header + Enricher is one of them. - - SpEL Support - - - In Spring Integration 2.0 we have introduced the convenience of the - Spring Expression Language (SpEL) - to help configure many different components. The Header Enricher is one of them. + Looking again at the POJO example above, you can see that the computation + logic to determine the header value is actually pretty simple. A natural + question would be: "is there a simpler way to accomplish this?". That + is where SpEL shows its true power. + - Looking again at the POJO example above, you can see that the computation logic to determine the header value is actually pretty simple. - A natural question would be: "is there a simpler way to accomplish this?". That is where SpEL shows its true power. - - + ]]> - As you can see, by using SpEL for such simple cases, we no longer have to provide a separate class and configure - it in the application context. All we need is the expression attribute configured with a valid - SpEL expression. The 'payload' and 'headers' variables are bound to the SpEL Evaluation Context, - giving you full access to the incoming Message. - - - Adapter specific Header Enrichers - - - As you go through the manual, you will see that as an added convenience, - Spring Integration also provides adapter specific Header Enrichers (e.g., MAIL, XMPP, etc.) - -
+ + As you can see, by using SpEL for such simple cases, we no longer have + to provide a separate class and configure it in the application context. + All we need is the expression attribute configured + with a valid SpEL expression. The 'payload' and 'headers' variables + are bound to the SpEL Evaluation Context, giving you full access to + the incoming Message. + +
+ +
+ (Generic) Enricher + + + + + + +]]> +
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 075f1b0080..c56e999fab 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 @@ -32,7 +32,7 @@ import org.springframework.util.xml.DomUtils; /** * Parser for the 'enricher' element. - * + * * @author Mark Fisher * @since 2.1 */ @@ -75,7 +75,17 @@ public class EnricherParser extends AbstractConsumerEndpointParser { } builder.addPropertyValue("propertyExpressions", propertyExpressions); } + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "should-clone-payload"); + + String requestPayloadExpression = element.getAttribute("request-payload-expression"); + + if (StringUtils.hasText(requestPayloadExpression)) { + BeanDefinitionBuilder expressionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class); + expressionBuilder.addConstructorArgValue(requestPayloadExpression); + builder.addPropertyValue("requestPayloadExpression", expressionBuilder.getBeanDefinition()); + } + return builder; } 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 a11abf4125..fe693d3bd6 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 @@ -31,6 +31,7 @@ import org.springframework.integration.MessageChannel; import org.springframework.integration.MessageHandlingException; import org.springframework.integration.gateway.MessagingGatewaySupport; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; +import org.springframework.integration.support.MessageBuilder; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -53,6 +54,7 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem private volatile boolean shouldClonePayload = false; + private Expression requestPayloadExpression; /** * Create a Content Enricher with the given request channel. An anonymous reply channel @@ -94,6 +96,34 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem } } + /** + * By default the original message's payload will be used as the actual payload + * that will be send to the request-channel. + * + * By providing a SpEL expression as value for this setter, a subset of the + * original payload, a header value or any other resolvable SpEL expression + * can be used as the basis for the payload, that will be send to the + * request-channel. + * + * For the Expression evaluation the full message is available as the root object. + * + * For instance the following SpEL expressions (among others) are possible: + * + * + * + * If more sophisticated logic is required (e.g. changing the message + * headers etc.) please use additional downstream transformers. + * + */ + public void setRequestPayloadExpression(Expression requestPayloadExpression) { + this.requestPayloadExpression = requestPayloadExpression; + } + /** * Specify whether to clone payload objects to create the target object. * This is only applicable for payload types that implement Cloneable. @@ -110,23 +140,46 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler implem @Override protected Object handleRequestMessage(Message requestMessage) { - Object targetPayload = requestMessage.getPayload(); - if (targetPayload instanceof Cloneable && this.shouldClonePayload) { + + final Object requestPayload = requestMessage.getPayload(); + + final Object targetPayload; + + if (requestPayload instanceof Cloneable && this.shouldClonePayload) { try { - Method cloneMethod = targetPayload.getClass().getMethod("clone", new Class[0]); - targetPayload = ReflectionUtils.invokeMethod(cloneMethod, targetPayload); + Method cloneMethod = requestPayload.getClass().getMethod("clone", new Class[0]); + targetPayload = ReflectionUtils.invokeMethod(cloneMethod, requestPayload); } catch (Exception e) { throw new MessageHandlingException(requestMessage, "Failed to clone payload object", e); } + } else { + targetPayload = requestPayload; } - Message replyMessage = this.gateway.sendAndReceiveMessage(requestMessage); + + final Message actualRequestMessage; + + if (this.requestPayloadExpression==null) { + + actualRequestMessage = requestMessage; + + } else { + + final Object requestMessagePayload = this.requestPayloadExpression.getValue(this.evaluationContext, requestMessage); + actualRequestMessage = MessageBuilder.withPayload(requestMessagePayload) + .copyHeaders(requestMessage.getHeaders()) + .build(); + } + + 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; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/transformer/package-info.java b/spring-integration-core/src/main/java/org/springframework/integration/transformer/package-info.java new file mode 100644 index 0000000000..62c9ce0e22 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/transformer/package-info.java @@ -0,0 +1,8 @@ +/** + * Contains core-implementation of various Transformers which includes Enrichers + * and Filters. + * + * @since 1.0 + * + */ +package org.springframework.integration.transformer; \ No newline at end of file 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 e9e95326d5..07ec3d65a5 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 @@ -1052,6 +1052,32 @@ 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/EnricherParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests.java index 0a30de97b7..7eb96882e1 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests.java @@ -17,6 +17,7 @@ package org.springframework.integration.config.xml; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNotSame; import java.util.Map; @@ -63,6 +64,8 @@ public class EnricherParserTests { DirectFieldAccessor accessor = new DirectFieldAccessor(enricher); assertEquals(context.getBean("output"), accessor.getPropertyValue("outputChannel")); assertEquals(true, 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())) { @@ -98,7 +101,7 @@ public class EnricherParserTests { private static class Source { - + private final String sourceName; Source(String sourceName) { @@ -113,23 +116,23 @@ public class EnricherParserTests { 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; } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserWithRequestPayloadExpressionTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserWithRequestPayloadExpressionTests-context.xml new file mode 100644 index 0000000000..6c808c411b --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserWithRequestPayloadExpressionTests-context.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserWithRequestPayloadExpressionTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserWithRequestPayloadExpressionTests.java new file mode 100644 index 0000000000..f8bcb98a77 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserWithRequestPayloadExpressionTests.java @@ -0,0 +1,160 @@ +/* + * 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.assertTrue; +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.core.SubscribableChannel; +import org.springframework.integration.endpoint.EventDrivenConsumer; +import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; +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 EnricherParserWithRequestPayloadExpressionTests { + + @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); + assertEquals(context.getBean("output"), accessor.getPropertyValue("outputChannel")); + assertEquals(false, accessor.getPropertyValue("shouldClonePayload")); + + Expression requestPayloadExpression = (Expression) accessor.getPropertyValue("requestPayloadExpression"); + assertEquals("payload.age", requestPayloadExpression.getExpressionString()); + + Map propertyExpressions = (Map) accessor.getPropertyValue("propertyExpressions"); + for (Map.Entry e : propertyExpressions.entrySet()) { + if ("name".equals(e.getKey().getExpressionString())) { + assertEquals("'Name as SpEL'", e.getValue().getExpressionString()); + } + else if ("age".equals(e.getKey().getExpressionString())) { + assertEquals("payload.sourceName", e.getValue().getExpressionString()); + } + else { + throw new IllegalStateException("expected 'name' and 'age' only, not: " + e.getKey().getExpressionString()); + } + } + } + + @Test + public void integrationTest() { + SubscribableChannel requests = context.getBean("requests", SubscribableChannel.class); + requests.subscribe(new AbstractReplyProducingMessageHandler() { + @Override + protected Object handleRequestMessage(Message requestMessage) { + + assertTrue("Expected the payload of the requestMessage to be a String", + requestMessage.getPayload() instanceof Integer); + + Integer payload = (Integer) requestMessage.getPayload(); + assertEquals("Expected value: 99", Integer.valueOf(99), payload); + + return new Source(String.valueOf(payload)); + } + }); + + Target original = new Target(); + original.setAge(99); + + 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("Name as SpEL", enriched.getName()); + assertEquals(99, enriched.getAge()); + assertSame(original, enriched); + } + + + private static class Source { + + private final String sourceName; + + Source(String sourceName) { + this.sourceName = sourceName; + } + + @SuppressWarnings("unused") + public String getSourceName() { + return sourceName; + } + } + + + 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; + } + } + +}