/*
* Copyright 2002-2016 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.ip.tcp;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.Lifecycle;
import org.springframework.integration.handler.AbstractMessageHandler;
import org.springframework.integration.ip.IpHeaders;
import org.springframework.integration.ip.tcp.connection.AbstractClientConnectionFactory;
import org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory;
import org.springframework.integration.ip.tcp.connection.ClientModeCapable;
import org.springframework.integration.ip.tcp.connection.ClientModeConnectionManager;
import org.springframework.integration.ip.tcp.connection.ConnectionFactory;
import org.springframework.integration.ip.tcp.connection.TcpConnection;
import org.springframework.integration.ip.tcp.connection.TcpConnectionFailedCorrelationEvent;
import org.springframework.integration.ip.tcp.connection.TcpSender;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;
/**
* Tcp outbound channel adapter using a TcpConnection to
* send data - if the connection factory is a server
* factory, the TcpListener owns the connections. If it is
* a client factory, this object owns the connection.
* @author Gary Russell
* @since 2.0
*
*/
public class TcpSendingMessageHandler extends AbstractMessageHandler implements
TcpSender, Lifecycle, ClientModeCapable {
private volatile AbstractConnectionFactory clientConnectionFactory;
private volatile AbstractConnectionFactory serverConnectionFactory;
private final Map<String, TcpConnection> connections = new ConcurrentHashMap<String, TcpConnection>();
private volatile boolean isClientMode;
private volatile boolean isSingleUse;
private volatile long retryInterval = 60000;
private volatile ScheduledFuture<?> scheduledFuture;
private volatile ClientModeConnectionManager clientModeConnectionManager;
protected final Object lifecycleMonitor = new Object();
private volatile boolean active;
protected TcpConnection obtainConnection(Message<?> message) {
TcpConnection connection = null;
Assert.notNull(this.clientConnectionFactory, "'clientConnectionFactory' cannot be null");
try {
connection = this.clientConnectionFactory.getConnection();
}
catch (Exception e) {
logger.error("Error creating connection", e);
throw new MessageHandlingException(message, "Failed to obtain a connection", e);
}
return connection;
}
/**
* Writes the message payload to the underlying socket, using the specified
* message format.
* @see org.springframework.messaging.MessageHandler#handleMessage(org.springframework.messaging.Message)
*/
@Override
public void handleMessageInternal(final Message<?> message) throws
MessageHandlingException {
if (this.serverConnectionFactory != null) {
// We don't own the connection, we are asynchronously replying
Object connectionId = message.getHeaders().get(IpHeaders.CONNECTION_ID);
TcpConnection connection = null;
if (connectionId != null) {
connection = this.connections.get(connectionId);
}
if (connection != null) {
try {
connection.send(message);
}
catch (Exception e) {
logger.error("Error sending message", e);
connection.close();
if (e instanceof MessageHandlingException) {
throw (MessageHandlingException) e;
}
else {
throw new MessageHandlingException(message, "Error sending message", e);
}
}
finally {
if (this.isSingleUse) { // close after replying
connection.close();
}
}
}
else {
logger.error("Unable to find outbound socket for " + message);
MessageHandlingException messageHandlingException = new MessageHandlingException(message,
"Unable to find outbound socket");
publishNoConnectionEvent(messageHandlingException, (String) connectionId);
throw messageHandlingException;
}
return;
}
else {
// we own the connection
TcpConnection connection = null;
try {
connection = doWrite(message);
}
catch (MessageHandlingException e) {
// retry - socket may have closed
if (e.getCause() instanceof IOException) {
if (logger.isDebugEnabled()) {
logger.debug("Fail on first write attempt", e);
}
connection = doWrite(message);
}
else {
throw e;
}
}
finally {
if (connection != null && this.isSingleUse
&& this.clientConnectionFactory.getListener() == null) {
// if there's no collaborating inbound adapter, close immediately, otherwise
// it will close after receiving the reply.
connection.close();
}
}
}
}
/**
* Method that actually does the write.
* @param message The message to write.
* @return the connection.
*/
protected TcpConnection doWrite(Message<?> message) {
TcpConnection connection = null;
try {
connection = obtainConnection(message);
if (logger.isDebugEnabled()) {
logger.debug("Got Connection " + connection.getConnectionId());
}
connection.send(message);
}
catch (Exception e) {
String connectionId = null;
if (connection != null) {
connectionId = connection.getConnectionId();
}
if (e instanceof MessageHandlingException) {
throw (MessageHandlingException) e;
}
throw new MessageHandlingException(message, "Failed to handle message using " + connectionId, e);
}
return connection;
}
private void publishNoConnectionEvent(MessageHandlingException messageHandlingException, String connectionId) {
AbstractConnectionFactory cf = this.serverConnectionFactory != null ? this.serverConnectionFactory
: this.clientConnectionFactory;
ApplicationEventPublisher applicationEventPublisher = cf.getApplicationEventPublisher();
if (applicationEventPublisher != null) {
applicationEventPublisher.publishEvent(
new TcpConnectionFailedCorrelationEvent(this, connectionId, messageHandlingException));
}
}
/**
* Sets the client or server connection factory; for this (an outbound adapter), if
* the factory is a server connection factory, the sockets are owned by a receiving
* channel adapter and this adapter is used to send replies.
*
* @param connectionFactory the connectionFactory to set
*/
public void setConnectionFactory(AbstractConnectionFactory connectionFactory) {
if (connectionFactory instanceof AbstractClientConnectionFactory) {
this.clientConnectionFactory = connectionFactory;
}
else {
this.serverConnectionFactory = connectionFactory;
connectionFactory.registerSender(this);
}
this.isSingleUse = connectionFactory.isSingleUse();
}
@Override
public void addNewConnection(TcpConnection connection) {
this.connections.put(connection.getConnectionId(), connection);
}
@Override
public void removeDeadConnection(TcpConnection connection) {
this.connections.remove(connection.getConnectionId());
}
@Override
public String getComponentType() {
return "ip:tcp-outbound-channel-adapter";
}
@Override
protected void onInit() throws Exception {
super.onInit();
if (this.isClientMode) {
Assert.notNull(this.clientConnectionFactory,
"For client-mode, connection factory must be type='client'");
Assert.isTrue(!this.clientConnectionFactory.isSingleUse(),
"For client-mode, connection factory must have single-use='false'");
}
}
@Override
public void start() {
synchronized (this.lifecycleMonitor) {
if (!this.active) {
this.active = true;
if (this.clientConnectionFactory != null) {
this.clientConnectionFactory.start();
}
if (this.serverConnectionFactory != null) {
this.serverConnectionFactory.start();
}
if (this.isClientMode) {
ClientModeConnectionManager manager = new ClientModeConnectionManager(
this.clientConnectionFactory);
this.clientModeConnectionManager = manager;
Assert.state(this.getTaskScheduler() != null, "Client mode requires a task scheduler");
this.scheduledFuture = this.getTaskScheduler().scheduleAtFixedRate(manager, this.retryInterval);
}
}
}
}
@Override
public void stop() {
synchronized (this.lifecycleMonitor) {
if (this.active) {
this.active = false;
if (this.scheduledFuture != null) {
this.scheduledFuture.cancel(true);
}
this.clientModeConnectionManager = null;
if (this.clientConnectionFactory != null) {
this.clientConnectionFactory.stop();
}
if (this.serverConnectionFactory != null) {
this.serverConnectionFactory.stop();
}
}
}
}
@Override
public boolean isRunning() {
return this.active;
}
/**
* @return the clientConnectionFactory
*/
protected ConnectionFactory getClientConnectionFactory() {
return this.clientConnectionFactory;
}
/**
* @return the serverConnectionFactory
*/
protected ConnectionFactory getServerConnectionFactory() {
return this.serverConnectionFactory;
}
/**
* @return the connections
*/
protected Map<String, TcpConnection> getConnections() {
return this.connections;
}
/**
* @return the isClientMode
*/
@Override
public boolean isClientMode() {
return this.isClientMode;
}
/**
* @param isClientMode the isClientMode to set
*/
public void setClientMode(boolean isClientMode) {
this.isClientMode = isClientMode;
}
@Override // super class is protected
public void setTaskScheduler(TaskScheduler taskScheduler) {
super.setTaskScheduler(taskScheduler);
}
/**
* @return the retryInterval
*/
public long getRetryInterval() {
return this.retryInterval;
}
/**
* @param retryInterval the retryInterval to set
*/
public void setRetryInterval(long retryInterval) {
this.retryInterval = retryInterval;
}
@Override
public boolean isClientModeConnected() {
if (this.isClientMode && this.clientModeConnectionManager != null) {
return this.clientModeConnectionManager.isConnected();
}
else {
return false;
}
}
@Override
public void retryConnection() {
if (this.active && this.isClientMode && this.clientModeConnectionManager != null) {
this.clientModeConnectionManager.run();
}
}
}