package eu.hgross.blaubot.core; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionAcceptor; import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionListener; import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionManagerListener; import eu.hgross.blaubot.core.connector.IBlaubotConnector; import eu.hgross.blaubot.core.statemachine.BlaubotAdapterHelper; import eu.hgross.blaubot.util.Log; /** * The Blaubot server */ public class BlaubotServer { private static final String LOG_TAG = "BlaubotServer"; /** * Max time for a kingdom to get disconnected after a call of disconnectKingdom() was made. */ private static final long KINGDOM_DISCONNECT_TIMEOUT = 5000; private final BlaubotConnectionManager connectionManager; /** * UniqueDeviceId -> BlaubotKingdom * The connected Kingdoms. */ private ConcurrentHashMap<String, BlaubotKingdom> kingdoms; /** * The attached acceptors over which connections from kingdoms are accepted */ private List<IBlaubotConnectionAcceptor> acceptors; /** * Lock for start/stop logic */ private final Object startStopMonitor = new Object(); /** * Lock for incoming connections regarding kingdom creation */ private final Object connectionLock = new Object(); /** * Listener to get informed about connects and disconnects of kingdoms. */ private CopyOnWriteArrayList<IBlaubotServerLifeCycleListener> blaubotServerLifeCycleListeners; /** * @param ownDevice the own device containing this server's uniqueDeviceId * @param acceptors acceptors */ public BlaubotServer(final IBlaubotDevice ownDevice, IBlaubotConnectionAcceptor... acceptors) { this.kingdoms = new ConcurrentHashMap<>(); this.blaubotServerLifeCycleListeners = new CopyOnWriteArrayList<>(); this.acceptors = Arrays.asList(acceptors); this.connectionManager = new BlaubotConnectionManager(this.acceptors, new ArrayList<IBlaubotConnector>()); this.connectionManager.addConnectionListener(new IBlaubotConnectionManagerListener() { @Override public void onConnectionClosed(IBlaubotConnection connection) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "A connection was closed (" + connection.getRemoteDevice().getUniqueDeviceID() + "). "); } } @Override public void onConnectionEstablished(final IBlaubotConnection connection) { // create a thread waiting for the KngdomConnection to be created. // the thread will terminate either with a successfully created kingdom connection or because the connection was lost // if successfully created, the connection will be used new Thread(new Runnable() { @Override public void run() { try { final BlaubotKingdomConnection kingdomConnection = BlaubotKingdomConnection.createFromInboundConnection(connection); synchronized (connectionLock) { // check if a connection for this unique id exists final BlaubotKingdom blaubotKingdom = kingdoms.get(connection.getRemoteDevice().getUniqueDeviceID()); if (blaubotKingdom != null) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "There was already a kingdom with king " + connection.getRemoteDevice().getUniqueDeviceID() + ". Disconnecting that kingdom first."); } final CountDownLatch discLatch = new CountDownLatch(1); blaubotKingdom.addDisconnectListener(new IBlaubotConnectionListener() { @Override public void onConnectionClosed(IBlaubotConnection connection) { discLatch.countDown(); } }); blaubotKingdom.disconnectKingdom(); try { boolean timedOut = !discLatch.await(KINGDOM_DISCONNECT_TIMEOUT, TimeUnit.MILLISECONDS); if (timedOut) { throw new RuntimeException("Kingdom did not disconnect fast enough (max " + KINGDOM_DISCONNECT_TIMEOUT + " ms)"); } } catch (InterruptedException e) { e.printStackTrace(); } } // create and start management of BlaubotKingdom if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Got new connection and creating a new kingdom with king " + connection.getRemoteDevice().getUniqueDeviceID() + " ..."); } IBlaubotDevice remoteKingDevice = kingdomConnection.getRemoteDevice(); final String kingUniqueDeviceId = remoteKingDevice.getUniqueDeviceID(); final BlaubotKingdom newKingdom = new BlaubotKingdom(ownDevice, remoteKingDevice); kingdomConnection.addConnectionListener(new IBlaubotConnectionListener() { @Override public void onConnectionClosed(IBlaubotConnection connection) { synchronized (connectionLock) { notifyKingdomDisconnected(newKingdom); BlaubotKingdom curKingdom = kingdoms.get(kingUniqueDeviceId); if (curKingdom == newKingdom) { kingdoms.remove(kingUniqueDeviceId, newKingdom); } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "There are now " + kingdoms.size() + " kingdoms connected to this server."); } } } }); BlaubotKingdom previous = kingdoms.put(remoteKingDevice.getUniqueDeviceID(), newKingdom); if (previous != null) { // already a kingdom for this unique device id. Should have been disconnected above... throw new IllegalStateException("Inconsistant state. There was already a kingdom for this king."); } newKingdom.manageConnection(kingdomConnection); notifyKingdomConnected(newKingdom); } } catch (IOException e) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "A kingdom connection was not created (closed before handshake completion."); } } } }).start(); } }); } /** * Starts the blaubot server. * Idempotent. */ public void startBlaubotServer() { synchronized (startStopMonitor) { if (isStarted()) { return; } // start all acceptors BlaubotAdapterHelper.startAcceptors(acceptors); } } /** * Stops the blaubot server. * Idempotent. */ public void stopBlaubotServer() { synchronized (startStopMonitor) { if (!_isStarted()) { return; } // stop all acceptors BlaubotAdapterHelper.stopAcceptors(acceptors); // disconnect all pending connections, if any for (IBlaubotConnection con : connectionManager.getAllConnections()) { con.disconnect(); } // disconnect all kingdoms, if any for (BlaubotKingdom kingdom : kingdoms.values()) { kingdom.disconnectKingdom(); } } } /** * Check the started state * * @return true iff the server is started */ public boolean isStarted() { synchronized (startStopMonitor) { return _isStarted(); } } /** * @return true if at least one acceptor is started */ private boolean _isStarted() { boolean started = false; for (IBlaubotConnectionAcceptor acceptor : acceptors) { if (acceptor.isStarted()) { started = true; break; } } return started; } /** * Removes a previously attached listener from the server * * @param blaubotServerLifeCycleListener the listener to be removed */ public void removeServerLifeCycleListener(IBlaubotServerLifeCycleListener blaubotServerLifeCycleListener) { this.blaubotServerLifeCycleListeners.remove(blaubotServerLifeCycleListener); } /** * Adds a listener to the server's lifecycle * * @param blaubotServerLifeCycleListener the listener to be added */ public void addServerLifeCycleListener(IBlaubotServerLifeCycleListener blaubotServerLifeCycleListener) { this.blaubotServerLifeCycleListeners.add(blaubotServerLifeCycleListener); } /** * notifies all attached listeners that a kingdom connected * * @param kingdom the connected kingdom */ private void notifyKingdomConnected(BlaubotKingdom kingdom) { for (IBlaubotServerLifeCycleListener lifeCycleListener : blaubotServerLifeCycleListeners) { lifeCycleListener.onKingdomConnected(kingdom); } } /** * notifies all attached listeners that a kingdom disconnected * * @param kingdom the disconnected kingdom */ private void notifyKingdomDisconnected(BlaubotKingdom kingdom) { if (kingdom == null) { throw new NullPointerException(); } for (IBlaubotServerLifeCycleListener lifeCycleListener : blaubotServerLifeCycleListeners) { lifeCycleListener.onKingdomDisconnected(kingdom); } } /** * @return the used acceptors */ public List<IBlaubotConnectionAcceptor> getAcceptors() { return acceptors; } public static void main(String[] args) { /** * Sample usage */ /* // Define the app's uuid and create own uniqueDeviceID UUID appUuid = UUID.fromString("de506eef-d894-4c18-97c3-d877ff26ca38"); BlaubotUUIDSet uuidSet = new BlaubotUUIDSet(appUuid); IBlaubotDevice ownDevice = new BlaubotDevice("Server1"); // we need an acceptor, so we create an adapter and use it's acceptor BlaubotWebsocketAdapter websocketAdapter = new BlaubotWebsocketAdapter(ownDevice, "0.0.0.0", 8080); List<IBlaubotConnectionAcceptor> acceptors = new ArrayList<>(); acceptors.add(websocketAdapter.getConnectionAcceptor()); // create and start the Blaubot server BlaubotServer server = new BlaubotServer(ownDevice, acceptors); server.addServerLifeCycleListener(new IBlaubotServerLifeCycleListener() { @Override public void onKingdomConnected(final BlaubotKingdom kingdom) { System.out.printf("onKingdomConnected: " + kingdom); // register to this kingdom's life cycle events kingdom.addLifecycleListener(new ILifecycleListener() { @Override public void onConnected() { System.out.println("onConnected() - " + kingdom); } @Override public void onDisconnected() { System.out.println("onDisconnected() - " + kingdom); } @Override public void onDeviceJoined(IBlaubotDevice blaubotDevice) { System.out.println("onDeviceJoined(" + blaubotDevice + ") - " + kingdom); } @Override public void onDeviceLeft(IBlaubotDevice blaubotDevice) { System.out.println("onDeviceLeft(" + blaubotDevice + ") - " + kingdom); } @Override public void onPrinceDeviceChanged(IBlaubotDevice oldPrince, IBlaubotDevice newPrince) { System.out.println("onPrinceDeviceChanged(" + oldPrince + ", " + newPrince + ") - " + kingdom); } @Override public void onKingDeviceChanged(IBlaubotDevice oldKing, IBlaubotDevice newKing) { System.out.println("onPrinceDeviceChanged(" + oldKing + ", " + newKing + ") - " + kingdom); } }); // register to the ping channel (from the debug view) final short pingChannelId = Short.MAX_VALUE; IBlaubotChannel channel = kingdom.getChannelManager().createOrGetChannel(pingChannelId); channel.subscribe(new IBlaubotMessageListener() { @Override public void onMessage(BlaubotMessage blaubotMessage) { String message = new String(blaubotMessage.getPayload(), BlaubotConstants.STRING_CHARSET); System.out.println("[king: "+kingdom.getKingDevice().getUniqueDeviceID()+"] GOT MESSAGE: " + message); } }); // send a ping once channel.publish("Ping!".getBytes(BlaubotConstants.STRING_CHARSET)); } @Override public void onKingdomDisconnected(BlaubotKingdom kingdom) { System.out.println("onKingdomDisconnected: " + kingdom); } }); server.startBlaubotServer(); */ } }