Reputation: 525
I use an API (for more info, see below) which accepts an OutputStream to capture data. Instead, I want to provide a Consumer of Strings which consumes the data line after line. Hence I have to write an OutputStream implementation which wraps such a Consumer. This is the easiest I can think of:
import java.io.OutputStream;
import java.util.Objects;
import java.util.function.Consumer;
public class OutputStreamConsumer extends OutputStream {
private final Consumer<String> consumer;
private final StringBuilder sb = new StringBuilder();
public OutputStreamConsumer(Consumer<String> consumer) {
this.consumer = Objects.requireNonNull(consumer);
}
@Override
public void write(int b) {
char c = (char) b;
if (c == '\r') {
return;
}
if (c == '\n') {
consume();
return;
}
this.sb.append(c);
}
@Override
public void close() {
if (sb.length() != 0) {
consume();
}
}
private void consume() {
this.consumer.accept(this.sb.toString());
this.sb.delete(0, Integer.MAX_VALUE);
}
}
However, this is probably not enough for production code in terms of encoding and performance. I think that the necessary logic is already contained in InputStreamReader and BufferedReader but I cannot use these classes in this scenario. What is the best way to write this kind of OutputStream? What jdk classes can I use to avoid writing a bunch of low level code handling encoding, end of lines etc.
Concrete use-case
In a Gradle plugin project, I start an external process using Gradle's project API: ExecSpec. There I can set OutputStreams using the methods setStandardOutput and setErrorOutput in order to capture the output of the process.
Upvotes: 3
Views: 1715
Reputation: 525
Since there are no answers so far, I will post my "best" solution as the time of writing. Feel free to post better solutions.
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.Consumer;
public class LineReadingOutputStream extends OutputStream {
private static final byte CR = '\r';
private static final byte LF = '\n';
private final Consumer<String> consumer;
private final StringBuilder stringBuilder = new StringBuilder();
private boolean lastCR = false;
private LineReadingOutputStream(final Consumer<String> consumer) {
this.consumer = Objects.requireNonNull(consumer);
}
@Override
public void write(final int b) throws IOException {
write(new byte[]{(byte) b});
}
@Override
public void write(final byte[] b, int start, final int len) {
if (b == null) {
throw new NullPointerException();
}
if (len < 0) {
throw new IllegalArgumentException();
}
final int end = start + len;
if ((start < 0) || (start > b.length) || (end < 0) || (end > b.length)) {
throw new IndexOutOfBoundsException();
}
if (this.lastCR && start < end && b[start] == LF) {
start++;
this.lastCR = false;
} else if (start < end) {
this.lastCR = b[end - 1] == CR;
}
int base = start;
for (int i = start; i < end; i++) {
if (b[i] == LF || b[i] == CR) {
final String chunk = asString(b, base, i);
this.stringBuilder.append(chunk);
consume();
}
if (b[i] == LF) {
base = i + 1;
} else if (b[i] == CR) {
if (i < end - 1 && b[i + 1] == LF) {
base = i + 2;
i++;
} else {
base = i + 1;
}
}
}
final String chunk = asString(b, base, end);
this.stringBuilder.append(chunk);
}
@Override
public void close() {
if (this.stringBuilder.length() != 0) {
consume();
}
}
private static String asString(final byte[] b, final int start, final int end) {
if (start > end) {
throw new IllegalArgumentException();
}
if (start == end) {
return "";
}
final byte[] copy = Arrays.copyOfRange(b, start, end);
return new String(copy, StandardCharsets.UTF_8);
}
private void consume() {
this.consumer.accept(this.stringBuilder.toString());
this.stringBuilder.delete(0, Integer.MAX_VALUE);
}
}
Upvotes: 3