Werner Thumann
Werner Thumann

Reputation: 525

Java OutputStream reading lines of strings

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

Answers (1)

Werner Thumann
Werner Thumann

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

Related Questions