/*
* Copyright 2002-2017 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.beans.factory.BeanFactoryAware;
import org.springframework.context.Lifecycle;
import org.springframework.expression.EvaluationContext;
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.expression.ExpressionUtils;
import org.springframework.integration.gateway.MessagingGatewaySupport;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.integration.transformer.support.HeaderValueMessageProcessor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* 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
* @author Gary Russell
* @author Artem Bilan
* @author Liujiong
* @author Kris Jacyna
* @since 2.1
*/
public class ContentEnricher extends AbstractReplyProducingMessageHandler
implements Lifecycle {
private final SpelExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true));
private volatile Map<Expression, Expression> nullResultPropertyExpressions = new HashMap<Expression, Expression>();
private volatile Map<String, HeaderValueMessageProcessor<?>> nullResultHeaderExpressions =
new HashMap<String, HeaderValueMessageProcessor<?>>();
private volatile Map<Expression, Expression> propertyExpressions = new HashMap<Expression, Expression>();
private volatile Map<String, HeaderValueMessageProcessor<?>> headerExpressions =
new HashMap<String, HeaderValueMessageProcessor<?>>();
private EvaluationContext sourceEvaluationContext;
private EvaluationContext targetEvaluationContext;
private volatile boolean shouldClonePayload = false;
private Expression requestPayloadExpression;
private volatile MessageChannel requestChannel;
private volatile String requestChannelName;
private volatile MessageChannel replyChannel;
private volatile String replyChannelName;
private volatile MessageChannel errorChannel;
private volatile String errorChannelName;
private volatile Gateway gateway = null;
private volatile Long requestTimeout;
private volatile Long replyTimeout;
public void setNullResultPropertyExpressions(Map<String, Expression> nullResultPropertyExpressions) {
Map<Expression, Expression> localMap = new HashMap<Expression, Expression>(nullResultPropertyExpressions.size());
for (Map.Entry<String, Expression> entry : nullResultPropertyExpressions.entrySet()) {
String key = entry.getKey();
Expression value = entry.getValue();
localMap.put(this.parser.parseExpression(key), value);
}
this.nullResultPropertyExpressions = localMap;
}
public void setNullResultHeaderExpressions(Map<String, HeaderValueMessageProcessor<?>> nullResultHeaderExpressions) {
this.nullResultHeaderExpressions = new HashMap<String, HeaderValueMessageProcessor<?>>(
nullResultHeaderExpressions);
}
/**
* 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.
* @param propertyExpressions The property expressions.
*/
public void setPropertyExpressions(Map<String, Expression> propertyExpressions) {
Assert.notEmpty(propertyExpressions, "propertyExpressions must not be empty");
Assert.noNullElements(propertyExpressions.keySet().toArray(), "propertyExpressions keys must not be empty");
Assert.noNullElements(propertyExpressions.values().toArray(), "propertyExpressions values must not be empty");
Map<Expression, Expression> localMap = new HashMap<Expression, Expression>(propertyExpressions.size());
for (Map.Entry<String, Expression> entry : propertyExpressions.entrySet()) {
String key = entry.getKey();
Expression value = entry.getValue();
localMap.put(this.parser.parseExpression(key), value);
}
this.propertyExpressions = localMap;
}
/**
* Provide the map of {@link HeaderValueMessageProcessor} to evaluate when enriching
* the target MessageHeaders. The keys should simply be header names, and the values
* should be Expressions that will evaluate against the reply Message as the root
* object.
* @param headerExpressions The header expressions.
*/
public void setHeaderExpressions(Map<String, HeaderValueMessageProcessor<?>> headerExpressions) {
Assert.notEmpty(headerExpressions, "headerExpressions must not be empty");
Assert.noNullElements(headerExpressions.keySet().toArray(), "headerExpressions keys must not be empty");
Assert.noNullElements(headerExpressions.values().toArray(), "headerExpressions values must not be empty");
this.headerExpressions = new HashMap<String, HeaderValueMessageProcessor<?>>(headerExpressions);
}
/**
* Sets the content enricher's request channel. If specified, then an internal 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.
* @param requestChannel The request channel.
*/
public void setRequestChannel(MessageChannel requestChannel) {
this.requestChannel = requestChannel;
}
public void setRequestChannelName(String requestChannelName) {
Assert.hasText(requestChannelName, "'requestChannelName' must not be empty");
this.requestChannelName = requestChannelName;
}
/**
* 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.
* @param replyChannel The reply channel.
*/
public void setReplyChannel(MessageChannel replyChannel) {
this.replyChannel = replyChannel;
}
public void setReplyChannelName(String replyChannelName) {
Assert.hasText(replyChannelName, "'replyChannelName' must not be empty");
this.replyChannelName = replyChannelName;
}
/**
* Set the content enricher's error channel to allow the error handling flow to return
* of an alternative object to use for enrichment if exceptions occur in the
* downstream flow.
* @param errorChannel The error channel.
* @since 4.1
*/
public void setErrorChannel(MessageChannel errorChannel) {
this.errorChannel = errorChannel;
}
public void setErrorChannelName(String errorChannelName) {
Assert.hasText(errorChannelName, "'errorChannelName' must not be empty");
this.errorChannelName = errorChannelName;
}
/**
* Set the timeout value for sending request messages. If not explicitly configured,
* the default is one second.
* @param requestTimeout the timeout value in milliseconds. Must not be null.
*/
public void setRequestTimeout(Long requestTimeout) {
Assert.notNull(requestTimeout, "requestTimeout must not be null");
this.requestTimeout = requestTimeout;
}
/**
* Set the timeout value for receiving reply messages. If not explicitly configured,
* the default is one second.
* @param replyTimeout the timeout value in milliseconds. Must not be null.
*/
public void setReplyTimeout(Long replyTimeout) {
Assert.notNull(replyTimeout, "replyTimeout must not be null");
this.replyTimeout = replyTimeout;
}
/**
* By default the original message's payload will be used as the actual payload that
* will be send to the request-channel.
* <p>
* 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.
* <p>
* For the Expression evaluation the full message is available as the <b>root
* object</b>.
* <p>
* 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>
* <p>
* If more sophisticated logic is required (e.g. changing the message headers etc.)
* please use additional downstream transformers.
* @param requestPayloadExpression The request payload expression.
*
*/
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.
* @param shouldClonePayload true if the payload should be cloned.
*/
public void setShouldClonePayload(boolean shouldClonePayload) {
this.shouldClonePayload = shouldClonePayload;
}
public void setIntegrationEvaluationContext(EvaluationContext evaluationContext) {
this.sourceEvaluationContext = evaluationContext;
}
@Override
public String getComponentType() {
return "enricher";
}
/**
* Initializes the Content Enricher. Will instantiate an internal Gateway if the
* requestChannel is set.
*/
@Override
protected void doInit() {
Assert.state(!(this.requestChannelName != null && this.requestChannel != null),
"'requestChannelName' and 'requestChannel' are mutually exclusive.");
Assert.state(!(this.replyChannelName != null && this.replyChannel != null),
"'replyChannelName' and 'replyChannel' are mutually exclusive.");
Assert.state(!(this.errorChannelName != null && this.errorChannel != null),
"'errorChannelName' and 'errorChannel' are mutually exclusive.");
if (this.replyChannel != null || this.replyChannelName != null) {
Assert.state(this.requestChannel != null || this.requestChannelName != null,
"If the replyChannel is set, then the requestChannel must not be null");
}
if (this.errorChannel != null || this.errorChannelName != null) {
Assert.state(this.requestChannel != null || this.requestChannelName != null,
"If the errorChannel is set, then the requestChannel must not be null");
}
if (this.requestChannel != null || this.requestChannelName != null) {
this.gateway = new Gateway();
this.gateway.setRequestChannel(this.requestChannel);
if (this.requestChannelName != null) {
this.gateway.setRequestChannelName(this.requestChannelName);
}
if (this.requestTimeout != null) {
this.gateway.setRequestTimeout(this.requestTimeout);
}
if (this.replyTimeout != null) {
this.gateway.setReplyTimeout(this.replyTimeout);
}
this.gateway.setReplyChannel(this.replyChannel);
if (this.replyChannelName != null) {
this.gateway.setReplyChannelName(this.replyChannelName);
}
this.gateway.setErrorChannel(this.errorChannel);
if (this.errorChannelName != null) {
this.gateway.setErrorChannelName(this.errorChannelName);
}
if (this.getBeanFactory() != null) {
this.gateway.setBeanFactory(this.getBeanFactory());
}
this.gateway.afterPropertiesSet();
}
if (this.sourceEvaluationContext == null) {
this.sourceEvaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
}
StandardEvaluationContext targetContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
// bean resolution is NOT allowed for the target of the enrichment
targetContext.setBeanResolver(null);
this.targetEvaluationContext = targetContext;
if (this.getBeanFactory() != null) {
for (HeaderValueMessageProcessor<?> headerValueMessageProcessor : this.headerExpressions.values()) {
if (headerValueMessageProcessor instanceof BeanFactoryAware) {
((BeanFactoryAware) headerValueMessageProcessor).setBeanFactory(getBeanFactory());
}
}
for (HeaderValueMessageProcessor<?> headerValueMessageProcessor : this.nullResultHeaderExpressions.values()) {
if (headerValueMessageProcessor instanceof BeanFactoryAware) {
((BeanFactoryAware) headerValueMessageProcessor).setBeanFactory(getBeanFactory());
}
}
}
}
@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");
targetPayload = ReflectionUtils.invokeMethod(cloneMethod, requestPayload);
}
catch (Exception e) {
throw new MessageHandlingException(requestMessage, "Failed to clone payload object", e);
}
}
else {
targetPayload = requestPayload;
}
final Message<?> actualRequestMessage;
if (this.requestPayloadExpression == null) {
actualRequestMessage = requestMessage;
}
else {
final Object requestMessagePayload =
this.requestPayloadExpression.getValue(this.sourceEvaluationContext, requestMessage);
actualRequestMessage = this.getMessageBuilderFactory().withPayload(requestMessagePayload)
.copyHeaders(requestMessage.getHeaders()).build();
}
final Message<?> replyMessage;
if (this.gateway == null) {
replyMessage = actualRequestMessage;
}
else {
replyMessage = this.gateway.sendAndReceiveMessage(actualRequestMessage);
if (replyMessage == null) {
if (this.nullResultPropertyExpressions.isEmpty() && this.nullResultHeaderExpressions.isEmpty()) {
return null;
}
for (Map.Entry<Expression, Expression> entry : this.nullResultPropertyExpressions.entrySet()) {
Expression propertyExpression = entry.getKey();
Expression valueExpression = entry.getValue();
Object value = valueExpression.getValue(this.sourceEvaluationContext, requestMessage);
propertyExpression.setValue(this.targetEvaluationContext, targetPayload, value);
}
if (this.nullResultHeaderExpressions.isEmpty()) {
return targetPayload;
}
else {
Map<String, Object> targetHeaders = new HashMap<String, Object>(
this.nullResultHeaderExpressions.size());
for (Map.Entry<String, HeaderValueMessageProcessor<?>> entry : this.nullResultHeaderExpressions
.entrySet()) {
String header = entry.getKey();
HeaderValueMessageProcessor<?> valueProcessor = entry.getValue();
Boolean overwrite = valueProcessor.isOverwrite();
overwrite = overwrite != null ? overwrite : true;
if (overwrite || !requestMessage.getHeaders().containsKey(header)) {
Object value = valueProcessor.processMessage(requestMessage);
targetHeaders.put(header, value);
}
}
return this.getMessageBuilderFactory().withPayload(targetPayload).copyHeaders(targetHeaders)
.build();
}
}
}
for (Map.Entry<Expression, Expression> entry : this.propertyExpressions.entrySet()) {
Expression propertyExpression = entry.getKey();
Expression valueExpression = entry.getValue();
Object value = valueExpression.getValue(this.sourceEvaluationContext, replyMessage);
propertyExpression.setValue(this.targetEvaluationContext, targetPayload, value);
}
if (this.headerExpressions.isEmpty()) {
return targetPayload;
}
else {
Map<String, Object> targetHeaders = new HashMap<String, Object>(this.headerExpressions.size());
for (Map.Entry<String, HeaderValueMessageProcessor<?>> entry : this.headerExpressions.entrySet()) {
String header = entry.getKey();
HeaderValueMessageProcessor<?> valueProcessor = entry.getValue();
Boolean overwrite = valueProcessor.isOverwrite();
overwrite = overwrite != null ? overwrite : true;
if (overwrite || !requestMessage.getHeaders().containsKey(header)) {
Object value = valueProcessor.processMessage(replyMessage);
targetHeaders.put(header, value);
}
}
return getMessageBuilderFactory()
.withPayload(targetPayload)
.copyHeaders(targetHeaders);
}
}
/**
* Lifecycle implementation. If no requestChannel is defined, this method has no
* effect as in that case no Gateway is initialized.
*/
@Override
public void 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.
*/
@Override
public void 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.
*/
@Override
public boolean isRunning() {
return this.gateway == null || this.gateway.isRunning();
}
/**
* Internal gateway implementation for request/reply handling. Simply exposes the
* sendAndReceiveMessage method.
*/
private static final class Gateway extends MessagingGatewaySupport {
Gateway() {
super();
}
@Override
protected Message<?> sendAndReceiveMessage(Object object) {
return super.sendAndReceiveMessage(object);
}
@Override
public String getComponentType() {
return "enricher$gateway";
}
}
}