Part of the contract of java.io.InputStream.read(byte[] b, int off, int len) reads:

Reads up to len bytes of data from the input stream into an array of bytes. An attempt is made to read as many as len bytes, but a smaller number may be read. The number of bytes actually read is returned as an integer.

We ran into a situation at work this week where an InputStream we were decorating reaaaally needed to respond with, say, eight bytes if there were at least eight bytes remaining to be read and we requested it to read with a len of eight. However, the read method above is not required to respond with eight bytes when you ask it; it could respond with fewer and still be in compilance.

We decided to introduce the concept of an "adamant" InputStream, which continues to read from its wrapped input stream until it either gets len bytes, or fewer if there really aren't len more bytes to be read--that is, the next call to read answers -1.

Here's the code and test:

 

====begin AdamantInputStream.java====
package pholser.io;

import java.io.FilterInputStream;
import java.io.InputStream;
import java.io.IOException;

public class AdamantInputStream extends FilterInputStream {
    public AdamantInputStream( final InputStream in ) {
        super( in );
    }

    @Override
    public int read( final byte[] sink, final int offset, final int length )
        throws IOException {

        int totalBytesRead = 0;
        int bytesJustRead;

        while ( ( bytesJustRead = more( sink, offset, length, totalBytesRead ) ) != -1 ) {
            totalBytesRead += bytesJustRead;
        }

        return totalBytesRead;
    }

    private int more( final byte[] sink, final int offset, final int length,
        final int totalBytesRead ) throws IOException {
        return super.read( sink, offset + totalBytesRead, length - totalBytesRead );
    }
}
====end AdamantInputStream.java====

====begin AdamantInputStreamTest.java====
package pholser.io;

import junit.framework.TestCase;
import java.io.ByteArrayInputStream;

public class AdamantInputStreamTest extends TestCase {
    public void testRequestingFewerBytesThanWrappedStreamDelivers() throws Exception {
        final String source = "abcdefg";
        final byte[] sink = new byte[ source.length() ];
        final StingyByteArrayInputStream bytesIn =
            new StingyByteArrayInputStream( source, 2 );
        final AdamantInputStream adamantIn = new AdamantInputStream( bytesIn );

        assertEquals( source.length(), adamantIn.read( sink ) );
        assertEquals( -1, adamantIn.read() );
        assertEquals( 5, bytesIn.callsToMultiByteRead );
    }

    private static final class StingyByteArrayInputStream extends ByteArrayInputStream {
        int callsToMultiByteRead;
        private int throttle;

        StingyByteArrayInputStream( final String source, final int throttle ) {
            super( source.getBytes() );

            this.throttle = throttle;
        }

        @Override
        public synchronized int read( final byte[] sink, final int offset,
            final int length ) {

            final int bytesRead =
                super.read( sink, offset, Math.min( throttle, length ) );
            ++callsToMultiByteRead;
            return bytesRead;
        }
    }
}
====end AdamantInputStreamTest.java====

We interposed this filtering stream between two others which were previously directly linked, and solved our problem.  I was really pleased with how the "decorative" aspects of FilterInputStream made the solution nice and clean.