/*
* 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.gateway;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.core.AttributeAccessor;
import org.springframework.integration.MessageTimeoutException;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.endpoint.EventDrivenConsumer;
import org.springframework.integration.endpoint.PollingConsumer;
import org.springframework.integration.handler.BridgeHandler;
import org.springframework.integration.history.HistoryWritingMessagePostProcessor;
import org.springframework.integration.mapping.InboundMessageMapper;
import org.springframework.integration.mapping.OutboundMessageMapper;
import org.springframework.integration.support.DefaultErrorMessageStrategy;
import org.springframework.integration.support.DefaultMessageBuilderFactory;
import org.springframework.integration.support.ErrorMessageStrategy;
import org.springframework.integration.support.ErrorMessageUtils;
import org.springframework.integration.support.MessageBuilderFactory;
import org.springframework.integration.support.converter.SimpleMessageConverter;
import org.springframework.integration.support.management.IntegrationManagedResource;
import org.springframework.integration.support.management.MessageSourceMetrics;
import org.springframework.integration.support.management.TrackableComponent;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.util.Assert;
/**
* A convenient base class for connecting application code to
* {@link MessageChannel}s for sending, receiving, or request-reply operations.
* Exposes setters for configuring request and reply {@link MessageChannel}s as
* well as the timeout values for sending and receiving Messages.
*
* @author Mark Fisher
* @author Gary Russell
* @author Artem Bilan
*/
@IntegrationManagedResource
public abstract class MessagingGatewaySupport extends AbstractEndpoint
implements TrackableComponent, MessageSourceMetrics {
private static final long DEFAULT_TIMEOUT = 1000L;
private final SimpleMessageConverter messageConverter = new SimpleMessageConverter();
protected final MessagingTemplate messagingTemplate;
private final HistoryWritingMessagePostProcessor historyWritingPostProcessor =
new HistoryWritingMessagePostProcessor();
private final Object replyMessageCorrelatorMonitor = new Object();
private final boolean errorOnTimeout;
private final AtomicLong messageCount = new AtomicLong();
private ErrorMessageStrategy errorMessageStrategy = new DefaultErrorMessageStrategy();
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 long replyTimeout = DEFAULT_TIMEOUT;
@SuppressWarnings("rawtypes")
private volatile InboundMessageMapper requestMapper = new DefaultRequestMapper();
private volatile boolean initialized;
private volatile AbstractEndpoint replyMessageCorrelator;
private volatile String managedType;
private volatile String managedName;
private volatile boolean countsEnabled;
private volatile boolean loggingEnabled = true;
/**
* Construct an instance that will return null if no reply is received.
*/
public MessagingGatewaySupport() {
this(false);
}
/**
* If errorOnTimeout is true, construct an instance that will send an
* {@link ErrorMessage} with a {@link MessageTimeoutException} payload to the error
* channel if a reply is expected but none is received. If no error channel is
* configured, the {@link MessageTimeoutException} will be thrown.
*
* @param errorOnTimeout true to create the error message.
* @since 4.2
*/
public MessagingGatewaySupport(boolean errorOnTimeout) {
MessagingTemplate template = new MessagingTemplate();
template.setMessageConverter(this.messageConverter);
template.setSendTimeout(DEFAULT_TIMEOUT);
template.setReceiveTimeout(this.replyTimeout);
this.messagingTemplate = template;
this.errorOnTimeout = errorOnTimeout;
}
/**
* Set the request channel.
* @param requestChannel the channel to which request messages will be sent
*/
public void setRequestChannel(MessageChannel requestChannel) {
this.requestChannel = requestChannel;
}
/**
* Set the request channel name.
* @param requestChannelName the channel bean name to which request messages will be sent
* @since 4.1
*/
public void setRequestChannelName(String requestChannelName) {
Assert.hasText(requestChannelName, "'requestChannelName' must not be empty");
this.requestChannelName = requestChannelName;
}
/**
* Set the reply channel. If no reply channel is provided, this gateway will
* always use an anonymous, temporary channel for handling replies.
* @param replyChannel the channel from which reply messages will be received
*/
public void setReplyChannel(MessageChannel replyChannel) {
this.replyChannel = replyChannel;
}
/**
* Set the reply channel name. If no reply channel is provided, this gateway will
* always use an anonymous, temporary channel for handling replies.
* @param replyChannelName the channel bean name from which reply messages will be received
* @since 4.1
*/
public void setReplyChannelName(String replyChannelName) {
Assert.hasText(replyChannelName, "'replyChannelName' must not be empty");
this.replyChannelName = replyChannelName;
}
/**
* Set the error channel. If no error channel is provided, this gateway will
* propagate Exceptions to the caller. To completely suppress Exceptions, provide
* a reference to the "nullChannel" here.
* @param errorChannel The error channel.
*/
public void setErrorChannel(MessageChannel errorChannel) {
this.errorChannel = errorChannel;
}
/**
* Set the error channel name. If no error channel is provided, this gateway will
* propagate Exceptions to the caller. To completely suppress Exceptions, provide
* a reference to the "nullChannel" here.
* @param errorChannelName The error channel bean name.
* @since 4.1
*/
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
*/
public void setRequestTimeout(long requestTimeout) {
this.messagingTemplate.setSendTimeout(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
*/
public void setReplyTimeout(long replyTimeout) {
this.replyTimeout = replyTimeout;
this.messagingTemplate.setReceiveTimeout(replyTimeout);
}
/**
* Provide an {@link InboundMessageMapper} for creating request Messages
* from any object passed in a send or sendAndReceive operation.
* @param requestMapper The request mapper.
*/
public void setRequestMapper(InboundMessageMapper<?> requestMapper) {
requestMapper = (requestMapper != null) ? requestMapper : new DefaultRequestMapper();
this.requestMapper = requestMapper;
this.messageConverter.setInboundMessageMapper(requestMapper);
}
/**
* Provide an {@link OutboundMessageMapper} for mapping to objects from
* any reply Messages received in receive or sendAndReceive operations.
* @param replyMapper The reply mapper.
*/
public void setReplyMapper(OutboundMessageMapper<?> replyMapper) {
this.messageConverter.setOutboundMessageMapper(replyMapper);
}
/**
* Specify whether this gateway should be tracked in the Message History
* of Messages that originate from its send or sendAndReceive operations.
*/
@Override
public void setShouldTrack(boolean shouldTrack) {
this.historyWritingPostProcessor.setShouldTrack(shouldTrack);
}
@Override
public int getMessageCount() {
return (int) this.messageCount.get();
}
@Override
public long getMessageCountLong() {
return this.messageCount.get();
}
@Override
public void setManagedName(String name) {
this.managedName = name;
}
@Override
public String getManagedName() {
return this.managedName;
}
@Override
public void setManagedType(String type) {
this.managedType = type;
}
@Override
public String getManagedType() {
return this.managedType;
}
@Override
public String getComponentType() {
return "gateway";
}
@Override
public void setLoggingEnabled(boolean enabled) {
this.loggingEnabled = enabled;
}
@Override
public boolean isLoggingEnabled() {
return this.loggingEnabled;
}
@Override
public void setCountsEnabled(boolean countsEnabled) {
this.countsEnabled = countsEnabled;
}
@Override
public boolean isCountsEnabled() {
return this.countsEnabled;
}
/**
* Set an {@link ErrorMessageStrategy} to use to build an error message when a exception occurs.
* Default is the {@link DefaultErrorMessageStrategy}.
* @param errorMessageStrategy the {@link ErrorMessageStrategy}.
* @since 4.3.10
*/
public final void setErrorMessageStrategy(ErrorMessageStrategy errorMessageStrategy) {
Assert.notNull(errorMessageStrategy, "'errorMessageStrategy' cannot be null");
this.errorMessageStrategy = errorMessageStrategy;
}
@Override
protected void onInit() throws Exception {
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.");
this.historyWritingPostProcessor.setTrackableComponent(this);
this.historyWritingPostProcessor.setMessageBuilderFactory(this.getMessageBuilderFactory());
if (this.getBeanFactory() != null) {
this.messagingTemplate.setBeanFactory(this.getBeanFactory());
if (this.requestMapper instanceof DefaultRequestMapper) {
((DefaultRequestMapper) this.requestMapper).setMessageBuilderFactory(this.getMessageBuilderFactory());
}
this.messageConverter.setBeanFactory(this.getBeanFactory());
}
this.initialized = true;
}
private void initializeIfNecessary() {
if (!this.initialized) {
this.afterPropertiesSet();
}
}
/**
* Return this gateway's request channel.
* @return the channel.
* @since 4.2
*/
public MessageChannel getRequestChannel() {
if (this.requestChannelName != null) {
synchronized (this) {
if (this.requestChannelName != null) {
this.requestChannel = getChannelResolver().resolveDestination(this.requestChannelName);
this.requestChannelName = null;
}
}
}
return this.requestChannel;
}
protected MessageChannel getReplyChannel() {
if (this.replyChannelName != null) {
synchronized (this) {
if (this.replyChannelName != null) {
this.replyChannel = getChannelResolver().resolveDestination(this.replyChannelName);
this.replyChannelName = null;
}
}
}
return this.replyChannel;
}
/**
* Return the error channel (if provided) to which error messages will
* be routed.
* @return the channel or null.
* @since 4.3
*/
public MessageChannel getErrorChannel() {
if (this.errorChannelName != null) {
synchronized (this) {
if (this.errorChannelName != null) {
this.errorChannel = getChannelResolver().resolveDestination(this.errorChannelName);
this.errorChannelName = null;
}
}
}
return this.errorChannel;
}
protected void send(Object object) {
this.initializeIfNecessary();
Assert.notNull(object, "request must not be null");
MessageChannel requestChannel = getRequestChannel();
Assert.state(requestChannel != null,
"send is not supported, because no request channel has been configured");
try {
if (this.countsEnabled) {
this.messageCount.incrementAndGet();
}
this.messagingTemplate.convertAndSend(requestChannel, object, this.historyWritingPostProcessor);
}
catch (Exception e) {
MessageChannel errorChannel = getErrorChannel();
if (errorChannel != null) {
this.messagingTemplate.send(errorChannel, new ErrorMessage(e));
}
else {
this.rethrow(e, "failed to send message");
}
}
}
protected Object receive() {
this.initializeIfNecessary();
MessageChannel replyChannel = getReplyChannel();
Assert.state(replyChannel != null && (replyChannel instanceof PollableChannel),
"receive is not supported, because no pollable reply channel has been configured");
return this.messagingTemplate.receiveAndConvert(replyChannel, null);
}
protected Message<?> receiveMessage() {
initializeIfNecessary();
MessageChannel replyChannel = getReplyChannel();
Assert.state(replyChannel instanceof PollableChannel,
"receive is not supported, because no pollable reply channel has been configured");
return this.messagingTemplate.receive(replyChannel);
}
protected Object sendAndReceive(Object object) {
return this.doSendAndReceive(object, true);
}
protected Message<?> sendAndReceiveMessage(Object object) {
return (Message<?>) this.doSendAndReceive(object, false);
}
@SuppressWarnings("unchecked")
private Object doSendAndReceive(Object object, boolean shouldConvert) {
this.initializeIfNecessary();
Assert.notNull(object, "request must not be null");
MessageChannel requestChannel = getRequestChannel();
if (requestChannel == null) {
throw new MessagingException("No request channel available. Cannot send request message.");
}
MessageChannel replyChannel = getReplyChannel();
if (replyChannel != null && this.replyMessageCorrelator == null) {
this.registerReplyMessageCorrelator();
}
Object reply = null;
Throwable error = null;
Message<?> requestMessage = null;
try {
if (this.countsEnabled) {
this.messageCount.incrementAndGet();
}
if (shouldConvert) {
reply = this.messagingTemplate.convertSendAndReceive(requestChannel, object, null,
this.historyWritingPostProcessor);
if (reply instanceof Throwable) {
error = (Throwable) reply;
}
}
else {
requestMessage = (object instanceof Message<?>)
? (Message<?>) object : this.requestMapper.toMessage(object);
requestMessage = this.historyWritingPostProcessor.postProcessMessage(requestMessage);
reply = this.messagingTemplate.sendAndReceive(requestChannel, requestMessage);
if (reply instanceof ErrorMessage) {
error = ((ErrorMessage) reply).getPayload();
}
}
if (reply == null && this.errorOnTimeout) {
if (object instanceof Message) {
error = new MessageTimeoutException((Message<?>) object, "No reply received within timeout");
}
else {
error = new MessageTimeoutException("No reply received within timeout");
}
}
}
catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.debug("failure occurred in gateway sendAndReceive: " + e.getMessage());
}
error = e;
}
if (error != null) {
MessageChannel errorChannel = getErrorChannel();
if (errorChannel != null) {
ErrorMessage errorMessage = buildErrorMessage(requestMessage, error);
Message<?> errorFlowReply = null;
try {
errorFlowReply = this.messagingTemplate.sendAndReceive(errorChannel, errorMessage);
}
catch (Exception errorFlowFailure) {
throw new MessagingException(errorMessage, "failure occurred in error-handling flow",
errorFlowFailure);
}
if (shouldConvert) {
Object result = (errorFlowReply != null) ? errorFlowReply.getPayload() : null;
if (result instanceof Throwable) {
this.rethrow((Throwable) result, "error flow returned Exception");
}
return result;
}
if (errorFlowReply != null && errorFlowReply.getPayload() instanceof Throwable) {
this.rethrow((Throwable) errorFlowReply.getPayload(), "error flow returned an Error Message");
}
if (errorFlowReply == null && this.errorOnTimeout) {
if (object instanceof Message) {
throw new MessageTimeoutException((Message<?>) object,
"No reply received from error channel within timeout");
}
else {
throw new MessageTimeoutException("No reply received from error channel within timeout");
}
}
return errorFlowReply;
}
else { // no errorChannel so we'll propagate
this.rethrow(error, "gateway received checked Exception");
}
}
return reply;
}
/**
* Build an error message for the message and throwable using the configured
* {@link ErrorMessageStrategy}.
* @param requestMessage the requestMessage.
* @param throwable the throwable.
* @return the error message.
* @since 4.3.10
*/
protected final ErrorMessage buildErrorMessage(Message<?> requestMessage, Throwable throwable) {
ErrorMessage errorMessage = this.errorMessageStrategy.buildErrorMessage(throwable,
getErrorMessageAttributes(requestMessage));
return errorMessage;
}
/**
* Populate an {@link AttributeAccessor} to be used when building an error message
* with the {@link #setErrorMessageStrategy(ErrorMessageStrategy)
* errorMessageStrategy}.
* @param message the message.
* @return the attributes.
* @since 4.3.10
*/
protected AttributeAccessor getErrorMessageAttributes(Message<?> message) {
return ErrorMessageUtils.getAttributeAccessor(message, null);
}
private void rethrow(Throwable t, String description) {
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
}
throw new MessagingException(description, t);
}
private void registerReplyMessageCorrelator() {
synchronized (this.replyMessageCorrelatorMonitor) {
if (this.replyMessageCorrelator != null) {
return;
}
AbstractEndpoint correlator = null;
BridgeHandler handler = new BridgeHandler();
if (this.getBeanFactory() != null) {
handler.setBeanFactory(this.getBeanFactory());
}
handler.afterPropertiesSet();
MessageChannel replyChannel = getReplyChannel();
if (replyChannel instanceof SubscribableChannel) {
correlator = new EventDrivenConsumer((SubscribableChannel) replyChannel, handler);
}
else if (replyChannel instanceof PollableChannel) {
PollingConsumer endpoint = new PollingConsumer((PollableChannel) replyChannel, handler);
endpoint.setBeanFactory(this.getBeanFactory());
endpoint.setReceiveTimeout(this.replyTimeout);
endpoint.afterPropertiesSet();
correlator = endpoint;
}
else {
throw new MessagingException("Unsupported 'replyChannel' type [" + replyChannel.getClass() + "]."
+ "SubscribableChannel or PollableChannel type are supported.");
}
if (this.isRunning()) {
correlator.start();
}
this.replyMessageCorrelator = correlator;
}
}
@Override // guarded by super#lifecycleLock
protected void doStart() {
if (this.replyMessageCorrelator != null) {
this.replyMessageCorrelator.start();
}
}
@Override // guarded by super#lifecycleLock
protected void doStop() {
if (this.replyMessageCorrelator != null) {
this.replyMessageCorrelator.stop();
}
}
@Override
public void reset() {
this.messageCount.set(0);
}
private static class DefaultRequestMapper implements InboundMessageMapper<Object> {
private volatile MessageBuilderFactory messageBuilderFactory = new DefaultMessageBuilderFactory();
DefaultRequestMapper() {
super();
}
void setMessageBuilderFactory(MessageBuilderFactory messageBuilderFactory) {
this.messageBuilderFactory = messageBuilderFactory;
}
@Override
public Message<?> toMessage(Object object) throws Exception {
if (object instanceof Message<?>) {
return (Message<?>) object;
}
return (object != null) ? this.messageBuilderFactory.withPayload(object).build() : null;
}
}
}