I'm still working on a parser that can parse log entries from a game called HearthStone, the overall idea is that it will read the log file live when the game is running, parses the log file and show interesting and useful data in real time.
For the question the focus is on the API I have created to parse log entries. I have written this code using Java 8 and my intention is that the API has at least surprises as possible, everything that makes you wonder might be a candidate to include in your review.
This question is a follow-up to Extensible Abstract Log Reader and is closely related to Parsing log files of HearthStone: The API.
Class summary:
- com.github.skiwi2.hearthmonitor.logreader
- LogReader
- Interface
- Used to read log entries.
- CloseableLogReader
- Interface
- Used to read log entries from a closeable resource.
- DefaultLogReader
- Class
- Used to read log entries from a log source via an iterator.
- It is encouraged to extend this class and pass the iterator via the subclass.
- LogReaderUtils
- Utility class to create a LogReader from other sources.
- EntryParser
- Interface
- Used to parse lines from a log source
- It has the option to read more lines from the log source via a line reader if deemed necessary.
- LineReader
- Interface
- Used to read lines from an input source.
- MatchingIterator
- Interface
- Used to iterate over elements while also having the possibility to check if the next elements matches a predicate.
- IteratorUtils
- Utility class to do complex things with iterators.
- NotParsableException
- Exception to indicate that a log entry is not parsable.
- NotReadableException
- Exception to indicate that a log entry is not readable.
- LogReader
- com.github.skiwi2.hearthmonitor.logreader.logreaders
- FileLogReader
- Used to read log entries from a log file.
- ListLogReader
- Used to read log entries from a list.
- MonitoringFileLogReader
- Used to read log entries from a log file, blocking until more input is available.
- FileLogReader
/**
* Used to read log entries.
*
* @author Frank van Heeswijk
*/
public interface LogReader {
/**
* Returns the next log entry.
*
* The NotReadableException has more information available.
* You can recover the lines that could not be read by calling NotReadableException#getLines.
* You can see which exceptions were thrown internally by calling NotReadableException#getOccurredExceptions.
*
* @return The next log entry.
* @throws NotReadableException If the log entry could not be read.
* @throws java.util.NoSuchElementException If there is no more input.
*/
LogEntry readNextEntry() throws NotReadableException;
/**
* Returns whether there is a next log entry.
*
* @return Whether there is a next log entry.
*/
boolean hasNextEntry();
}
/**
* Used to read log entries from a closeable resource.
*
* @author Frank van Heeswijk
*/
public interface CloseableLogReader extends LogReader, AutoCloseable { }
/**
* Used to read log entries from a log source via an iterator.
*
* It is encouraged to extend this class and pass the iterator via the subclass.
*
* @author Frank van Heeswijk
*/
public class DefaultLogReader implements LogReader {
private final Set<? extends EntryParser> entryParsers;
private final MatchingIterator<String> matchingIterator;
private final List<String> linesInMemory = new ArrayList<>();
/**
* Constructs a new DefaultLogReader instance.
*
* @param entryParsers The set of entry parsers
* @param readIterator The iterator used to read lines from the log source
* @param filterPredicate The predicate to use to filter the lines read from the log source
* @throws java.lang.NullPointerException If entryParsers, readIterator or filterPredicate is null.
*/
protected DefaultLogReader(final Set<? extends EntryParser> entryParsers, final Iterator<String> readIterator, final Predicate<? super String> filterPredicate) {
Objects.requireNonNull(filterPredicate, "filterPredicate");
Objects.requireNonNull(readIterator, "readIterator");
this.entryParsers = Objects.requireNonNull(entryParsers, "entryParsers");
Iterator<String> filteredIterator = IteratorUtils.filteredIterator(readIterator, filterPredicate);
this.matchingIterator = MatchingIterator.fromIterator(filteredIterator);
}
/**
* Constructs a new DefaultLogReader instance.
*
* The filter predicate can be used to filter the lines you want to traverse.
*
* @param entryParsers The set of entry parsers
* @param readIterator The iterator used to read lines from the log source
* @throws java.lang.NullPointerException If entryParsers or readIterator is null.
*/
protected DefaultLogReader(final Set<? extends EntryParser> entryParsers, final Iterator<String> readIterator) {
this(entryParsers, readIterator, string -> true);
}
@Override
public LogEntry readNextEntry() throws NotReadableException {
List<Exception> occurredExceptions = new ArrayList<>();
String line = matchingIterator.next();
linesInMemory.add(line);
for (EntryParser entryParser : entryParsers) {
if (!entryParser.isParsable(line)) {
continue;
}
try {
LogEntry result = entryParser.parse(line, new LineReader() {
@Override
public String readNextLine() {
String nextLine = matchingIterator.next();
linesInMemory.add(nextLine);
return nextLine;
}
@Override
public boolean hasNextLine() {
return matchingIterator.hasNext();
}
@Override
public boolean nextLineMatches(final Predicate<? super String> condition) {
return matchingIterator.nextMatches(condition);
}
});
linesInMemory.clear();
return result;
} catch (NotParsableException | NoSuchElementException ex) {
occurredExceptions.add(ex);
//try next entry parser
}
}
List<String> notParsableLines = new ArrayList<>(linesInMemory);
linesInMemory.clear();
throw new NotReadableException(notParsableLines, occurredExceptions);
}
@Override
public boolean hasNextEntry() {
return matchingIterator.hasNext();
}
}
/**
* Utility class to create a LogReader from other sources.
*
* @author Frank van Heeswijk
*/
public final class LogReaderUtils {
private LogReaderUtils() {
throw new UnsupportedOperationException();
}
/**
* Creates a LogReader that can read log entries from an input string, a LineReader for the extra lines and a read condition.
*
* The LogReader will attempt to read extra lines from the LineReader as long as the read condition is met using the supplied entry readers.
*
* Note: The input will always be included and not checked against the extra read condition.
*
* @param input The input line
* @param extraLineReader The extra line reader
* @param extraReadCondition The extra read condition
* @param entryParsers The set of entry parsers
* @return A new LogReader that can read log entries from the input string, the LineReader for the extra lines and the read condition.
*/
public static LogReader fromInputAndExtraLineReader(final String input, final LineReader extraLineReader, final Predicate<? super String> extraReadCondition, final Set<? extends EntryParser> entryParsers) {
Objects.requireNonNull(extraLineReader, "extraLineReader");
Objects.requireNonNull(extraReadCondition, "extraReadCondition");
Objects.requireNonNull(entryParsers, "entryParsers");
return new DefaultLogReader(entryParsers, createReadIteratorForFromInputAndExtraLineReader(input, extraLineReader, extraReadCondition));
}
/**
* Returns an iterator for the given input and extra line reader.
*
* @param input The given input
* @param extraLineReader The given extra line reader
* @param extraReadCondition The given extra read condition
* @return The iterator for the given input and extra line reader.
*/
private static Iterator<String> createReadIteratorForFromInputAndExtraLineReader(final String input, final LineReader extraLineReader, final Predicate<? super String> extraReadCondition) {
LineReader conditionalLineReader = LineReader.readWhile(extraLineReader, extraReadCondition);
Iterator<String> lineReaderIterator = new Iterator<String>() {
@Override
public boolean hasNext() {
return conditionalLineReader.hasNextLine();
}
@Override
public String next() {
return conditionalLineReader.readNextLine();
}
};
return Stream.concat(
Stream.of(input),
StreamSupport.stream(Spliterators.spliteratorUnknownSize(lineReaderIterator, Spliterator.NONNULL), false)
).iterator();
}
}
/**
* Used to parse lines from a log source.
*
* It has the option to read more lines from the log source via a line reader if deemed necessary.
*
* @author Frank van Heeswijk
*/
public interface EntryParser {
/**
* Returns whether this entry parser can parse the input.
*
* @param input The input check parsability for
* @return Whether this entry parser 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.
*/
LogEntry parse(final String input, final LineReader lineReader) throws NotParsableException;
}
/**
* Used to read lines from an input source.
*
* @author Frank van Heeswijk
*/
public interface LineReader {
/**
* Reads the next line.
*
* @return The next line.
* @throws java.util.NoSuchElementException If there are no lines left anymore
*/
String readNextLine() throws NoSuchElementException;
/**
* Returns whether there is a next line to read.
*
* @return Whether there is a next line to read.
*/
boolean hasNextLine();
/**
* Returns whether the next line matches the given condition.
*
* @param condition The condition that the next line should match
* @return Whether the next line matches the given condition.
*/
boolean nextLineMatches(final Predicate<? super String> condition);
/**
* Returns a LineReader that reads from another LineReader while the read condition is true.
*
* @param lineReader The LineReader to be read from
* @param readCondition The read condition
* @return A LineReader that reads from another LineReader while the read condition is true.
* @throws java.lang.NullPointerException If lineReader or readCondition is null.
*/
static LineReader readWhile(final LineReader lineReader, final Predicate<? super String> readCondition) {
Objects.requireNonNull(lineReader, "lineReader");
Objects.requireNonNull(readCondition, "readCondition");
return new LineReader() {
@Override
public String readNextLine() throws NoSuchElementException {
if (!lineReader.nextLineMatches(readCondition)) {
throw new NoSuchElementException();
}
return lineReader.readNextLine();
}
@Override
public boolean hasNextLine() {
return lineReader.nextLineMatches(readCondition);
}
@Override
public boolean nextLineMatches(final Predicate<? super String> condition) {
return lineReader.nextLineMatches(line -> (readCondition.test(line) && condition.test(line)));
}
};
}
}
/**
* Used to iterate over elements while also having the possibility to check if the next elements matches a predicate.
*
* @author Frank van Heeswijk
* @param <E> The type of elements
*/
public interface MatchingIterator<E> extends Iterator<E> {
/**
* Returns whether the next element matches the given condition.
*
* @param condition The condition predicate that the next element may match
* @return Whether the next element matches the given condition.
* @throws java.lang.NullPointerException If condition is null.
*/
boolean nextMatches(final Predicate<? super E> condition);
/**
* Returns a matching iterator constructed from an iterator.
*
* @param iterator The input iterator
* @param <E> The type of the elements in the iterator
* @return The matching iterator constructed from the iterator.
*/
static <E> MatchingIterator<E> fromIterator(final Iterator<? extends E> iterator) {
return new MatchingIterator<E>() {
private final List<E> peekedElements = new ArrayList<>();
@Override
public boolean hasNext() {
Optional<E> peekElement = peek();
return peekElement.isPresent();
}
@Override
public E next() {
if (!peekedElements.isEmpty()) {
return peekedElements.remove(0);
}
return iterator.next();
}
@Override
public boolean nextMatches(final Predicate<? super E> condition) {
Objects.requireNonNull(condition, "condition");
Optional<E> peekElement = peek();
return (peekElement.isPresent() && condition.test(peekElement.get()));
}
/**
* Returns an optional containing the next element, or an empty optional if there is none.
*
* @return The optional containing the next element, or the empty optional if there is none.
*/
private Optional<E> peek() {
if (!peekedElements.isEmpty()) {
return Optional.ofNullable(peekedElements.get(0));
}
if (!iterator.hasNext()) {
return Optional.empty();
}
E element = iterator.next();
peekedElements.add(element);
return Optional.ofNullable(element);
}
};
}
}
/**
* Utility class to do complex things with iterators.
*
* @author Frank van Heeswijk
*/
public final class IteratorUtils {
private IteratorUtils() {
throw new UnsupportedOperationException();
}
/**
* Returns an iterator that is a view on the given iterator only considering elements that match the condition predicate.
*
* @param iterator The input iterator
* @param condition The condition predicate that elements have to match
* @param <E> The type of elements
* @return The iterator that is a view on the given iterator only considering elements that match the condition predicate.
*/
public static <E> Iterator<E> filteredIterator(final Iterator<? extends E> iterator, final Predicate<? super E> condition) {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false)
.filter(condition)
.iterator();
}
}
/**
* Exception to indicate that a log entry is not parsable.
*
* @author Frank van Heeswijk
*/
public class NotParsableException extends Exception {
private static final long serialVersionUID = 3147294996191143729L;
public NotParsableException() {
}
public NotParsableException(final String message) {
super(message);
}
public NotParsableException(final String message, final Throwable cause) {
super(message, cause);
}
public NotParsableException(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<>();
private final List<Exception> occurredExceptions = new ArrayList<>();
/**
* Constructs a new NotReadableException instance.
*
* @param lines The lines that were not readable
* @param occurredExceptions The exceptions that occurred during reading
*/
public NotReadableException(final List<String> lines, final List<Exception> occurredExceptions) {
Objects.requireNonNull(lines, "lines");
Objects.requireNonNull(occurredExceptions, "occurredExceptions");
this.lines.addAll(lines);
this.occurredExceptions.addAll(occurredExceptions);
}
/**
* Returns the lines that were not readable.
*
* @return The lines that were not readable.
*/
public List<String> getLines() {
return new ArrayList<>(lines);
}
/**
* Returns the exceptions that occurred during reading.
*
* @return The exceptions that occurred during reading.
*/
public List<Exception> getOccurredExceptions() {
return new ArrayList<>(occurredExceptions);
}
}
/**
* Used to read log entries from a log file.
*
* @author Frank van Heeswijk
*/
public class FileLogReader extends DefaultLogReader implements CloseableLogReader {
private final BufferedReader bufferedReader;
/**
* Constructs a new FileLogReader instance.
*
* @param bufferedReader The buffered reader from which to read
* @param entryParsers The set of entry parsers
* @throws java.lang.NullPointerException If bufferedReader or entryParsers is null.
*/
public FileLogReader(final BufferedReader bufferedReader, final Set<? extends EntryParser> entryParsers) {
super(entryParsers, bufferedReader.lines().iterator());
this.bufferedReader = Objects.requireNonNull(bufferedReader, "bufferedReader");
}
/**
* Constructs a new FileLogReader instance.
*
* The filter predicate can be used to filter the lines you want to traverse.
*
* @param bufferedReader The buffered reader from which to read
* @param entryParsers The set of entry parsers
* @param filterPredicate The predicate to filter the lines with
* @throws java.lang.NullPointerException If bufferedReader, filterPredicate or entryParsers is null.
*/
public FileLogReader(final BufferedReader bufferedReader, final Set<? extends EntryParser> entryParsers, final Predicate<? super String> filterPredicate) {
super(entryParsers, bufferedReader.lines().iterator(), filterPredicate);
this.bufferedReader = Objects.requireNonNull(bufferedReader, "bufferedReader");
}
@Override
public void close() throws IOException {
bufferedReader.close();
}
}
/**
* Used to read log entries from a list.
*
* @author Frank van Heeswijk
*/
public class ListLogReader extends DefaultLogReader {
/**
* Constructs a new ListLogReader instance.
*
* This method saves a snapshot of the list at this time, and uses that to iterate over.
*
* @param inputList The input list to read from
* @param entryParsers The set of entry parsers
* @throws java.lang.NullPointerException If inputList or entryParsers is null.
*/
public ListLogReader(final List<String> inputList, final Set<? extends EntryParser> entryParsers) {
super(entryParsers, new ArrayList<>(inputList).iterator());
}
/**
* Constructs a new ListLogReader instance.
*
* This method saves a snapshot of the list at this time, and uses that to iterate over.
* The filter predicate can be used to filter the lines you want to traverse.
*
* @param inputList The input list to read from
* @param entryParsers The set of entry parsers
* @param filterPredicate The predicate to filter the lines with
* @throws java.lang.NullPointerException If inputList, filterPredicate or entryParsers is null.
*/
public ListLogReader(final List<String> inputList, final Set<? extends EntryParser> entryParsers, final Predicate<? super String> filterPredicate) {
super(entryParsers, new ArrayList<>(inputList).iterator(), filterPredicate);
}
}
/**
* Used to read log entries from a log file, blocking until more input is available.
*
* @author Frank van Heeswijk
*/
public class MonitoringFileLogReader extends DefaultLogReader implements CloseableLogReader {
private final BufferedReader bufferedReader;
/**
* Constructs a new MonitoringFileLogReader instance.
*
* @param bufferedReader The buffered reader from which to read
* @param entryParsers The set of entry parsers
* @throws java.lang.NullPointerException If bufferedReader or entryParsers is null.
*/
public MonitoringFileLogReader(final BufferedReader bufferedReader, final Set<? extends EntryParser> entryParsers) {
super(entryParsers, createReadIterator(bufferedReader));
this.bufferedReader = Objects.requireNonNull(bufferedReader, "bufferedReader");
}
/**
* Constructs a new MonitoringFileLogReader instance.
*
* The filter predicate can be used to filter the lines you want to traverse.
*
* @param bufferedReader The buffered reader from which to read
* @param entryParsers The set of entry parsers
* @param filterPredicate The predicate to filter the lines with
* @throws java.lang.NullPointerException If bufferedReader, filterPredicate or entryParsers is null.
*/
public MonitoringFileLogReader(final BufferedReader bufferedReader, final Set<? extends EntryParser> entryParsers, final Predicate<? super String> filterPredicate) {
super(entryParsers, createReadIterator(bufferedReader), filterPredicate);
this.bufferedReader = Objects.requireNonNull(bufferedReader, "bufferedReader");
}
/**
* Returns an iterator for the given buffered reader.
*
* @param bufferedReader The given buffered reader
* @return The iterator for the given buffered reader.
*/
private static Iterator<String> createReadIterator(final BufferedReader bufferedReader) {
Iterator<String> bufferedReaderIterator = bufferedReader.lines().iterator();
return new Iterator<String>() {
private boolean isInterrupted = false;
@Override
public boolean hasNext() {
return !isInterrupted;
}
@Override
public String next() {
if (isInterrupted) {
throw new NoSuchElementException();
}
try {
while (!bufferedReaderIterator.hasNext()) {
Thread.sleep(100);
}
return bufferedReaderIterator.next();
} catch (InterruptedException | UncheckedIOException ex) {
isInterrupted = true;
Thread.currentThread().isInterrupted();
throw new NoSuchElementException();
}
}
};
}
@Override
public void close() throws IOException {
bufferedReader.close();
}
}
This project (including its unit tests) is available on Github, example usage can also be found in the unit tests.
I'd like a review on every aspect of this code with an extra focus on the public API (I deem this stable for a next release).
com.github.skiwi2.hearthmonitor.logreaders
and onecom.github.skiwi2.hearthmonitor.logreader
package? What's the difference between them? They sound way too similar IMO. \$\endgroup\$com.github.skiwi2.hearthmonitor.logreader.logreaders
\$\endgroup\$LogEntry
an abstract base class/interface with subclasses for various concrete types of log entries? \$\endgroup\$public class CreateGameLogEntry implements LogEntry {
as example. \$\endgroup\$