1 //
2 // ========================================================================
3 // Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4 // ------------------------------------------------------------------------
5 // All rights reserved. This program and the accompanying materials
6 // are made available under the terms of the Eclipse Public License v1.0
7 // and Apache License v2.0 which accompanies this distribution.
8 //
9 // The Eclipse Public License is available at
10 // http://www.eclipse.org/legal/epl-v10.html
11 //
12 // The Apache License v2.0 is available at
13 // http://www.opensource.org/licenses/apache2.0.php
14 //
15 // You may elect to redistribute this code under either of these licenses.
16 // ========================================================================
17 //
18
19 package org.eclipse.jetty.client.util;
20
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.nio.ByteBuffer;
24 import java.util.Iterator;
25 import java.util.NoSuchElementException;
26
27 import org.eclipse.jetty.client.api.ContentProvider;
28 import org.eclipse.jetty.util.BufferUtil;
29 import org.eclipse.jetty.util.log.Log;
30 import org.eclipse.jetty.util.log.Logger;
31
32 /**
33 * A {@link ContentProvider} for an {@link InputStream}.
34 * <p />
35 * The input stream is read once and therefore fully consumed.
36 * Invocations to the {@link #iterator()} method after the first will return an "empty" iterator
37 * because the stream has been consumed on the first invocation.
38 * <p />
39 * However, it is possible for subclasses to override {@link #onRead(byte[], int, int)} to copy
40 * the content read from the stream to another location (for example a file), and be able to
41 * support multiple invocations of {@link #iterator()}, returning the iterator provided by this
42 * class on the first invocation, and an iterator on the bytes copied to the other location
43 * for subsequent invocations.
44 * <p />
45 * It is possible to specify, at the constructor, a buffer size used to read content from the
46 * stream, by default 4096 bytes.
47 * <p />
48 * The {@link InputStream} passed to the constructor is by default closed when is it fully
49 * consumed (or when an exception is thrown while reading it), unless otherwise specified
50 * to the {@link #InputStreamContentProvider(java.io.InputStream, int, boolean) constructor}.
51 */
52 public class InputStreamContentProvider implements ContentProvider
53 {
54 private static final Logger LOG = Log.getLogger(InputStreamContentProvider.class);
55
56 private final InputStream stream;
57 private final int bufferSize;
58 private final boolean autoClose;
59
60 public InputStreamContentProvider(InputStream stream)
61 {
62 this(stream, 4096);
63 }
64
65 public InputStreamContentProvider(InputStream stream, int bufferSize)
66 {
67 this(stream, bufferSize, true);
68 }
69
70 public InputStreamContentProvider(InputStream stream, int bufferSize, boolean autoClose)
71 {
72 this.stream = stream;
73 this.bufferSize = bufferSize;
74 this.autoClose = autoClose;
75 }
76
77 @Override
78 public long getLength()
79 {
80 return -1;
81 }
82
83 /**
84 * Callback method invoked just after having read from the stream,
85 * but before returning the iteration element (a {@link ByteBuffer}
86 * to the caller.
87 * <p />
88 * Subclasses may override this method to copy the content read from
89 * the stream to another location (a file, or in memory if the content
90 * is known to fit).
91 *
92 * @param buffer the byte array containing the bytes read
93 * @param offset the offset from where bytes should be read
94 * @param length the length of the bytes read
95 * @return a {@link ByteBuffer} wrapping the byte array
96 */
97 protected ByteBuffer onRead(byte[] buffer, int offset, int length)
98 {
99 if (length <= 0)
100 return BufferUtil.EMPTY_BUFFER;
101 return ByteBuffer.wrap(buffer, offset, length);
102 }
103
104 @Override
105 public Iterator<ByteBuffer> iterator()
106 {
107 return new InputStreamIterator();
108 }
109
110 /**
111 * Iterating over an {@link InputStream} is tricky, because {@link #hasNext()} must return false
112 * if the stream reads -1. However, we don't know what to return until we read the stream, which
113 * means that stream reading must be performed by {@link #hasNext()}, which introduces a side-effect
114 * on what is supposed to be a simple query method (with respect to the Query Command Separation
115 * Principle).
116 * <p />
117 * Alternatively, we could return {@code true} from {@link #hasNext()} even if we don't know that
118 * we will read -1, but then when {@link #next()} reads -1 it must return an empty buffer.
119 * However this is problematic, since GETs with no content indication would become GET with chunked
120 * content, and not understood by servers.
121 * <p />
122 * Therefore we need to make sure that {@link #hasNext()} does not perform any side effect (so that
123 * it can be called multiple times) until {@link #next()} is called.
124 */
125 private class InputStreamIterator implements Iterator<ByteBuffer>
126 {
127 private Throwable failure;
128 private ByteBuffer buffer;
129 private Boolean hasNext;
130
131 @Override
132 public boolean hasNext()
133 {
134 try
135 {
136 if (hasNext != null)
137 return hasNext;
138
139 byte[] bytes = new byte[bufferSize];
140 int read = stream.read(bytes);
141 LOG.debug("Read {} bytes from {}", read, stream);
142 if (read > 0)
143 {
144 buffer = onRead(bytes, 0, read);
145 hasNext = Boolean.TRUE;
146 return true;
147 }
148 else if (read < 0)
149 {
150 hasNext = Boolean.FALSE;
151 close();
152 return false;
153 }
154 else
155 {
156 buffer = BufferUtil.EMPTY_BUFFER;
157 hasNext = Boolean.TRUE;
158 return true;
159 }
160 }
161 catch (Throwable x)
162 {
163 LOG.debug(x);
164 if (failure == null)
165 {
166 failure = x;
167 // Signal we have more content to cause a call to
168 // next() which will throw NoSuchElementException.
169 hasNext = Boolean.TRUE;
170 close();
171 return true;
172 }
173 throw new IllegalStateException();
174 }
175 }
176
177 @Override
178 public ByteBuffer next()
179 {
180 if (failure != null)
181 throw (NoSuchElementException)new NoSuchElementException().initCause(failure);
182 ByteBuffer result = buffer;
183 if (result == null)
184 throw new NoSuchElementException();
185 buffer = null;
186 hasNext = null;
187 return result;
188 }
189
190 @Override
191 public void remove()
192 {
193 throw new UnsupportedOperationException();
194 }
195
196 private void close()
197 {
198 if (autoClose)
199 {
200 try
201 {
202 stream.close();
203 }
204 catch (IOException x)
205 {
206 LOG.ignore(x);
207 }
208 }
209 }
210 }
211 }