My goal with the following code is to provide an extensible class that can be extended in order to read log files and transforms them into meaningful output.
I'd like to have comments about the code itself, the test code, the javadoc and the overall structure.
/**
* Exception to indicate that there is no more input.
*
* @author Frank van Heeswijk
*/
public class NoMoreInputException extends Exception {
private static final long serialVersionUID = -4640787627068619913L;
public NoMoreInputException() {
}
public NoMoreInputException(final String message) {
super(message);
}
public NoMoreInputException(final String message, final Throwable cause) {
super(message, cause);
}
public NoMoreInputException(final Throwable cause) {
super(cause);
}
}
/**
* Exception to indicate that a log entry is not readable.
*
* @author Frank van Heeswijk
*/
public class NotReadableException extends Exception {
private static final long serialVersionUID = -117259271357929934L;
private final List<String> lines = new ArrayList<>();
/**
* Constructs a new NotReadableException instance.
*
* @param lines The lines that were not readable
*/
public NotReadableException(final List<String> lines) {
this.lines.addAll(lines);
}
/**
* Returns the lines that were not readable.
*
* @return The lines that were not readable.
*/
public List<String> getLines() {
return new ArrayList<>(lines);
}
}
/**
* Exception to indicate that a log entry is not parsable.
*
* @author Frank van Heeswijk
*/
class NotParsableException extends Exception {
private static final long serialVersionUID = 3147294996191143729L;
NotParsableException() {
}
NotParsableException(final String message) {
super(message);
}
NotParsableException(final String message, final Throwable cause) {
super(message, cause);
}
NotParsableException(final Throwable cause) {
super(cause);
}
}
/**
* Used to read log entries from log files.
*
* @author Frank van Heeswijk
*/
public interface LogReader {
/**
* Returns the next log entry.
*
* If you get a NotReadableException, then you can recover the lines that could not be read by calling NotReadableException#lines.
*
* @return The next log entry.
* @throws NotReadableException If the log entry could not be read.
* @throws NoMoreInputException If there is no more input.
*/
LogEntry readEntry() throws NotReadableException, NoMoreInputException;
}
/**
* Used to read lines from an input source.
*
* This functional interface differs from Supplier<String> in that it can throw a NoMoreInputException.
*
* @author Frank van Heeswijk
*/
@FunctionalInterface
public interface LineReader {
/**
* Reads a line.
*
* @return The read line.
* @throws NoMoreInputException If no more input could be obtained.
*/
String readLine() throws NoMoreInputException;
}
/**
* Used to read entries from log files.
*
* It has the option to read more lines from the line reader if deemed necessary.
*
* @author Frank van Heeswijk
*/
public interface EntryReader {
/**
* Returns whether this entry reader can parse the input.
*
* @param input The input check parsability for
* @return Whether this entry reader can parse the input.
*/
boolean isParsable(final String input);
/**
* Parses the input String resulting in a LogEntry.
*
* If deemed necessary, extra lines may be obtained from the LineReader.
*
* @param input The input to parse
* @param lineReader The line reader from which extra lines can be obtained
* @return The LogEntry obtained after parsing the input.
* @throws NotParsableException If this entry reader cannot parse the input to return a LogEntry.
* @throws NoMoreInputException If the line reader has no more input.
*/
LogEntry parse(final String input, final LineReader lineReader) throws NotParsableException, NoMoreInputException;
}
/**
* Can be extended to read lines from log files.
*
* @author Frank van Heeswijk
*/
public abstract class AbstractLineLogReader implements LogReader {
private final Set<EntryReader> entryReaders = entryReaders();
private final List<String> linesInMemory = new ArrayList<>();
@Override
public LogEntry readEntry() throws NotReadableException, NoMoreInputException {
String line = readLineFromLogAndSave();
for (EntryReader entryReader : entryReaders) {
if (!entryReader.isParsable(line)) {
continue;
}
try {
LogEntry result = entryReader.parse(line, this::readLineFromLogAndSave);
linesInMemory.clear();
return result;
} catch (NotParsableException | NoMoreInputException ex) {
//try next entry reader
}
}
List<String> notReadableLines = new ArrayList<>(linesInMemory);
linesInMemory.clear();
throw new NotReadableException(notReadableLines);
}
/**
* Returns the set of entry readers used to read the log files.
*
* @return The set of entry readers used to read the log files.
*/
protected abstract Set<EntryReader> entryReaders();
/**
* Returns the next line from the log file.
*
* @return The next line from the log file.
* @throws NoMoreInputException If no more lines are present.
*/
protected abstract String readLineFromLog() throws NoMoreInputException;
/**
* Returns the next line from the log file and saves it.
*
* @return The next line from the log file.
* @throws NoMoreInputException If no more lines are present.
* @throws java.lang.NullPointerException If line is null.
*/
private String readLineFromLogAndSave() throws NoMoreInputException {
String line = readLineFromLog();
Objects.requireNonNull(line, "line");
linesInMemory.add(line);
return line;
}
}
public class AbstractLineLogReaderTest {
@Test
public void testReadEntryExpectedEntries() throws NoMoreInputException, NotReadableException {
LogReader logReader = new ABCListLineLogReader("A", "B");
assertEquals(ALogEntry.class, logReader.readEntry().getClass());
assertEquals(BLogEntry.class, logReader.readEntry().getClass());
}
@Test(expected = NoMoreInputException.class)
public void testReadEntryNoMoreEntries() throws NoMoreInputException, NotReadableException {
LogReader logReader = new ABCListLineLogReader("A", "B");
logReader.readEntry();
logReader.readEntry();
logReader.readEntry();
}
@Test
public void testReadEntryNoReadersAvailable() throws NoMoreInputException {
LogReader logReader = new EmptyListLineLogReader("A", "B");
try {
logReader.readEntry();
fail();
} catch (NotReadableException ex) {
assertEquals(1, ex.getLines().size());
assertEquals("A", ex.getLines().get(0));
}
}
@Test
public void testReadLogEntrySpanningMultipleLines() throws NoMoreInputException, NotReadableException {
LogReader logReader = new ABCListLineLogReader("A", "C", "1","2", "3", "B");
assertEquals(ALogEntry.class, logReader.readEntry().getClass());
assertEquals(CLogEntry.class, logReader.readEntry().getClass());
assertEquals(BLogEntry.class, logReader.readEntry().getClass());
}
@Test
public void testReadLogEntrySpanningMultipleLinesLastEntryNotReadable() throws NoMoreInputException, NotReadableException {
LogReader logReader = new ABCListLineLogReader("A", "B", "C", "1","2");
assertEquals(ALogEntry.class, logReader.readEntry().getClass());
assertEquals(BLogEntry.class, logReader.readEntry().getClass());
try {
logReader.readEntry();
fail();
} catch (NotReadableException ex) {
assertEquals(Arrays.asList("C", "1", "2"), ex.getLines());
}
}
@Test
public void testReadLogEntrySpanningMultipleLinesIncorrectInput() throws NoMoreInputException, NotReadableException {
LogReader logReader = new ABCListLineLogReader("A", "C", "1","2", "4", "B");
assertEquals(ALogEntry.class, logReader.readEntry().getClass());
try {
logReader.readEntry();
fail();
} catch (NotReadableException ex) {
assertEquals(Arrays.asList("C", "1", "2", "4"), ex.getLines());
}
assertEquals(BLogEntry.class, logReader.readEntry().getClass());
}
private abstract static class ListLineLogReader extends AbstractLineLogReader {
private final Iterator<String> iterator;
private ListLineLogReader(final String... inputList) {
this(Arrays.asList(inputList));
}
private ListLineLogReader(final List<String> inputList) {
Objects.requireNonNull(inputList, "inputList");
this.iterator = inputList.iterator();
}
@Override
protected String readLineFromLog() throws NoMoreInputException {
if (!iterator.hasNext()) {
throw new NoMoreInputException();
}
return iterator.next();
}
}
private static class ABCListLineLogReader extends ListLineLogReader {
private ABCListLineLogReader(final String... inputList) {
super(inputList);
}
private ABCListLineLogReader(final List<String> inputList) {
super(inputList);
}
@Override
protected Set<EntryReader> entryReaders() {
return new HashSet<>(Arrays.asList(
new EntryReader() {
@Override
public boolean isParsable(String input) {
return input.equals("A");
}
@Override
public LogEntry parse(String input, LineReader lineReader) throws NotParsableException, NoMoreInputException {
if (!input.startsWith("A")) {
throw new NotParsableException();
}
return new ALogEntry();
}
},
new EntryReader() {
@Override
public boolean isParsable(String input) {
return input.equals("B");
}
@Override
public LogEntry parse(String input, LineReader lineReader) throws NotParsableException, NoMoreInputException {
if (!input.equals("B")) {
throw new NotParsableException();
}
return new BLogEntry();
}
},
new EntryReader() {
@Override
public boolean isParsable(String input) {
return input.equals("C");
}
@Override
public LogEntry parse(String input, LineReader lineReader) throws NotParsableException, NoMoreInputException {
if (!input.equals("C")) {
throw new NotParsableException();
}
if (!Objects.equals("1", lineReader.readLine())) {
throw new NotParsableException();
}
if (!Objects.equals("2", lineReader.readLine())) {
throw new NotParsableException();
}
if (!Objects.equals("3", lineReader.readLine())) {
throw new NotParsableException();
}
return new CLogEntry();
}
}
));
}
}
private static class EmptyListLineLogReader extends ListLineLogReader {
private EmptyListLineLogReader(final String... inputList) {
super(inputList);
}
private EmptyListLineLogReader(final List<String> inputList) {
super(inputList);
}
@Override
protected Set<EntryReader> entryReaders() {
return new HashSet<>();
}
}
private static class ALogEntry implements LogEntry { }
private static class BLogEntry implements LogEntry { }
private static class CLogEntry implements LogEntry { }
}
A snapshot of my Github repository containing this code can be found here.