package com.esri.geoevent.solutions.transport.tcpsquirt;
import static com.esri.geoevent.solutions.transport.tcpsquirt.ClientServerMode.CLIENT;
import static com.esri.geoevent.solutions.transport.tcpsquirt.ClientServerMode.SERVER;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.esri.ges.core.component.ComponentException;
import com.esri.ges.core.component.RunningException;
import com.esri.ges.core.component.RunningState;
import com.esri.ges.core.property.Property;
import com.esri.ges.transport.OutboundTransportBase;
import com.esri.ges.transport.TransportDefinition;
public class TcpSquirtOutboundTransport extends OutboundTransportBase implements Runnable{
private static final int CLIENT_BUFFER_SIZE = 500 * 1024;
private static final int MAIN_BUFFER_SIZE = 500 * 1024;
static final private Log log = LogFactory.getLog(TcpSquirtOutboundTransport.class);
private ClientServerMode mode;
private String host;
private int port;
private SocketChannel socketChannel = null;
private Thread thread;
private String errorMessage;
private final ByteBuffer buffer = ByteBuffer.allocate(MAIN_BUFFER_SIZE);
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private final Map<SocketChannel, ByteBuffer> connectionBuffers = new HashMap<SocketChannel, ByteBuffer>();
private boolean haveDataInIndividualBuffers;
private boolean receiving = false;
private boolean socketOpened = false;
private int clientConnectionTimeout;
private long timeStartedConnectingAsClient;
private ArrayList<ByteBuffer> bufferCache = new ArrayList<ByteBuffer>();
public TcpSquirtOutboundTransport(TransportDefinition definition)
throws ComponentException {
super(definition);
}
Integer activity = 0;
/*@Override
public boolean isClusterable()
{
return false;
}*/
//@Override
public void start() throws RunningException {
switch (getRunningState())
{
case STARTING:
case STARTED:
return;
}
setRunningState(RunningState.STARTING);
thread = new Thread(this);
thread.setPriority(thread.getPriority()+1);
thread.start();
}
private void processBuffer(ByteBuffer bb)
{
}
//@Override
public void receive(ByteBuffer bb, String channelId) {
if (getRunningState() == RunningState.STARTED) {
receiving = true;
synchronized (buffer) {
try {
if (buffer.remaining() < bb.remaining())
log.error("The TCP/IP outbound transport is unable to keep up with the incoming data rate, dropping "
+ bb.remaining() + " bytes.");
else
buffer.put(bb);
} catch (BufferOverflowException ex) {
log.error("The TCP/IP outbound transport is unable to keep up with the incoming data rate, dropping "
+ bb.remaining() + " bytes.");
}
}
wakupThread();
}
}
private synchronized void OpenSockets() {
if (mode == CLIENT) {
try
{
attemptClientConnection();
receiving = true;
} catch (IOException e) {
log.error(e);
}
} else if (mode == SERVER) {
try {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
receiving = true;
} catch (IOException ex) {
selector = null;
}
}
}
//@Override
public void run() {
try {
errorMessage = null;
applyProperties();
setRunningState(RunningState.STARTED);
while (getRunningState() == RunningState.STARTED) {
try {
if (receiving) {
synchronized (this) {
activity = 0;
while (activity > -1) {
if(!socketOpened)
{
OpenSockets();
socketOpened = true;
}
activity = manageAllSockets();
if (haveDataInIndividualBuffers
&& connectionBuffers.isEmpty())
haveDataInIndividualBuffers = false;
if (activity == 0
&& !haveDataInIndividualBuffers) {
receiving = false;
activity = -1;
closeConnection();
socketOpened = false;
}
}
}
}
} catch (Exception e) {
log.error("Error trying to write buffer", e);
setRunningState(RunningState.ERROR);
}
}
cleanup();
if (getRunningState() == RunningState.STOPPING)
setRunningState(RunningState.STOPPED);
}
catch (Exception ex)
{
errorMessage = ex.getMessage();
log.error("Exiting TCP Transport due to unforeseen error", ex);
setRunningState(RunningState.ERROR);
return;
}
}
private synchronized void wakupThread()
{
notifyAll();
}
private Integer manageAllSockets()
{
int a = 0;
if (mode == CLIENT)
{
if( ! socketChannel.isConnected() )
{
try
{
attemptClientConnection();
}catch(IOException ex)
{
if( (System.currentTimeMillis() - timeStartedConnectingAsClient) > (clientConnectionTimeout * 1000) )
{
errorMessage = "Error connecting to the host "+host+":"+port+" ("+ex.getMessage()+").";
log.error( errorMessage, ex );
setRunningState(RunningState.ERROR);
}
}
}
else
{
synchronized(buffer)
{
if( buffer.position() != 0 )
{
buffer.flip();
int bytesWritten = 0;
try
{
bytesWritten = socketChannel.write( buffer );
} catch (IOException e)
{
if(e.getMessage().contains("An existing connection was forcibly closed by the remote host"))
{
try
{
socketChannel.close();
} catch (IOException e1)
{
// Do nothing.
}
}
log.error( "Error writing to the client "+host+":"+port+".", e );
}
buffer.compact();
a = bytesWritten;
}
}
}
}
else if (mode == SERVER)
{
int dataMoved = moveDataIntoIndividualChannelBuffers();
// Service all the sockets
int activeChannels = manageSelector();
a = activeChannels + dataMoved;
//if( haveDataInIndividualBuffers && connectionBuffers.isEmpty() )
//haveDataInIndividualBuffers = false;
}
//if( activity == 0 && !haveDataInIndividualBuffers )
//{
//receiving=false;
//closeConnection();
//}
return a;
}
private void attemptClientConnection() throws IOException
{
if( timeStartedConnectingAsClient == 0 )
{
timeStartedConnectingAsClient = System.currentTimeMillis();
}
socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(host, port));
timeStartedConnectingAsClient = 0;
}
private synchronized void snooze(long timer)
{
try
{
wait(timer);
}catch(InterruptedException ex)
{
}
}
private synchronized void closeConnection()
{
if (mode == CLIENT)
{
if (socketChannel != null)
{
try
{
socketChannel.close();
socketChannel = null;
}
catch (IOException ioe)
{
log.debug("Ignoring Exception", ioe);
}
}
}
else if (mode == SERVER)
{
for (SocketChannel chan : connectionBuffers.keySet())
{
try
{
if( chan.isOpen() )
chan.close();
}catch(IOException ioe)
{
log.debug("Exception while closing all sockets as part of the transport shutdown process.", ioe);
}
}
try
{
if(serverSocketChannel != null && serverSocketChannel.isOpen())
{
serverSocketChannel.close();
serverSocketChannel=null;
}
if( selector != null && selector.isOpen() ){
selector.close();
selector = null;
}
} catch (IOException e)
{
log.debug("Exception while trying to stop listening for client connections. " + e);
}
}
}
private int moveDataIntoIndividualChannelBuffers()
{
byte[] dst = {};
synchronized( buffer )
{
if (buffer.position() == 0)
return 0;
buffer.flip();
dst = new byte[buffer.remaining()];
buffer.get(dst);
buffer.compact();
}
for( SocketChannel chan : connectionBuffers.keySet() )
{
if(!chan.isConnected())
{
connectionBuffers.remove(chan);
continue;
}
ByteBuffer connectionBuffer = connectionBuffers.get(chan);
try
{
if( connectionBuffer.remaining() > dst.length )
{
connectionBuffer.put(dst);
haveDataInIndividualBuffers = true;
}
else
{
String remoteClientAddress = chan.toString();
log.error( "Overflow while trying to write to the output buffer associated with address "+remoteClientAddress+".");
}
}catch(BufferOverflowException ex)
{
log.error( "Overflow while trying to write to an output buffer.");
}
}
return dst.length;
}
private int manageSelector()
{
int activeSelectors = 0;
int activeChannels = 0;
try
{
activeSelectors = selector.selectNow();
if( activeSelectors > 0 )
{
for (Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); iterator.hasNext();)
{
SelectionKey selectionKey = iterator.next();
iterator.remove();
try
{
activeChannels += processSelectionKey(selectionKey);
}
catch (IOException ex)
{
log.error(ex);
selectionKey.cancel();
}
}
}
}
catch (IOException ex)
{
log.error(ex);
}
return activeChannels;
}
private int processSelectionKey(SelectionKey key) throws IOException
{
if (!key.isValid())
return 0;
int count = 0;
if (key.isAcceptable())
{
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null)
{
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_WRITE);
connectionBuffers.put(socketChannel, ByteBuffer.allocate(CLIENT_BUFFER_SIZE));
count++;
}
}
if (key.isWritable())
{
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buf = connectionBuffers.get(channel);
try
{
buf.flip();
int numberOfBytesWritten = channel.write(buf);
buf.compact();
if (numberOfBytesWritten == -1)
{
// The channel is closed.
connectionBuffers.remove(channel);
channel.register(selector, 0);
channel.close();
}
else
count += numberOfBytesWritten;
// Now we are going to see if all the individual buffers are empty
boolean foundOne = false;
for(SocketChannel chan : connectionBuffers.keySet())
{
ByteBuffer individualBuffer = connectionBuffers.get(chan);
if( individualBuffer.position() > 0 )
{
foundOne = true;
break;
}
}
haveDataInIndividualBuffers = foundOne;
}
catch (Exception ex)
{
if( !ex.getMessage().contains("An existing connection was forcibly closed by the remote host"))
log.error("Exception while writing to a socket.", ex);
connectionBuffers.remove(channel);
channel.register(selector, 0);
channel.close();
}
}
return count;
}
private void readProperties()
{
Property prop = getProperty( "host" );
host = prop.getValueAsString();
prop = getProperty( "port" );
port = (Integer)prop.getValue();
prop = getProperty( "mode" );
String modeString = prop.getValueAsString();
clientConnectionTimeout = hasProperty("clientConnectionTimeout") ? (Integer) getProperty("clientConnectionTimeout").getValue() : 60;
if( modeString != null && modeString.toUpperCase().trim().equals("CLIENT") )
mode = CLIENT;
else if( modeString != null && modeString.toUpperCase().trim().equals("SERVER") )
mode = SERVER;
else
{
log.error("Setting the TCP Transport to mode \""+modeString+"\" is not allowed. Must be SERVER or CLIENT.");
setRunningState(RunningState.ERROR);
return;
}
}
/*private void applyProperties() throws IOException
{
if (mode == CLIENT)
{
attemptClientConnection();
}
else if (mode == SERVER)
{
try
{
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
catch (IOException ex)
{
selector = null;
throw ex;
}
}
}*/
private void applyProperties() throws IOException
{
if(mode==SERVER)
{
selector = Selector.open();
}
}
@Override
public void afterPropertiesSet()
{
try
{
readProperties();
if( getRunningState() == RunningState.STARTED )
{
cleanup();
applyProperties();
}
}catch( IOException ex )
{
errorMessage = ex.getMessage();
log.error(errorMessage);
setRunningState(RunningState.ERROR);
}
}
public synchronized void cleanup()
{
if(receiving)
receiving = false;
if (mode == CLIENT)
{
if (socketChannel != null)
{
try
{
socketChannel.close();
}
catch (IOException ioe)
{
log.debug("Ignoring Exception", ioe);
}
}
}
else if (mode == SERVER)
{
for (SocketChannel chan : connectionBuffers.keySet())
{
try
{
if( chan.isOpen() )
chan.close();
}catch(IOException ioe)
{
log.debug("Exception while closing all sockets as part of the transport shutdown process.", ioe);
}
}
try
{
if(serverSocketChannel != null && serverSocketChannel.isOpen())
serverSocketChannel.close();
if( selector != null && selector.isOpen() )
selector.close();
} catch (IOException e)
{
log.debug("Exception while trying to stop listening for client connections. " + e);
}
}
}
@Override
public void stop()
{
super.stop();
errorMessage = null;
if( getRunningState() == RunningState.ERROR )
setRunningState( RunningState.STOPPED );
}
@Override
public String getStatusDetails()
{
return errorMessage;
}
}