/**
* Copyright 2016 LinkedIn Corp. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*/
package com.github.ambry.commons;
import com.github.ambry.router.Callback;
import com.github.ambry.router.ReadableStreamChannel;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* Class that converts a {@link ReadableStreamChannel} into a blocking {@link InputStream}.
* <p/>
* This class is not thread-safe and will result in undefined behaviour if accesses to the stream are not synchronized.
*/
public class ReadableStreamChannelInputStream extends InputStream {
private final ByteBufferAsyncWritableChannel asyncWritableChannel = new ByteBufferAsyncWritableChannel();
private final CloseWriteChannelCallback callback = new CloseWriteChannelCallback(asyncWritableChannel);
private final ReadableStreamChannel readableStreamChannel;
private ByteBuffer currentChunk = null;
private volatile long bytesAvailable;
/**
* Create a ReadableStreamChannelInputStream with the given {@link ReadableStreamChannel}.
* @param readableStreamChannel the {@link ReadableStreamChannel} that needs to be converted into an
* {@link InputStream}.
*/
public ReadableStreamChannelInputStream(ReadableStreamChannel readableStreamChannel) {
this.readableStreamChannel = readableStreamChannel;
bytesAvailable = readableStreamChannel.getSize() < 0 ? 0 : readableStreamChannel.getSize();
readableStreamChannel.readInto(asyncWritableChannel, callback);
}
@Override
public int available() {
return currentChunk == null ? 0 : currentChunk.remaining();
}
@Override
public int read() throws IOException {
int data = -1;
if (loadData()) {
data = currentChunk.get() & 0xFF;
reportBytesRead(1);
}
return data;
}
@Override
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int startOff = off;
while (len > 0 && loadData()) {
int toRead = Math.min(len, currentChunk.remaining());
currentChunk.get(b, off, toRead);
len -= toRead;
off += toRead;
reportBytesRead(toRead);
}
int bytesRead = off - startOff;
if (bytesRead <= 0) {
bytesRead = -1;
}
return bytesRead;
}
@Override
public void close() throws IOException {
readableStreamChannel.close();
asyncWritableChannel.close();
}
/**
* Loads more data for reading. Blocks until data is either available or no more data is expected.
* @return {@code true} if data is available for reading. {@link false} otherwise.
* @throws IllegalStateException if the wait for the next chunk is interrupted.
* @throws IOException if there is any problem with I/O.
*/
private boolean loadData() throws IOException {
if (currentChunk == null || !currentChunk.hasRemaining()) {
if (currentChunk != null) {
asyncWritableChannel.resolveOldestChunk(null);
}
try {
while (true) {
currentChunk = asyncWritableChannel.getNextChunk();
if (currentChunk != null && !currentChunk.hasRemaining()) {
asyncWritableChannel.resolveOldestChunk(null);
} else {
break;
}
}
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
if (currentChunk == null) {
if (callback.exception != null) {
if (callback.exception instanceof IOException) {
throw (IOException) callback.exception;
} else {
throw new IllegalStateException(callback.exception);
}
} else if (bytesAvailable != 0) {
throw new IllegalStateException("All the bytes available could not be read");
}
}
return currentChunk != null;
}
/**
* Keeps track of the bytes read.
* @param count the number of bytes read in this read.
*/
private void reportBytesRead(int count) {
if (bytesAvailable >= count) {
bytesAvailable -= count;
}
}
/**
* Callback for {@link ByteBufferAsyncWritableChannel} that closes the channel on
* {@link #onCompletion(Long, Exception)}.
*/
private static class CloseWriteChannelCallback implements Callback<Long> {
/**
* Stores any exception that occurred.
*/
public Exception exception = null;
private final ByteBufferAsyncWritableChannel channel;
/**
* Creates a callback to close {@code channel} on {@link #onCompletion(Long, Exception)}.
* @param channel the {@link ByteBufferAsyncWritableChannel} that needs to be closed.
*/
public CloseWriteChannelCallback(ByteBufferAsyncWritableChannel channel) {
this.channel = channel;
}
@Override
public void onCompletion(Long result, Exception exception) {
this.exception = exception;
channel.close();
}
}
}