package org.netbeans.gradle.project.output;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.jtrim.utils.ExceptionHelper;
public final class ReaderInputStream extends InputStream {
private final Reader reader;
private final AtomicReference<byte[]> cacheRef;
private final Lock encoderLock;
private final CharsetEncoder encoder;
private boolean eofReached;
private char[] remainingChars;
public ReaderInputStream(Reader reader) {
this(reader, Charset.defaultCharset());
}
public ReaderInputStream(Reader reader, Charset encoding) {
ExceptionHelper.checkNotNullArgument(reader, "reader");
ExceptionHelper.checkNotNullArgument(encoding, "encoding");
this.reader = reader;
this.encoderLock = new ReentrantLock();
this.encoder = encoding.newEncoder();
this.remainingChars = null;
this.eofReached = false;
this.cacheRef = new AtomicReference<>(new byte[0]);
}
private int readFromCache(byte[] b, int offset, int length) {
byte[] cache;
byte[] newCache;
int toRead;
do {
cache = cacheRef.get();
toRead = Math.min(cache.length, length);
System.arraycopy(cache, 0, b, offset, toRead);
newCache = new byte[cache.length - toRead];
System.arraycopy(cache, toRead, newCache, 0, newCache.length);
} while (!cacheRef.compareAndSet(cache, newCache));
return toRead;
}
private ByteBuffer encodeChars(char[] chars, int charCount) throws CharacterCodingException {
return encodeChars(chars, charCount, false);
}
private ByteBuffer encodeChars(char[] chars, int charCount, boolean finalBytes) throws CharacterCodingException {
encoderLock.lock();
try {
CharBuffer input;
if (remainingChars == null || remainingChars.length == 0) {
input = CharBuffer.wrap(chars, 0, charCount);
}
else {
char[] toPrepend = remainingChars;
remainingChars = null;
char[] newChars = new char[charCount + toPrepend.length];
System.arraycopy(toPrepend, 0, newChars, 0, toPrepend.length);
System.arraycopy(chars, 0, newChars, toPrepend.length, charCount);
input = CharBuffer.wrap(newChars);
}
int n = (int)(input.remaining() * encoder.averageBytesPerChar());
ByteBuffer out = ByteBuffer.allocate(n);
while (true) {
CoderResult result = encoder.encode(input, out, finalBytes);
if (result.isOverflow()) {
n = 2 * n + 1;
ByteBuffer newOut = ByteBuffer.allocate(n);
out.flip();
newOut.put(out);
out = newOut;
}
else if (result.isError()) {
result.throwException();
}
else {
int remainingCount = input.remaining();
if (remainingCount > 0) {
char[] currentRemaining = new char[remainingCount];
input.get(currentRemaining);
remainingChars = currentRemaining;
}
break;
}
}
out.flip();
return out;
} finally {
encoderLock.unlock();
}
}
private ByteBuffer completeStream() throws CharacterCodingException {
encoderLock.lock();
try {
if (eofReached) {
return null;
}
eofReached = true;
char[] finalChars = remainingChars;
remainingChars = null;
if (finalChars == null) {
finalChars = new char[0];
}
ByteBuffer out = encodeChars(finalChars, finalChars.length, true);
int n = out.capacity();
while (true) {
CoderResult result = encoder.flush(out);
if (result.isUnderflow()) {
break;
}
if (result.isOverflow()) {
n = 2 * n + 1;
ByteBuffer newOut = ByteBuffer.allocate(n);
out.flip();
newOut.put(out);
out = newOut;
}
else {
result.throwException();
}
}
out.flip();
return out;
} finally {
encoderLock.unlock();
}
}
private boolean readToCache(int requiredBytes) throws IOException {
assert requiredBytes > 0;
// We rely on the encoder to choose the number of bytes to read but
// it does not have to be actually accurate, it only matters
// performance wise but this is not a performance critical code.
int toRead = (int)((float)requiredBytes / encoder.averageBytesPerChar()) + 1;
toRead = Math.max(toRead, requiredBytes);
char[] readChars = new char[toRead];
int readCount = reader.read(readChars);
ByteBuffer encodedBuffer;
if (readCount <= 0) {
// readCount should never be zero but if reader returns zero
// regardless, assume that it believes that EOF has been
// reached.
encodedBuffer = completeStream();
if (encodedBuffer == null || encodedBuffer.remaining() <= 0) {
return false;
}
}
else {
encodedBuffer = encodeChars(readChars, readCount);
}
byte[] encoded = new byte[encodedBuffer.remaining()];
encodedBuffer.get(encoded);
appendToCache(encoded);
return true;
}
private void appendToCache(byte[] newBytes) {
byte[] oldCache;
byte[] newCache;
do {
oldCache = cacheRef.get();
newCache = new byte[oldCache.length + newBytes.length];
System.arraycopy(oldCache, 0, newCache, 0, oldCache.length);
System.arraycopy(newBytes, 0, newCache, oldCache.length, newBytes.length);
} while (!cacheRef.compareAndSet(oldCache, newCache));
}
@Override
public int read() throws IOException {
byte[] result = new byte[1];
if (read(result) <= 0) {
// Althouth the above read should never return zero.
return -1;
}
else {
return (int)result[0] & 0xFF;
}
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
ExceptionHelper.checkNotNullArgument(b, "b");
ExceptionHelper.checkArgumentInRange(off, 0, b.length, "off");
ExceptionHelper.checkArgumentInRange(len, 0, b.length - off, "len");
// Note that while this method is implemented to be thread-safe
// calling it concurrently is unadvised, since read is not atomic.
if (len == 0) {
return 0;
}
int currentOffset = off;
int currentLength = len;
int readCount = 0;
do {
int currentRead = readFromCache(b, currentOffset, currentLength);
readCount += currentRead;
currentOffset += currentRead;
currentLength -= currentRead;
if (readCount > 0) {
return readCount;
}
} while (readToCache(currentLength));
return readCount > 0 ? readCount : -1;
}
@Override
public void close() throws IOException {
reader.close();
}
@Override
public void mark(int readlimit) {
}
@Override
public void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
@Override
public boolean markSupported() {
return false;
}
}