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 aslen
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.