
org.swisspush.reststorage.s3.FileReadStream Maven / Gradle / Ivy
package org.swisspush.reststorage.s3;
import io.vertx.core.AsyncResult;
import io.vertx.core.Context;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.streams.ReadStream;
import io.vertx.core.streams.impl.InboundBuffer;
import org.slf4j.Logger;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import static io.vertx.core.file.impl.AsyncFileImpl.DEFAULT_READ_BUFFER_SIZE;
import static org.slf4j.LoggerFactory.getLogger;
public class FileReadStream implements ReadStream, Closeable {
private static final Logger log = getLogger(FileReadStream.class);
private final long expectedSize;
private final String path;
private final ReadableByteChannel ch;
private final Vertx vertx;
private final Context context;
private boolean closed;
private boolean readInProgress;
private Handler dataHandler;
private Handler endHandler;
private Handler exceptionHandler;
private final InboundBuffer queue;
private final int readBufferSize = DEFAULT_READ_BUFFER_SIZE;
private long writtenBytes = 0;
/**
* @param expectedSize Actual file size which is expected to be streamed through that stream
* in bytes.
* @param path Token printed alongside the logs so when reading logs, we can see which
* log belongs to which file. A possible candidate is to use the file path
* but it theoretically can be anything which helps you to find logs
* related to your observed file.
* @param stream The file (or stream) we wanna observe.
*/
public FileReadStream(Vertx vertx, long expectedSize, String path, InputStream stream) {
this.vertx = vertx;
this.context = vertx.getOrCreateContext();
this.expectedSize = expectedSize;
this.path = path;
this.ch = Channels.newChannel(stream);
this.queue = new InboundBuffer<>(context, 0);
queue.handler(buff -> {
if (buff.length() > 0) {
handleData(buff);
} else {
handleEnd();
}
});
queue.drainHandler(v -> {
doRead();
});
}
public void close() {
closeInternal(null);
}
public void close(Handler> handler) {
closeInternal(handler);
}
@Override
public ReadStream exceptionHandler(Handler exceptionHandler) {
log.trace("exceptionHandler registered for reading '{}'", path);
check();
this.exceptionHandler = exceptionHandler;
return this;
}
@Override
public ReadStream handler(Handler handler) {
log.trace("handler registered");
check();
this.dataHandler = handler;
if (this.dataHandler != null && !this.closed) {
this.doRead();
} else {
queue.clear();
}
return this;
}
@Override
public ReadStream pause() {
log.debug("Pause reading at offset {} for '{}'", writtenBytes, path);
check();
queue.pause();
return this;
}
@Override
public ReadStream resume() {
log.debug("Resume reading at offset {} for '{}'", writtenBytes, path);
check();
if (!closed) {
queue.resume();
}
return this;
}
@Override
public ReadStream fetch(long amount) {
log.debug("fetch amount {}", amount);
queue.fetch(amount);
return this;
}
@Override
public ReadStream endHandler(Handler endHandler) {
log.trace("endHandler registered.");
check();
this.endHandler = endHandler;
log.debug("End handler called ({} bytes remaining) for '{}'", expectedSize - writtenBytes, path);
return this;
}
private void doRead() {
check();
doRead(ByteBuffer.allocate(readBufferSize));
}
private synchronized void doRead(ByteBuffer bb) {
if (!readInProgress) {
readInProgress = true;
Buffer buff = Buffer.buffer(readBufferSize);
doRead(buff, 0, bb, writtenBytes, ar -> {
if (ar.succeeded()) {
readInProgress = false;
Buffer buffer = ar.result();
writtenBytes += buffer.length();
// Empty buffer represents end of file
if (queue.write(buffer) && buffer.length() > 0) {
doRead(bb);
}
} else {
handleException(ar.cause());
}
});
}
}
private void doRead(Buffer writeBuff, int offset, ByteBuffer buff, long position, Handler> handler) {
// ReadableByteChannel doesn't have a completion handler, so we wrap it into
// an executeBlocking and use the future there
vertx.executeBlocking(future -> {
try {
Integer bytesRead = ch.read(buff);
future.complete(bytesRead);
} catch (IOException e) {
log.error("Failed to read data from buffer.", e);
future.fail(e);
}
}, res -> {
if (res.failed()) {
log.error("Failed to read data from buffer.", res.cause());
context.runOnContext((v) -> handler.handle(Future.failedFuture(res.cause())));
} else {
// Do the completed check
Integer bytesRead = (Integer) res.result();
if (bytesRead == -1) {
//End of file
context.runOnContext((v) -> {
buff.flip();
writeBuff.setBytes(offset, buff);
buff.compact();
handler.handle(Future.succeededFuture(writeBuff));
});
} else if (buff.hasRemaining()) {
long pos = position;
pos += bytesRead;
// resubmit
doRead(writeBuff, offset, buff, pos, handler);
} else {
// It's been fully written
context.runOnContext((v) -> {
buff.flip();
writeBuff.setBytes(offset, buff);
buff.compact();
handler.handle(Future.succeededFuture(writeBuff));
});
}
}
});
}
private void handleData(Buffer buff) {
Handler handler;
synchronized (this) {
handler = this.dataHandler;
}
if (handler != null) {
checkContext();
handler.handle(buff);
}
}
private synchronized void handleEnd() {
Handler endHandler;
synchronized (this) {
dataHandler = null;
endHandler = this.endHandler;
}
if (endHandler != null) {
checkContext();
endHandler.handle(null);
}
}
private void handleException(Throwable t) {
if (exceptionHandler != null && t instanceof Exception) {
exceptionHandler.handle(t);
} else {
log.error("Unhandled exception", t);
}
}
private void check() {
if (this.closed) {
throw new IllegalStateException("Inputstream is closed");
}
}
public boolean isClosed() {
return this.closed;
}
private void checkContext() {
if (!vertx.getOrCreateContext().equals(context)) {
throw new IllegalStateException("AsyncInputStream must only be used in the context that created it, expected: " + this.context
+ " actual " + vertx.getOrCreateContext());
}
}
private synchronized void closeInternal(Handler> handler) {
check();
closed = true;
doClose(handler);
}
private void doClose(Handler> handler) {
try {
ch.close();
if (handler != null) {
this.vertx.runOnContext(v -> handler.handle(Future.succeededFuture()));
}
} catch (IOException e) {
if (handler != null) {
this.vertx.runOnContext(v -> handler.handle(Future.failedFuture(e)));
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy