From bc7ec4eeb4a2e89a76d667cf5b0968dead1be36d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Nov 2018 19:50:46 +0100 Subject: [PATCH] Implement file-based repository and persist number of generated codes per study --- .../java/de/vanitasvitae/imi/codes/Main.java | 38 ++++- .../imi/codes/{ => input}/Arguments.java | 16 +-- .../imi/codes/{ => input}/InputValidator.java | 6 +- .../{ => input}/InvalidOptionException.java | 2 +- .../imi/codes/persistence/FileRepository.java | 133 ++++++++++++++++++ .../{ => types}/BoundedCharSequence.java | 4 +- .../imi/codes/types/SampleTubeCode.java | 67 +++++++++ .../imi/codes/{ => types}/SampleType.java | 5 +- .../vanitasvitae/imi/codes/types/Study.java | 13 ++ .../imi/codes/{ => types}/StudyNumber.java | 7 +- .../imi/codes/InputValidatorTest.java | 4 + 11 files changed, 274 insertions(+), 21 deletions(-) rename src/main/java/de/vanitasvitae/imi/codes/{ => input}/Arguments.java (76%) rename src/main/java/de/vanitasvitae/imi/codes/{ => input}/InputValidator.java (94%) rename src/main/java/de/vanitasvitae/imi/codes/{ => input}/InvalidOptionException.java (95%) create mode 100644 src/main/java/de/vanitasvitae/imi/codes/persistence/FileRepository.java rename src/main/java/de/vanitasvitae/imi/codes/{ => types}/BoundedCharSequence.java (92%) create mode 100644 src/main/java/de/vanitasvitae/imi/codes/types/SampleTubeCode.java rename src/main/java/de/vanitasvitae/imi/codes/{ => types}/SampleType.java (81%) create mode 100644 src/main/java/de/vanitasvitae/imi/codes/types/Study.java rename src/main/java/de/vanitasvitae/imi/codes/{ => types}/StudyNumber.java (73%) diff --git a/src/main/java/de/vanitasvitae/imi/codes/Main.java b/src/main/java/de/vanitasvitae/imi/codes/Main.java index 476092e..93bef80 100644 --- a/src/main/java/de/vanitasvitae/imi/codes/Main.java +++ b/src/main/java/de/vanitasvitae/imi/codes/Main.java @@ -1,7 +1,15 @@ package de.vanitasvitae.imi.codes; import java.io.File; +import java.io.IOException; +import de.vanitasvitae.imi.codes.input.Arguments; +import de.vanitasvitae.imi.codes.input.InputValidator; +import de.vanitasvitae.imi.codes.input.InvalidOptionException; +import de.vanitasvitae.imi.codes.persistence.FileRepository; +import de.vanitasvitae.imi.codes.types.SampleTubeCode; +import de.vanitasvitae.imi.codes.types.SampleType; +import de.vanitasvitae.imi.codes.types.StudyNumber; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; @@ -53,12 +61,32 @@ public class Main { return; } - // Debug. TODO: Remove - System.out.println("StudyNr: " + studyNumber + " sampleType: " + sampleType + " #codes: " + numberOfCodes - + " output: " + outputPath + " browser: " + externalBrowser); + FileRepository repository = new FileRepository(new File(".imicodes")); + // Read the next sample code from file + int nextSampleCode = repository.nextSampleCode(studyNumber); + + int nextTotal = nextSampleCode + numberOfCodes; + // Check, if we'd have an overflow of sample numbers + // We check like this to prevent integer overflows + if (nextSampleCode > 9999 || numberOfCodes > 9999 || nextTotal - 1 > 9999) { + System.out.println("Study " + studyNumber + "would have too many sample tubes" + + " (" + (nextTotal - 1) + "). Aborting."); + return; + } + + // Write back the number of the next sample number that should be generated next time + try { + repository.writeNextSampleCode(studyNumber, nextSampleCode + numberOfCodes); + } catch (IOException e) { + e.printStackTrace(); + return; + } + // Now we are finished with dangerous IO... + + // Generate codes for (int i = 0; i < numberOfCodes; i++) { - System.out.format("%s%04d%n", studyNumber.toString() + sampleType, i); + System.out.println(new SampleTubeCode(studyNumber, sampleType, nextSampleCode + i)); } } @@ -72,6 +100,4 @@ public class Main { formatter.printHelp(NAME_JAR, HELP_HEADER, options, HELP_FOOTER, true); } - - } diff --git a/src/main/java/de/vanitasvitae/imi/codes/Arguments.java b/src/main/java/de/vanitasvitae/imi/codes/input/Arguments.java similarity index 76% rename from src/main/java/de/vanitasvitae/imi/codes/Arguments.java rename to src/main/java/de/vanitasvitae/imi/codes/input/Arguments.java index f88e443..35bc4df 100644 --- a/src/main/java/de/vanitasvitae/imi/codes/Arguments.java +++ b/src/main/java/de/vanitasvitae/imi/codes/input/Arguments.java @@ -1,11 +1,11 @@ -package de.vanitasvitae.imi.codes; +package de.vanitasvitae.imi.codes.input; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; -class Arguments { +public class Arguments { - static final Option STUDY_NUMBER = Option.builder("s") + public static final Option STUDY_NUMBER = Option.builder("s") .longOpt("study") .desc("Three-letter code of the associated study") .required() @@ -13,7 +13,7 @@ class Arguments { .argName("SNR") .build(); - static final Option SAMPLE_TYPE = Option.builder("t") + public static final Option SAMPLE_TYPE = Option.builder("t") .longOpt("type") .desc("Type of the sample (b = blood, u = urine)") .required() @@ -21,7 +21,7 @@ class Arguments { .argName("sample type") .build(); - static final Option NUMBER_CODES = Option.builder("n") + public static final Option NUMBER_CODES = Option.builder("n") .longOpt("number") .desc("Number of codes to be generated") .required() @@ -29,19 +29,19 @@ class Arguments { .argName("n") .build(); - static final Option OUTPUT_DESTINATION = Option.builder("o") + public static final Option OUTPUT_DESTINATION = Option.builder("o") .longOpt("output") .desc("Filename for the generated html output") .hasArg() .argName("file name") .build(); - static final Option EXTERNAL_BROWSER = Option.builder("b") + public static final Option EXTERNAL_BROWSER = Option.builder("b") .longOpt("browser") .desc("Open the generated HTML output in a an external browser") .build(); - static final Option HELP = Option.builder("h") + public static final Option HELP = Option.builder("h") .longOpt("help") .desc("Print this help text") .build(); diff --git a/src/main/java/de/vanitasvitae/imi/codes/InputValidator.java b/src/main/java/de/vanitasvitae/imi/codes/input/InputValidator.java similarity index 94% rename from src/main/java/de/vanitasvitae/imi/codes/InputValidator.java rename to src/main/java/de/vanitasvitae/imi/codes/input/InputValidator.java index 780bb01..ed0cd16 100644 --- a/src/main/java/de/vanitasvitae/imi/codes/InputValidator.java +++ b/src/main/java/de/vanitasvitae/imi/codes/input/InputValidator.java @@ -1,7 +1,9 @@ -package de.vanitasvitae.imi.codes; +package de.vanitasvitae.imi.codes.input; import java.io.File; -import java.util.regex.Pattern; + +import de.vanitasvitae.imi.codes.types.SampleType; +import de.vanitasvitae.imi.codes.types.StudyNumber; public class InputValidator { diff --git a/src/main/java/de/vanitasvitae/imi/codes/InvalidOptionException.java b/src/main/java/de/vanitasvitae/imi/codes/input/InvalidOptionException.java similarity index 95% rename from src/main/java/de/vanitasvitae/imi/codes/InvalidOptionException.java rename to src/main/java/de/vanitasvitae/imi/codes/input/InvalidOptionException.java index 4d948f6..46e6155 100644 --- a/src/main/java/de/vanitasvitae/imi/codes/InvalidOptionException.java +++ b/src/main/java/de/vanitasvitae/imi/codes/input/InvalidOptionException.java @@ -1,4 +1,4 @@ -package de.vanitasvitae.imi.codes; +package de.vanitasvitae.imi.codes.input; /** * Exception which gets thrown when the user-provided input arguments are not valid. diff --git a/src/main/java/de/vanitasvitae/imi/codes/persistence/FileRepository.java b/src/main/java/de/vanitasvitae/imi/codes/persistence/FileRepository.java new file mode 100644 index 0000000..982693e --- /dev/null +++ b/src/main/java/de/vanitasvitae/imi/codes/persistence/FileRepository.java @@ -0,0 +1,133 @@ +package de.vanitasvitae.imi.codes.persistence; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.HashMap; + +import de.vanitasvitae.imi.codes.input.InvalidOptionException; +import de.vanitasvitae.imi.codes.types.StudyNumber; + +public class FileRepository { + + private final File base; + private final HashMap hardCoded = new HashMap<>(); + + /** + * Create a new file-based repository which stores files in the given directory {@code base}. + * + * @param base base directory. + */ + public FileRepository(File base) { + this.base = base; + + // Populate the repository with some hard coded values + try { + hardCoded.put(new StudyNumber("AAA"), 35); + hardCoded.put(new StudyNumber("BBB"), 42); + } catch (InvalidOptionException e) { + throw new AssertionError(e); + } + } + + private int getHardCodedOrZero(StudyNumber studyNumber) { + Integer i = hardCoded.get(studyNumber); + return (i != null ? i : 0); + } + + /** + * Return the sample number of the next code which will be generated for the given {@link StudyNumber}. + * + * @param study code for the study + * @return sample number + */ + public int nextSampleCode(StudyNumber study) { + File studyNumberFile = new File(base, study.toString()); + int next; + // Check, if we have hard-coded values available + if (!studyNumberFile.exists()) { + next = getHardCodedOrZero(study); + } // Otherwise read from file + else { + next = readInt(studyNumberFile); + } + + return next; + } + + /** + * Write the next sample code number, which should be generated to file. + * The next sample code number should be the last generated sample code number + 1. + * + * @param study study number + * @param nextSampleCode next sample code to be generated. + * + * @throws IOException in case the file cannot be written + */ + public void writeNextSampleCode(StudyNumber study, int nextSampleCode) throws IOException { + writeInt(new File(base, study.toString()), nextSampleCode); + } + + /** + * Reads an integer from the first line inside a file. If the file does not exist or is not readable, 0 is returned. + * + * @param file file to read from + * @return integer or 0 + */ + private int readInt(File file) { + try(BufferedReader in = new BufferedReader(new FileReader(file))) { + return Integer.parseInt(in.readLine()); + } catch (FileNotFoundException e) { + System.out.println("File " + file.getAbsolutePath() + " does not exist."); + return 0; + } catch (IOException e) { + e.printStackTrace(); + return 0; + } + } + + /** + * Write an integer to a destination file. + * This method throws an {@link IOException} in case the parent directories or the file + * cannot be created or written to. + * + * @param file destination + * @param integer integer to be written + */ + private void writeInt(File file, int integer) throws IOException { + // Check if destination is directory, which would be illegal + if (file.isDirectory()) { + throw new IOException("Provided output directory points to a directory, which is not allowed."); + } + + // Make sure, the parent folder exists + File parent = file.getParentFile(); + if (!parent.exists() && !parent.mkdirs()) { + throw new IOException("Cannot create parent directory " + parent.getAbsolutePath()); + } + + // Check if destination file exists and if not, try to create it + if (!file.exists()) { + try { + if (!file.createNewFile()) { + throw new IOException("Output file " +file.getAbsolutePath() + " cannot be created."); + } + } catch (IOException e) { + throw new IOException("Output file " + file.getAbsolutePath() + " cannot be created." , e); + } + } + + try(Writer wr = new FileWriter(file)) { + wr.write(Integer.toString(integer)); + } catch (FileNotFoundException e) { + // Must not happen + throw new AssertionError(e); + } catch (IOException e) { + throw new IOException("Cannot write to output file " + file.getAbsolutePath(), e); + } + } +} diff --git a/src/main/java/de/vanitasvitae/imi/codes/BoundedCharSequence.java b/src/main/java/de/vanitasvitae/imi/codes/types/BoundedCharSequence.java similarity index 92% rename from src/main/java/de/vanitasvitae/imi/codes/BoundedCharSequence.java rename to src/main/java/de/vanitasvitae/imi/codes/types/BoundedCharSequence.java index 236d9ef..cecaaef 100644 --- a/src/main/java/de/vanitasvitae/imi/codes/BoundedCharSequence.java +++ b/src/main/java/de/vanitasvitae/imi/codes/types/BoundedCharSequence.java @@ -1,8 +1,10 @@ -package de.vanitasvitae.imi.codes; +package de.vanitasvitae.imi.codes.types; import java.util.regex.Pattern; import java.util.stream.IntStream; +import de.vanitasvitae.imi.codes.input.InvalidOptionException; + public abstract class BoundedCharSequence implements CharSequence { private final Pattern regex; diff --git a/src/main/java/de/vanitasvitae/imi/codes/types/SampleTubeCode.java b/src/main/java/de/vanitasvitae/imi/codes/types/SampleTubeCode.java new file mode 100644 index 0000000..3d07a49 --- /dev/null +++ b/src/main/java/de/vanitasvitae/imi/codes/types/SampleTubeCode.java @@ -0,0 +1,67 @@ +package de.vanitasvitae.imi.codes.types; + +import java.util.stream.IntStream; + +public class SampleTubeCode implements CharSequence { + + private final StudyNumber studyNumber; + private final SampleType sampleType; + private final int sampleNumber; + + public SampleTubeCode(StudyNumber studyNumber, SampleType sampleType, int sampleNumber) { + if (studyNumber == null) { + throw new IllegalArgumentException("StudyNumber MUST NOT be null."); + } + if (sampleType == null) { + throw new IllegalArgumentException("SampleType MUST NOT be null."); + } + if (sampleNumber < 0 || sampleNumber > 9999) { + throw new IllegalArgumentException("SampleNumber MUST BE an integer between 0 and 9999 (inclusive)."); + } + this.studyNumber = studyNumber; + this.sampleType = sampleType; + this.sampleNumber = sampleNumber; + } + + + @Override + public int length() { + return toString().length(); + } + + @Override + public char charAt(int i) { + return toString().charAt(i); + } + + @Override + public CharSequence subSequence(int a, int b) { + return toString().subSequence(a, b); + } + + @Override + public IntStream chars() { + return toString().chars(); + } + + @Override + public IntStream codePoints() { + return toString().codePoints(); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public boolean equals(Object o) { + return toString().equals(o.toString()); + } + + @Override + public String toString() { + String number = String.format("%04d", sampleNumber); + return studyNumber.toString() + sampleType.toString() + number; + } +} diff --git a/src/main/java/de/vanitasvitae/imi/codes/SampleType.java b/src/main/java/de/vanitasvitae/imi/codes/types/SampleType.java similarity index 81% rename from src/main/java/de/vanitasvitae/imi/codes/SampleType.java rename to src/main/java/de/vanitasvitae/imi/codes/types/SampleType.java index 2f5b0fd..e5f2463 100644 --- a/src/main/java/de/vanitasvitae/imi/codes/SampleType.java +++ b/src/main/java/de/vanitasvitae/imi/codes/types/SampleType.java @@ -1,7 +1,10 @@ -package de.vanitasvitae.imi.codes; +package de.vanitasvitae.imi.codes.types; import java.util.regex.Pattern; +import de.vanitasvitae.imi.codes.input.InputValidator; +import de.vanitasvitae.imi.codes.input.InvalidOptionException; + /** * Sub-class of {@link CharSequence}, which can only represent char sequences of length 1 from the alphabet [a-zA-Z0-9]. */ diff --git a/src/main/java/de/vanitasvitae/imi/codes/types/Study.java b/src/main/java/de/vanitasvitae/imi/codes/types/Study.java new file mode 100644 index 0000000..6ebfdda --- /dev/null +++ b/src/main/java/de/vanitasvitae/imi/codes/types/Study.java @@ -0,0 +1,13 @@ +package de.vanitasvitae.imi.codes.types; + +import java.util.ArrayList; +import java.util.List; + +public class Study { + private final StudyNumber number; + private final List codes = new ArrayList<>(); + + public Study(StudyNumber number) { + this.number = number; + } +} diff --git a/src/main/java/de/vanitasvitae/imi/codes/StudyNumber.java b/src/main/java/de/vanitasvitae/imi/codes/types/StudyNumber.java similarity index 73% rename from src/main/java/de/vanitasvitae/imi/codes/StudyNumber.java rename to src/main/java/de/vanitasvitae/imi/codes/types/StudyNumber.java index df860a5..86841e9 100644 --- a/src/main/java/de/vanitasvitae/imi/codes/StudyNumber.java +++ b/src/main/java/de/vanitasvitae/imi/codes/types/StudyNumber.java @@ -1,7 +1,10 @@ -package de.vanitasvitae.imi.codes; +package de.vanitasvitae.imi.codes.types; import java.util.regex.Pattern; +import de.vanitasvitae.imi.codes.input.InputValidator; +import de.vanitasvitae.imi.codes.input.InvalidOptionException; + /** * Sub-class of {@link CharSequence}, which can only represent char sequences of length 3 from the alphabet [a-zA-Z0-9]. */ @@ -16,7 +19,7 @@ public class StudyNumber extends BoundedCharSequence { * @param value input string * @throws InvalidOptionException if the input doesn't match the regex */ - protected StudyNumber(String value) throws InvalidOptionException { + public StudyNumber(String value) throws InvalidOptionException { super(REGEX, value); } } diff --git a/src/test/java/de/vanitasvitae/imi/codes/InputValidatorTest.java b/src/test/java/de/vanitasvitae/imi/codes/InputValidatorTest.java index d2ebf20..790adfa 100644 --- a/src/test/java/de/vanitasvitae/imi/codes/InputValidatorTest.java +++ b/src/test/java/de/vanitasvitae/imi/codes/InputValidatorTest.java @@ -6,6 +6,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Random; +import de.vanitasvitae.imi.codes.input.InputValidator; +import de.vanitasvitae.imi.codes.input.InvalidOptionException; +import de.vanitasvitae.imi.codes.types.SampleType; +import de.vanitasvitae.imi.codes.types.StudyNumber; import org.junit.Test; public class InputValidatorTest {