diff --git a/src/main/java/de/vanitasvitae/imi/codes/Arguments.java b/src/main/java/de/vanitasvitae/imi/codes/Arguments.java index 9f0b250..f88e443 100644 --- a/src/main/java/de/vanitasvitae/imi/codes/Arguments.java +++ b/src/main/java/de/vanitasvitae/imi/codes/Arguments.java @@ -3,7 +3,7 @@ package de.vanitasvitae.imi.codes; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; -public class Arguments { +class Arguments { static final Option STUDY_NUMBER = Option.builder("s") .longOpt("study") @@ -16,20 +16,28 @@ public class Arguments { static final Option SAMPLE_TYPE = Option.builder("t") .longOpt("type") .desc("Type of the sample (b = blood, u = urine)") - .required(true) + .required() .hasArg() .argName("sample type") .build(); + static final Option NUMBER_CODES = Option.builder("n") + .longOpt("number") + .desc("Number of codes to be generated") + .required() + .hasArg() + .argName("n") + .build(); + static final Option OUTPUT_DESTINATION = Option.builder("o") - .longOpt("OUTPUT_DESTINATION") + .longOpt("output") .desc("Filename for the generated html output") .hasArg() .argName("file name") .build(); static final Option EXTERNAL_BROWSER = Option.builder("b") - .longOpt("EXTERNAL_BROWSER") + .longOpt("browser") .desc("Open the generated HTML output in a an external browser") .build(); @@ -43,14 +51,12 @@ public class Arguments { * @return command line options. */ public static Options getCommandLineOptions() { - Options options = new Options(); - - options.addOption(STUDY_NUMBER); - options.addOption(SAMPLE_TYPE); - options.addOption(OUTPUT_DESTINATION); - options.addOption(EXTERNAL_BROWSER); - options.addOption(HELP); - - return options; + return new Options() + .addOption(STUDY_NUMBER) + .addOption(SAMPLE_TYPE) + .addOption(NUMBER_CODES) + .addOption(OUTPUT_DESTINATION) + .addOption(EXTERNAL_BROWSER) + .addOption(HELP); } } diff --git a/src/main/java/de/vanitasvitae/imi/codes/InputValidator.java b/src/main/java/de/vanitasvitae/imi/codes/InputValidator.java index 892c8f7..c9641b9 100644 --- a/src/main/java/de/vanitasvitae/imi/codes/InputValidator.java +++ b/src/main/java/de/vanitasvitae/imi/codes/InputValidator.java @@ -5,7 +5,7 @@ import java.util.regex.Pattern; public class InputValidator { // Allowed characters - private static final String ALPHABET = "[a-zA-Z0-9]"; + public static final String ALPHABET = "[a-zA-Z0-9]"; /* REGEX pattern for study numbers. Allowed are all 3-letter numbers from the alphabet [a-zA-Z0-9]. @@ -22,11 +22,12 @@ public class InputValidator { * * @param in input String * @return unmodified input String if it matches. - * @throws IllegalArgumentException if the input String doesn't match the regex. + * + * @throws InvalidOptionException if the input String doesn't match the regex. */ - public static String validateStudyNumber(String in) { + public static String validateStudyNumber(String in) throws InvalidOptionException { if (!STUDY_NUMBER_MATCHER.matcher(in).matches()) { - throw new IllegalArgumentException("Study number does not match REGEX."); + throw new InvalidOptionException("Study number must be a three digit number from the alphabet [a-zA-Z0-9]."); } return in; @@ -38,10 +39,38 @@ public class InputValidator { * @param in input String * @return parsed {@link SampleType}. * - * @throws IllegalArgumentException if the given String doesn't match a {@link SampleType}. + * @throws InvalidOptionException if the given String doesn't match a {@link SampleType}. * @throws NullPointerException in case the given String is {@code null}. */ - public static SampleType validateSampleType(String in) { - return SampleType.valueOf(in); + public static SampleType validateSampleType(String in) throws InvalidOptionException { + try { + return SampleType.getEnum(in); + } catch (IllegalArgumentException e) { + String message = "Invalid sample type \"" + in + "\"."; + throw new InvalidOptionException(message, e); + } + } + + /** + * Validate, that the user provided number of codes to be generated is an integer greater than 0. + * + * @param in input String + * @return number of codes to be generated. + * + * @throws InvalidOptionException in case the input string is + */ + public static int validateNumberOfCodes(String in) throws InvalidOptionException { + int i; + try { + i = Integer.parseInt(in); + } catch (NumberFormatException e) { + throw new InvalidOptionException("Invalid input \"" + in + "\". Please enter a positive number.", e); + } + + if (i <= 0) { + throw new InvalidOptionException("Number of generated codes must be greater than 0."); + } + + return i; } } diff --git a/src/main/java/de/vanitasvitae/imi/codes/InvalidOptionException.java b/src/main/java/de/vanitasvitae/imi/codes/InvalidOptionException.java new file mode 100644 index 0000000..4d948f6 --- /dev/null +++ b/src/main/java/de/vanitasvitae/imi/codes/InvalidOptionException.java @@ -0,0 +1,28 @@ +package de.vanitasvitae.imi.codes; + +/** + * Exception which gets thrown when the user-provided input arguments are not valid. + */ +public class InvalidOptionException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Create a new {@link InvalidOptionException}. The message MUST be human readable, as it will get printed to the + * CLI. + * @param message human readable error message. + */ + public InvalidOptionException(String message) { + super(message); + } + + /** + * Create a new {@link InvalidOptionException}. The message MUST be human readable, as it will get printed to the + * CLI. + * @param message human readable error message. + * @param cause cause of the exception. + */ + public InvalidOptionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/vanitasvitae/imi/codes/Main.java b/src/main/java/de/vanitasvitae/imi/codes/Main.java index 25d1998..dc21d3c 100644 --- a/src/main/java/de/vanitasvitae/imi/codes/Main.java +++ b/src/main/java/de/vanitasvitae/imi/codes/Main.java @@ -14,7 +14,6 @@ public class Main { private static final String HELP_FOOTER = "\nAuthor: Paul Schaub "; public static void main(String[] args) { - Options options = Arguments.getCommandLineOptions(); CommandLineParser parser = new DefaultParser(); @@ -27,6 +26,7 @@ public class Main { return; } + // User issues '-h', so just show help text and exit. if (arguments.hasOption(Arguments.HELP)) { printHelp(options); return; diff --git a/src/main/java/de/vanitasvitae/imi/codes/SampleType.java b/src/main/java/de/vanitasvitae/imi/codes/SampleType.java index 621aeea..8eb29fc 100644 --- a/src/main/java/de/vanitasvitae/imi/codes/SampleType.java +++ b/src/main/java/de/vanitasvitae/imi/codes/SampleType.java @@ -2,19 +2,61 @@ package de.vanitasvitae.imi.codes; public enum SampleType { - b("Blood"), - u("Urine"), + // Hard coded values. Prepended with an underscore to allow numeric values (eg. _2). + _b('b', "Blood"), + _u('u', "Urine"), ; // Members of the enum - private final String name; + private final char name; + private final String description; - SampleType(String name) { + /** + * Create a SampleType enum which is described by a name (one letter from the alphabet [a-zA-Z0-9]) and a + * human readable description. + * + * @param name one letter name + * @param description description + */ + SampleType(char name, String description) { this.name = name; + this.description = description; } - public String getName() { - return name; + /** + * Override {@link Enum#toString()} in order to return the name of the enum instead of its value. + * + * @return name + */ + @Override + public String toString() { + return "" + name; + } + + /** + * Return the human readable description of the enum. + */ + public String getDescription() { + return description; + } + + /** + * Replacement method for {@link #valueOf(String)}. You MUST use this method instead, as we have to escape + * the names of the enums with a underscore in order to allow for numerals to be used as enum name (for example "_0"). + * This method takes a string value (eg. "u") and returns the corresponding enum ("_u"). + * + * @param name name of the enum without an underscore. + * @return enum corresponding to the name. + * + * @throws IllegalArgumentException if no matching enum was found. + */ + public static SampleType getEnum(String name) { + for (SampleType s : SampleType.values()) { + if (s.toString().equals(name)) { + return s; + } + } + throw new IllegalArgumentException("No SampleType found for name \"" + name + "\"."); } } diff --git a/src/test/java/de/vanitasvitae/imi/codes/InputValidatorTest.java b/src/test/java/de/vanitasvitae/imi/codes/InputValidatorTest.java new file mode 100644 index 0000000..47275cc --- /dev/null +++ b/src/test/java/de/vanitasvitae/imi/codes/InputValidatorTest.java @@ -0,0 +1,164 @@ +package de.vanitasvitae.imi.codes; + +import static junit.framework.TestCase.assertEquals; + +import java.util.Random; + +import org.junit.Test; + +public class InputValidatorTest { + + private static final Random RAND = new Random(); + + private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + /** + * Test, whether {@link InputValidator#validateStudyNumber(String)} accepts 3 digit inputs from the allowed alphabet. + * + * @throws InvalidOptionException NOT expected. + */ + @Test + public void testValidateStudyNumber() throws InvalidOptionException { + String in = generateTestStringOfLength(3); + assertEquals("validateStudyNumber must accept valid three digit inputs from the alphabet [a-zA-Z0-9].", + in, InputValidator.validateStudyNumber(in)); + } + + /** + * Test, whether {@link InputValidator#validateStudyNumber(String)} fails for inputs of correct length, but from + * illegal characters. + * + * @throws InvalidOptionException expected + */ + @Test(expected = InvalidOptionException.class) + public void testValidateStudyNumberFailsForInvalidAlphabet() throws InvalidOptionException { + String in = "7*A"; + + // -> InvalidOptionException + InputValidator.validateStudyNumber(in); + } + + /** + * Test, whether {@link InputValidator#validateStudyNumber(String)} fails for Strings of length 0, 1 or two from + * the allowed alphabet. + * + * @throws InvalidOptionException expected. + */ + @Test(expected = InvalidOptionException.class) + public void testValidateStudyNumberFailsForInputOfInsufficientLength() throws InvalidOptionException { + String in = generateTestStringOfLength(RAND.nextInt(3)); + + // -> InvalidOptionException + InputValidator.validateStudyNumber(in); + } + + /** + * Test, whether {@link InputValidator#validateStudyNumber(String)} fails for input Strings which are too long. + * + * @throws InvalidOptionException expected. + */ + @Test(expected = InvalidOptionException.class) + public void testValidateStudyNumberFailsForLongerInput() throws InvalidOptionException { + String in = generateTestStringOfLength(RAND.nextInt(50) + 4); + + // -> InvalidOptionException + InputValidator.validateStudyNumber(in); + } + + /* + Generates a String of length len which consists of letters from the alphabet [a-zA-Z0-9]. + */ + private static String generateTestStringOfLength(int len) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) { + sb.append(ALPHABET.charAt(RAND.nextInt(ALPHABET.length()))); + } + + return sb.toString(); + } + + /** + * Test, whether {@link InputValidator#validateSampleType(String)} returns the expected {@link SampleType} for + * given input strings. + * + * @throws InvalidOptionException NOT expected + */ + @Test + public void testValidateSampleType() throws InvalidOptionException { + for (SampleType t : SampleType.values()) { + assertEquals("validateSampleType didn't return expected value for input \"" + t.toString() + "\"", + t, InputValidator.validateSampleType(t.toString())); + } + } + + /** + * Test, whether {@link InputValidator#validateSampleType(String)} throws a {@link InvalidOptionException} for + * unknown sample types (in this case {@code x}). + * + * @throws InvalidOptionException expected + */ + @Test(expected = InvalidOptionException.class) + public void testValidateSampleTypeFailsForUnknownInputs() throws InvalidOptionException { + // -> InvalidOptionException + InputValidator.validateSampleType("x"); + } + + /** + * Test, whether {@link InputValidator#validateNumberOfCodes(String)} returns the expected {@link Integer}. + * In this method, posit + * + * @throws InvalidOptionException NOT expected + */ + @Test + public void testValidateNumberOfCodes() throws InvalidOptionException { + // nextInt(bounds) returns a number between 0 (inclusive) and bound (exclusive), so +1 should be safe. + int number = RAND.nextInt(Integer.MAX_VALUE) + 1; + String string = Integer.toString(number); + + assertEquals("validateNumberOfCodes failed to validate input \"" + string + "\"", + number, InputValidator.validateNumberOfCodes(string)); + } + + /** + * Test, whether {@link InputValidator#validateNumberOfCodes(String)} throws a {@link InvalidOptionException} + * for negative numbers and 0. + * + * @throws InvalidOptionException expected + */ + @Test(expected = InvalidOptionException.class) + public void testValidateNumberOfCodesFailsForNegativeNumbers() throws InvalidOptionException { + int number = -RAND.nextInt(Integer.MAX_VALUE); + String string = Integer.toString(number); + + // -> InvalidOptionException + InputValidator.validateNumberOfCodes(string); + } + + /** + * Test, whether {@link InputValidator#validateNumberOfCodes(String)} throws a {@link InvalidOptionException} + * for non-numeric inputs, which cannot be parsed into an {@link Integer}. + * + * @throws InvalidOptionException expected + */ + @Test(expected = InvalidOptionException.class) + public void testValidateNumberOfCodesFailsForNonNumericInput() throws InvalidOptionException { + String string = "notNumeric"; + + // -> InvalidOptionException + InputValidator.validateNumberOfCodes(string); + } + + /** + * Test, whether {@link InputValidator#validateNumberOfCodes(String)} throws a {@link InvalidOptionException} + * for inputs that exceed the range of an {@link Integer} ({@link Integer#MAX_VALUE}). + * + * @throws InvalidOptionException expected + */ + @Test(expected = InvalidOptionException.class) + public void testValidateNumberOfCodesFailsForNumbersTooLarge() throws InvalidOptionException { + // 10 * Integer.MAX_VALUE + String string = "21474836470"; + + InputValidator.validateNumberOfCodes(string); + } +} diff --git a/src/test/java/de/vanitasvitae/imi/codes/SampleTypeTest.java b/src/test/java/de/vanitasvitae/imi/codes/SampleTypeTest.java new file mode 100644 index 0000000..8580842 --- /dev/null +++ b/src/test/java/de/vanitasvitae/imi/codes/SampleTypeTest.java @@ -0,0 +1,39 @@ +package de.vanitasvitae.imi.codes; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertTrue; + +import java.util.regex.Pattern; + +import org.junit.Test; + +/** + * Test functionality of the {@link SampleType} enum. + */ +public class SampleTypeTest { + + private static final Pattern SAMPLE_TYPE_VALUE_PATTERN = Pattern.compile(InputValidator.ALPHABET); + + /** + * Test, whether all {@link SampleType} names are of length 1 and from the alphabet [a-zA-Z0-9]. + */ + @Test + public void testSampleTypeNames() { + for (SampleType t : SampleType.values()) { + assertTrue("SampleType value names must be one letter from the alphabet [a-zA-Z0-9].", + SAMPLE_TYPE_VALUE_PATTERN.matcher(t.toString()).matches()); + } + } + + /** + * Test, whether {@link SampleType#getEnum(String)} correctly maps {@link SampleType#name} to the corresponding + * values. + */ + @Test + public void testGetEnum() { + for (SampleType t : SampleType.values()) { + assertEquals("getEnum must return the SampleType, which corresponds to the given name.", + t, SampleType.getEnum(t.toString())); + } + } +}