meerkat-java/voting-booth/src/main/java/meerkat/voting/controller/VotingBoothImpl.java

350 lines
12 KiB
Java

package meerkat.voting.controller;
import meerkat.protobuf.Voting.*;
import meerkat.voting.controller.callbacks.*;
import meerkat.voting.controller.commands.*;
import meerkat.voting.controller.selector.QuestionSelector;
import meerkat.voting.encryptor.VBCryptoManager;
import meerkat.voting.encryptor.VBCryptoManager.EncryptionAndSecrets;
import meerkat.voting.output.BallotOutputDevice;
import meerkat.voting.storage.StorageManager;
import meerkat.voting.ui.VotingBoothUI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.security.SignatureException;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Created by hai on 28/03/16.
*/
public class VotingBoothImpl implements VotingBoothController {
private BallotOutputDevice outputDevice;
private VBCryptoManager crypto;
private VotingBoothUI ui;
private StorageManager storageManager;
private List<BallotQuestion> questionsForChoosingChannel;
private List<BallotQuestion> questions;
private QuestionSelector questionSelector;
private LinkedBlockingQueue<ControllerCommand> queue;
private Logger logger;
private ControllerState state;
private volatile boolean shutDownHasBeenCalled;
protected final int MAX_REQUEST_IDENTIFIER = 100000;
private static int requestCounter = 0;
public VotingBoothImpl () {
logger = LoggerFactory.getLogger(VotingBoothImpl.class);
logger.info("A VotingBoothImpl is constructed");
shutDownHasBeenCalled = false;
queue = new LinkedBlockingQueue<>();
state = new ControllerState();
}
@Override
public void init(BallotOutputDevice outputDevice,
VBCryptoManager vbCrypto,
VotingBoothUI vbUI,
StorageManager vbStorageManager) {
logger.info("init is called");
this.outputDevice = outputDevice;
this.crypto = vbCrypto;
this.ui = vbUI;
this.storageManager = vbStorageManager;
}
@Override
public void setBallotChannelChoiceQuestions(List<BallotQuestion> questions) {
logger.info("setting questions");
this.questionsForChoosingChannel = questions;
}
@Override
public void setChannelQuestionSelector(QuestionSelector selector) {
this.questionSelector = selector;
}
@Override
public void setBallotRaceQuestions(List<BallotQuestion> questions) {
logger.info("setting questions");
this.questions = questions;
}
@Override
public void run() {
logger.info("run command has been called");
runVotingFlow();
doShutDown();
}
private void runVotingFlow () {
logger.info("entered the voting flow");
queue.add(new RestartVotingCommand(generateRequestIdentifier(), state.currentBallotSerialNumber));
while (! wasShutDownCalled()) {
try {
ControllerCommand task = queue.take();
handleSingleTask (task);
}
catch (InterruptedException e) {
logger.warn ("Interrupted while reading from task queue " + e);
}
}
}
@Override
public void callShutDown() {
logger.info("callShutDown command has been called");
shutDownHasBeenCalled = true;
queue.clear();
ui.callShutDown();
outputDevice.callShutDown();
}
private void handleSingleTask (ControllerCommand task) {
if (task.getBallotSerialNumber() != state.currentBallotSerialNumber && !(task instanceof RestartVotingCommand)) {
// probably an old command relating to some old ballot serial number. Simply log it and ignore it.
String errorMessage = "handleSingleTask: received a task too old. " +
task.getBallotSerialNumber() + " " + state.currentBallotSerialNumber;
logger.debug(errorMessage);
return;
}
if (task instanceof RestartVotingCommand) {
doRestartVoting ();
}
else if (task instanceof ChannelChoiceCommand) {
doChooseChannel();
}
else if (task instanceof ChannelDeterminedCommand) {
doSetChannelAndAskQuestions ((ChannelDeterminedCommand)task);
}
else if (task instanceof ChooseFinalizeOptionCommand) {
doChooseFinalizeOption();
}
else if (task instanceof CastCommand) {
doFinalize(false);
}
else if (task instanceof AuditCommand) {
doFinalize(true);
}
else if (task instanceof EncryptAndCommitBallotCommand) {
doCommit ((EncryptAndCommitBallotCommand)task);
}
else if (task instanceof ReportErrorCommand) {
doReportErrorAndForceRestart((ReportErrorCommand)task);
}
else {
logger.error("handleSingleTask: unknown type of ControllerCommand received: " + task.getClass().getName());
doReportErrorAndForceRestart(SystemMessages.getSomethingWrongMessage());
}
}
private boolean wasShutDownCalled () {
return shutDownHasBeenCalled;
}
private void doShutDown () {
logger.info("running callShutDown");
state.clearPlaintext();
state.clearCiphertext();
state.stateIdentifier = VBState.SHUT_DOWN;
//TODO: add commands to actually shut down the machine
}
private void doRestartVoting () {
queue.clear();
state.clearPlaintext();
state.clearCiphertext();
state.stateIdentifier = VBState.NEW_VOTER;
state.currentBallotSerialNumber += 1;
ui.startNewVoterSession(new NewVoterCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
}
private void doReportErrorAndForceRestart(ReportErrorCommand task) {
doReportErrorAndForceRestart(task.getErrorMessage());
}
private void doReportErrorAndForceRestart(UIElement errorMessage) {
queue.clear();
state.clearPlaintext();
state.clearCiphertext();
state.stateIdentifier = VBState.FATAL_ERROR_FORCE_NEW_VOTER;
state.currentBallotSerialNumber += 1;
ui.showErrorMessageWithButtons(errorMessage,
new UIElement[]{SystemMessages.getRestartVotingButton()},
new ErrorMessageRestartCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
}
private void doChooseChannel () {
if (state.stateIdentifier == VBState.NEW_VOTER) {
logger.debug("doing chooseChannel");
state.stateIdentifier = VBState.CHOOSE_CHANNEL;
ui.chooseChannel(this.questionsForChoosingChannel,
new ChannelChoiceCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
}
else {
logger.debug("doChooseChannel: current state is " + state.stateIdentifier);
// ignore this request
}
}
private void doSetChannelAndAskQuestions (ChannelDeterminedCommand task) {
if (state.stateIdentifier == VBState.CHOOSE_CHANNEL) {
logger.debug("doing set channel and ask questions");
state.stateIdentifier = VBState.ANSWER_QUESTIONS;
List<BallotAnswer> channelChoiceAnswers = task.channelChoiceAnswers;
state.channelIdentifier = questionSelector.getChannelIdentifier(channelChoiceAnswers);
state.channelSpecificQuestions = questionSelector.selectQuestionsForVoter(state.channelIdentifier);
ui.askVoterQuestions(state.channelSpecificQuestions,
new VotingCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
}
else {
logger.debug("doSetChannelAndAskQuestions: current state is " + state.stateIdentifier);
// ignore this request
}
}
private void doChooseFinalizeOption() {
if (state.stateIdentifier == VBState.COMMITTING_TO_BALLOT) {
logger.debug("doChooseFinalizeOption");
state.stateIdentifier = VBState.CAST_OR_AUDIT;
ui.castOrAudit(new CastOrAuditCallback(generateRequestIdentifier(),
state.currentBallotSerialNumber,
this.queue));
}
else {
logger.debug("doChooseFinalizeOption: current state is " + state.stateIdentifier);
// ignore this request
}
}
private void doCommit (EncryptAndCommitBallotCommand task) {
if (state.stateIdentifier == VBState.ANSWER_QUESTIONS) {
logger.debug("doing commit");
state.stateIdentifier = VBState.COMMITTING_TO_BALLOT;
setBallotData (task);
ui.notifyVoterToWaitForFinish(SystemMessages.getWaitForCommitMessage(),
new WaitForFinishCallback(generateRequestIdentifier(),
state.currentBallotSerialNumber,
this.queue));
outputDevice.commitToBallot(state.plaintextBallot,
state.signedEncryptedBallot,
new OutputDeviceCommitCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
}
else {
logger.debug("doCommit: current state is " + state.stateIdentifier);
// ignore this request
}
}
private void setBallotData (EncryptAndCommitBallotCommand task) {
state.plaintextBallot = PlaintextBallot.newBuilder()
.setSerialNumber(task.getBallotSerialNumber())
.addAllAnswers(task.getVotingAnswers())
.build();
EncryptionAndSecrets encryptionAndSecrets = null;
try {
encryptionAndSecrets = crypto.encrypt(state.plaintextBallot);
}
catch (SignatureException | IOException e) {
// TODO: handle exception
}
state.signedEncryptedBallot = encryptionAndSecrets.getSignedEncryptedBallot();
state.secrets = encryptionAndSecrets.getSecrets();
}
private void doFinalize (boolean auditRequested) {
if (state.stateIdentifier == VBState.CAST_OR_AUDIT) {
logger.debug("finalizing");
state.stateIdentifier = VBState.FINALIZING;
if (auditRequested) {
ui.notifyVoterToWaitForFinish(SystemMessages.getWaitForAuditMessage(),
new WaitForFinishCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
outputDevice.audit(state.secrets,
new OutputDeviceFinalizeCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
}
else {
ui.notifyVoterToWaitForFinish(SystemMessages.getWaitForCastMessage(),
new WaitForFinishCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
outputDevice.castBallot(
new OutputDeviceFinalizeCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
}
}
else {
logger.debug("doFinalize: current state is " + state.stateIdentifier);
// ignore this request
}
}
private enum VBState {
NEW_VOTER,
CHOOSE_CHANNEL,
ANSWER_QUESTIONS,
COMMITTING_TO_BALLOT,
CAST_OR_AUDIT,
FINALIZING,
FATAL_ERROR_FORCE_NEW_VOTER,
SHUT_DOWN
}
private class ControllerState {
public VBState stateIdentifier;
public byte[] channelIdentifier;
public List<BallotQuestion> channelSpecificQuestions;
public PlaintextBallot plaintextBallot;
public SignedEncryptedBallot signedEncryptedBallot;
public BallotSecrets secrets;
public int lastRequestIdentifier;
public long currentBallotSerialNumber;
public ControllerState () {
plaintextBallot = null;
signedEncryptedBallot = null;
secrets = null;
lastRequestIdentifier = -1;
channelIdentifier = null;
channelSpecificQuestions = null;
currentBallotSerialNumber = 0;
}
public void clearPlaintext () {
plaintextBallot = null;
}
public void clearCiphertext () {
signedEncryptedBallot = null;
secrets = null;
}
}
private int generateRequestIdentifier() {
++requestCounter;
if (requestCounter >= MAX_REQUEST_IDENTIFIER) {
requestCounter = 1;
}
return requestCounter;
}
}