package eu.hgross.blaubot.core.statemachine.states;
import java.util.List;
import eu.hgross.blaubot.core.ConnectionStateMachineConfig;
import eu.hgross.blaubot.core.IBlaubotConnection;
import eu.hgross.blaubot.core.State;
import eu.hgross.blaubot.core.acceptor.ConnectionMetaDataDTO;
import eu.hgross.blaubot.core.statemachine.BlaubotAdapterHelper;
import eu.hgross.blaubot.core.statemachine.StateMachineSession;
import eu.hgross.blaubot.core.statemachine.events.AbstractBlaubotDeviceDiscoveryEvent;
import eu.hgross.blaubot.core.statemachine.events.AbstractTimeoutStateMachineEvent;
import eu.hgross.blaubot.admin.ACKPronouncePrinceAdminMessage;
import eu.hgross.blaubot.admin.AbstractAdminMessage;
import eu.hgross.blaubot.admin.BowDownToNewKingAdminMessage;
import eu.hgross.blaubot.admin.CensusMessage;
import eu.hgross.blaubot.admin.DiscoveredDeviceAdminMessage;
import eu.hgross.blaubot.admin.PronouncePrinceAdminMessage;
import eu.hgross.blaubot.util.Log;
/**
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*/
public class PeasantState implements IBlaubotState, IBlaubotSubordinatedState {
private static final int MAX_RETRIES_TO_CONNECT_TO_PRINCE_OR_KING = 4;
private static final String LOG_TAG = "PeasantState";
private StateMachineSession session;
private IBlaubotConnection kingConnection;
private final ConnectionAccomplishmentType connectionAccomplishmentType;
public enum ConnectionAccomplishmentType {
/**
* We connected voluntarily to the current king (Free to Peasant)
*/
VOLUNTARILY,
/**
* We connected to the king after a bow down.
*/
BOWED_DOWN,
/**
* We connected to the Prince after the King died.
*/
FOLLOWED_THE_HEIR_TO_THE_THRONE,
/**
* We were degraded from {@link PrinceState} to {@link PeasantState}
*/
DEGRADATION
}
/**
* @param kingConnection the connection to our king
* @param connectionAccomplishmentType information about how the connection was accomplished
*/
public PeasantState(IBlaubotConnection kingConnection, ConnectionAccomplishmentType connectionAccomplishmentType) {
this.connectionAccomplishmentType = connectionAccomplishmentType;
this.kingConnection = kingConnection;
if (kingConnection == null)
throw new NullPointerException();
}
/**
* @return VOLUNTARILY if we connected to the king from Free state, BOWED_DOWN if we connected to the king due to a {@link BowDownToNewKingAdminMessage}
*/
public ConnectionAccomplishmentType getConnectionAccomplishmentType() {
return connectionAccomplishmentType;
}
@Override
public IBlaubotState onConnectionEstablished(IBlaubotConnection connection) {
if (connection != kingConnection) {
if (session.getConnectionManager().getAllConnections().contains(connection)) {
// we got a connection but are in peasant state
// this can happen if we were prince, and had to bow down to another king
// the king sends out the bow down order and kills all connections, then the peasants try to connect to the prince
// TODO: validate this order ... peasants should know the difference when to bow down and when the connection got really lost
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Got a connection as peasant. The only allowed connection in peasant state is the king's connection.");
System.out.println("KingConnection: " + kingConnection);
System.out.println("Got connection: " + connection);
}
// we kill the connection
connection.disconnect();
} else {
// everything ok - we probably got an event after we transitioned from KingState to PeasantState
}
}
return this;
}
@Override
public IBlaubotState onConnectionClosed(IBlaubotConnection connection) {
// -- we are peasant and lost a connection
/*
* We previously asserted at this point, that we have NO connections anymore and threw
* an exception if there was at least one connection remaining. But now we moved on to
* a single threaded event queue for the ConnectionStateMachine which changes the game.
*
* It is possible that more than one closed events occur here. This can happen if we
* just transitioned from king to peasant state because we merged our kingdom and bowed
* down to another king.
* In this process the former king (we) close all connections to our peasants right
* before we transition to the peasant state. The connectionClosed events are now pushed
* to the ConnectionStateMachine's event queue and will therefore occur in the new state
* (this state) and can be safely ignored.
*/
if (connection == kingConnection) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "We lost the king-connection (to " + kingConnection.getRemoteDevice().getUniqueDeviceID() + ") in PeasantState. We assume our king to be lost and start connecting to the prince.");
}
// we lost our king - look out for the prince
CensusMessage lastCensusMessage = session.getLastCensusMessage();
if (lastCensusMessage == null) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Could not determine prince device (never got a census message.");
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Connection to prince failed. Changing state to FreeState");
}
// TODO: maybe retry?
return new FreeState();
}
// Get the adapter's config
ConnectionStateMachineConfig conf = session.getConnectionStateMachineConfigForDevice(connection.getRemoteDevice());
final int CROWNING_PREPARATION_TIME = conf.getCrowningPreparationTimeout();
// find the prince
for (String uniqueId : lastCensusMessage.getDeviceStates().keySet()) {
State state = lastCensusMessage.getDeviceStates().get(uniqueId);
if (state.equals(State.Prince)) {
// -- we found a prince
if (uniqueId.equals(session.getOwnDevice().getUniqueDeviceID())) {
/**
* If we bowed down to another king due to a merge, we could have been prince before and therefore would try to
* connect to ourselves. So we need to check this.
*/
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "The prince's uniqueDeviceId is ours. Not trying to connect.");
}
break;
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "I know the prince device is " + uniqueId + ". I will give the king some time to prepare it's crowning and connect after that ...");
}
try {
Thread.sleep(CROWNING_PREPARATION_TIME);
} catch (InterruptedException e) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Crowning got interrupted ...");
}
return this;
}
IBlaubotConnection conn = session.getConnectionManager().connectToBlaubotDevice(uniqueId, MAX_RETRIES_TO_CONNECT_TO_PRINCE_OR_KING);
boolean result = conn != null;
if (result) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Connection to prince successful. Remaining in Peasant state.");
}
return new PeasantState(conn, ConnectionAccomplishmentType.FOLLOWED_THE_HEIR_TO_THE_THRONE);
}
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Connection to prince device failed ... changing to FreeState");
}
// connection failed, go to free state
break;
}
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Could not determine prince device (not present in last census). Changing to FreeState. Last CensusMessage was: " + session.getLastCensusMessage());
}
return new FreeState();
}
return this; // ignore if not king connection
}
@Override
public IBlaubotState onDeviceDiscoveryEvent(AbstractBlaubotDeviceDiscoveryEvent discoveryEvent) {
if (true || discoveryEvent.getRemoteDeviceState().equals(State.King)) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Got a DeviceDiscoverEvent for a king as peasant. Dispatching to king.");
}
final DiscoveredDeviceAdminMessage discoveredDeviceAdminMessage = new DiscoveredDeviceAdminMessage(discoveryEvent);
// dispatch to king
session.getChannelManager().publishToAllConnections(discoveredDeviceAdminMessage.toBlaubotMessage());
}
return this;
}
@Override
public void handleState(StateMachineSession session) {
this.session = session;
session.getServerConnectionManager().setMaster(false);
BlaubotAdapterHelper.stopAcceptors(session.getConnectionStateMachine().getConnectionAcceptors());
BlaubotAdapterHelper.startBeacons(session.getBeaconService());
// we deactivate discovery - so other devices can get our state but we are not actively scanning
BlaubotAdapterHelper.setDiscoveryActivated(session.getBeaconService(), false);
// // check if we are the prince
CensusMessage lastCensusMessage = session.getLastCensusMessage();
if (lastCensusMessage != null) {
// simulate adminMessage
onAdminMessage(lastCensusMessage);
}
}
@Override
public IBlaubotState onAdminMessage(AbstractAdminMessage adminMessage) {
if (adminMessage instanceof PronouncePrinceAdminMessage) {
PronouncePrinceAdminMessage ppam = (PronouncePrinceAdminMessage) adminMessage;
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "We got a PronouncePrinceAdminMessage. The new prince is " + ppam.getUniqueDeviceId());
}
String newPrinceUniqueId = ppam.getUniqueDeviceId();
boolean weArePrince = session.isOwnDevice(newPrinceUniqueId);
if (weArePrince) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "We are the new prince, sending ACK and changing state.");
}
// we are the new prince! -> send ACK and change state
final List<ConnectionMetaDataDTO> ownConnectionMetaDataList = session.getBeaconService().getCurrentBeaconMessage().getOwnConnectionMetaDataList();
final ACKPronouncePrinceAdminMessage ackMsg = new ACKPronouncePrinceAdminMessage(newPrinceUniqueId, ownConnectionMetaDataList);
session.getChannelManager().broadcastAdminMessage(ackMsg.toBlaubotMessage());
return new PrinceState(kingConnection);
} else {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "We are not prince. Remaining in PeasantState.");
}
}
} else if (adminMessage instanceof BowDownToNewKingAdminMessage) {
kingConnection.disconnect();
BowDownToNewKingAdminMessage bowDownToNewKingAdminMessage = (BowDownToNewKingAdminMessage) adminMessage;
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "We got a BowDownToNewKingAdminMessage. The new king is " + bowDownToNewKingAdminMessage.getNewKingsUniqueDeviceId());
Log.d(LOG_TAG, "Trying to connect to new king ...");
}
IBlaubotConnection conn = session.getConnectionManager().connectToBlaubotDevice(bowDownToNewKingAdminMessage.getNewKingsUniqueDeviceId(), MAX_RETRIES_TO_CONNECT_TO_PRINCE_OR_KING);
boolean result = conn != null;
if (result) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Connection to new king successful. Remaining in Peasant state.");
}
return new PeasantState(conn, ConnectionAccomplishmentType.BOWED_DOWN);
} else {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Connection to new king failed! Oh my, now we are an outlaw :-(. Changing to FreeState to find a new king.");
}
return new FreeState();
}
}
return this;
}
@Override
public String getKingUniqueId() {
return kingConnection.getRemoteDevice().getUniqueDeviceID();
}
@Override
public String toString() {
return "PeasantState";
}
@Override
public IBlaubotState onTimeoutEvent(AbstractTimeoutStateMachineEvent timeoutEvent) {
return this;
}
@Override
public IBlaubotConnection getKingConnection() {
return kingConnection;
}
}