From 4715c0cf07eb7b565362579020f91902b13081fc Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Sat, 4 Dec 2021 12:34:07 +0100 Subject: [PATCH 1/2] Include `LookaheadStream` from analysis-model. --- .../edu/hm/hafner/util/LookaheadStream.java | 137 ++++++++++++++++++ .../hm/hafner/util/LookaheadStreamTest.java | 83 +++++++++++ 2 files changed, 220 insertions(+) create mode 100644 src/main/java/edu/hm/hafner/util/LookaheadStream.java create mode 100644 src/test/java/edu/hm/hafner/util/LookaheadStreamTest.java diff --git a/src/main/java/edu/hm/hafner/util/LookaheadStream.java b/src/main/java/edu/hm/hafner/util/LookaheadStream.java new file mode 100644 index 00000000..fe3da4bf --- /dev/null +++ b/src/main/java/edu/hm/hafner/util/LookaheadStream.java @@ -0,0 +1,137 @@ +package edu.hm.hafner.util; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; + +/** + * A stream of lines with a lookahead of one line. Useful to parse a stream of lines when it is required to check if the + * next line matches a given regular expression. + * + * @author Ullrich Hafner + */ +public class LookaheadStream implements AutoCloseable { + private final Stream stream; + private final Iterator lineIterator; + private final String fileName; + + private boolean isLookaheadFilled = false; + private String lookaheadLine = StringUtils.EMPTY; + private int line = 0; + + /** + * Wraps the specified stream of lines into a {@link LookaheadStream}. + * + * @param stream + * the lines to wrap + */ + public LookaheadStream(final Stream stream) { + this(stream, StringUtils.EMPTY); + } + + /** + * Wraps the specified stream of lines into a {@link LookaheadStream}. + * + * @param stream + * the lines to wrap + * @param fileName + * the file name of the stream + */ + public LookaheadStream(final Stream stream, final String fileName) { + this.stream = stream; + lineIterator = stream.iterator(); + this.fileName = fileName; + } + + public String getFileName() { + return fileName; + } + + @Override + public void close() { + stream.close(); + } + + /** + * Returns {@code true} if the stream has more elements. (In other words, returns {@code true} if {@link #next} + * would return an element rather than throwing an exception.) + * + * @return {@code true} if the stream has more elements + */ + public boolean hasNext() { + return lineIterator.hasNext() || isLookaheadFilled; + } + + /** + * Returns {@code true} if the stream has at least one more element that matches the given regular expression. + * + * @param regexp + * the regular expression + * + * @return {@code true} if the stream has more elements that match the regexp + */ + public boolean hasNext(final String regexp) { + if (!isLookaheadFilled) { + if (!hasNext()) { + return false; + } + fillLookahead(); + } + + return Pattern.compile(regexp).matcher(lookaheadLine).find(); + } + + /** + * Peeks the next element in the stream. I.e., the next element is returned but not removed from the stream so that + * the next call of {@link #next()} will again return this value. + * + * @return the next element in the stream + * @throws NoSuchElementException + * if the stream has no more elements + */ + public String peekNext() { + if (!isLookaheadFilled) { + fillLookahead(); + } + return lookaheadLine; + } + + private void fillLookahead() { + lookaheadLine = lineIterator.next(); + isLookaheadFilled = true; + } + + /** + * Returns the next element in the stream. + * + * @return the next element in the stream + * @throws NoSuchElementException + * if the stream has no more elements + */ + public String next() { + line++; + + if (isLookaheadFilled) { + isLookaheadFilled = false; + return lookaheadLine; + } + return lineIterator.next(); + } + + /** + * Returns the line number of the line that has been handed out using the {@link #next()} method. + * + * @return the current line, or 0 if no line has been handed out yet + */ + public int getLine() { + return line; + } + + @Override + public String toString() { + return String.format("[%d] -> '%s'", line, lookaheadLine); + } +} diff --git a/src/test/java/edu/hm/hafner/util/LookaheadStreamTest.java b/src/test/java/edu/hm/hafner/util/LookaheadStreamTest.java new file mode 100644 index 00000000..5fd97228 --- /dev/null +++ b/src/test/java/edu/hm/hafner/util/LookaheadStreamTest.java @@ -0,0 +1,83 @@ +package edu.hm.hafner.util; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests the class {@link LookaheadStream}. + * + * @author Ullrich Hafner + */ +class LookaheadStreamTest extends ResourceTest { + private static final String FIRST_LINE = "First Line"; + + @Test + void shouldHandleEmptyLines() { + try (LookaheadStream stream = new LookaheadStream(getTextLinesAsStream(""))) { + assertThat(stream.hasNext()).isFalse(); + assertThat(stream.getLine()).isEqualTo(0); + assertThatExceptionOfType(java.util.NoSuchElementException.class).isThrownBy(stream::next); + } + } + + @Test + void shouldReturnSingleLine() { + try (LookaheadStream stream = new LookaheadStream(getTextLinesAsStream(FIRST_LINE))) { + assertThat(stream.hasNext()).isTrue(); + assertThat(stream.next()).isEqualTo(FIRST_LINE); + assertThat(stream.getLine()).isEqualTo(1); + + assertThat(stream.hasNext()).isFalse(); + } + } + + @Test + void shouldReturnMultipleLines() { + try (LookaheadStream stream = new LookaheadStream(getTextLinesAsStream("First Line\nSecond Line"))) { + assertThat(stream.hasNext()).isTrue(); + assertThat(stream.next()).isEqualTo(FIRST_LINE); + assertThat(stream.getLine()).isEqualTo(1); + assertThat(stream.hasNext()).isTrue(); + assertThat(stream.next()).isEqualTo("Second Line"); + assertThat(stream.getLine()).isEqualTo(2); + + assertThat(stream.hasNext()).isFalse(); + } + } + + @Test + void shouldReturnLookAheadLines() { + try (LookaheadStream stream = new LookaheadStream(getTextLinesAsStream("First Line\nSecond Line"))) { + assertThat(stream.hasNext()).isTrue(); + assertThat(stream.hasNext("Line$")).isTrue(); + assertThat(stream.hasNext("Second.*")).isFalse(); + assertThat(stream.next()).isEqualTo(FIRST_LINE); + assertThat(stream.getLine()).isEqualTo(1); + + assertThat(stream.hasNext()).isTrue(); + assertThat(stream.hasNext("Line$")).isTrue(); + assertThat(stream.hasNext("First.*")).isFalse(); + assertThat(stream.next()).isEqualTo("Second Line"); + assertThat(stream.getLine()).isEqualTo(2); + + assertThat(stream.hasNext()).isFalse(); + assertThat(stream.hasNext(".*")).isFalse(); + } + } + + @Test + @SuppressWarnings("unchecked") + void shouldCloseStream() { + try (Stream lines = mock(Stream.class)) { + try (LookaheadStream stream = new LookaheadStream(lines)) { + assertThat(stream.getLine()).isZero(); + } + + verify(lines).close(); // lines will be closed by stream + } + } +} From 99d7f7b53ddf657ab99b6ec3012a5ae7eb696058 Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Sat, 4 Dec 2021 12:53:23 +0100 Subject: [PATCH 2/2] Improve coverage of `LookaheadStreamTest`. --- .../edu/hm/hafner/util/LookaheadStream.java | 2 +- .../hm/hafner/util/LookaheadStreamTest.java | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/edu/hm/hafner/util/LookaheadStream.java b/src/main/java/edu/hm/hafner/util/LookaheadStream.java index fe3da4bf..bb4d5cde 100644 --- a/src/main/java/edu/hm/hafner/util/LookaheadStream.java +++ b/src/main/java/edu/hm/hafner/util/LookaheadStream.java @@ -130,7 +130,7 @@ public int getLine() { return line; } - @Override + @Override @Generated public String toString() { return String.format("[%d] -> '%s'", line, lookaheadLine); } diff --git a/src/test/java/edu/hm/hafner/util/LookaheadStreamTest.java b/src/test/java/edu/hm/hafner/util/LookaheadStreamTest.java index 5fd97228..62116e2d 100644 --- a/src/test/java/edu/hm/hafner/util/LookaheadStreamTest.java +++ b/src/test/java/edu/hm/hafner/util/LookaheadStreamTest.java @@ -2,9 +2,10 @@ import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.*; +import static edu.hm.hafner.util.assertions.Assertions.*; import static org.mockito.Mockito.*; /** @@ -14,12 +15,14 @@ */ class LookaheadStreamTest extends ResourceTest { private static final String FIRST_LINE = "First Line"; + private static final String EMPTY = StringUtils.EMPTY; @Test void shouldHandleEmptyLines() { try (LookaheadStream stream = new LookaheadStream(getTextLinesAsStream(""))) { - assertThat(stream.hasNext()).isFalse(); - assertThat(stream.getLine()).isEqualTo(0); + assertThat(stream).doesNotHaveNext().hasLine(0).hasFileName(EMPTY); + + assertThatExceptionOfType(java.util.NoSuchElementException.class).isThrownBy(stream::peekNext); assertThatExceptionOfType(java.util.NoSuchElementException.class).isThrownBy(stream::next); } } @@ -27,11 +30,14 @@ void shouldHandleEmptyLines() { @Test void shouldReturnSingleLine() { try (LookaheadStream stream = new LookaheadStream(getTextLinesAsStream(FIRST_LINE))) { - assertThat(stream.hasNext()).isTrue(); - assertThat(stream.next()).isEqualTo(FIRST_LINE); - assertThat(stream.getLine()).isEqualTo(1); + assertThat(stream).hasNext().hasLine(0); + assertThat(stream.peekNext()).isEqualTo(FIRST_LINE); + // Now reading from the buffer: + assertThat(stream).hasNext(); + assertThat(stream.peekNext()).isEqualTo(FIRST_LINE); - assertThat(stream.hasNext()).isFalse(); + assertThat(stream.next()).isEqualTo(FIRST_LINE); + assertThat(stream).hasLine(1).doesNotHaveNext(); } }