diff options
Diffstat (limited to 'src/eu/equalparts/cardbase/cli/CardbaseCLI.java')
| -rw-r--r-- | src/eu/equalparts/cardbase/cli/CardbaseCLI.java | 493 | 
1 files changed, 493 insertions, 0 deletions
| diff --git a/src/eu/equalparts/cardbase/cli/CardbaseCLI.java b/src/eu/equalparts/cardbase/cli/CardbaseCLI.java new file mode 100644 index 0000000..7a2e581 --- /dev/null +++ b/src/eu/equalparts/cardbase/cli/CardbaseCLI.java @@ -0,0 +1,493 @@ +package eu.equalparts.cardbase.cli; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.Scanner; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; + +import eu.equalparts.cardbase.Cardbase; +import eu.equalparts.cardbase.data.Card; +import eu.equalparts.cardbase.data.FullCardSet; +import eu.equalparts.cardbase.data.CardSetInformation; +import eu.equalparts.cardbase.utils.MTGUniverse; + +/** + * This provides a lightweight CLI for interacting with cardbase files.  + *  + * @author Eduardo Pedroni + */ +public class CardbaseCLI { +	 +	/** +	 * Enum type to store actions. +	 *  +	 * @author Eduardo Pedroni +	 */ +	private enum Action { +		ADD, REMOVE; +		public Card card; +		public Integer count; +		/** +		 * Sets both fields at once. +		 *  +		 * @param card the card last modified. +		 * @param count the amount that was added or removed. +		 */ +		public void set(Card card, Integer count) { +			this.card = card; +			this.count = count; +		} +	} + +	/** +	 * Location of the help file. +	 */ +	private static final String HELP_FILE_PATH = "/help"; +	/** +	 * The last action performed by the user. +	 */ +	private Action lastAction = null; +	/** +	 * The currently selected set, from which new cards are added. +	 */ +	private FullCardSet selectedSet = null; +	/** +	 * The actual cardbase being interfaced with. +	 */ +	private Cardbase cardbase; +	/** +	 * Printed to the console when the user enters the help command. +	 */ +	private String help = "Not available, check project page on GitHub."; +	/** +	 * The cardbase file off which we are currently working, if any. +	 */ +	private File cardbaseFile = null; +	/** +	 * Save flag is raised when cards are added or removed and causes a prompt to be shown +	 * if the user tries to exit with unsaved changed. +	 */ +	private boolean savePrompt = false; +	/** +	 * Exit flag, program breaks out of the main loop when true. +	 */ +	private boolean exit = false; + +	/** +	 * Execute the interface. +	 *  +	 * @param args the first argument is the cardbase file. Further arguments are ignored. +	 */ +	public static void main(String... args) { +		new CardbaseCLI(args).startInterface(); +	} + +	/** +	 * Reads in an optional cardbase JSON and initialises other necessary components. +	 * This does not actually produce the CLI, for that use {@code startInterface()} +	 * on the constructed object. +	 *  +	 * @param args a list of arguments. Only the first argument is used, as a cardbase JSON. +	 */ +	private CardbaseCLI(String... args) { +		System.out.println("Welcome to Cardbase CLI!"); + +		// set debug flag if we are debugging +		if (Cardbase.DEBUG) System.out.println("Debug mode is on."); + +		// make the CardbaseManager +		if (args.length > 0) { +			File cardbaseFile = new File(args[0]); +			if (cardbaseFile.exists() && cardbaseFile.isFile() && cardbaseFile.canRead()) { +				System.out.println("Loading cardbase from \"" + args[0] + "\"."); +				try { +					cardbase = new Cardbase(cardbaseFile); +				} catch (JsonParseException e) { +					System.out.println("Error: poorly formatted cardbase, check the syntax and try again."); +					// although the problem could also be with the upstream CardSetList json. +					if (Cardbase.DEBUG) e.printStackTrace(); +					System.exit(1); +				} catch (JsonMappingException e) { +					System.out.println("Error: unexpected fields found in cardbase, it may be from an old version?"); +					if (Cardbase.DEBUG) e.printStackTrace(); +					System.exit(1); +				} catch (IOException e) { +					System.out.println("Error: something went wrong reading cardbase file, abort..."); +					if (Cardbase.DEBUG) e.printStackTrace(); +					System.exit(1); +				} +			} else { +				System.out.println(args[0] + " appears to be invalid."); +				System.exit(0); +			} +		} else { +			System.out.println("No cardbase file was provided, creating a clean cardbase."); +			cardbase = new Cardbase(); +		} + +		// load help information +		InputStream is = CardbaseCLI.class.getResourceAsStream(HELP_FILE_PATH); +		if (is != null) { +			help = new Scanner(is).useDelimiter("\\Z").next(); +		} else { +			System.out.println("Help file was not found, check the project page on GitHub for help instead."); +		} + +	} + +	/** +	 * Read stdin for user input, sanitise and interpret any commands entered. +	 */ +	private void startInterface() { +		BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in)); +		try { +			// the main loop +			while (!exit) { +				// print prompt +				System.out.print(selectedSet == null ? "> " : selectedSet.code + " > "); +				// condition input and interpret +				String[] raw = consoleReader.readLine().trim().split("[ \t]+"); +				String command = raw[0]; +				String[] args = Arrays.copyOfRange(raw, 1, raw.length); + +				if (command.equalsIgnoreCase("help")) { +					help(); +				} else if (command.equalsIgnoreCase("write") +						|| command.equalsIgnoreCase("save")) { +					write(args); +				} else if (command.equalsIgnoreCase("exit")) { +					exit(); +				} else if (command.equalsIgnoreCase("sets")) { +					sets(); +				} else if (command.equalsIgnoreCase("set")) { +					set(args); +				} else if (command.equalsIgnoreCase("glance")) { +					glance(); +				} else if (command.equalsIgnoreCase("peruse")) { +					peruse(args); +				} else if (command.equalsIgnoreCase("undo")) { +					undo(); +				} else if (command.equalsIgnoreCase("remove")  +						|| command.equalsIgnoreCase("rm")) { +					remove(args); +				} else { +					add(command, args); +				} +			} +		} catch (IOException e) { +			System.out.println("Error: something went wrong with stdin, exiting..."); +			if (Cardbase.DEBUG) e.printStackTrace(); +		} +	} + +	/** +	 * Print help to console. +	 */ +	public void help() { +		System.out.println(help); +	} + +	/** +	 * Write current cardbase to file. +	 * +	 * @param args optionally the file to which to write. +	 */ +	public void write(String[] args) { +		File outputFile; +		// user-provided file overrides everything else +		if (args.length > 0) { +			outputFile = new File(sanitiseFileName(args[0])); +		} else { +			outputFile = cardbaseFile; +		} + +		if (outputFile != null) { +			if (outputFile.exists() && (!outputFile.isFile() || !outputFile.canWrite())) { +				System.out.println("Could not write to \"" + outputFile.getAbsolutePath() + "\"."); +			} else { +				// handle these exceptions locally - they don't necessarily mean the program should exit +				try { +					cardbase.writeCardbase(outputFile); +					// we are now working off outputFile, which may or may not be the same as cardbaseFile at this point +					cardbaseFile = outputFile; +					System.out.println("Cardbase was saved to \"" + outputFile.getAbsolutePath() + "\". " +							+ "Subsequent writes will be done to this same file unless otherwise requested."); +					savePrompt = false; +				} catch (JsonGenerationException | JsonMappingException e) { +					System.out.println("Error: something terrible happened to the internal cardbase data structure. Oops."); +					if (Cardbase.DEBUG) e.printStackTrace(); +				} catch (IOException e) { +					System.out.println("Error: lost contact with the output file, try again?"); +					if (Cardbase.DEBUG) e.printStackTrace(); +				} +			} +		} else { +			System.out.println("Please provide a file name."); +		} +	} + +	/** +	 * Exit procedure. +	 */ +	public void exit() { +		if (savePrompt) { +			System.out.println("Don't forget to save. If you really wish to quit without saving, type \"exit\" again."); +			savePrompt = false; +		} else { +			exit = true; +		} +	} + +	/** +	 * Print a list of valid set codes. +	 */ +	public void sets() { +		for (CardSetInformation set : MTGUniverse.getCardSetList()) { +			// CardSet has an overridden toString() +			System.out.println(set); +		} +	} + +	/** +	 * Select a set. +	 *  +	 * @param args the code of the chosen set. +	 */ +	public void set(String[] args) { +		if (args.length > 0) { +			try { +				selectedSet = MTGUniverse.getFullCardSet(args[0]); +				// if the set code is invalid, null is returned +				if (selectedSet != null) { +					System.out.println("Selected set: " + selectedSet.name + "."); +					// undoing is not allowed if the set is changed - it would get tricky +					lastAction = null; +				} else { +					System.out.println("\"" + args[0] + "\" does not correspond to any set (use \"sets\" to see all valid set codes)."); +				} +			} catch (JsonParseException e) { +				System.out.println("Error: JSON fetched from upstream was not formatted properly."); +				if (Cardbase.DEBUG) e.printStackTrace(); +			} catch (JsonMappingException e) { +				System.out.println("Error: JSON fetched from upstream does not match the data structure used internally."); +				if (Cardbase.DEBUG) e.printStackTrace(); +			} catch (IOException e) { +				System.out.println("Error: JSON could not be fetched from upstream."); +				if (Cardbase.DEBUG) e.printStackTrace(); +			} +		} else { +			System.out.println("Please enter a set code (use \"sets\" to see all valid set codes)."); +		} +	} + +	/** +	 * Print a brief list of the whole cardbase. +	 */ +	public void glance() { +		int total = 0; +		for (Card card : cardbase.getCards()) { +			printGlance(card); +			total += card.count; +		} +		System.out.println("Total: " + total); +	} + +	/** +	 * Print detailed information of a single card or the whole cardbase. +	 * +	 * @param args optionally a card within the set (by number) to peruse. +	 */ +	public void peruse(String[] args) { +		// if a card is specified, peruse only that +		if (args.length > 0) { +			if (selectedSet != null) { +				Card card = cardbase.getCard(selectedSet.code, args[0]); +				if (card != null) { +					printPerusal(card); +				} else { +					System.out.println("Card not in cardbase."); +				} +			} else { +				System.out.println("Please select a set before perusing a specific card."); +			} +		} else { +			// peruse all cards in cardbase +			int total = 0; +			for (Card card : cardbase.getCards()) { +				printPerusal(card); +				total += card.count; +			} +			System.out.println("Total: " + total); +		} +	} + +	/** +	 * Undo previous action. +	 */ +	public void undo() { +		if (lastAction != null) { +			if (lastAction == Action.ADD) { +				removeCard(lastAction.card, lastAction.count); +			} else if (lastAction ==  Action.REMOVE) { +				addCard(lastAction.card, lastAction.count); +			} +			// can only undo once +			lastAction = null; +		} else { +			System.out.println("Nothing to undo."); +		} +	} + +	/** +	 * Remove one or more of a card. +	 * +	 * @param args the set number of the card to remove and optionally the count to be removed. +	 */ +	public void remove(String[] args) { +		if (selectedSet != null) { +			if (args.length > 0) { +				Card cardToRemove = selectedSet.getCardByNumber(args[0]); +				if (cardToRemove != null) { +					Integer count = 1; +					if (args.length > 1 && args[1].matches("[0-9]+")) { +						count = Integer.valueOf(args[1]); +						if (count <= 0) { +							System.out.println("Can't remove " + count + " cards."); +							return; +						} +					} +					removeCard(cardToRemove, count); +				} else { +					System.out.println(args[0] + " does not correspond to a card in " + selectedSet.name + "."); +				} +			} else { +				System.out.println("Please specify a card number to remove."); +			} +		} else { +			System.out.println("Select a set before removing cards."); +		} +	} + +	/** +	 * Add one or more of a card. +	 * +	 * @param number the number of the card to add. +	 * @param args optionally the count to add. +	 */ +	public void add(String number, String[] args) { +		if (selectedSet != null) { +			// a blank line after adding a card repeats the addition unlimitedly +			if (number.isEmpty()) { +				if (lastAction == Action.ADD) +					addCard(lastAction.card, lastAction.count); +			} else { +				Card cardToAdd = selectedSet.getCardByNumber(number); +				if (cardToAdd != null) { +					Integer count = 1; +					if (args.length > 0 && args[0].matches("[0-9]+")) { +						count = Integer.valueOf(args[0]); +						if (count <= 0) { +							System.out.println("Can't add " + count + " cards."); +							return; +						} +					} +					addCard(cardToAdd, count); +				} else { +					System.out.println(number + " does not correspond to a card in " + selectedSet.name + "."); +				} +			} +		} else { +			System.out.println("Select a set before adding cards."); +		} +	} +	 +	/** +	 * Add the specified count of the specified card +	 * to the cardbase. +	 *  +	 * @param card the card to add. +	 * @param count the number of times to add it. +	 */ +	private void addCard(Card card, Integer count) { +		cardbase.addCard(card, count); +		System.out.println("Added " + count + "x " + card.name + "."); +		savePrompt = true; +		lastAction = Action.ADD; +		lastAction.set(card, count); +	} + +	/** +	 * Remove the specified count of the specified card +	 * from the cardbase. +	 *  +	 * @param card the card to remove. +	 * @param count the number of times to remove it. +	 */ +	private void removeCard(Card card, Integer count) { +		Integer removed = cardbase.removeCard(card, count);  +		if (removed > 0) { +			System.out.println("Removed " + removed + "x " + card.name + "."); +			savePrompt = true; +			lastAction = Action.REMOVE; +			lastAction.set(card, removed); +		} else { +			System.out.println(card.name + " is not in the cardbase."); +		} + +	} + +	/** +	 * Return a {@code String} that is guaranteed to be a legal file name. +	 *  +	 * @param name the file name candidate to sanitise. +	 * @return the sanitised name. +	 */ +	private String sanitiseFileName(String name) { +		// POSIX-compliant valid filename characters +		name = name.replaceAll("[^-_.A-Za-z0-9]", ""); +		// extension is not indispensable, but good practice +		if (!name.endsWith(".cb")) { +			name = name.concat(".cb"); +		} +		return name; +	} + +	/** +	 * Prints a glance of the specified card. A glance contains simply +	 * the card count, name, set code and set number.  +	 *  +	 * @param card the card to glance. +	 */ +	private void printGlance(Card card) { +		System.out.println(String.format("%1$-4d %2$s (%3$s, %4$s)", card.count, card.name, card.setCode, card.number)); +	} + +	/** +	 * Prints a perusal of the specified card. A perusal contains more +	 * information than a glance, but not every single field the card has +	 * as that would be too verbose while adding little value. +	 *  +	 * @param card the card to peruse. +	 */ +	private void printPerusal(Card card) { +		printGlance(card); +		if (card.type != null) System.out.println("\t" + card.type); +		if (card.manaCost != null) System.out.println("\tCost: " + card.manaCost); +		if (card.power != null && card.toughness != null) System.out.println("\t" + card.power + "/" + card.toughness); +		if (card.loyalty != null) System.out.println("\tLoyalty: " + card.loyalty); + +		if (card.text != null) System.out.println("\t" + card.text.replaceAll("\n", "\n\t")); +		if (card.flavor != null) System.out.println("\t" + card.flavor.replaceAll("\n", "\n\t")); + +		if (card.rarity != null) System.out.println("\t" + card.rarity); +		if (card.multiverseid != null) System.out.println("\tMID: " + card.multiverseid); +		if (card.artist != null) System.out.println("\tIllus. " + card.artist); +	} +} | 
