Search code examples
javafileinputstreamnio

In Java, how can I create an InputStream for a specific part of a file?


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:

  • The beginning of the file is skipped silently.
  • Then the desired portion of the file is returned.
  • Subsequent calls to the 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?


Solution

  • 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();
        }
    }