Fixed InputStreamContentProvider.hasNext() to be idempotent until next() is called.

This commit is contained in:
Simone Bordet 2013-07-23 15:16:50 +02:00
parent aa6226e1fa
commit 55c204b3ba
1 changed files with 79 additions and 47 deletions

View File

@ -25,6 +25,8 @@ import java.util.NoSuchElementException;
import org.eclipse.jetty.client.api.ContentProvider; import org.eclipse.jetty.client.api.ContentProvider;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/** /**
* A {@link ContentProvider} for an {@link InputStream}. * A {@link ContentProvider} for an {@link InputStream}.
@ -44,6 +46,8 @@ import org.eclipse.jetty.util.BufferUtil;
*/ */
public class InputStreamContentProvider implements ContentProvider public class InputStreamContentProvider implements ContentProvider
{ {
private static final Logger LOG = Log.getLogger(InputStreamContentProvider.class);
private final InputStream stream; private final InputStream stream;
private final int bufferSize; private final int bufferSize;
@ -88,55 +92,84 @@ public class InputStreamContentProvider implements ContentProvider
@Override @Override
public Iterator<ByteBuffer> iterator() public Iterator<ByteBuffer> iterator()
{ {
return new Iterator<ByteBuffer>() return new InputStreamIterator();
}
/**
* Iterating over an {@link InputStream} is tricky, because {@link #hasNext()} must return false
* if the stream reads -1. However, we don't know what to return until we read the stream, which
* means that stream reading must be performed by {@link #hasNext()}, which introduces a side-effect
* on what is supposed to be a simple query method (with respect to the Query Command Separation
* Principle).
* <p />
* Alternatively, we could return {@code true} from {@link #hasNext()} even if we don't know that
* we will read -1, but then when {@link #next()} reads -1 it must return an empty buffer.
* However this is problematic, since GETs with no content indication would become GET with chunked
* content, and not understood by servers.
* <p />
* Therefore we need to make sure that {@link #hasNext()} does not perform any side effect (so that
* it can be called multiple times) until {@link #next()} is called.
*/
private class InputStreamIterator implements Iterator<ByteBuffer>
{ {
private final byte[] bytes = new byte[bufferSize]; private final byte[] bytes = new byte[bufferSize];
private Exception failure; private Exception failure;
private ByteBuffer buffer; private ByteBuffer buffer;
private Boolean hasNext;
@Override @Override
public boolean hasNext() public boolean hasNext()
{ {
try try
{ {
if (hasNext != null)
return hasNext;
int read = stream.read(bytes); int read = stream.read(bytes);
LOG.debug("Read {} bytes from {}", read, stream);
if (read > 0) if (read > 0)
{ {
buffer = onRead(bytes, 0, read); buffer = onRead(bytes, 0, read);
hasNext = Boolean.TRUE;
return true; return true;
} }
else if (read < 0) else if (read < 0)
{ {
hasNext = Boolean.FALSE;
return false; return false;
} }
else else
{ {
buffer = BufferUtil.EMPTY_BUFFER; buffer = BufferUtil.EMPTY_BUFFER;
hasNext = Boolean.TRUE;
return true; return true;
} }
} }
catch (Exception x) catch (Exception x)
{ {
LOG.debug(x);
if (failure == null) if (failure == null)
{ {
failure = x; failure = x;
// Signal we have more content to cause a call to // Signal we have more content to cause a call to
// next() which will throw NoSuchElementException. // next() which will throw NoSuchElementException.
hasNext = Boolean.TRUE;
return true; return true;
} }
return false; throw new IllegalStateException();
} }
} }
@Override @Override
public ByteBuffer next() public ByteBuffer next()
{ {
ByteBuffer result = buffer;
buffer = null;
if (failure != null) if (failure != null)
throw (NoSuchElementException)new NoSuchElementException().initCause(failure); throw (NoSuchElementException)new NoSuchElementException().initCause(failure);
ByteBuffer result = buffer;
if (result == null) if (result == null)
throw new NoSuchElementException(); throw new NoSuchElementException();
buffer = null;
hasNext = null;
return result; return result;
} }
@ -145,6 +178,5 @@ public class InputStreamContentProvider implements ContentProvider
{ {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
};
} }
} }