I need an InputStream
that reads from a specific portion of a file, and nothing more.
From the perspective of the consumer of the InputStream it would seem that the content is only that specific portion. The Consumer<InputStream>
would be unaware its data came from a much larger file.
Therefor the InputStream should behave as follows:
is.read()
would return -1
, even if the file contained more data.Path file= Paths.get("file.dat");
int start = 12000;
int size = 600;
try(InputStream input = getPartialInputStream(file, start, size)){
// This should receive an inputstream that returns exactly 600 bytes.
// Those bytes should correspond to the bytes in "file.dat" found from position 12000 upto 12600.
thirdPartyMethod(input);
}
Is there a good way to do this without having to implement a custom InputStream
myself?
What could such a getPartialInputStream
method look like?
I wrote a utility class that you can use like this:
try(FileChannel channel = FileChannel.open(file, READ);
InputStream input = new PartialChannelInputStream(channel, start, start + size)) {
thirdPartyMethod(input);
}
It reads the content of the file using a ByteBuffer, so you control the memory footprint.
import java.io.IOException;
import java.io.InputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class PartialChannelInputStream extends InputStream {
private static final int DEFAULT_BUFFER_CAPACITY = 2048;
private final FileChannel channel;
private final ByteBuffer buffer;
private long position;
private final long end;
public PartialChannelInputStream(FileChannel channel, long start, long end)
throws IOException {
this(channel, start, end, DEFAULT_BUFFER_CAPACITY);
}
public PartialChannelInputStream(FileChannel channel, long start, long end, int bufferCapacity)
throws IOException {
if (start > end) {
throw new IllegalArgumentException("start(" + start + ") > end(" + end + ")");
}
this.channel = channel;
this.position = start;
this.end = end;
this.buffer = ByteBuffer.allocateDirect(bufferCapacity);
fillBuffer(end - start);
}
private void fillBuffer(long stillToRead) throws IOException {
if (stillToRead < buffer.limit()) {
buffer.limit((int) stillToRead);
}
channel.read(buffer, position);
buffer.flip();
}
@Override
public int read() throws IOException {
long stillToRead = end - position;
if (stillToRead <= 0) {
return -1;
}
if (!buffer.hasRemaining()) {
buffer.flip();
fillBuffer(stillToRead);
}
try {
position++;
return buffer.get();
} catch (BufferUnderflowException e) {
// Encountered EOF
position = end;
return -1;
}
}
}
This implementation above allows to create multiple PartialChannelInputStream
reading from the same FileChannel
and use them concurrently.
If that's not necessary, the simplified code below takes a Path
directly.
import static java.nio.file.StandardOpenOption.READ;
import java.io.IOException;
import java.io.InputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
public class PartialFileInputStream extends InputStream {
private static final int DEFAULT_BUFFER_CAPACITY = 2048;
private final FileChannel channel;
private final ByteBuffer buffer;
private long stillToRead;
public PartialChannelInputStream(Path file, long start, long end)
throws IOException {
this(channel, start, end, DEFAULT_BUFFER_CAPACITY);
}
public PartialChannelInputStream(Path file, long start, long end, int bufferCapacity)
throws IOException {
if (start > end) {
throw new IllegalArgumentException("start(" + start + ") > end(" + end + ")");
}
this.channel = FileChannel.open(file, READ).position(start);
this.buffer = ByteBuffer.allocateDirect(bufferCapacity);
this.stillToRead = end - start;
fillBuffer();
}
private void fillBuffer() throws IOException {
if (stillToRead < buffer.limit()) {
buffer.limit((int) stillToRead);
}
channel.read(buffer);
buffer.flip();
}
@Override
public int read() throws IOException {
if (stillToRead <= 0) {
return -1;
}
if (!buffer.hasRemaining()) {
buffer.flip();
fillBuffer();
}
try {
stillToRead--;
return buffer.get();
} catch (BufferUnderflowException e) {
// Encountered EOF
stillToRead = 0;
return -1;
}
}
@Override
public void close() throws IOException {
channel.close();
}
}