Implement file-based repository and persist number of generated codes per study

This commit is contained in:
Paul Schaub 2018-11-15 19:50:46 +01:00
parent ddd0b27a5c
commit bc7ec4eeb4
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
11 changed files with 274 additions and 21 deletions

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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 {

View File

@ -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.

View File

@ -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<StudyNumber, Integer> 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);
}
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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].
*/

View File

@ -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<SampleTubeCode> codes = new ArrayList<>();
public Study(StudyNumber number) {
this.number = number;
}
}

View File

@ -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);
}
}

View File

@ -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 {