/*******************************************************************************
* Copyright (c) 2013 Rene Schneider, GEBIT Solutions GmbH and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package de.gebit.integrity.remoting.transport;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
import de.gebit.integrity.remoting.transport.messages.AbstractMessage;
import de.gebit.integrity.remoting.transport.messages.DisconnectMessage;
import de.gebit.integrity.remoting.transport.messages.ShutdownRequestMessage;
/**
* An endpoint is a client- or serverside termination point of a message channel. The endpoint uses a TCP connection to
* transmit messages bidirectionally, with processors waiting for new messages to arrive.
*
* @author Rene Schneider - initial API and implementation
*
*/
public class Endpoint {
/**
* The TCP socket used for communication.
*/
private Socket socket;
/**
* The input processor thread waiting for new messages to arrive and process.
*/
private EndpointInputProcessor inputProcessor;
/**
* The output processor thread waiting for new messages to be sent.
*/
private EndpointOutputProcessor outputProcessor;
/**
* The listener.
*/
private EndpointListener listener;
/**
* Whether the endpoint is active.
*/
private volatile boolean isActive;
/**
* An object used to wait on for the disconnect message handshake when closing the socket.
*/
private Object closeSyncObject = new Object();
/**
* Whether this endpoint has received a disconnect request from the other side or sent such a packet.
*/
private volatile boolean disconnectRequested;
/**
* The classloader to use when deserializing objects.
*/
private ClassLoader classLoader;
/**
* The maximum time in seconds to wait for the disconnect message handshake to be performed before closing the
* socket.
*/
private static final int DISCONNECT_WAIT_TIME = 10000;
/**
* A map of message processors.
*/
private Map<Class<? extends AbstractMessage>, MessageProcessor<?>> messageProcessors = new HashMap<Class<? extends AbstractMessage>, MessageProcessor<?>>();
/**
* The queue for outgoing messages. Is emptied by {@link #outputProcessor}.
*/
private LinkedBlockingQueue<AbstractMessage> outputQueue = new LinkedBlockingQueue<AbstractMessage>();
/**
* Creates a new endpoint from a specific, already-connected socket.
*
* @param aSocket
* the connected socket
* @param aProcessorMap
* the processors
* @param aListener
* the listener
* @param aClassLoader
* the classloader to use when deserializing objects
*/
public Endpoint(Socket aSocket, Map<Class<? extends AbstractMessage>, MessageProcessor<?>> aProcessorMap,
EndpointListener aListener, ClassLoader aClassLoader) {
listener = aListener;
socket = aSocket;
messageProcessors = aProcessorMap;
isActive = true;
inputProcessor = new EndpointInputProcessor();
inputProcessor.start();
outputProcessor = new EndpointOutputProcessor();
outputProcessor.start();
classLoader = aClassLoader;
}
/**
* Creates a new endpoint and connects to a remote host, on which a {@link ServerEndpoint} is expected to run.
*
* @param aHost
* the host name or IP
* @param aPort
* the port to connect to
* @param aProcessorMap
* the map of processors
* @param aListener
* the listener
* @param aClassLoader
* the classloader to use when deserializing objects
* @throws UnknownHostException
* @throws IOException
*/
public Endpoint(String aHost, int aPort, Map<Class<? extends AbstractMessage>, MessageProcessor<?>> aProcessorMap,
EndpointListener aListener, ClassLoader aClassLoader) throws UnknownHostException, IOException {
messageProcessors = aProcessorMap;
listener = aListener;
socket = new Socket(aHost, aPort);
isActive = true;
inputProcessor = new EndpointInputProcessor();
inputProcessor.start();
outputProcessor = new EndpointOutputProcessor();
outputProcessor.start();
classLoader = aClassLoader;
}
/**
* Sends a message. This queues the message into the outqueue, which is then emptied asynchronously by the
* {@link #outputProcessor}.
*
* @param aMessage
* the message to send
*/
public void sendMessage(AbstractMessage aMessage) {
if (isActive) {
// We consider shutdown requests to also be requests for a disconnect, since that naturally happens during
// the shutdown of the other side.
if (aMessage instanceof ShutdownRequestMessage) {
disconnectRequested = true;
}
try {
outputQueue.put(aMessage);
} catch (InterruptedException exc) {
// don't care
}
}
}
public boolean isActive() {
return isActive && socket.isConnected();
}
public boolean isDisconnectRequested() {
return disconnectRequested;
}
/**
* Close the connection.
*
* @param anEmptyOutputQueueFlag
* whether the output queue shall be sent to the other endpoint before closing
*/
public void close(boolean anEmptyOutputQueueFlag) {
isActive = false;
if (anEmptyOutputQueueFlag) {
// first: make sure the whole outqueue was sent
while (outputQueue.size() > 0) {
synchronized (outputQueue) {
outputProcessor.interrupt();
try {
outputQueue.wait();
} catch (InterruptedException exc) {
// don't care
}
}
}
// second: send a disconnect message (request) and wait for the confirmation or the timeout
synchronized (closeSyncObject) {
try {
outputQueue.put(new DisconnectMessage(false));
} catch (InterruptedException exc) {
// can't happen here, so don't care
}
disconnectRequested = true;
try {
closeSyncObject.wait(DISCONNECT_WAIT_TIME);
} catch (InterruptedException exc) {
// don't care
}
}
// third: close the socket and kill the output processor
}
closeInternal();
if (listener != null) {
listener.onClosed(this);
}
}
private void closeInternal() {
isActive = false;
outputProcessor.kill();
outputProcessor.interrupt();
if (!socket.isClosed()) {
try {
socket.shutdownOutput();
socket.close();
} catch (IOException exc) {
// ignored; we're closing the socket anyway
}
}
}
private class EndpointInputProcessor extends Thread {
EndpointInputProcessor() {
super("Integrity - Endpoint Input Processor");
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public void run() {
ObjectInputStream tempObjectStream = null;
try {
InputStream tempInStream = socket.getInputStream();
while (true) {
int tempMessageLength = 0;
for (int i = 0; i < 4; i++) {
int tempByte = tempInStream.read();
if (tempByte == -1) {
// socket was closed
return;
}
tempMessageLength |= tempByte << (i * 8);
}
byte[] tempMessage = new byte[tempMessageLength];
int tempMessagePosition = 0;
while (tempMessagePosition < tempMessageLength) {
int tempBytesRead = tempInStream.read(tempMessage, tempMessagePosition,
tempMessageLength - tempMessagePosition);
if (tempBytesRead > -1) {
tempMessagePosition += tempBytesRead;
} else {
// socket was closed
return;
}
}
tempObjectStream = new ClassloaderAwareObjectInputStream(
new InflaterInputStream(new ByteArrayInputStream(tempMessage)), classLoader);
try {
AbstractMessage tempMessageObject = (AbstractMessage) tempObjectStream.readObject();
if (tempMessageObject instanceof DisconnectMessage) {
// disconnect messages are handled directly in the endpoints
if (((DisconnectMessage) tempMessageObject).isConfirmation()) {
synchronized (closeSyncObject) {
closeSyncObject.notifyAll();
}
} else {
// this is a disconnect request message and should be answered by a confirmation when
// received
disconnectRequested = true;
sendMessage(new DisconnectMessage(true));
}
} else {
// We need to know if this endpoints' shutdown was requested. That message must be processed
// by the message processor, but we also regard is as a request for a disconnect (which of
// course happens during a shutdown).
if (tempMessageObject instanceof ShutdownRequestMessage) {
disconnectRequested = true;
}
// just a standard message; use the appropriate processor
MessageProcessor tempProcessor = messageProcessors.get(tempMessageObject.getClass());
if (tempProcessor != null) {
tempProcessor.processMessage(tempMessageObject, Endpoint.this);
}
}
} catch (ClassNotFoundException exc) {
exc.printStackTrace();
}
}
} catch (IOException exc) {
// filter out socket closed messages; we're expecting those to happen and they're handled just fine
if (!"socket closed".equals(exc.getMessage().toLowerCase())) {
exc.printStackTrace();
}
} finally {
closeInternal();
if (tempObjectStream != null) {
try {
tempObjectStream.close();
} catch (IOException exc) {
// ignore
}
}
if (listener != null) {
listener.onConnectionLost(Endpoint.this);
}
}
}
}
private class EndpointOutputProcessor extends Thread {
/**
* Used to gracefully kill this thread.
*/
private boolean killSwitch;
EndpointOutputProcessor() {
super("Integrity - Endpoint Output Processor");
}
public void kill() {
killSwitch = true;
}
@Override
public void run() {
try {
OutputStream tempOutStream = socket.getOutputStream();
while (socket.isConnected() && !killSwitch) {
AbstractMessage tempMessageObject = null;
try {
tempMessageObject = outputQueue.poll(1, TimeUnit.SECONDS);
if (outputQueue.size() == 0 && !isActive) {
synchronized (outputQueue) {
outputQueue.notify();
}
}
} catch (InterruptedException exc) {
// don't care
}
if (tempMessageObject != null && socket.isConnected()) {
ByteArrayOutputStream tempStream = new ByteArrayOutputStream();
DeflaterOutputStream tempDeflateStream = new DeflaterOutputStream(tempStream);
ObjectOutputStream tempObjectStream = new ObjectOutputStream(tempDeflateStream);
tempObjectStream.writeObject(tempMessageObject);
tempObjectStream.close();
byte[] tempMessage = tempStream.toByteArray();
byte[] tempLength = new byte[4];
for (int i = 0; i < 4; i++) {
tempLength[i] = (byte) ((tempMessage.length >> (i * 8)) & 0xFF);
}
tempOutStream.write(tempLength);
tempOutStream.write(tempMessage);
tempOutStream.flush();
}
}
} catch (IOException exc) {
exc.printStackTrace();
}
}
}
/**
* This object input stream allows to specify the classloader which is used to resolve the classes.
*
*
* @author Rene Schneider - initial API and implementation
*
*/
protected class ClassloaderAwareObjectInputStream extends ObjectInputStream {
/**
* The classloader to use.
*/
private ClassLoader classLoader;
/**
* Creates an instance.
*
* @param anInputStream
* the input stream to read from
* @param aClassLoader
* the classloader to use for resolving
* @throws IOException
*/
public ClassloaderAwareObjectInputStream(InputStream anInputStream, ClassLoader aClassLoader)
throws IOException {
super(anInputStream);
classLoader = aClassLoader;
}
@Override
protected Class<?> resolveClass(ObjectStreamClass aDescription) throws IOException, ClassNotFoundException {
if (classLoader != null) {
try {
return classLoader.loadClass(aDescription.getName());
} catch (ClassNotFoundException exc) {
return super.resolveClass(aDescription);
}
} else {
return super.resolveClass(aDescription);
}
}
}
}