Merge pull request #169 from ghillert/INT-2224

Enricher - Allow to send a sub-set of the payload

  see also: https://jira.springsource.org/browse/INT-2224
This commit is contained in:
Mark Fisher
2011-11-04 13:48:57 -04:00
8 changed files with 420 additions and 82 deletions

View File

@@ -1,115 +1,168 @@
<?xml version="1.0" encoding="UTF-8"?>
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="content-enricher"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Content Enricher</title>
<title>Content Enricher</title>
<section id="content-enricher-introduction">
<title>Introduction</title>
<para>
At times you may have a requirement to enhance a request with more
information than was provided by the target system. The
<link href="http://www.eaipatterns.com/DataEnricher.html">Content Enricher</link>
pattern describes various scenarios as well as the component
(Enricher), which allows you to address such requirements.
</para>
<para>
The Spring Integration <code>Core</code> module includes 2 enrichers:
</para>
<itemizedlist>
<listitem>Header Enricher</listitem>
<listitem>(Generic) Enricher</listitem>
</itemizedlist>
<para>
Furthermore, several <emphasis>Adapter specific Header Enrichers</emphasis>
are included as well:
</para>
<itemizedlist>
<listitem>XPath Header Enricher (XML Module)</listitem>
<listitem>Email Header Enricher (Mail Module)</listitem>
<listitem>XMPP Header Enricher (XMPP Module)</listitem>
</itemizedlist>
<para>
Please go to the adapter specific sections of this reference manual
to learn more about those adapters.
</para>
</section>
<section id="header-enricher">
<title>Header Enricher</title>
<para>
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 <emphasis>Header Enricher</emphasis>
pattern. It is exposed via the <code>&lt;header-enricher&gt;</code> element.
</para>
<section id="content-enricher-introduction">
<title>Introduction</title>
<para>
At times you may have a requirement to enhance a request with more information than was
provided by the target system. The <link href="http://www.eaipatterns.com/DataEnricher.html">Content Enricher</link> pattern
describes various scenarios as well as the component (Enricher), which allows you to address such requirements.
</para>
</section>
<section id="header-enricher">
<title>Header Enricher</title>
<para>
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 <emphasis>Header Enricher</emphasis> pattern. It is exposed via
the <code>&lt;header-enricher&gt;</code> element.
<programlisting language="xml"><![CDATA[<int:header-enricher input-channel="in" output-channel="out">
<int:header name="foo" value="123"/>
<int:header name="bar" ref="someBean"/>
</int:header-enricher>]]></programlisting>
</para>
<para>
The <emphasis>Header Enricher</emphasis> also provides helpful sub-elements to set well-known header names.
<programlisting language="xml"><![CDATA[<int:header-enricher input-channel="in" output-channel="out">
<para>
The <emphasis>Header Enricher</emphasis> also provides helpful sub-elements
to set well-known header names.
</para>
<programlisting language="xml"><![CDATA[<int:header-enricher input-channel="in" output-channel="out">
<int:error-channel ref="applicationErrorChannel"/>
<int:reply-channel ref="quoteReplyChannel"/>
<int:correlation-id value="123"/>
<int:priority value="HIGHEST"/>
<int:header name="bar" ref="someBean"/>
</int:header-enricher>]]></programlisting>
In the above configuration you can clearly see that for well-known headers such as <code>errorChannel</code>,
<code>correlationId</code>, <code>priority</code>, <code>replyChannel</code>etc., instead of using generic
<emphasis>&lt;header&gt;</emphasis> sub-elements where you would have to provide both header 'name' and 'value',
you can use convenient sub-elements to set those values directly.
</para>
<para>
<emphasis>POJO Support</emphasis>
</para>
<para>
Often a header value cannot be defined statically and has to be determined dynamically based on some content in the Message. That is why
<emphasis>Header Enricher</emphasis> allows you to also specify a bean 'ref' and 'method' that will calculate the
header value. Let's look at the following configuration:
<para>
In the above configuration you can clearly see that for well-known
headers such as <code>errorChannel</code>, <code>correlationId</code>,
<code>priority</code>, <code>replyChannel</code>etc., instead of
using generic <emphasis>&lt;header&gt;</emphasis> sub-elements where
you would have to provide both header 'name' and 'value', you can use
convenient sub-elements to set those values directly.
</para>
<programlisting language="xml"><![CDATA[<int:header-enricher input-channel="in" output-channel="out">
<para>
<emphasis>POJO Support</emphasis>
</para>
<para>
Often a header value cannot be defined statically and has to be
determined dynamically based on some content in the Message. That is why
<emphasis>Header Enricher</emphasis> allows you to also specify a bean
reference using the <code>ref</code> and <code>method</code> attribute.
The specified method will calculate the header value. Let's look at
the following configuration:
</para>
<programlisting language="xml"><![CDATA[<int:header-enricher input-channel="in" output-channel="out">
<int:header name="foo" method="computeValue" ref="myBean"/>
</int:header-enricher>
<bean id="myBean" class="foo.bar.MyBean"/>]]></programlisting>
<programlisting language="java"><![CDATA[public class MyBean {
<programlisting language="java"><![CDATA[public class MyBean {
public String computeValue(String payload){
return payload.toUpperCase() + "_US";
}
}]]></programlisting>
</para>
<para>
You can also configure your POJO as inner bean
<para>
You can also configure your POJO as inner bean:
</para>
<programlisting language="xml"><![CDATA[<int:header-enricher input-channel="inputChannel" output-channel="outputChannel">
<int:header name="some_header">
<bean class="org.MyEnricher"/>
</int:header>
</int:header-enricher>]]></programlisting>
<para>
as well as point to a Groovy script:
</para>
as well as point to a Groovy script
<programlisting language="xml"><![CDATA[<int:header-enricher input-channel="inputChannel" output-channel="outputChannel">
<programlisting language="xml"><![CDATA[<int:header-enricher input-channel="inputChannel" output-channel="outputChannel">
<int:header name="some_header">
<int-groovy:script location="org/SampleGroovyHeaderEnricher.groovy"/>
</int:header>
</int:header-enricher>]]></programlisting>
</para>
<para>
<emphasis>SpEL Support</emphasis>
</para>
<para>
In Spring Integration 2.0 we have introduced the convenience of the
<link href="http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/expressions.html">Spring Expression Language (SpEL)</link>
to help configure many different components. The <emphasis>Header
Enricher</emphasis> is one of them.
<para>
<emphasis>SpEL Support</emphasis>
</para>
<para>
In Spring Integration 2.0 we have introduced the convenience of the
<link href="http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/expressions.html">Spring Expression Language (SpEL)</link>
to help configure many different components. The <emphasis>Header Enricher</emphasis> 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.
</para>
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.
<programlisting language="xml"><![CDATA[<int:header-enricher input-channel="in" output-channel="out">
<programlisting language="xml"><![CDATA[<int:header-enricher input-channel="in" output-channel="out">
<int:header name="foo" expression="payload.toUpperCase() + '_US'"/>
</int:header-enricher>]]></programlisting>
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 <emphasis>expression</emphasis> 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.
</para>
<para>
<emphasis>Adapter specific Header Enrichers</emphasis>
</para>
<para>
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.)
</para>
</section>
<para>
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 <emphasis>expression</emphasis> 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.
</para>
</section>
<section id="generic-enricher">
<title>(Generic) Enricher</title>
<para></para>
<programlisting language="xml"><![CDATA[<int:enricher request-channel=""
auto-startup=""
id=""
input-channel=""
order=""
output-channel=""
reply-channel=""
send-timeout=""
should-clone-payload="true">
<int:poller></int:poller>
<int:property name="" expression=""/>
<int:property name="" value=""/>
</int:enricher>]]></programlisting>
</section>
</section>

View File

@@ -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;
}

View File

@@ -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 <b>root object</b>.
*
* For instance the following SpEL expressions (among others) are possible:
*
* <ul>
* <li>payload.foo</li>
* <li>headers.foobar</li>
* <li>new java.util.Date()</li>
* <li>'foo' + 'bar'</li>
* </ul>
*
* 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<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;
}

View File

@@ -0,0 +1,8 @@
/**
* Contains core-implementation of various Transformers which includes Enrichers
* and Filters.
*
* @since 1.0
*
*/
package org.springframework.integration.transformer;

View File

@@ -1052,6 +1052,32 @@ endpoint itself is a Polling Consumer for a channel with a queue.
<xsd:union memberTypes="xsd:boolean xsd:string" />
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="request-payload-expression" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
By default the original message's payload will be used as payload
that will be send to the request-channel. By specifying a SpEL expression
as value for the 'request-payload-expression' attribute, 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:
- payload.foo
- headers.foobar
- new java.util.Date()
- 'foo' + 'bar'
If more sophisticated logic is required (e.g. changing the
message headers etc.) please use additional downstream transformers.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="propertySubElementType">

View File

@@ -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<Expression, Expression> propertyExpressions = (Map<Expression, Expression>) accessor.getPropertyValue("propertyExpressions");
for (Map.Entry<Expression, Expression> 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;
}

View File

@@ -0,0 +1,25 @@
<?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>
<channel id="requests"/>
<enricher id="enricher" input-channel="input" request-channel="requests"
output-channel="output" order="99"
request-payload-expression="payload.age">
<property name="name" expression="'Name as SpEL'"/>
<property name="age" expression="payload.sourceName"/>
</enricher>
</beans:beans>

View File

@@ -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<Expression, Expression> propertyExpressions = (Map<Expression, Expression>) accessor.getPropertyValue("propertyExpressions");
for (Map.Entry<Expression, Expression> 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;
}
}
}