@SuppressWarnings("NullableProblems")
    @Override
    public int read(final byte[] b, final int off, final 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 totalBytesRead = 0;

        if (data == null || indexInputStream == null) {
            throw new IOException("Stream closed");
        }

        if (includeAll) {
            // If we are including all and haven't specified specific excludes
            // then just read the data stream sequentially.
            if (excluded == null || excluded.size() == 0) {
                totalBytesRead = doRead(b, off, len);

            } else {
                // Otherwise we are assuming all segments are included except
                // ones that are specifically excluded.

                // Get the initial excluded range.
                if (windowPos == windowByteStart) {
                    excludedIterator = excluded.iterator();
                    range = getNextExcludedRange();
                }

                int bytesRead = 0;
                int remaining = len;
                int offset = off;
                while (remaining > 0 && bytesRead != -1) {
                    while (range != null && windowPos == range.getStart()) {
                        // Jump over the excluded data.
                        windowPos += range.getEnd() - range.getStart();
                        doSeek(windowPos);

                        // Get the next excluded range.
                        range = getNextExcludedRange();
                    }

                    // Find out how much data we can read before the start of
                    // the next range.
                    int available = remaining;
                    if (range != null) {
                        final long beforeRangeStart = range.getStart() - windowPos;
                        if (available > beforeRangeStart) {
                            available = (int) beforeRangeStart;
                        }
                    }

                    // Read what is available before the next range.
                    // We need to cater here for windows ranges of 0 bytes long
                    if (available > 0) {
                        // +ve range
                        bytesRead = doRead(b, offset, available);
                        if (bytesRead != -1) {
                            totalBytesRead += bytesRead;
                            offset += bytesRead;
                            remaining -= bytesRead;
                        }
                    } else {
                        // empty range
                        remaining = 0;
                    }
                }
            }
        } else {
            // If we are excluding all and haven't specified specific includes
            // then just return -1.
            if (included == null || included.size() == 0) {
                totalBytesRead = -1;

            } else {
                // Otherwise we are assuming all segments are excluded except
                // ones that are specifically included.

                // Get the initial included range.
                if (windowPos == windowByteStart) {
                    includedIterator = included.iterator();
                    range = getNextIncludedRange();
                }

                int bytesRead = 0;
                int remaining = len;
                int offset = off;
                while (remaining > 0 && bytesRead != -1 && range != null) {
                    // Jump to the start of the included range if we aren't
                    // there already.
                    if (windowPos < range.getStart()) {
                        windowPos = range.getStart();
                        doSeek(windowPos);
                    }

                    // Find out how much data we can read before we need the
                    // next range.
                    int available = remaining;
                    long beforeRangeEnd = range.getEnd() - windowPos;
                    if (available > beforeRangeEnd) {
                        available = (int) beforeRangeEnd;
                    }

                    // Read what is available before the next range.
                    // We need to cater here for windows ranges of 0 bytes long
                    if (available > 0) {
                        // Read what is available before the range end.
                        bytesRead = doRead(b, offset, available);
                        if (bytesRead != -1) {
                            totalBytesRead += bytesRead;
                            offset += bytesRead;
                            remaining -= bytesRead;
                            beforeRangeEnd -= bytesRead;
                        }
                    } else {
                        // Empty range
                        beforeRangeEnd = 0;
                    }

                    if (beforeRangeEnd == 0) {
                        // Get the next included range for next time.
                        range = getNextIncludedRange();
                    }
                }
            }
        }

        // If we didn't read anything then we must be at the end of the file.
        if (totalBytesRead == 0) {
            return -1;
        }

//        LOGGER.info("bytes: {} {}", ByteArrayUtils.byteArrayToHex(b, off, len), b[off]);
        return totalBytesRead;
    }
Reads up to <code>len</code> bytes of data from the input stream into an
array of bytes. An attempt is made to read as many as <code>len</code>
bytes, but a smaller number may be read. The number of bytes actually
read is returned as an integer.
<p>
<p>
This method blocks until input data is available, end of file is
detected, or an exception is thrown.
<p>
<p>
If <code>len</code> is zero, then no bytes are read and <code>0</code> is
returned; otherwise, there is an attempt to read at least one byte. If no
byte is available because the stream is at end of file, the value
<code>-1</code> is returned; otherwise, at least one byte is read and
stored into <code>b</code>.
<p>
<p>
The first byte read is stored into element <code>b[off]</code>, the next
one into <code>b[off+1]</code>, and so on. The number of bytes read is,
at most, equal to <code>len</code>. Let <i>k</i> be the number of bytes
actually read; these bytes will be stored in elements <code>b[off]</code>
through <code>b[off+</code><i>k</i><code>-1]</code>, leaving elements
<code>b[off+</code><i>k</i><code>]</code> through
<code>b[off+len-1]</code> unaffected.
<p>
<p>
In every case, elements <code>b[0]</code> through <code>b[off]</code> and
elements <code>b[off+len]</code> through <code>b[b.length-1]</code> are
unaffected.
<p>
<p>
The <code>read(b,</code> <code>off,</code> <code>len)</code> method for
class <code>InputStream</code> simply calls the method
<code>read()</code> repeatedly. If the first such call results in an
<code>IOException</code>, that exception is returned from the call to the
<code>read(b,</code> <code>off,</code> <code>len)</code> method. If any
subsequent call to <code>read()</code> results in a
<code>IOException</code>, the exception is caught and treated as if it
were end of file; the bytes read up to that point are stored into
<code>b</code> and the number of bytes read before the exception occurred
is returned. The default implementation of this method blocks until the
requested amount of input data <code>len</code> has been read, end of
file is detected, or an exception is thrown. Subclasses are encouraged to
provide a more efficient implementation of this method.

@param b   the buffer into which the data is read.
@param off the start offset in array <code>b</code> at which the data is
           written.
@param len the maximum number of bytes to read.
@return the total number of bytes read into the buffer, or
<code>-1</code> if there is no more data because the end of the
stream has been reached.
@throws IOException If the first byte cannot be read for any reason other than
                    end of file, or if the input stream has been closed, or if
                    some other I/O error occurs.
@see java.io.InputStream#read()
stroom-data/stroom-data-store-impl-fs/src/main/java/stroom/data/store/impl/fs/RASegmentInputStream.java