package NetworkAdapter.Implementation;
import EnvironmentPluginAPI.Exceptions.TechnicalException;
import EnvironmentPluginAPI.CustomNetworkMessages.NetworkMessage;
import ZeroTypes.Exceptions.ErrorMessages;
import NetworkAdapter.Interface.Exceptions.ConnectionLostException;
import NetworkAdapter.Interface.Exceptions.NotConnectedException;
import NetworkAdapter.Interface.IServerNetworkAdapter;
import NetworkAdapter.Interface.MessageChannel;
import NetworkAdapter.Interface.NetworkEventType;
import NetworkAdapter.Messages.ClientAckMessage;
import NetworkAdapter.Messages.ClientJoinMessage;
import NetworkAdapter.Messages.ConnectionEndMessage;
import NetworkAdapter.Messages.NACKMessage;
import ZeroTypes.Settings.AppSettings;
import ZeroTypes.Settings.SettingException;
import ZeroTypes.TransportTypes.TNetworkClient;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.ReentrantLock;
class ServerNetworkAdapterUseCase
extends AbstractNetworkAdapterUseCase
implements IServerNetworkAdapter,
NetworkChannel.NetworkMessageReceivedHandle<NetworkMessage> {
private ReentrantLock clientsAccess = new ReentrantLock(true);
private BlockingQueue<NetworkMessage> messages = new LinkedBlockingQueue<NetworkMessage>();
private BlockingQueue<Exception> exceptions = new LinkedBlockingQueue<Exception>();
private int controlListeningPort = 0;
private int dataListeningPort = 0;
private ServerSocket controlSocket; //socket for client control communication
private SessionCreator sessionCreator; // extra thread for waiting on client control connections
private List<ClientSession> clients = new ArrayList<ClientSession>();
private ServerSocket dataSocket; //socket for data transfer
private DataChannelCreator dataChannelCreator; // extra thread for waiting on client data channel connections
private ClassLoader currentClassLoader;
/**
* Sets up a tcp host, listening on the ports specified in the settings.properties.
*
* @throws ZeroTypes.Settings.SettingException,
* if a problem with the application's settings file occurs
* @throws EnvironmentPluginAPI.Exceptions.TechnicalException,
* if a problem with the connection occurs
*/
public ServerNetworkAdapterUseCase() throws SettingException, TechnicalException {
//try to read the port settings from the application's settings file
controlListeningPort = AppSettings.getInt("controlListeningPort");
dataListeningPort = AppSettings.getInt("dataListeningPort");
}
public ServerNetworkAdapterUseCase(int port) {
controlListeningPort = port;
dataListeningPort = controlListeningPort + 1;
}
/**
* @throws NetworkAdapter.Interface.Exceptions.ConnectionLostException
* if any application settings were unreachable or configured incorrectly
* @throws TechnicalException if any severe technical exceptions occur, i.e. the port is blocked.
*/
public void startHosting() throws TechnicalException, ConnectionLostException {
//try setting up the listening ports
try {
if (controlSocket == null) {
controlSocket = new ServerSocket(controlListeningPort);
}
} catch (IOException ex) {
throw new TechnicalException(ErrorMessages.get("unableToOpenListenPort") + controlListeningPort + "\n" + ex.getLocalizedMessage());
}
try {
if (dataSocket == null) {
dataSocket = new ServerSocket(dataListeningPort);
}
} catch (IOException ex) {
throw new TechnicalException(ErrorMessages.get("unableToOpenListenPort") + dataListeningPort + "\n" + ex.getLocalizedMessage());
}
// starts a thread, that listens for client control connections and starts control sessions for them
if (sessionCreator == null) {
sessionCreator = new SessionCreator();
sessionCreator.start();
}
// starts a thread, that listens for client connections and starts data channel sessions for them
if (dataChannelCreator == null) {
dataChannelCreator = new DataChannelCreator();
dataChannelCreator.start();
}
}
public void stopHosting() {
// stop session and data channel creators
dataChannelCreator.interrupt();
sessionCreator.interrupt();
// stop all waiting Threads
for (ClientSession c : clients) {
try {
c.close(new ConnectionEndMessage(c.getClientId(), "serverShuttingDown"));
} catch (ConnectionLostException e) {
} // server is shutting down anyway
}
}
@Override
public void setContextClassLoader(ClassLoader classLoader) {
currentClassLoader = classLoader;
for (ClientSession client : clients) {
client.controlChannel.setContextClassLoader(classLoader);
if (client.dataChannel != null) {
client.dataChannel.setContextClassLoader(classLoader);
}
}
}
/**
* puts the message in the message queue of the main process
*
* @param message
*/
@Override
public void handleMessage(NetworkMessage message) {
informSubscribers(message);
}
/**
* puts the exception in the exception queue of the main process
*
* @param exception the exception to handle. Will be one of: ConnectionLostException, TechnicalException
*/
public void handleException(Exception exception) {
if (exception instanceof ConnectionLostException) {
int clientId = ((ConnectionLostException) exception).getClientId();
if (clientId > -1 && clientId < clients.size()) {
ClientSession tmp = clients.get(clientId);
tmp.forceClose();
clients.set(clientId, null);
fireNetworkEvent(NetworkEventType.ConnectionLost, clientId);
}
}
exception.printStackTrace();
}
@Override
public void sendNetworkMessage(NetworkMessage message, MessageChannel channel) throws NotConnectedException, ConnectionLostException {
if (isValidClientId(message.getClientId())) {
ClientSession clientSession = clients.get(message.getClientId());
if (channel == MessageChannel.DATA) {
clientSession.dataChannel.sendNetworkMessage(message);
} else {
clientSession.controlChannel.sendNetworkMessage(message);
}
} else {
throw new NotConnectedException();
}
}
@Override
public List<TNetworkClient> getConnectedClients() {
List<TNetworkClient> result = new LinkedList<TNetworkClient>();
for (ClientSession session : clients) {
if (session != null) {
result.add(new TNetworkClient(session.getClientId(), session.getClientName(), session.controlChannel.getRemoteAddress(), session.getConnectionTime()));
}
}
return result;
}
/**
* @param i
* @return Determines if a clientId is valid. Valid if i is an existing array index and there is no client session
* in that slot.
*/
private boolean isValidClientId(int i) {
if (i < 0
|| i >= clients.size()
|| clients.isEmpty()) {
return false;
}
return clients.get(i) != null;
}
/**
* finds an index in the clients list that is not occupied and stores a new client session on the
* given port in there. (thread safe)
*/
private int insertControlSession(NetworkAccessProtocol networkAccessProtocol, String clientName) throws ConnectionLostException {
//remember information for ack answer and session creation
boolean freeId = false;
int id = -1;
clientsAccess.lock(); //prevent our client list from being changed while we rely on the structure
//find and id that is not occupied with a session
for (int i = 0; i < clients.size(); i++) {
if (clients.get(i) == null) {
freeId = true;
id = i;
break;
}
}
// create a client session in the right spot
ClientSession session;
if (freeId) {
session = new ClientSession(id, clientName);
clients.set(id, session);
} else {
id = clients.size();
session = new ClientSession(id, clientName);
clients.add(session);
}
clientsAccess.unlock();
//start listening on the control connection
session.controlChannel = new NetworkChannel<NetworkMessage>(networkAccessProtocol, this);
if (currentClassLoader != null) {
session.controlChannel.setContextClassLoader(currentClassLoader);
}
session.controlChannel.start();
return id;
}
private boolean attachDataChannel(NetworkAccessProtocol networkAccessProtocol, int clientId) {
boolean result = false;
clientsAccess.lock();
if (isValidClientId(clientId)) {
ClientSession clientSession = clients.get(clientId);
clientSession.dataChannel = new NetworkChannel<NetworkMessage>(networkAccessProtocol, this);
if(currentClassLoader != null) {
clientSession.dataChannel.setContextClassLoader(currentClassLoader);
}
clientSession.dataChannel.start();
result = true;
}
clientsAccess.unlock();
return result;
}
/**
* extra thread for waiting (blocked) for new client control connections and establishing them.
*/
private class SessionCreator extends Thread {
private SessionCreator() {
super("SessionCreator");
}
@Override
public void run() {
try {
while (!isInterrupted() && !controlSocket.isClosed()) {
/*
* Read first message from the socket. If it is a protocol-conform ClientJoin Message, create client
* session and send an ack. If not, notice about protocol violation and close socket.
*/
Socket tmp = controlSocket.accept();
if (isInterrupted()) {
break;
}
NetworkAccessProtocol networkAccessProtocol = new NetworkAccessProtocol(tmp);
NetworkMessage message = networkAccessProtocol.readMessage();
if (message instanceof ClientJoinMessage) {
int clientId = insertControlSession(networkAccessProtocol, ((ClientJoinMessage) message).getAgentName());
networkAccessProtocol.setClientId(clientId);
networkAccessProtocol.writeMessage(new ClientAckMessage(clientId));
} else {
networkAccessProtocol.writeMessage(new NACKMessage(-1, "networkProtocolViolated"));
}
}
} catch (IOException ex) {
exceptions.add(new TechnicalException("unableToReadControlSocket"));
} catch (ConnectionLostException e) {
exceptions.add(e);
}
}
}
/**
* extra thread for waiting (blocked) for new client data channel connections and attaching them to their clientConnection.
*/
private class DataChannelCreator extends Thread {
private DataChannelCreator() {
super("DataChannelCreator");
}
@Override
public void run() {
try {
while (!isInterrupted() && !dataSocket.isClosed()) {
/*
* Read first message from the socket. If it is a protocol-conform ClientJoin Message, and the
* clientId has a running connection, attach the data connection to the session and confirm via ack.
* If something goes wrong, notice about protocol violation or clientId inconsistency and close
* socket.
*/
Socket socket = dataSocket.accept();
if (isInterrupted()) {
break;
}
NetworkAccessProtocol networkAccessProtocol = new NetworkAccessProtocol(socket);
NetworkMessage message = networkAccessProtocol.readMessage();
if (message instanceof ClientJoinMessage) {
int clientId = message.getClientId();
if (attachDataChannel(networkAccessProtocol, clientId)) {
networkAccessProtocol.writeMessage(new ClientAckMessage(clientId));
networkAccessProtocol.setClientId(clientId);
informSubscribers(message);
fireNetworkEvent(NetworkEventType.Connected, clientId);
} else {
networkAccessProtocol.writeMessage(new NACKMessage(clientId, "invalidClientId"));
}
} else {
networkAccessProtocol.writeMessage(new NACKMessage(message.getClientId(), "networkProtocolViolated"));
}
}
} catch (IOException ex) {
exceptions.add(new TechnicalException("unableToReadControlSocket"));
} catch (ConnectionLostException e) {
exceptions.add(e);
}
}
}
}