Implement file-based repository and persist number of generated codes per study
This commit is contained in:
parent
ddd0b27a5c
commit
bc7ec4eeb4
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
|
@ -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 {
|
||||
|
|
@ -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.
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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].
|
||||
*/
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue