package eu.hgross.blaubot.core.acceptor.discovery;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import eu.hgross.blaubot.core.BlaubotDevice;
import eu.hgross.blaubot.core.IBlaubotConnection;
import eu.hgross.blaubot.core.IBlaubotDevice;
import eu.hgross.blaubot.core.State;
import eu.hgross.blaubot.core.BlaubotConnectionManager;
import eu.hgross.blaubot.core.acceptor.ConnectionMetaDataDTO;
import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionAcceptor;
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.core.statemachine.ConnectionStateMachine;
import eu.hgross.blaubot.core.statemachine.events.AbstractBlaubotDeviceDiscoveryEvent;
import eu.hgross.blaubot.core.statemachine.states.IBlaubotState;
import eu.hgross.blaubot.core.statemachine.states.IBlaubotSubordinatedState;
import eu.hgross.blaubot.util.Log;
/**
* This service handles the beacons on all interfaces. The beacon implementations simply hands the accepted connections
* to this object and the actual conversation is generalized at this object's level.
*
* After receiving a {@link IBlaubotConnection} from a {@link IBlaubotBeacon} the BeaconService exchanges {@link State}s
* with the remote peer.
*
* The remote beacon implementation will most likely use the {@link ExchangeStatesTask} to exchange their state with the
* {@link BeaconConnectionHandler} of this objects.
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*
*/
public class BlaubotBeaconService {
private static final String LOG_TAG = "BlaubotBeaconService";
private final List<IBlaubotConnectionAcceptor> connectionAcceptors;
private final IBlaubotDevice ownDevice;
private List<IBlaubotBeacon> blaubotBeacons;
private ExecutorService executorService;
private BlaubotConnectionManager beaconConnectionManager;
private volatile BeaconMessage currentBeaconMessage; // maintained through onStateChanged(..) calls from the StateMachine
private final List<IBlaubotDiscoveryEventListener> discoveryEventListeners; // proxy listeners
private final ConnectionStateMachine connectionStateMachine;
private final IBlaubotBeaconStore beaconStore;
/**
* @param ownDevice our own device
* @param blaubotBeacons the list of beacons to be managed
* @param connectionAcceptors the acceptors to be exposed by the beacons
* @param connectionStateMachine the connection state machine which state should be exposed by the beacons
*/
public BlaubotBeaconService(IBlaubotDevice ownDevice, List<IBlaubotBeacon> blaubotBeacons, List<IBlaubotConnectionAcceptor> connectionAcceptors, ConnectionStateMachine connectionStateMachine) {
this.connectionStateMachine = connectionStateMachine;
this.ownDevice = ownDevice;
this.currentBeaconMessage = new BeaconMessage(ownDevice.getUniqueDeviceID(), State.Stopped, BlaubotAdapterHelper.getConnectionMetaDataList(connectionAcceptors));
this.blaubotBeacons = blaubotBeacons;
this.executorService = Executors.newCachedThreadPool();
this.discoveryEventListeners = new CopyOnWriteArrayList<>();
this.beaconStore = new BlaubotBeaconStore();
this.addDiscoveryEventListener((BlaubotBeaconStore)this.beaconStore);
this.connectionAcceptors = connectionAcceptors;
this.beaconConnectionManager = new BlaubotConnectionManager(new ArrayList<IBlaubotConnectionAcceptor>(blaubotBeacons), new ArrayList<IBlaubotConnector>());
this.beaconConnectionManager.addConnectionListener(new IBlaubotConnectionManagerListener() {
@Override
public void onConnectionEstablished(IBlaubotConnection connection) {
handleBeaconConnection(connection);
}
@Override
public void onConnectionClosed(IBlaubotConnection connection) {
// ignore
}
});
// listen to all the used beacons
for (IBlaubotBeacon beaconInterface : blaubotBeacons) {
beaconInterface.setDiscoveryEventListener(discoveryEventListener);
}
}
/**
* Listener is attached to all beacons and will therefore receive all discovery events
*/
private IBlaubotDiscoveryEventListener discoveryEventListener = new IBlaubotDiscoveryEventListener() {
@Override
public void onDeviceDiscoveryEvent(AbstractBlaubotDeviceDiscoveryEvent discoveryEvent) {
IBlaubotState currentState = connectionStateMachine.getCurrentState();
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "[Current state: " + currentState + "] Got discovery Event " + discoveryEvent);
}
if (discoveryEvent.getRemoteDevice().getUniqueDeviceID().equals(ownDevice.getUniqueDeviceID())) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "[Current state: " + currentState + "] Discovery event was our own event, ignoring");
}
// dont dispatch events of our own
return;
}
// We forward the discovery events of all beacons to the registered listeners.
for(IBlaubotDiscoveryEventListener listener : discoveryEventListeners) {
listener.onDeviceDiscoveryEvent(discoveryEvent);
}
// inject the currentState
discoveryEvent.setConnectionStateMachineState(currentState);
connectionStateMachine.pushStateMachineEvent(discoveryEvent);
}
};
/**
* Adds an {@link IBlaubotDiscoveryEventListener} to the beacon service.
* The listeners are called for each discovery event of any {@link IBlaubotBeacon}.
*
* @param discoveryEventListener the listener to be added
*/
public void addDiscoveryEventListener(IBlaubotDiscoveryEventListener discoveryEventListener) {
this.discoveryEventListeners.add(discoveryEventListener);
}
/**
* Removes a listener
*
* @param discoveryEventListener the listener to be removed
*/
public void removeDiscoveryEventListener(IBlaubotDiscoveryEventListener discoveryEventListener) {
this.discoveryEventListeners.remove(discoveryEventListener);
}
private void handleBeaconConnection(IBlaubotConnection beaconConnection) {
BeaconConnectionHandler task = new BeaconConnectionHandler(beaconConnection);
this.executorService.execute(task);
}
public void startBeaconInterfaces() {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Starting beacon interfaces ... ");
}
for (IBlaubotBeacon beaconInterface : this.blaubotBeacons) {
if (beaconInterface.isStarted()) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Beacon " + beaconInterface + " is already started - skipping.");
}
continue;
}
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Starting beacon interface " + beaconInterface + " ... ");
}
beaconInterface.startListening();
}
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Beacons started.");
}
}
public void stopBeaconInterfaces() {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Stopping beacon interfaces - going through all beacons ...");
}
for (IBlaubotBeacon beaconInterface : this.blaubotBeacons) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "\tStopping beacon interface: " + beaconInterface);
}
beaconInterface.stopListening();
}
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Beacon interfaces stopped.");
}
}
/**
* A Task that handles the conversation with beacon clients.
* It is the counterpart of the {@link ExchangeStatesTask}.
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*
*/
private class BeaconConnectionHandler implements Runnable {
private static final String LOG_TAG = "BeaconConnectionHandler";
private IBlaubotConnection connection;
public BeaconConnectionHandler(IBlaubotConnection connection) {
this.connection = connection;
}
@Override
public void run() {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Starting to handle beacon connection " + connection);
}
/*
* TODO: add this to documentation:
* The beacon message protocol is as follows:
* 1. The connecting side retrieves our state.
* 2. After that it sends us it's state which we will retrieve.
* 3. The connection closes
*/
// Send them our state
BeaconMessage ourMessage = currentBeaconMessage;
try {
connection.write(ourMessage.toBytes());
} catch (IOException e) {
if(Log.logErrorMessages()) {
Log.e(LOG_TAG, "Failed to send message to connected beacon client. Closing connection");
}
connection.disconnect();
return;
}
// Get their state
BeaconMessage theirMessage = BeaconMessage.fromBlaubotConnection(connection);
connection.disconnect();
// propagate events
if(theirMessage != null) {
IBlaubotDevice remoteDevice = connection.getRemoteDevice();
State theirState = theirMessage.getCurrentState();
AbstractBlaubotDeviceDiscoveryEvent discoveryEvent = theirState.createDiscoveryEventForDevice(remoteDevice, theirMessage.getOwnConnectionMetaDataList());
discoveryEventListener.onDeviceDiscoveryEvent(discoveryEvent);
if(theirMessage.getCurrentState() == State.Peasant || theirMessage.getCurrentState() == State.Prince) {
// if they are peasant or prince they should have a king and have sent us their uniqueDeviceId as well as the kings acceptor meta data
// so we generate another discovery event from that without bothering for any beacon transactions
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "The other side has a king, generating the king event for the remote king");
}
String kingDeviceUniqueId = theirMessage.getKingDeviceUniqueId();
IBlaubotDevice kingDevice = new BlaubotDevice(kingDeviceUniqueId);
AbstractBlaubotDeviceDiscoveryEvent kingDiscoveryEvent = State.King.createDiscoveryEventForDevice(kingDevice, theirMessage.getKingsConnectionMetaDataList());
discoveryEventListener.onDeviceDiscoveryEvent(kingDiscoveryEvent);
}
}
connection.disconnect();
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Done handling beacon connection " + connection);
}
}
}
/**
* Informs the BlaubotBeaconService that the blaubot connection state has changed.
*
* @param newState the new state
*/
public void onStateChanged(IBlaubotState newState) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "State changed, building new BeaconMessage");
}
// build the beacon message
final State state = State.getStateByStatemachineClass(newState.getClass());
final List<ConnectionMetaDataDTO> ownConnectionMetaDataList = BlaubotAdapterHelper.getConnectionMetaDataList(connectionAcceptors);
final String ownDeviceUniqueDeviceID = ownDevice.getUniqueDeviceID();
if (newState instanceof IBlaubotSubordinatedState) { // prince or peasant state
final String kingUniqueId = ((IBlaubotSubordinatedState) newState).getKingUniqueId();
final List<ConnectionMetaDataDTO> kingConnectionMetaDataList = beaconStore.getLastKnownConnectionMetaData(kingUniqueId);
if (kingConnectionMetaDataList == null) {
throw new IllegalStateException("Could not get connection metadata information for our king but we are in a subordinate state!");
}
currentBeaconMessage = new BeaconMessage(ownDeviceUniqueDeviceID, state, ownConnectionMetaDataList, kingUniqueId, kingConnectionMetaDataList);
} else {
currentBeaconMessage = new BeaconMessage(ownDeviceUniqueDeviceID, state, ownConnectionMetaDataList);
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "New cached BeaconMessage: " + currentBeaconMessage);
}
}
/**
* Get all managed beacons
* @return the list of managed beacons
*/
public List<IBlaubotBeacon> getBeacons() {
return blaubotBeacons;
}
/**
* Get the beacon store corresponding to the managed beacons
* @return the store
*/
public IBlaubotBeaconStore getBeaconStore() {
return beaconStore;
}
/**
* The current beacon message holding our state and connection meta data
* @return the beacon message ready to send
*/
public BeaconMessage getCurrentBeaconMessage() {
return currentBeaconMessage;
}
}