From 8059c378fa11b835beb813e73ea6c57ddeca8553 Mon Sep 17 00:00:00 2001 From: Eduardo Pedroni Date: Sat, 6 Jun 2015 16:30:51 +0200 Subject: Refactored CLI, exceptions are handled properly now. Started looking into UI testing for the CLI. --- src/eu/equalparts/cardbase/data/CardSet.java | 32 +--- .../equalparts/cardbase/data/CardbaseManager.java | 50 +++--- src/eu/equalparts/cardbase/data/FullCardSet.java | 25 +++ src/eu/equalparts/cardbase/data/MetaCardSet.java | 14 -- src/eu/equalparts/cardbase/query/IO.java | 110 ++++++++----- .../cardbase/standalone/CardbaseCLI.java | 182 ++++++++++++--------- 6 files changed, 238 insertions(+), 175 deletions(-) create mode 100644 src/eu/equalparts/cardbase/data/FullCardSet.java delete mode 100644 src/eu/equalparts/cardbase/data/MetaCardSet.java (limited to 'src/eu/equalparts/cardbase') diff --git a/src/eu/equalparts/cardbase/data/CardSet.java b/src/eu/equalparts/cardbase/data/CardSet.java index 2c8f950..959e0a3 100644 --- a/src/eu/equalparts/cardbase/data/CardSet.java +++ b/src/eu/equalparts/cardbase/data/CardSet.java @@ -1,28 +1,14 @@ package eu.equalparts.cardbase.data; -import java.util.ArrayList; - public class CardSet { - public String name; - public String code; - public String releaseDate; - public String border; - public String type; - public String block; - public String gathererCode; - public ArrayList cards; - /** - * Searches for a card by number (the one shown on the card itself). - * - * @param number the number of the card to search. - * @return the card, or null if no card is found with that number. - */ - public Card getCardByNumber(String number) { - for (Card card : cards) { - if (card.number.equals(number)) - return card; - } - return null; + public String name = ""; + public String code = ""; + public String releaseDate = ""; + + @Override + public String toString() { + return String.format("%1$-12s : %2$s", code, name, releaseDate); } -} \ No newline at end of file + +} diff --git a/src/eu/equalparts/cardbase/data/CardbaseManager.java b/src/eu/equalparts/cardbase/data/CardbaseManager.java index cf18390..e6a4f97 100644 --- a/src/eu/equalparts/cardbase/data/CardbaseManager.java +++ b/src/eu/equalparts/cardbase/data/CardbaseManager.java @@ -11,33 +11,37 @@ import eu.equalparts.cardbase.query.IO; public class CardbaseManager { - private ArrayList metaSets; - public Cardbase cardBase; + private ArrayList cardSets; + public Cardbase cardbase; /** - * Parse a cardbase file and create an associated CardBase object. - * @throws IOException - * @throws JsonMappingException - * @throws JsonParseException + * Parse a cardbase file and create an associated Cardbase object. + * + * @param cardbaseFile + * + * @throws JsonParseException if underlying input contains invalid content of type JsonParser supports (JSON for default case). + * @throws JsonMappingException if the input JSON structure does not match structure expected for result type (or has other mismatch issues). + * @throws IOException if a low-level I/O problem (unexpected end-of-input, network error) occurs. */ - public CardbaseManager(File cardBaseFile) throws JsonParseException, JsonMappingException, IOException { - metaSets = IO.getAllMetaSets(); - cardBase = IO.readCardBase(cardBaseFile); + public CardbaseManager(File cardbaseFile) throws JsonParseException, JsonMappingException, IOException { + cardSets = IO.getCardSetList(); + cardbase = IO.readCardbase(cardbaseFile); } /** - * Create an empty CardBase. - * @throws IOException - * @throws JsonMappingException - * @throws JsonParseException + * Create an empty Cardbase. + * + * @throws JsonParseException if underlying input contains invalid content of type JsonParser supports (JSON for default case). + * @throws JsonMappingException if the input JSON structure does not match structure expected for result type (or has other mismatch issues). + * @throws IOException if a low-level I/O problem (unexpected end-of-input, network error) occurs. */ public CardbaseManager() throws JsonParseException, JsonMappingException, IOException { - metaSets = IO.getAllMetaSets(); - cardBase = new Cardbase(); + cardSets = IO.getCardSetList(); + cardbase = new Cardbase(); } - public ArrayList getAllMetaSets() { - return metaSets; + public ArrayList getCardSetList() { + return cardSets; } /** @@ -50,12 +54,12 @@ public class CardbaseManager { * @param count */ public void addCard(Card newCard, Integer count) { - Card card = cardBase.getCardByNumber(newCard.setCode, newCard.number); + Card card = cardbase.getCardByNumber(newCard.setCode, newCard.number); if (card != null) { card.count += count; } else { newCard.count = count; - cardBase.cards.add(newCard); + cardbase.cards.add(newCard); } } @@ -70,10 +74,10 @@ public class CardbaseManager { * @param count */ public void removeCard(Card remove, Integer count) { - Card card = cardBase.getCardByNumber(remove.setCode, remove.number); + Card card = cardbase.getCardByNumber(remove.setCode, remove.number); if (card != null) { if (card.count <= count) { - cardBase.cards.remove(card); + cardbase.cards.remove(card); } else { card.count -= count; } @@ -84,7 +88,7 @@ public class CardbaseManager { * @return an iterator to the cards in the cardbase. */ public Iterator cardIterator() { - return cardBase.cards.iterator(); + return cardbase.cards.iterator(); } /** @@ -96,6 +100,6 @@ public class CardbaseManager { * @return */ public Card getCard(String code, String number) { - return cardBase.getCardByNumber(code, number); + return cardbase.getCardByNumber(code, number); } } diff --git a/src/eu/equalparts/cardbase/data/FullCardSet.java b/src/eu/equalparts/cardbase/data/FullCardSet.java new file mode 100644 index 0000000..0b5c099 --- /dev/null +++ b/src/eu/equalparts/cardbase/data/FullCardSet.java @@ -0,0 +1,25 @@ +package eu.equalparts.cardbase.data; + +import java.util.ArrayList; + +public class FullCardSet extends CardSet { + public String border; + public String type; + public String block; + public String gathererCode; + public ArrayList cards; + + /** + * Searches for a card by number (the one shown on the card itself). + * + * @param number the number of the card to search. + * @return the card, or null if no card is found with that number. + */ + public Card getCardByNumber(String number) { + for (Card card : cards) { + if (card.number.equals(number)) + return card; + } + return null; + } +} \ No newline at end of file diff --git a/src/eu/equalparts/cardbase/data/MetaCardSet.java b/src/eu/equalparts/cardbase/data/MetaCardSet.java deleted file mode 100644 index 3b7d4fd..0000000 --- a/src/eu/equalparts/cardbase/data/MetaCardSet.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.equalparts.cardbase.data; - -public class MetaCardSet { - - public String name = ""; - public String code = ""; - public String releaseDate = ""; - - @Override - public String toString() { - return String.format("%1$-12s : %2$s", code, name, releaseDate); - } - -} diff --git a/src/eu/equalparts/cardbase/query/IO.java b/src/eu/equalparts/cardbase/query/IO.java index 2026dc6..6fe9390 100644 --- a/src/eu/equalparts/cardbase/query/IO.java +++ b/src/eu/equalparts/cardbase/query/IO.java @@ -2,7 +2,6 @@ package eu.equalparts.cardbase.query; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; @@ -13,67 +12,102 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; -import eu.equalparts.cardbase.data.CardSet; +import eu.equalparts.cardbase.data.FullCardSet; import eu.equalparts.cardbase.data.Cardbase; -import eu.equalparts.cardbase.data.MetaCardSet; +import eu.equalparts.cardbase.data.CardSet; +/** + * Class responsible for all I/O operations, such as fetching content from remote servers, reading from + * and writing to files. + *
+ * All relevant methods here are static, this class should not be instantiated. + * + * @author Eduardo Pedroni + */ public class IO { - public static final String BASE_URL = "http://mtgjson.com/json/"; - public static final String SETS_URL = BASE_URL + "SetList.json"; - + /** + * The base URL from where the information is fetched. + */ + private static final String BASE_URL = "http://mtgjson.com/json/"; + /** + * The URL where the complete list of sets is fetched. + */ + private static final String SETS_URL = BASE_URL + "SetList.json"; + /** + * The Jackson ObjectMapper which parses fetched JSON files. + */ private static final ObjectMapper mapper = createMapper(); + /** + * Private constructor, this class is not to be instantiated. + */ + private IO() {} + + /** + * Instantiate and configure Jackson mapper statically. + * + * @return the {@code ObjectMapper}, ready to use. + */ private static ObjectMapper createMapper() { - ObjectMapper om = new ObjectMapper(); - om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - return om; + ObjectMapper objectMapper = new ObjectMapper(); + // TODO decide what to do about this + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + return objectMapper; } - + /** - * @param code - * @return the actual cardset (containing cards). - * @throws IOException - * @throws MalformedURLException - * @throws JsonMappingException - * @throws JsonParseException + * Fetches a complete set by code, where the code is a short string determined by WotC. + * The full list of valid codes can be acquired with {@code IO.getCardSetList()}. + * + * @param setCode the code of the set to be fetched. + * @return the complete specified set in a {@code FullCardSet} object. + * + * @throws JsonParseException if underlying input contains invalid content of type JsonParser supports (JSON for default case). + * @throws JsonMappingException if the input JSON structure does not match structure expected for result type (or has other mismatch issues). + * @throws IOException if a low-level I/O problem (unexpected end-of-input, network error) occurs. */ - public static CardSet getCardSet(String code) throws JsonParseException, JsonMappingException, MalformedURLException, IOException { - return mapper.readValue(new URL(BASE_URL + code + ".json"), CardSet.class); + public static FullCardSet getCardSet(String setCode) throws JsonParseException, JsonMappingException, IOException { + return mapper.readValue(new URL(BASE_URL + setCode + ".json"), FullCardSet.class); } /** - * @return a list of metadata for every available set. - * @throws JsonParseException - * @throws JsonMappingException - * @throws IOException + * @return a list of all card sets in the form of {@code CardSet} objects. + * + * @throws JsonParseException if underlying input contains invalid content of type JsonParser supports (JSON for default case). + * @throws JsonMappingException if the input JSON structure does not match structure expected for result type (or has other mismatch issues). + * @throws IOException if a low-level I/O problem (unexpected end-of-input, network error) occurs. */ - public static ArrayList getAllMetaSets() throws JsonParseException, JsonMappingException, IOException { - return mapper.readValue(new URL(SETS_URL), new TypeReference>() {}); + public static ArrayList getCardSetList() throws JsonParseException, JsonMappingException, IOException { + return mapper.readValue(new URL(SETS_URL), new TypeReference>() {}); } /** - * @param file - * @return a CardBase object equivalent to the given file. - * @throws JsonParseException - * @throws JsonMappingException - * @throws IOException + * Attemps to the read the specified file into a {@code Cardbase.} Exceptions are thrown as outlined below. + * + * @param file the file to read. + * @return a {@code Cardbase} object equivalent to the given file. + * + * @throws JsonParseException if underlying input contains invalid content of type JsonParser supports (JSON for default case). + * @throws JsonMappingException if the input JSON structure does not match structure expected for result type (or has other mismatch issues). + * @throws IOException if a low-level I/O problem (unexpected end-of-input, network error) occurs. */ - public static Cardbase readCardBase(File file) throws JsonParseException, JsonMappingException, IOException { + public static Cardbase readCardbase(File file) throws JsonParseException, JsonMappingException, IOException { return mapper.readValue(file, Cardbase.class); } /** - * Writes the provided CardBase to the provided file in JSON format. + * Writes the provided {@code Cardbase} to the provided file in JSON format. + * + * @param file the file to which to write the {@code Cardbase}. + * @param cardbase the {@code Cardbase} to write out. * - * @param file - * @param cardBase - * @throws JsonGenerationException - * @throws JsonMappingException - * @throws IOException + * @throws JsonGenerationException if the data structure given does not generate valid JSON. + * @throws JsonMappingException if the data structure given does not generate valid JSON as well? + * @throws IOException if a low-level I/O problem (unexpected end-of-input, network error) occurs. */ - public static void writeCardBase(File file, Cardbase cardBase) throws JsonGenerationException, JsonMappingException, IOException { - mapper.writeValue(file, cardBase); + public static void writeCardbase(File file, Cardbase cardbase) throws JsonGenerationException, JsonMappingException, IOException { + mapper.writeValue(file, cardbase); } } diff --git a/src/eu/equalparts/cardbase/standalone/CardbaseCLI.java b/src/eu/equalparts/cardbase/standalone/CardbaseCLI.java index 7abccef..4071fad 100644 --- a/src/eu/equalparts/cardbase/standalone/CardbaseCLI.java +++ b/src/eu/equalparts/cardbase/standalone/CardbaseCLI.java @@ -5,18 +5,18 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.net.MalformedURLException; import java.util.HashMap; import java.util.Iterator; 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.data.Card; import eu.equalparts.cardbase.data.CardbaseManager; +import eu.equalparts.cardbase.data.FullCardSet; import eu.equalparts.cardbase.data.CardSet; -import eu.equalparts.cardbase.data.MetaCardSet; import eu.equalparts.cardbase.query.IO; /** @@ -52,36 +52,36 @@ public class CardbaseCLI { /** * The last action performed by the user. */ - private static Action lastAction = null; + private Action lastAction = null; /** * The currently selected set, from which new cards are added. */ - private static CardSet selectedSet = null; + private FullCardSet selectedSet = null; /** * A cache of CardSets to avoid querying the server many times for the same information. */ - private static HashMap setCache = new HashMap(); + private HashMap setCache = new HashMap(); /** * The manager object which provides a façade to the cardbase data structure. */ - private static CardbaseManager cbm; + private CardbaseManager cbm; /** * Exit flag, program breaks out of the main loop when true. */ - private static boolean exit = false; + private boolean exit = false; /** * Printed to the console when the user enter the help command. */ - private static String help = "Not available, check project page."; + private String help = "Not available, check project page."; /** * The cardbase file off which we are currently working, if any. */ - private static File cardbaseFile = null; + 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 static boolean savePrompt = false; + private boolean savePrompt = false; /** * Execute the interface. @@ -89,65 +89,69 @@ public class CardbaseCLI { * @param args the first argument is the cardbase file. Further arguments are ignored. */ public static void main(String... args) { - - System.out.println("Welcome to Cardbase CLI!"); - try { - // make the CardbaseManager - if (args.length > 0) { - cardbaseFile = new File(args[0]); - if (cardbaseFile.exists() && cardbaseFile.isFile() && cardbaseFile.canRead()) { - System.out.println("Loading cardbase from \"" + args[0] + "\"."); - cbm = new CardbaseManager(cardbaseFile); - } 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."); - cbm = new CardbaseManager(); - } + new CardbaseCLI(args).run(); + } 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. + e.printStackTrace(); + } catch (JsonMappingException e) { + System.out.println("Error: unexpected fields found in cardbase, it may be from an old version?"); + e.printStackTrace(); + } catch (IOException e) { + System.out.println("Error: something went wrong reading a file, abort..."); + e.printStackTrace(); + } + } + + private CardbaseCLI(String... args) throws JsonParseException, JsonMappingException, IOException { + System.out.println("Welcome to Cardbase CLI!"); - // prepare interface - BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in)); - InputStream is = CardbaseCLI.class.getResourceAsStream("/help"); - if (is != null) { - help = new Scanner(is).useDelimiter("\\Z").next(); + // make the CardbaseManager + if (args.length > 0) { + cardbaseFile = new File(args[0]); + if (cardbaseFile.exists() && cardbaseFile.isFile() && cardbaseFile.canRead()) { + System.out.println("Loading cardbase from \"" + args[0] + "\"."); + cbm = new CardbaseManager(cardbaseFile); } else { - System.out.println("Help file was not found, check the project page for help instead."); + System.out.println(args[0] + " appears to be invalid."); + System.exit(0); } - - // the main loop + } else { + System.out.println("No cardbase file was provided, creating a clean cardbase."); + cbm = new CardbaseManager(); + } + + // load help information + InputStream is = CardbaseCLI.class.getResourceAsStream("/help"); + if (is != null) { + help = new Scanner(is).useDelimiter("\\Z").next(); + } else { + System.out.println("Help file was not found, check the project page for help instead."); + } + + } + + private void run() { + BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in)); + // the main loop + try { while (!exit) { System.out.print(selectedSet == null ? "> " : selectedSet.code + " > "); interpret(consoleReader.readLine().trim().toLowerCase().split("[ \t]+")); } - - } catch (JsonParseException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (JsonMappingException e) { - // TODO Auto-generated catch block - e.printStackTrace(); } catch (IOException e) { - // TODO Auto-generated catch block + System.out.println("Error: something went wrong with stdin, exiting..."); e.printStackTrace(); } - } /** * Handle console commands appropriately. * - * REMINDER sort out these exceptions - * - * @param commands the array of commands, already sanitised. - * @throws JsonParseException - * @throws JsonMappingException - * @throws MalformedURLException - * @throws IOException + * @param commands an array of {@code String} containing the arguments in order. */ - private static void interpret(String[] commands) throws JsonParseException, JsonMappingException, MalformedURLException, IOException { + private void interpret(String[] commands) { if (commands.length > 0) { switch (commands[0]) { /* @@ -173,11 +177,21 @@ public class CardbaseCLI { System.out.println("Could not write to \"" + outputFile.getAbsolutePath() + "\"."); return; } - IO.writeCardBase(outputFile, cbm.cardBase); - // 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() + "\"."); - savePrompt = false; + // handle these exceptions locally - they don't necessarily mean the program should halt. + try { + IO.writeCardbase(outputFile, cbm.cardbase); + // 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."); + e.printStackTrace(); + } catch (IOException e) { + System.out.println("Error: lost contact with the output file, try again?"); + e.printStackTrace(); + } } else { System.out.println("Please provide a file name."); } @@ -199,7 +213,7 @@ public class CardbaseCLI { * Print a list of valid set codes. */ case "sets": - for (MetaCardSet set : cbm.getAllMetaSets()) { + for (CardSet set : cbm.getCardSetList()) { // MetaCardSet has an overridden toString(). System.out.println(set); } @@ -210,15 +224,29 @@ public class CardbaseCLI { */ case "set": // first check if the set code is valid - for (MetaCardSet set : cbm.getAllMetaSets()) { + for (CardSet set : cbm.getCardSetList()) { if (set.code.equalsIgnoreCase(commands[1])) { // if the set is already cached, use that if (setCache.containsKey(set.code)) { selectedSet = setCache.get(set.code); } else { // if not, download it and cache it - selectedSet = IO.getCardSet(set.code); - setCache.put(set.code, selectedSet); + try { + selectedSet = IO.getCardSet(set.code); + setCache.put(set.code, selectedSet); + } catch (JsonParseException e) { + System.out.println("Error: JSON fetched from upstream was not formatted properly."); + e.printStackTrace(); + return; + } catch (JsonMappingException e) { + System.out.println("Error: JSON fetched from upstream does not match the data structure used internally."); + e.printStackTrace(); + return; + } catch (IOException e) { + System.out.println("Error: JSON could not be fetched from upstream."); + e.printStackTrace(); + return; + } } System.out.println("Selected set: " + set.name + "."); // undoing is not allowed if the set is changed - it would get tricky @@ -352,7 +380,7 @@ public class CardbaseCLI { * @param card the card to add. * @param count the number of times to add it. */ - private static void add(Card card, Integer count) { + private void add(Card card, Integer count) { // MTG JSON does not contain this information, but it is useful for sorting card.setCode = selectedSet.code; cbm.addCard(card, count); @@ -369,7 +397,7 @@ public class CardbaseCLI { * @param card the card to remove. * @param count the number of times to remove it. */ - private static void remove(Card card, Integer count) { + private void remove(Card card, Integer count) { cbm.removeCard(card, count); System.out.println("Removed " + count + "x " + card.name + "."); savePrompt = true; @@ -378,12 +406,12 @@ public class CardbaseCLI { } /** - * Make return a string that is guaranteed to be a legal file name. + * 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 static String sanitiseFileName(String 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 @@ -392,7 +420,17 @@ public class CardbaseCLI { } 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 @@ -400,7 +438,7 @@ public class CardbaseCLI { * * @param card the card to peruse. */ - private static void printPerusal(Card card) { + 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); @@ -414,14 +452,4 @@ public class CardbaseCLI { if (card.multiverseid != null) System.out.println("\tMID: " + card.multiverseid); if (card.artist != null) System.out.println("\tIllus. " + card.artist); } - - /** - * 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 static 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)); - } } -- cgit v1.2.3