package eu.hgross.blaubot.core;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionAcceptor;
import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionManagerListener;
import eu.hgross.blaubot.core.acceptor.IBlaubotIncomingConnectionListener;
import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeacon;
import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeaconStore;
import eu.hgross.blaubot.core.connector.IBlaubotConnector;
import eu.hgross.blaubot.core.statemachine.BlaubotAdapterHelper;
import eu.hgross.blaubot.core.statemachine.ConnectionStateMachine;
import eu.hgross.blaubot.core.statemachine.IBlaubotConnectionStateMachineListener;
import eu.hgross.blaubot.core.statemachine.events.AbstractBlaubotDeviceDiscoveryEvent;
import eu.hgross.blaubot.core.statemachine.events.AbstractBlaubotStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.states.IBlaubotState;
import eu.hgross.blaubot.core.statemachine.states.IBlaubotSubordinatedState;
import eu.hgross.blaubot.core.statemachine.states.KingState;
import eu.hgross.blaubot.core.statemachine.states.PeasantState;
import eu.hgross.blaubot.core.statemachine.states.PeasantState.ConnectionAccomplishmentType;
import eu.hgross.blaubot.messaging.BlaubotChannelManager;
import eu.hgross.blaubot.messaging.IBlaubotChannel;
import eu.hgross.blaubot.util.Log;
/**
* The top level Blaubot object. Consists of the
* {@link BlaubotConnectionManager}, {@link ConnectionStateMachine},
* {@link eu.hgross.blaubot.messaging.BlaubotChannelManager} and a bunch of {@link IBlaubotAdapter}s.
*
* The {@link BlaubotConnectionManager} takes {@link IBlaubotConnection}s that
* are established by the {@link IBlaubotAdapter} implementations which receive
* incoming connections ({@link IBlaubotConnectionAcceptor}) and create
* connections ({@link IBlaubotConnector}).
*
* The {@link ConnectionStateMachine} manages the {@link IBlaubotState}s by
* receiving {@link AbstractBlaubotStateMachineEvent}s created from incoming
* admin {@link eu.hgross.blaubot.messaging.BlaubotMessage}s (from {@link IBlaubotConnection}s),
* {@link AbstractBlaubotDeviceDiscoveryEvent} (from
* {@link eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeacon}s) as well as new or closed connections (via
* {@link IBlaubotConnectionManagerListener}s and their events).
*
* Based on the {@link ConnectionStateMachine}s state changes, the
* {@link eu.hgross.blaubot.messaging.BlaubotChannelManager} will be set up to act as a Master or a Slave, reset
* it's context or stop/start it's services.
*
* <h1>USAGE:</h1> To create {@link IBlaubotChannel}s for communication a user can use
* the {@link Blaubot#createChannel(short)} method. Developers
* should attach {@link ILifecycleListener}s to the {@link Blaubot} instance to
* get high level information events about the currently formed {@link Blaubot}
* network (Joins/Leaves of devices, established or broken networks, ...)
*
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*/
public class Blaubot implements Closeable {
private static final String LOG_TAG = "Blaubot";
private final ConcurrentHashMap<IBlaubotConnection, KeepAliveSender> keepAliveSenders;
private final BlaubotConnectionManager connectionManager;
private final ConnectionStateMachine connectionStateMachine;
private final BlaubotChannelManager channelManager;
private final List<IBlaubotAdapter> adapters;
private final IBlaubotDevice ownDevice;
private final BlaubotUUIDSet uuidSet;
private BlaubotServerConnector serverConnector;
private ServerConnectionManager serverConnectionManager;
/**
* Receives events from the connection state machine and the BlaubotConnectionManager to generate
* the user api lifecycle events.
* Has to be attached to the connection state machine and the the channel manager.
*/
private final LifeCycleEventDispatcher lifeCycleEventDispatchingListener;
/**
* Creates a blaubot instance upon the given adapters and beacons.
*
* @param ownDevice the own device with the unique device id for this instance
* @param uuidSet the uuid set containing the beacon and app uuid
* @param adapters the adapters to be used (currently limited to 1)
* @param beacons the beacons to be used
*/
public Blaubot(IBlaubotDevice ownDevice, BlaubotUUIDSet uuidSet, List<IBlaubotAdapter> adapters, List<IBlaubotBeacon> beacons) {
if (adapters.size() != 1) {
throw new IllegalArgumentException("No or too much adapters given. Only one adapter supported at the moment.");
}
this.uuidSet = uuidSet;
final AdminMessageBeacon adminMessageBeacon = new AdminMessageBeacon(); // has to have priority on admin messages
beacons = new ArrayList<>(beacons);
beacons.add(adminMessageBeacon);
this.ownDevice = ownDevice;
this.adapters = adapters;
// Dependency injection of blaubot
for (IBlaubotAdapter adapter : adapters) {
adapter.setBlaubot(this);
}
this.keepAliveSenders = new ConcurrentHashMap<>();
this.connectionManager = new BlaubotConnectionManager(BlaubotAdapterHelper.getConnectionAcceptors(adapters), BlaubotAdapterHelper.getConnectors(adapters));
this.channelManager = new BlaubotChannelManager(ownDevice.getUniqueDeviceID());
this.channelManager.addAdminMessageListener(adminMessageBeacon);
// create and connect the dispatcher for life cycle events
this.lifeCycleEventDispatchingListener = new LifeCycleEventDispatcher(ownDevice);
// the server connection management
this.serverConnectionManager = new ServerConnectionManager(channelManager, ownDevice, connectionManager);
this.addLifecycleListener(serverConnectionManager);
// note: the lifecycle (setMaster(true/false)) is managed by the states of the state machine
// state machine
this.connectionStateMachine = new ConnectionStateMachine(ownDevice, connectionManager, adapters, beacons, this, serverConnectionManager);
this.connectionStateMachine.addConnectionStateMachineListener(lifeCycleChannelManager);
this.connectionManager.setBeaconStore(this.connectionStateMachine.getBeaconService().getBeaconStore());
// connect listeners
this.connectionManager.addConnectionListener(new ConnectionManagerListener());
this.connectionStateMachine.addConnectionStateMachineListener(new ConnectionStateMachineListener());
this.channelManager.addAdminMessageListener(lifeCycleEventDispatchingListener);
this.connectionStateMachine.addConnectionStateMachineListener(lifeCycleEventDispatchingListener);
// dependency injection of beacon store for connectors, acceptors and beacons
IBlaubotBeaconStore beaconStore = this.connectionStateMachine.getBeaconService().getBeaconStore();
for (IBlaubotAdapter adapter : adapters) {
adapter.getConnector().setBeaconStore(beaconStore);
adapter.getConnectionAcceptor().setBeaconStore(beaconStore);
}
for (IBlaubotBeacon beacon : beacons) {
beacon.setBeaconStore(beaconStore);
beacon.setBlaubot(this);
}
}
/**
* Sets the server connector to be used.
*
* @param serverConnector the server connector
*/
public void setServerConnector(final BlaubotServerConnector serverConnector) {
if (serverConnector == null) {
throw new NullPointerException("serverConnector may not be null");
}
// else if(this.serverConnector != null) {
// throw new RuntimeException("The server connector can not be changed.");
// } else if(isStarted()) {
// throw new RuntimeException("The server connector can not be added to a started blaubot instance. Add it before start.");
// }
this.serverConnector = serverConnector;
this.serverConnectionManager.setServerConnector(serverConnector);
}
/**
* The attached server connector
*
* @return the server connector or null, if setServerConnector was never called.
*/
public BlaubotServerConnector getServerConnector() {
return serverConnector;
}
/**
* The device object identifying this blaubot instance.
*
* @return the own blaubot device containing our unique device id.
*/
public IBlaubotDevice getOwnDevice() {
return ownDevice;
}
/**
* Starts blaubot.
*/
public void startBlaubot() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Starting Blaubot, ChannelManager and ConnectionStateMachine... ");
}
channelManager.activate();
connectionStateMachine.startEventDispatcher();
connectionStateMachine.startStateMachine();
}
/**
* Stops blaubot
*/
public void stopBlaubot() {
if (Log.logDebugMessages())
Log.d(LOG_TAG, "Stopping ConnectionStateMachine ... ");
connectionStateMachine.stopStateMachine();
// ChannelManager will be stopped in callback to stop of
// connectionStateMachine.stopEventDispatcher();
}
public BlaubotConnectionManager getConnectionManager() {
return connectionManager;
}
public ConnectionStateMachine getConnectionStateMachine() {
return connectionStateMachine;
}
public boolean isStarted() {
return connectionStateMachine.isStateMachineStarted();
}
public BlaubotChannelManager getChannelManager() {
return channelManager;
}
public List<IBlaubotAdapter> getAdapters() {
return adapters;
}
/**
* Creates the channel with the given id.
*
* @param channelId the channel's id
* @return a channel object that is usable, when blaubot is connected.
*/
public IBlaubotChannel createChannel(short channelId) {
return channelManager.createOrGetChannel(channelId);
}
/**
* Adds an {@link ILifecycleListener} to this {@link Blaubot} instance.
*
* @param lifecycleListener the listener to add
*/
public void addLifecycleListener(ILifecycleListener lifecycleListener) {
this.lifeCycleEventDispatchingListener.addLifecycleListener(lifecycleListener);
}
/**
* Removes an {@link ILifecycleListener} from this {@link Blaubot} instance.
*
* @param lifecycleListener the listener to remove
*/
public void removeLifecycleListener(ILifecycleListener lifecycleListener) {
this.lifeCycleEventDispatchingListener.removeLifecycleListener(lifecycleListener);
}
@Override
public String toString() {
return "Blaubot [ownDevice=" + getOwnDevice() + "]";
}
@Override
public void close() throws IOException {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "close() wa called. Closing all blaubot components.");
}
stopBlaubot();
ArrayList<Object> components = new ArrayList<>();
components.addAll(BlaubotAdapterHelper.getConnectionAcceptors(adapters));
components.addAll(BlaubotAdapterHelper.getConnectors(adapters));
components.addAll(getConnectionStateMachine().getBeaconService().getBeacons());
for (Object component : components) {
if (component instanceof Closeable) {
((Closeable) component).close();
}
}
}
/**
* This listener handles the creation, start and stop of
* {@link KeepAliveSender}s for all connected {@link IBlaubotDevice}s.
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*/
class ConnectionManagerListener implements IBlaubotConnectionManagerListener {
/**
* Is used instead of the config, if the config is not retrievable
*/
private static final int DEFAULT_KEEP_ALIVE_PERIOD = 500;
@Override
public void onConnectionEstablished(IBlaubotConnection connection) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Got onConnectionEstablished from CONNECTIONMANAGER: " + connection);
Log.d(LOG_TAG, "Current connections: " + connectionManager.getAllConnections());
Log.d(LOG_TAG, "Connected devices: " + connectionManager.getConnectedDevices());
}
startKeepAlives(connection);
}
/**
* Starts sending keep alive packets periodically through the connection.
*
* @param connection the connection
*/
private void startKeepAlives(IBlaubotConnection connection) {
// send keep alive to all connected devices (king: to all peasants,
// peasant: to king)
final IBlaubotDevice remoteDevice = connection.getRemoteDevice();
final IBlaubotConnector connectorForDevice = connectionManager.getConnectorForDevice(remoteDevice.getUniqueDeviceID());
final int keepAlivePeriod;
if (connectorForDevice == null) {
// we never got infos from our beacon, so we use a default period
keepAlivePeriod = DEFAULT_KEEP_ALIVE_PERIOD;
} else {
keepAlivePeriod = connectorForDevice.getAdapter().getBlaubotAdapterConfig().getKeepAliveInterval();
}
KeepAliveSender keepAliveSender = new KeepAliveSender(remoteDevice, channelManager, keepAlivePeriod);
keepAliveSender.start();
keepAliveSenders.put(connection, keepAliveSender);
}
@Override
public void onConnectionClosed(IBlaubotConnection connection) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Got onConnectionClosed from CONNECTIONMANAGER: " + connection);
Log.d(LOG_TAG, "Current connections: " + connectionManager.getAllConnections());
Log.d(LOG_TAG, "Connected devices: " + connectionManager.getConnectedDevices());
}
// handle keep alive
KeepAliveSender keepAliveSender = keepAliveSenders.get(connection);
if (keepAliveSender != null) {
keepAliveSender.stop();
keepAliveSenders.remove(connection);
}
}
}
/**
* Just for logging purposes.
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*/
static class ConnectionStateMachineListener implements IBlaubotConnectionStateMachineListener {
@Override
public void onStateChanged(IBlaubotState oldState, IBlaubotState newState) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "ConnectionStateMachineListener got onStateChange(). New state: " + newState);
}
}
@Override
public void onStateMachineStopped() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "StateMachine stopped");
}
}
@Override
public void onStateMachineStarted() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "StateMachine started");
}
}
}
/**
* Listens to the {@link ConnectionStateMachine} to set the
* {@link eu.hgross.blaubot.messaging.BlaubotChannelManager}'s state.
*/
private IBlaubotConnectionStateMachineListener lifeCycleChannelManager = new IBlaubotConnectionStateMachineListener() {
private static final String LOG_TAG = "ChannelManagerGlue";
@Override
public void onStateMachineStopped() {
// channelManager must be put into a defined state and be stopped
// after ConnectionStateMachineStop
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Setting PM CLIENT mode, resetting and deactivating (CSM stopped)");
}
channelManager.setMaster(false);
channelManager.reset();
channelManager.deactivate();
}
@Override
public void onStateMachineStarted() {
// channelmanager must have been started before
// ConnectionStateMachineStart (in BlaubotStart)
}
/**
* Sets up the {@link eu.hgross.blaubot.messaging.BlaubotChannelManager} as master including the listener
* wiring to inform the channel manager of new
* {@link IBlaubotConnection}s.
*
* @param oldState
* @param newState
*/
private void onChangedToMaster(IBlaubotState oldState, IBlaubotState newState) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Setting CM to MASTER mode");
}
// set to master if new state instanceof KingState
channelManager.setMaster(true);
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "CM now in MASTER mode");
}
// We are king and await incoming connections from peasants
// We register a listener to the KingState to inform the
// ChannellManager about new Peasant connections
KingState kingState = (KingState) newState;
kingState.setPeasantConnectionsListener(new IBlaubotIncomingConnectionListener() {
@Override
public void onConnectionEstablished(IBlaubotConnection connection) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Got new connection as King -> ChannelManager.addConnection()");
}
channelManager.addConnection(connection);
}
});
}
@Override
public void onStateChanged(IBlaubotState oldState, IBlaubotState newState) {
if (oldState == newState) {
// do nothing if the state remains the same
return;
}
if (oldState instanceof KingState) {
// remove listeners from obsolete KingStates
// should not be necessary but we like it to be on safe on this
((KingState) oldState).setPeasantConnectionsListener(null);
}
boolean isNowInKingState = newState instanceof KingState;
// iff state class has changed, set the master state and register
// listeners
if (stateClassChanged(oldState, newState) && isNowInKingState) {
onChangedToMaster(oldState, newState);
return;
}
// check if the new state is a subordinate state and handle it
onChangedToClient(oldState, newState);
}
/**
* Sets up the {@link eu.hgross.blaubot.messaging.BlaubotChannelManager} as client and adds the king's
* {@link IBlaubotConnection}.
*
* @param oldState
* @param newState
*/
private void onChangedToClient(IBlaubotState oldState, IBlaubotState newState) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Setting CM to CLIENT mode");
}
channelManager.setMaster(false);
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "CM now in CLIENT mode");
}
// Iff we are subordinate, we have exactly one connection to our
// king
// Check and inform channel manager only, if the connection to the
// king has changed
if (newState instanceof IBlaubotSubordinatedState) {
// -- we are peasant or prince
IBlaubotConnection kingConnection = ((IBlaubotSubordinatedState) newState).getKingConnection();
if (oldState instanceof PeasantState && newState instanceof PeasantState && newState != oldState) {
PeasantState ps = (PeasantState) oldState;
if (ps.getConnectionAccomplishmentType() == ConnectionAccomplishmentType.BOWED_DOWN) {
Log.d(LOG_TAG, "We bowed down to a new king with this state change -> ChannelManager.reset()");
channelManager.reset();
}
}
if (oldState instanceof IBlaubotSubordinatedState) {
IBlaubotConnection oldKingConnection = ((IBlaubotSubordinatedState) oldState).getKingConnection();
if (oldKingConnection == kingConnection) {
// -- the king connection has not changed
// do not inform the channel manager
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "The new state's kingConnection is the same as the old state's kingConnection. Readding to ChannelManager.");
}
return;
}
}
// -- the new state is a subordinate state and the
// KingConnection has changed
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Adding king connection to ChannelManager.");
}
channelManager.addConnection(kingConnection);
}
}
private boolean stateClassChanged(IBlaubotState oldState, IBlaubotState newState) {
if (oldState == null) {
return true;
}
return (!newState.getClass().equals(oldState.getClass()));
}
};
/**
* The uuid set created upon the app uuid.
*
* @return the uuid set
*/
public BlaubotUUIDSet getUuidSet() {
return uuidSet;
}
/**
* The server connection managing connections to a server that are created from the ServerConnector
* of this blaubot instance or another blaubot instance from a connected network.
*
* @return the manager
*/
public ServerConnectionManager getServerConnectionManager() {
return serverConnectionManager;
}
/**
* Just for hashcode and equals
*/
private UUID guid = UUID.randomUUID();
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((guid == null) ? 0 : guid.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Blaubot other = (Blaubot) obj;
if (guid == null) {
if (other.guid != null)
return false;
} else if (!guid.equals(other.guid))
return false;
return true;
}
}