Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include LookaheadStream from analysis-model #467

Merged
merged 2 commits into from
Dec 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions src/main/java/edu/hm/hafner/util/LookaheadStream.java
Original file line number Diff line number Diff line change
@@ -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<String> stream;
private final Iterator<String> 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<String> 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<String> 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 @Generated
public String toString() {
return String.format("[%d] -> '%s'", line, lookaheadLine);
}
}
89 changes: 89 additions & 0 deletions src/test/java/edu/hm/hafner/util/LookaheadStreamTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package edu.hm.hafner.util;

import java.util.stream.Stream;

import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Test;

import static edu.hm.hafner.util.assertions.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";
private static final String EMPTY = StringUtils.EMPTY;

@Test
void shouldHandleEmptyLines() {
try (LookaheadStream stream = new LookaheadStream(getTextLinesAsStream(""))) {
assertThat(stream).doesNotHaveNext().hasLine(0).hasFileName(EMPTY);

assertThatExceptionOfType(java.util.NoSuchElementException.class).isThrownBy(stream::peekNext);
assertThatExceptionOfType(java.util.NoSuchElementException.class).isThrownBy(stream::next);
}
}

@Test
void shouldReturnSingleLine() {
try (LookaheadStream stream = new LookaheadStream(getTextLinesAsStream(FIRST_LINE))) {
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.next()).isEqualTo(FIRST_LINE);
assertThat(stream).hasLine(1).doesNotHaveNext();
}
}

@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<String> lines = mock(Stream.class)) {
try (LookaheadStream stream = new LookaheadStream(lines)) {
assertThat(stream.getLine()).isZero();
}

verify(lines).close(); // lines will be closed by stream
}
}
}