I'd like comments on the following code, explanations, etc. are given in the javadoc:
package ocr.base;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Handles the checking of a directory at an interval.
*
* This interval is variable and is to be found in the config file.
* The found files get processed via a file consumer.
* The config file is only opened if the last modified date has changed.
*
* @author Frank van Heeswijk
*/
public abstract class BaseChecker implements Runnable {
/** Scheduler to run the file checking on. **/
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
/** The directory to be checked. **/
private final File directory;
/** A consumer that accepts the files in the directory. **/
private final Consumer<File> fileConsumer;
/** The configuration file. **/
private final File configFile;
/** The runtime sub class of BaseChecker. **/
private final Class<? extends BaseChecker> subClass;
/** The current duration, changes only if the file has been modified. **/
private Duration duration;
/** The last modified date of the last readout of the config file. **/
private long configLastModified;
/**
* Constructs this object.
*
* @param directory The directory to be checked.
* @param fileConsumer The consumer that accepts the files.
* @param configFile The configuration file.
*/
public BaseChecker(final File directory, final Consumer<File> fileConsumer, final File configFile) {
this.directory = Objects.requireNonNull(directory);
this.fileConsumer = Objects.requireNonNull(fileConsumer);
this.configFile = Objects.requireNonNull(configFile);
this.subClass = this.getClass();
}
/**
* Checks the directory now and calls innerRun().
*/
@Override
public void run() {
checkDirectory();
}
/**
* Gets the duration from the config file and schedules one execution of checkDirectory() at that time.
*
* It only actually opens the config file if it has been modified.
*/
private void innerRun() {
if (configFile.lastModified() != configLastModified) {
duration = durationFromConfig();
}
scheduler.schedule(this::checkDirectory, duration.period, duration.unit);
}
/**
* Checks the directory and calls the consumer for every file, and then calls innerRun().
*/
private void checkDirectory() {
Arrays.stream(directory.listFiles()).forEach(fileConsumer);
innerRun();
}
/**
* Returns the duration from the config file.
*
* @return The duration.
*/
private Duration durationFromConfig() {
try {
return durationFromConfigInner();
} catch (IOException ex) {
throw new IllegalStateException("The config file (\"" + configFile.getPath() + "\") has not been found.");
}
}
/**
* Returns the duration from the config file.
*
* Searches the log file for the first line indicating the config entry for this instance.
*
* @return The duration.
* @throws FileNotFoundException If the config file has not been found.
*/
private Duration durationFromConfigInner() throws IOException {
String entry = subClass.getSimpleName() + "=";
Optional<String> optional = Files.newBufferedReader(configFile.toPath(), Charset.forName("UTF-8")).lines()
.filter(s -> s.startsWith(entry))
.map(s -> s.replaceAll(" ", ""))
.findFirst();
configLastModified = configFile.lastModified();
if (!optional.isPresent()) {
throw new IllegalStateException("Entry (\"" + entry + "\") has not been found in the config file.");
}
return Duration.of(optional.get().replace(entry, ""));
}
/**
* Record class to hold the duration.
*/
private static class Duration {
/** The period of the duration. **/
public final int period;
/** The time unit of the duration. **/
public final TimeUnit unit;
/**
* Constructs the duration.
*
* @param period The period.
* @param unit The time unit.
*/
public Duration(final int period, final TimeUnit unit) {
this.period = period;
this.unit = Objects.requireNonNull(unit);
}
/**
* Returns a duration based on a string value.
*
* The implementation accepts all strings starting with an integer number and ending on a 's', 'm', 'h' or 'd'.
* These characters respectively denote seconds, minutes, hours and days.
*
* @param value The string value to be converted
* @return The duration that the string value represented.
*/
public static Duration of(final String value) {
Objects.requireNonNull(value);
if (value.length() < 2) {
throw new IllegalArgumentException("Invalid duration (\"" + value + "\").");
}
//period
int period = Integer.parseInt(value.substring(0, value.length() - 1));
if (period <= 0) {
throw new IllegalArgumentException("Invalid period in value (\"" + value + "\").");
}
//unit
char unit = value.charAt(value.length() - 1);
TimeUnit returnUnit;
switch (unit) {
case 's':
returnUnit = TimeUnit.SECONDS;
break;
case 'm':
returnUnit = TimeUnit.MINUTES;
break;
case 'h':
returnUnit = TimeUnit.HOURS;
break;
case 'd':
returnUnit = TimeUnit.DAYS;
break;
default:
throw new IllegalArgumentException("Invalid unit in value (\"" + value + "\").");
}
return new Duration(period, returnUnit);
}
@Override
public String toString() {
return "Duration(" + period + ", " + unit + ")";
}
}
}