package eu.hgross.blaubot.core.statemachine;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeoutException;
import eu.hgross.blaubot.admin.AbstractAdminMessage;
import eu.hgross.blaubot.admin.RelayAdminMessage;
import eu.hgross.blaubot.core.Blaubot;
import eu.hgross.blaubot.core.BlaubotConnectionManager;
import eu.hgross.blaubot.core.IBlaubotAdapter;
import eu.hgross.blaubot.core.IBlaubotConnection;
import eu.hgross.blaubot.core.IBlaubotDevice;
import eu.hgross.blaubot.core.ServerConnectionManager;
import eu.hgross.blaubot.core.State;
import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionAcceptor;
import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionManagerListener;
import eu.hgross.blaubot.core.acceptor.discovery.BlaubotBeaconService;
import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeacon;
import eu.hgross.blaubot.core.statemachine.events.AbstractBlaubotDeviceDiscoveryEvent;
import eu.hgross.blaubot.core.statemachine.events.AbstractBlaubotStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.events.AbstractTimeoutStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.events.AdminMessageStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.events.ConnectionClosedStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.events.ConnectionEstablishedStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.events.StartStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.events.StopStateMachineEvent;
import eu.hgross.blaubot.core.statemachine.states.FreeState;
import eu.hgross.blaubot.core.statemachine.states.IBlaubotState;
import eu.hgross.blaubot.core.statemachine.states.StoppedState;
import eu.hgross.blaubot.messaging.IBlaubotAdminMessageListener;
import eu.hgross.blaubot.util.Log;
/**
* Statemachine for the network creation. Simply delegates the events from the {@link BlaubotConnectionManager} and the
* {@link AbstractBlaubotDeviceDiscoveryEvent}s from the beacons to the current state. The state will handle the event
* based state changes by themselves (calling nextState).
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*
*/
public class ConnectionStateMachine {
private static final String LOG_TAG = "ConnectionStateMachine";
protected final Blaubot blaubot;
private final List<IBlaubotConnectionStateMachineListener> connectionStateMachineListeners;
private final List<IBlaubotBeacon> beacons;
private final List<IBlaubotConnectionAcceptor> acceptors;
private final List<IBlaubotAdapter> adapters;
private final BlaubotBeaconService beaconService;
private final BlockingQueue<AbstractBlaubotStateMachineEvent> stateMachineEventQueue;
private final StateMachineSession stateMachineSession;
private StateMachineEventDispatcher stateMachineEventDispatcher;
protected IBlaubotState currentState;
/**
* Create the connection state machine for a blaubot instance.
*
* @param ownDevice the own device
* @param connectionManager the connection manager managing the blaubot acceptors
* @param adapters the adapters
* @param beacons the beacons to be used
* @param blaubot the blaubot instance
* @param serverConnectionManager server connection manager
*/
public ConnectionStateMachine(IBlaubotDevice ownDevice, final BlaubotConnectionManager connectionManager, final List<IBlaubotAdapter> adapters, List<IBlaubotBeacon> beacons, final Blaubot blaubot, ServerConnectionManager serverConnectionManager) {
this.blaubot = blaubot;
this.adapters = adapters;
this.stateMachineEventQueue = new LinkedBlockingQueue<AbstractBlaubotStateMachineEvent>();
this.beacons = beacons;
this.acceptors = BlaubotAdapterHelper.getConnectionAcceptors(adapters);
this.beaconService = new BlaubotBeaconService(ownDevice, beacons, acceptors, this);
this.connectionStateMachineListeners = new CopyOnWriteArrayList<>();
// connect to admin messages
this.blaubot.getChannelManager().addAdminMessageListener(adminMessageChannelListener);
connectionManager.addConnectionListener(connectionListener);
this.stateMachineSession = new StateMachineSession(this, ownDevice, serverConnectionManager);
this.currentState = new StoppedState();
}
private final Object eventDispatcherLock = new Object();
/**
* Starts the CSM's event dispatcher. Mandatory for the CSM to do anything.
*/
public void startEventDispatcher() {
synchronized (eventDispatcherLock) {
if(isEventDispatcherRunning()) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "EventDispatcher already running - ignoring startEventDispatcher()");
}
return;
}
stateMachineEventDispatcher = new StateMachineEventDispatcher();
stateMachineEventDispatcher.start();
}
}
public void stopEventDispatcher() {
synchronized (eventDispatcherLock) {
if(stateMachineEventDispatcher != null) {
stateMachineEventDispatcher.interrupt();
stateMachineEventDispatcher = null;
}
}
}
/**
* Starts the state machine by pushing a start event to the CSM's event queue.
*/
public void startStateMachine() {
pushStateMachineEvent(new StartStateMachineEvent(currentState));
}
/**
* Actually handles the start of the CSM
*/
private void _startStateMachine() {
if (!(currentState instanceof StoppedState)) {
if(Log.logWarningMessages()) {
Log.w(LOG_TAG, "ConnectionStateMachine already started. Ignoring _startStateMachine()");
}
return; // already started
}
StoppedState stoppedState = (StoppedState) this.currentState;
stoppedState.handleState(this.stateMachineSession);
changeState(new FreeState());
}
/**
* Requests a stop of the CSM by pushing an event to the CSM's event queue.
*/
public void stopStateMachine() {
pushStateMachineEvent(new StopStateMachineEvent(currentState));
}
/**
* Actually handles the stop of the state machine
*/
private void _stopStateMachine() {
this.changeState(new StoppedState());
}
/**
* changes state
*
* @param newState
*/
private void changeState(IBlaubotState newState) {
if(newState == currentState)
return; // do nothing if same state
IBlaubotState oldState = currentState;
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "[Current state: " + currentState + "] Changing to state " + newState);
}
assertStateChange(currentState, newState);
boolean sendStarted = false;
boolean sendStopped = false;
if (currentState instanceof StoppedState && !(newState instanceof StoppedState)) {
sendStarted = true;
} else if (newState instanceof StoppedState && !(currentState instanceof StoppedState)) {
sendStopped = true;
}
currentState = newState;
// let the beacons signal the new state, if not a StoppedState, which would make no sense at all
// inform the beacon service
beaconService.onStateChanged(currentState);
if (!(newState instanceof StoppedState)) {
for (IBlaubotBeacon beacon : beacons) {
beacon.onConnectionStateMachineStateChanged(newState);
}
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Handling state " + currentState.getClass().getSimpleName() + " ...");
}
currentState.handleState(stateMachineSession);
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "State " + currentState.getClass().getSimpleName() + " handled.");
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Notifying ConnectionStateMachineListeners onStateChanged() ...");
}
// inform the listeners
for (IBlaubotConnectionStateMachineListener connectionStateMachineListener : this.connectionStateMachineListeners) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Notifying listener " + connectionStateMachineListener);
}
if (sendStarted)
connectionStateMachineListener.onStateMachineStarted();
if (sendStopped)
connectionStateMachineListener.onStateMachineStopped();
connectionStateMachineListener.onStateChanged(oldState, newState);
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Done notifying listener " + connectionStateMachineListener);
}
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Done notifying onStateChanged() ...");
}
}
/**
* @param currentState
* @param nextState
*/
private static void assertStateChange(IBlaubotState currentState, IBlaubotState nextState) {
State cur = State.getStateByStatemachineClass(currentState.getClass());
State to = State.getStateByStatemachineClass(nextState.getClass());
if(!cur.isStateChangeAllowed(to)) {
throw new IllegalStateException("A state change from " + cur + " to " + to + " is not allowed.");
}
}
/**
* Registers a listener to get informed about state changes of the state machine
* @param connectionStateMachineListener the listener to add
*/
public void addConnectionStateMachineListener(IBlaubotConnectionStateMachineListener connectionStateMachineListener) {
this.connectionStateMachineListeners.add(connectionStateMachineListener);
}
/**
* Removes a previously added listener
* @param connectionStateMachineListener the listener to remove
*/
public void removeConnectionStateMachineListener(IBlaubotConnectionStateMachineListener connectionStateMachineListener) {
this.connectionStateMachineListeners.remove(connectionStateMachineListener);
}
public List<IBlaubotConnectionAcceptor> getConnectionAcceptors() {
return acceptors;
}
public BlaubotBeaconService getBeaconService() {
return beaconService;
}
private IBlaubotConnectionManagerListener connectionListener = new IBlaubotConnectionManagerListener() {
@Override
public void onConnectionClosed(IBlaubotConnection connection) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "[Current state: " + currentState + "] Connection down: " + connection + ". Pushing as CSM event to queue.");
}
ConnectionClosedStateMachineEvent event = new ConnectionClosedStateMachineEvent(connection);
event.setConnectionStateMachineState(currentState);
pushStateMachineEvent(event);
}
@Override
public void onConnectionEstablished(IBlaubotConnection connection) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "[Current state: " + currentState + "] Got new connection: " + connection + ". Pushing as CSM event to queue.");
}
ConnectionEstablishedStateMachineEvent event = new ConnectionEstablishedStateMachineEvent(connection);
event.setConnectionStateMachineState(currentState);
pushStateMachineEvent(event);
}
};
private IBlaubotAdminMessageListener adminMessageChannelListener = new IBlaubotAdminMessageListener() {
@Override
public void onAdminMessage(AbstractAdminMessage adminMessage) {
if (adminMessage instanceof RelayAdminMessage) {
// don't process relay messages
return;
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "[Current state: " + currentState + "] Got admin message: " + adminMessage + ". Pushing as CSM event to queue.");
}
pushStateMachineEvent(new AdminMessageStateMachineEvent(currentState, adminMessage));
}
};
public IBlaubotState getCurrentState() {
return this.currentState;
}
public boolean isStateMachineStarted() {
return !(currentState instanceof StoppedState);
}
private boolean isEventDispatcherRunning() {
synchronized (eventDispatcherLock) {
return this.stateMachineEventDispatcher != null && this.stateMachineEventDispatcher.isAlive();
}
}
/**
* Dispatches events from the eventQueue to the current state. (UI-Thread alike)
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*
*/
class StateMachineEventDispatcher extends Thread {
private static final String LOG_TAG = "StateMachineEventDispatcher";
/**
* Max time the processing of an event may take. If it takes longer,
* an exception will be thrown.
*/
private static final int MAX_EVENT_PROCESSING_TIME = 60000; // ms
private Timer processingTimeoutTimer;
public StateMachineEventDispatcher() {
setName("csm-event-dispatcher");
}
private void handleState(IBlaubotState state) {
changeState(state);
}
/**
* Starts a timer that will log warnings, if the ConnectionStateMachine takes too long
* to process an event.
* @param event the event that took too long to be processed
*/
private void startTimer(final AbstractBlaubotStateMachineEvent event) {
processingTimeoutTimer = new Timer();
final TimerTask timerTask = new TimerTask() {
@Override
public void run() {
final String message = " [curState: " + currentState + "] The processing of " + event + " took longer than " + MAX_EVENT_PROCESSING_TIME + " ms";
if (Log.logWarningMessages()) {
Log.e(LOG_TAG, message);
}
// throw new RuntimeException(new TimeoutException(message));
}
};
processingTimeoutTimer.schedule(timerTask, MAX_EVENT_PROCESSING_TIME);
}
/**
* cancels the timer
*/
private void cancelTimer() {
if (processingTimeoutTimer != null) {
processingTimeoutTimer.cancel();
processingTimeoutTimer = null;
}
}
@Override
public void run() {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "StateMachineEventDispatcher started.");
}
while(!isInterrupted() && Thread.currentThread() == stateMachineEventDispatcher) {
try {
AbstractBlaubotStateMachineEvent event = stateMachineEventQueue.take();
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "[curState: "+ currentState +"] CSM EventQueue (size=" + stateMachineEventQueue.size() + ") took: " + event);
}
// start the timeout timer
startTimer(event);
// we measure the processing time
long startTime = System.currentTimeMillis();
if(event instanceof AdminMessageStateMachineEvent) {
AbstractAdminMessage aam = ((AdminMessageStateMachineEvent)event).getAdminMessage();
IBlaubotState state = currentState.onAdminMessage(aam);
handleState(state);
} else if(event instanceof ConnectionClosedStateMachineEvent) {
IBlaubotState state = currentState.onConnectionClosed(((ConnectionClosedStateMachineEvent) event).getConnection());
handleState(state);
} else if(event instanceof ConnectionEstablishedStateMachineEvent) {
IBlaubotState state = currentState.onConnectionEstablished(((ConnectionEstablishedStateMachineEvent) event).getConnection());
handleState(state);
} else if(event instanceof AbstractBlaubotDeviceDiscoveryEvent) {
// filter discovery events that discover ourselves
if(!stateMachineSession.getOwnDevice().getUniqueDeviceID().equals(((AbstractBlaubotDeviceDiscoveryEvent) event).getRemoteDevice().getUniqueDeviceID())) {
IBlaubotState state = currentState.onDeviceDiscoveryEvent((AbstractBlaubotDeviceDiscoveryEvent) event);
handleState(state);
}
} else if (event instanceof AbstractTimeoutStateMachineEvent) {
IBlaubotState state = currentState.onTimeoutEvent((AbstractTimeoutStateMachineEvent) event);
handleState(state);
} else if(event instanceof StopStateMachineEvent) {
_stopStateMachine();
} else if(event instanceof StartStateMachineEvent) {
_startStateMachine();
} else {
throw new RuntimeException("Unknown event in event queue!");
}
// stop timeout timer
cancelTimer();
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Event processing took " + (System.currentTimeMillis() - startTime) + " ms");
}
} catch (InterruptedException e) {
break;
}
}
cancelTimer();
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "StateMachineEventDispatcher stopped.");
}
}
}
/**
* Pushes a AbstractBlaubotStateMachineEvent to the StateMachine's event queue.
* @param stateMachineEvent the event
*/
public void pushStateMachineEvent(AbstractBlaubotStateMachineEvent stateMachineEvent) {
try {
stateMachineEventQueue.put(stateMachineEvent);
} catch (InterruptedException e) {
// ignore
}
}
}