initial implementation

added enricher parser

addressed PR comments
This commit is contained in:
Mark Fisher
2011-10-17 15:33:25 -04:00
parent 24714fa711
commit aeaf7d6e89
7 changed files with 686 additions and 1 deletions

View File

@@ -0,0 +1,82 @@
/*
* 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 java.util.List;
import org.w3c.dom.Element;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.config.ExpressionFactoryBean;
import org.springframework.integration.transformer.ContentEnricher;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
/**
* Parser for the 'enricher' element.
*
* @author Mark Fisher
* @since 2.1
*/
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);
}
List<Element> propertyElements = DomUtils.getChildElementsByTagName(element, "property");
if (!CollectionUtils.isEmpty(propertyElements)) {
ManagedMap<String, Object> propertyExpressions = new ManagedMap<String, Object>();
for (Element propertyElement : propertyElements) {
String name = propertyElement.getAttribute("name");
String value = propertyElement.getAttribute("value");
String expression = propertyElement.getAttribute("expression");
if (StringUtils.hasText(value) && StringUtils.hasText(expression)) {
parserContext.getReaderContext().error("The 'value' and 'expression' attributes are mutually exclusive on " +
"an <enricher> element's <property> sub-element.", parserContext.extractSource(propertyElement));
}
if (StringUtils.hasText(value)) {
BeanDefinitionBuilder expressionBuilder = BeanDefinitionBuilder.genericBeanDefinition(LiteralExpression.class);
expressionBuilder.addConstructorArgValue(value);
propertyExpressions.put(name, expressionBuilder.getBeanDefinition());
}
else if (StringUtils.hasText(expression)) {
BeanDefinitionBuilder expressionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class);
expressionBuilder.addConstructorArgValue(expression);
propertyExpressions.put(name, expressionBuilder.getBeanDefinition());
}
else {
parserContext.getReaderContext().error("Exactly one of 'value' or 'expression' attributes must be provided on " +
"an <enricher> element's <property> sub-element.", parserContext.extractSource(propertyElement));
}
}
builder.addPropertyValue("propertyExpressions", propertyExpressions);
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "should-clone-payload");
return builder;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* 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.
@@ -31,6 +31,7 @@ public class IntegrationNamespaceHandler extends AbstractIntegrationNamespaceHan
registerBeanDefinitionParser("publish-subscribe-channel", new PublishSubscribeChannelParser());
registerBeanDefinitionParser("service-activator", new ServiceActivatorParser());
registerBeanDefinitionParser("transformer", new TransformerParser());
registerBeanDefinitionParser("enricher", new EnricherParser());
registerBeanDefinitionParser("filter", new FilterParser());
registerBeanDefinitionParser("router", new DefaultRouterParser());
registerBeanDefinitionParser("header-value-router", new HeaderValueRouterParser());

View File

@@ -0,0 +1,161 @@
/*
* 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.transformer;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.springframework.context.Lifecycle;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.Message;
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.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.
*
* @author Mark Fisher
* @since 2.1
*/
public class ContentEnricher extends AbstractReplyProducingMessageHandler implements Lifecycle {
private final Map<Expression, Expression> propertyExpressions = new HashMap<Expression, Expression>();
private final Gateway gateway = new Gateway();
private final SpelExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true));
private final StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
private volatile boolean shouldClonePayload = false;
/**
* 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);
}
/**
* 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);
}
}
/**
* Provide the map of expressions to evaluate when enriching the target payload.
* The keys should simply be property names, and the values should be Expressions
* that will evaluate against the reply Message as the root object.
*/
public void setPropertyExpressions(Map<String, Expression> propertyExpressions) {
Assert.notEmpty(propertyExpressions, "propertyExpressions must not be empty");
synchronized (this.propertyExpressions) {
this.propertyExpressions.clear();
for (Map.Entry<String, Expression> entry : propertyExpressions.entrySet()) {
String key = entry.getKey();
Expression value = entry.getValue();
Assert.notNull(key, "propertyExpressions key must not be null");
Assert.notNull(value, "propertyExpressions value must not be null");
this.propertyExpressions.put(parser.parseExpression(key), value);
}
}
}
/**
* Specify whether to clone payload objects to create the target object.
* This is only applicable for payload types that implement Cloneable.
*/
public void setShouldClonePayload(boolean shouldClonePayload) {
this.shouldClonePayload = shouldClonePayload;
}
@Override
public void onInit() {
super.onInit();
this.gateway.afterPropertiesSet();
}
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
Object targetPayload = requestMessage.getPayload();
if (targetPayload instanceof Cloneable && this.shouldClonePayload) {
try {
Method cloneMethod = targetPayload.getClass().getMethod("clone", new Class<?>[0]);
targetPayload = ReflectionUtils.invokeMethod(cloneMethod, targetPayload);
}
catch (Exception e) {
throw new MessageHandlingException(requestMessage, "Failed to clone payload object", e);
}
}
Message<?> replyMessage = this.gateway.sendAndReceiveMessage(requestMessage);
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(targetPayload, value);
}
return targetPayload;
}
/*
* Lifecycle implementation
*/
public void start() {
this.gateway.start();
}
public void stop() {
this.gateway.stop();
}
public boolean isRunning() {
return this.gateway.isRunning();
}
/**
* Internal gateway implementation for request/reply handling.
* Simply exposes the sendAndReceiveMessage method.
*/
private static final class Gateway extends MessagingGatewaySupport {
@Override
protected Message<?> sendAndReceiveMessage(Object object) {
return super.sendAndReceiveMessage(object);
}
}
}

View File

@@ -981,6 +981,109 @@ endpoint itself is a Polling Consumer for a channel with a queue.
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="enricher">
<xsd:annotation>
<xsd:documentation>
Defines an endpoint that passes a Message to its request-channel
and then expects a reply Message. The reply Message then becomes
the root object for evaluation of expressions to enriche the
target payload.
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="enricher-type">
<xsd:attributeGroup ref="inputOutputChannelGroup" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="enricher-type">
<xsd:sequence>
<xsd:element name="poller" type="basePollerType" minOccurs="0" maxOccurs="1"/>
<xsd:element name="property" type="propertySubElementType" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
Each property sub-element provides the name of a property (via the required 'name' attribute).
That property should be settable on the target payload instance. Exactly one of the 'value'
or 'expression' attributes must be provided as well. The former for a literal value to set,
and the latter for a SpEL expression to be evaluated. The root object of the evaluation
context is the Message that was returned from the flow initiated by this enricher.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="request-channel" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>
Channel to which a Message will be sent to get the data to use for enrichment.
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.MessageChannel"/>
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="reply-channel" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation>
Channel where a reply Message is expected. This is optional; typically the auto-generated
temporary reply channel is sufficient.
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.MessageChannel"/>
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="should-clone-payload">
<xsd:annotation>
<xsd:documentation>
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.
Default is false.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="xsd:boolean xsd:string" />
</xsd:simpleType>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="propertySubElementType">
<xsd:annotation>
<xsd:documentation> <![CDATA[
Sub-element type for the 'enricher' element.
]]></xsd:documentation>
</xsd:annotation>
<xsd:attribute name="name" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation><![CDATA[
The name of the property on the target payload.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="value" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
The value of the property. Either this or 'expression' must be provided.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="expression" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Expression to be evaluated to produce a value for the property.
Either this or 'value' must be provided.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:element name="delayer">
<xsd:annotation>
<xsd:documentation>
@@ -1113,6 +1216,7 @@ endpoint itself is a Polling Consumer for a channel with a queue.
<xsd:element name="transformer" type="expressionOrInnerEndpointDefinitionAware" />
<xsd:element name="header-enricher" type="header-enricher-type" />
<xsd:element name="header-filter" type="header-filter-type" />
<xsd:element name="enricher" type="enricher-type" />
<xsd:element name="filter" type="filter-type" />
<xsd:element name="aggregator" type="aggregator-type" />
<xsd:element name="resequencer" type="resequencer-type" />

View File

@@ -0,0 +1,23 @@
<?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" order="99" should-clone-payload="true" output-channel="output">
<property name="name" expression="payload.sourceName"/>
<property name="age" value="42"/>
</enricher>
</beans:beans>

View File

@@ -0,0 +1,145 @@
/*
* 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.assertNotSame;
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 EnricherParserTests {
@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(true, accessor.getPropertyValue("shouldClonePayload"));
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.sourceName", 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() {
SubscribableChannel requests = context.getBean("requests", SubscribableChannel.class);
requests.subscribe(new AbstractReplyProducingMessageHandler() {
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
return new Source("foo");
}
});
Target original = new Target();
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("foo", enriched.getName());
assertEquals(42, enriched.getAge());
assertNotSame(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;
}
}
}

View File

@@ -0,0 +1,169 @@
/*
* 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.transformer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import java.util.HashMap;
import java.util.Map;
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.channel.DirectChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.integration.support.MessageBuilder;
/**
* @author Mark Fisher
* @since 2.1
*/
public class ContentEnricherTests {
@Test
public void simpleProperty() {
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);
SpelExpressionParser parser = new SpelExpressionParser();
Map<String, Expression> propertyExpressions = new HashMap<String, Expression>();
propertyExpressions.put("name", parser.parseExpression("payload.lastName + ', ' + payload.firstName"));
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("Doe, John", ((Target) reply.getPayload()).getName());
}
@Test
public void nestedProperty() {
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);
SpelExpressionParser parser = new SpelExpressionParser();
Map<String, Expression> propertyExpressions = new HashMap<String, Expression>();
propertyExpressions.put("child.name", parser.parseExpression("payload.lastName + ', ' + payload.firstName"));
enricher.setPropertyExpressions(propertyExpressions);
Target target = new Target("test");
Message<?> requestMessage = MessageBuilder.withPayload(target).setReplyChannel(replyChannel).build();
enricher.handleMessage(requestMessage);
Message<?> reply = replyChannel.receive(0);
Target result = (Target) reply.getPayload();
assertEquals("test", result.getName());
assertEquals("Doe, John", result.getChild().getName());
}
@Test
public void clonePayload() {
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);
Target target = new Target("replace me");
Message<?> requestMessage = MessageBuilder.withPayload(target).setReplyChannel(replyChannel).build();
enricher.handleMessage(requestMessage);
Message<?> reply = replyChannel.receive(0);
Target result = (Target) reply.getPayload();
assertEquals("Doe, John", result.getName());
assertNotSame(target, result);
}
@SuppressWarnings("unused")
private static final class Source {
private final String firstName, lastName;
Source(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
public static final class Target implements Cloneable {
private volatile String name;
private volatile Target child;
public Target() {
this.name = "default";
}
private Target(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Target getChild() {
return this.child;
}
public void setChild(Target child) {
this.child = child;
}
public Object clone() {
Target clone = new Target(this.name);
clone.setChild(this.child);
return clone;
}
}
}