package edu.umd.rhsmith.diads.meater.core.config.setup; import java.io.File; import java.util.LinkedList; import java.util.List; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.XMLConfiguration; import edu.umd.rhsmith.diads.meater.core.app.MEaterConfigurationException; import edu.umd.rhsmith.diads.meater.core.config.ConfigUnit; import edu.umd.rhsmith.diads.meater.core.config.MEaterConfig; import edu.umd.rhsmith.diads.meater.core.config.setup.ops.SetupConsoleOperation; import edu.umd.rhsmith.diads.meater.core.config.setup.ops.nav.ExitOperation; import edu.umd.rhsmith.diads.meater.core.config.setup.ops.nav.HelpOperation; import edu.umd.rhsmith.diads.meater.core.config.setup.ops.nav.LoadOperation; import edu.umd.rhsmith.diads.meater.core.config.setup.ops.nav.NavCloseOperation; import edu.umd.rhsmith.diads.meater.core.config.setup.ops.nav.SaveAsOperation; import edu.umd.rhsmith.diads.meater.core.config.setup.ops.nav.SaveOperation; import edu.umd.rhsmith.diads.meater.util.Util; import edu.umd.rhsmith.diads.meater.util.console.BooleanPrompter; import edu.umd.rhsmith.diads.meater.util.console.Console; import edu.umd.rhsmith.diads.meater.util.console.StringPrompter; public class MEaterSetupConsole implements Runnable { public static final String DEFAULT_MEATER_CONFIG_FILENAME = "meater.xml"; public static void main(String[] args) { // interpret args String configurationFilename; if (args.length == 0) { configurationFilename = DEFAULT_MEATER_CONFIG_FILENAME; } else { configurationFilename = args[1]; } // let's go new MEaterSetupConsole(new Console(System.in, System.out, "\t"), configurationFilename).run(); } /* * -------------------------------- * Actual setup routine * -------------------------------- */ // console used for setup private final Console console; // filename we're setting up private String mainConfigurationFilename; // main config we're setting up private final MEaterConfig mainConfiguration; // stack of config units for 'back' operation private final LinkedList<ConfigUnit> navStack; // external configs we're setting up private final LinkedList<ConfigUnit> externalNavStack; private final LinkedList<String> externalFilenameStack; // whether we've been asked to exit private boolean exit; public MEaterSetupConsole(Console console, String mainFilename) { this.console = console; this.mainConfigurationFilename = mainFilename; this.mainConfiguration = new MEaterConfig(); this.mainConfiguration.resetInternalConfiguration(); this.navStack = new LinkedList<ConfigUnit>(); this.externalNavStack = new LinkedList<ConfigUnit>(); this.externalFilenameStack = new LinkedList<String>(); } @Override public void run() { // hello this.printWelcome(); // check for a load file checkLoadSaveFile(); // do setup loop this.setupLoop(); // goodbye this.printGoodbye(); } public final MEaterConfig getMainConfiguration() { return mainConfiguration; } /* * -------------------------------- * Main setup loop * -------------------------------- */ private void setupLoop() { // not exiting this.exit = false; // start at root module this.selectUnit(this.mainConfiguration); // go while we have something to setup and are not exiting while (this.navStack.size() > 0 && !exit) { SetupConsoleOperation doOp = null; // divider this.console.divide(4); this.console.say(MSG_CURR_UNIT_FMT, this.getSelectedUnit() .getUiName(), this.getLoadSaveFilename()); // get the operations supported by the current module List<SetupConsoleOperation> ops = this.getSelectedUnit() .getSetupOperations(); // then prompt the user for input doOp = promptForOperation(ops, true); // if the user gave us an operation, do it if (doOp != null) { doOp.go(this); } } } /* * -------------------------------- * User interaction * -------------------------------- */ public Console getConsole() { return this.console; } public void printHelp() { // print help if this module is not null if (this.getSelectedUnit() != null) { // divider this.console.divide(4); this.console.say(MSG_CURR_UNIT_FMT, this.getSelectedUnit() .getUiName(), this.getLoadSaveFilename()); this.console.say("%s", this.getSelectedUnit().getUiDescription()); this.console.divide(2); // get the operations supported by the current module List<SetupConsoleOperation> ops = this.getSelectedUnit() .getSetupOperations(); // list them + the default operations this.console.say(MSG_OP_SELECT); printOperations(ops, true); } } public SetupConsoleOperation promptForOperation( List<SetupConsoleOperation> ops, boolean includeDefaults) { SetupConsoleOperation doOp = null; // ask user for input; do nothing if they gave none String command = this.console.prompt(StringPrompter.PROMPT, false); if (command.length() == 0) { return null; } // try to match against a name... if (ops != null && doOp == null) { for (SetupConsoleOperation op : ops) { if (command.equals(op.getShortName())) { doOp = op; } } } if (includeDefaults && doOp == null) { for (SetupConsoleOperation op : DEFAULT_SETUP_OPERATIONS) { if (command.equals(op.getShortName())) { doOp = op; } } } // ...ok, try for a number if (doOp == null) { try { int commandIdx = Integer.parseInt(command); if (commandIdx < ops.size()) { doOp = ops.get(commandIdx); } else if (includeDefaults) { doOp = DEFAULT_SETUP_OPERATIONS[commandIdx - ops.size()]; } } catch (NumberFormatException e) { // it's okay, just means it didn't match } catch (ArrayIndexOutOfBoundsException e) { // ditto } } // out of options. let the user know they gave us garbage. if (doOp == null) { this.console.say(MSG_OP_UNKNOWN_FMT, command, HelpOperation.OP_SHORTNAME); } // if we got nothing, doOp will remain null. otherwise we'll return // whatever matched. return doOp; } public void printOperations(List<SetupConsoleOperation> ops, boolean includeDefaults) { int idx = 0; if (ops != null) { for (SetupConsoleOperation op : ops) { this.printOperation(op, idx); ++idx; } } if (includeDefaults) { this.console.divide(0); for (SetupConsoleOperation op : DEFAULT_SETUP_OPERATIONS) { this.printOperation(op, idx); ++idx; } } } public void exitSetupLoop() { this.console.say(MSG_EXIT_CONFIRM); if (this.console.prompt(BooleanPrompter.PROMPT_YESNO, false)) { this.exit = true; } } /* * -------------------------------- * Load / save * -------------------------------- */ private ConfigUnit getLoadSaveUnit() { if (this.externalNavStack.isEmpty()) { return this.mainConfiguration; } return this.externalNavStack.getFirst(); } private String getLoadSaveFilename() { if (this.externalFilenameStack.isEmpty()) { return this.mainConfigurationFilename; } return this.externalFilenameStack.getFirst(); } private void setLoadSaveFilename(String filename) { if (this.externalNavStack.isEmpty()) { this.mainConfigurationFilename = filename; return; } this.externalFilenameStack.set(0, filename); } private void checkLoadSaveFile() { // see if we're working with an existing file boolean loadExisting = false; File configurationFile = new File(this.getLoadSaveFilename()); // if we are, ask the user if they want to load it if (configurationFile.exists()) { this.console.say(MSG_FILE_EXISTS_CHECK_FMT, this .getLoadSaveFilename()); loadExisting = this.console.prompt(BooleanPrompter.PROMPT_YESNO, false); } // if the user wants to load it, do so if (loadExisting) { try { this.loadFile(this.getLoadSaveFilename()); } catch (ConfigurationException | MEaterConfigurationException e) { this.console.error(MSG_ERR_COULDNT_LOAD_FMT, this .getLoadSaveFilename(), Util.traceMessage(e)); } } } public void loadFile(String filename) throws ConfigurationException, MEaterConfigurationException { XMLConfiguration xml = new XMLConfiguration(filename); this.getLoadSaveUnit().loadConfigurationFrom(xml); // record that we are now on this file this.setLoadSaveFilename(filename); } public void saveFile() throws ConfigurationException, MEaterConfigurationException { this.saveFileAs(this.getLoadSaveFilename()); } public void saveFileAs(String filename) throws ConfigurationException, MEaterConfigurationException { // ask the user if they really want to save over the file File configurationFile = new File(filename); if (configurationFile.exists()) { this.console.say("File \"%s\" already exists - overwrite?", filename); if (!this.console.prompt(BooleanPrompter.PROMPT_YESNO, false)) { return; } } XMLConfiguration xml = new XMLConfiguration(); this.getLoadSaveUnit().saveConfigurationTo(xml); xml.save(filename); // record that we are now on this file this.setLoadSaveFilename(filename); } /* * -------------------------------- * Navigation * -------------------------------- */ public ConfigUnit getSelectedUnit() { if (this.navStack.isEmpty()) { return null; } return this.navStack.getFirst(); } public void selectUnit(ConfigUnit unit) { this.navStack.addFirst(unit); this.printHelp(); } public void closeSelectedUnit() { ConfigUnit u = this.getSelectedUnit(); // can't close nothing if (u == null) { return; } // warn the user before closing a load/savable unit if (u == this.getLoadSaveUnit()) { this.console.warn(MSG_BACK_CONFIRM_CLOSE_LOADSAVE_FMT, this .getLoadSaveUnit().getUiName(), this.getLoadSaveFilename()); if (!this.console.prompt(BooleanPrompter.PROMPT_YESNO, false)) { return; } if (!this.externalNavStack.isEmpty()) { this.externalFilenameStack.removeFirst(); this.externalNavStack.removeFirst(); } } this.navStack.removeFirst(); } public boolean hasPreviousUnit() { return this.navStack.size() > 1; } public void selectExternalUnit(ConfigUnit unit, String filename) { this.externalNavStack.addFirst(unit); this.externalFilenameStack.addFirst(filename); this.checkLoadSaveFile(); this.selectUnit(unit); } public ConfigUnit getSelectedExternalUnit() { if (this.externalNavStack.isEmpty()) { return null; } return this.externalNavStack.getFirst(); } public String getSelectedExternalUnitFilename() { if (this.externalFilenameStack.isEmpty()) { return null; } return this.externalFilenameStack.getFirst(); } /* * -------------------------------- * Setup constants * -------------------------------- */ private static final SetupConsoleOperation[] DEFAULT_SETUP_OPERATIONS = { // help new HelpOperation(), // load/save new LoadOperation(), new SaveOperation(), new SaveAsOperation(), // navigation new NavCloseOperation(), new ExitOperation(), }; /* * -------------------------------- * Messages * -------------------------------- */ private static final String MSG_WELCOME = "MEater setup"; private static final String MSG_ONFILE_FMT = "(Working with configuration file: \"%s\")"; private static final String MSG_GOODBYE = "Exiting MEater setup"; private static final String MSG_FILE_EXISTS_CHECK_FMT = "Configuration file \"%s\" exists - load its contents?"; private static final String MSG_ERR_COULDNT_LOAD_FMT = "Couldn't load contents of configuration file \"%s\": \n%s"; private static final String MSG_OP_SELECT = "Select an operation: "; private static final String MSG_OP_NOSHORT_FMT = "%d) %s"; private static final String MSG_OP_SHORT_FMT = "%d [%s]) - %s"; private static final String MSG_OP_UNKNOWN_FMT = "%s: ??? (try \"%s\")"; private static final String MSG_CURR_UNIT_FMT = "Current configuration unit: %s (%s)"; private static final String MSG_EXIT_CONFIRM = "Really exit setup?"; private static final String MSG_BACK_CONFIRM_CLOSE_LOADSAVE_FMT = "Any unsaved changes to %s (%s) will be lost. Are you sure you want to close it?"; private void printWelcome() { this.console.divide(5); this.console.say(MSG_WELCOME); this.console.say(MSG_ONFILE_FMT, this.mainConfigurationFilename); this.console.divide(4); } private void printGoodbye() { this.console.divide(5); this.console.say(MSG_GOODBYE); } private void printOperation(SetupConsoleOperation op, int idx) { if (op.getShortName().length() > 0) { this.console.say(MSG_OP_SHORT_FMT, idx, op.getShortName(), op .getUiName()); } else { this.console.say(MSG_OP_NOSHORT_FMT, idx, op.getShortName(), op .getUiName()); } } }