ReplaceInputStream ready + tests
[nikiroo-utils.git] / src / be / nikiroo / utils / BufferedInputStream.java
1 package be.nikiroo.utils;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.util.Arrays;
6
7 /**
8 * A simple {@link InputStream} that is buffered with a bytes array.
9 * <p>
10 * It is mostly intended to be used as a base class to create new
11 * {@link InputStream}s with special operation modes, and to give some default
12 * methods.
13 *
14 * @author niki
15 */
16 public class BufferedInputStream extends InputStream {
17 protected int pos;
18 protected int len;
19 protected byte[] buffer;
20 protected boolean eof;
21
22 private boolean closed;
23 private InputStream in;
24 private int openCounter;
25
26 // special use, prefetched next buffer
27 private byte[] buffer2;
28 private int pos2;
29 private int len2;
30 private byte[] originalBuffer;
31
32 private long bytesRead;
33
34 /**
35 * Create a new {@link BufferedInputStream} that wraps the given
36 * {@link InputStream}.
37 *
38 * @param in
39 * the {@link InputStream} to wrap
40 */
41 public BufferedInputStream(InputStream in) {
42 this.in = in;
43
44 this.buffer = new byte[4096];
45 this.originalBuffer = this.buffer;
46 this.pos = 0;
47 this.len = 0;
48 }
49
50 /**
51 * Create a new {@link BufferedInputStream} that wraps the given bytes array
52 * as a data source.
53 *
54 * @param in
55 * the array to wrap, cannot be NULL
56 */
57 public BufferedInputStream(byte[] in) {
58 this(in, 0, in.length);
59 }
60
61 /**
62 * Create a new {@link BufferedInputStream} that wraps the given bytes array
63 * as a data source.
64 *
65 * @param in
66 * the array to wrap, cannot be NULL
67 * @param offset
68 * the offset to start the reading at
69 * @param length
70 * the number of bytes to take into account in the array,
71 * starting from the offset
72 *
73 * @throws NullPointerException
74 * if the array is NULL
75 * @throws IndexOutOfBoundsException
76 * if the offset and length do not correspond to the given array
77 */
78 public BufferedInputStream(byte[] in, int offset, int length) {
79 if (in == null) {
80 throw new NullPointerException();
81 } else if (offset < 0 || length < 0 || length > in.length - offset) {
82 throw new IndexOutOfBoundsException();
83 }
84
85 this.in = null;
86
87 this.buffer = in;
88 this.originalBuffer = this.buffer;
89 this.pos = offset;
90 this.len = length;
91 }
92
93 /**
94 * Return this very same {@link BufferedInputStream}, but keep a counter of
95 * how many streams were open this way. When calling
96 * {@link BufferedInputStream#close()}, decrease this counter if it is not
97 * already zero instead of actually closing the stream.
98 * <p>
99 * You are now responsible for it &mdash; you <b>must</b> close it.
100 * <p>
101 * This method allows you to use a wrapping stream around this one and still
102 * close the wrapping stream.
103 *
104 * @return the same stream, but you are now responsible for closing it
105 *
106 * @throws IOException
107 * in case of I/O error or if the stream is closed
108 */
109 public synchronized InputStream open() throws IOException {
110 checkClose();
111 openCounter++;
112 return this;
113 }
114
115 // max is buffer.size !
116 public boolean startsWiths(String search) throws IOException {
117 return startsWith(search.getBytes("UTF-8"));
118 }
119
120 // max is buffer.size !
121 public boolean startsWith(byte[] search) throws IOException {
122 if (search.length > originalBuffer.length) {
123 throw new IOException(
124 "This stream does not support searching for more than "
125 + buffer.length + " bytes");
126 }
127
128 checkClose();
129
130 if (available() < search.length) {
131 preRead();
132 }
133
134 if (available() >= search.length) {
135 // Easy path
136 return startsWith(search, buffer, pos);
137 } else if (!eof) {
138 // Harder path
139 if (buffer2 == null && buffer.length == originalBuffer.length) {
140 buffer2 = Arrays.copyOf(buffer, buffer.length * 2);
141
142 pos2 = buffer.length;
143 len2 = in.read(buffer2, pos2, buffer.length);
144 if (len2 > 0) {
145 bytesRead += len2;
146 }
147
148 // Note: here, len/len2 = INDEX of last good byte
149 len2 += pos2;
150 }
151
152 if (available() + (len2 - pos2) >= search.length) {
153 return startsWith(search, buffer2, pos2);
154 }
155 }
156
157 return false;
158 }
159
160 /**
161 * The number of bytes read from the under-laying {@link InputStream}.
162 *
163 * @return the number of bytes
164 */
165 public long getBytesRead() {
166 return bytesRead;
167 }
168
169 /**
170 * Check if this stream is totally spent (no more data to read or to
171 * process).
172 * <p>
173 * Note: an empty stream that is still not started will return FALSE, as we
174 * don't know yet if it is empty.
175 *
176 * @return TRUE if it is
177 */
178 public boolean eof() {
179 return closed || (len < 0 && !hasMoreData());
180 }
181
182 @Override
183 public int read() throws IOException {
184 checkClose();
185
186 preRead();
187 if (eof) {
188 return -1;
189 }
190
191 return buffer[pos++];
192 }
193
194 @Override
195 public int read(byte[] b) throws IOException {
196 return read(b, 0, b.length);
197 }
198
199 @Override
200 public int read(byte[] b, int boff, int blen) throws IOException {
201 checkClose();
202
203 if (b == null) {
204 throw new NullPointerException();
205 } else if (boff < 0 || blen < 0 || blen > b.length - boff) {
206 throw new IndexOutOfBoundsException();
207 } else if (blen == 0) {
208 return 0;
209 }
210
211 int done = 0;
212 while (hasMoreData() && done < blen) {
213 preRead();
214 if (hasMoreData()) {
215 int now = Math.min(blen, len) - pos;
216 if (now > 0) {
217 System.arraycopy(buffer, pos, b, boff, now);
218 pos += now;
219 done += now;
220 }
221 }
222 }
223
224 return done > 0 ? done : -1;
225 }
226
227 @Override
228 public long skip(long n) throws IOException {
229 if (n <= 0) {
230 return 0;
231 }
232
233 long skipped = 0;
234 while (hasMoreData() && n > 0) {
235 preRead();
236
237 long inBuffer = Math.min(n, available());
238 pos += inBuffer;
239 n -= inBuffer;
240 skipped += inBuffer;
241 }
242
243 return skipped;
244 }
245
246 @Override
247 public int available() {
248 if (closed) {
249 return 0;
250 }
251
252 return Math.max(0, len - pos);
253 }
254
255 /**
256 * Closes this stream and releases any system resources associated with the
257 * stream.
258 * <p>
259 * Including the under-laying {@link InputStream}.
260 * <p>
261 * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
262 * prior to this one, it will just decrease the internal count of how many
263 * open streams it held and do nothing else. The stream will actually be
264 * closed when you have called {@link BufferedInputStream#close()} once more
265 * than {@link BufferedInputStream#open()}.
266 *
267 * @exception IOException
268 * in case of I/O error
269 */
270 @Override
271 public synchronized void close() throws IOException {
272 close(true);
273 }
274
275 /**
276 * Closes this stream and releases any system resources associated with the
277 * stream.
278 * <p>
279 * Including the under-laying {@link InputStream} if
280 * <tt>incudingSubStream</tt> is true.
281 * <p>
282 * You can call this method multiple times, it will not cause an
283 * {@link IOException} for subsequent calls.
284 * <p>
285 * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
286 * prior to this one, it will just decrease the internal count of how many
287 * open streams it held and do nothing else. The stream will actually be
288 * closed when you have called {@link BufferedInputStream#close()} once more
289 * than {@link BufferedInputStream#open()}.
290 *
291 * @exception IOException
292 * in case of I/O error
293 */
294 public synchronized void close(boolean includingSubStream)
295 throws IOException {
296 if (!closed) {
297 if (openCounter > 0) {
298 openCounter--;
299 } else {
300 closed = true;
301 if (includingSubStream && in != null) {
302 in.close();
303 }
304 }
305 }
306 }
307
308 /**
309 * Check if we still have some data in the buffer and, if not, fetch some.
310 *
311 * @return TRUE if we fetched some data, FALSE if there are still some in
312 * the buffer
313 *
314 * @throws IOException
315 * in case of I/O error
316 */
317 protected boolean preRead() throws IOException {
318 boolean hasRead = false;
319 if (!eof && in != null && pos >= len) {
320 pos = 0;
321 if (buffer2 != null) {
322 buffer = buffer2;
323 pos = pos2;
324 len = len2;
325
326 buffer2 = null;
327 pos2 = 0;
328 len2 = 0;
329 } else {
330 buffer = originalBuffer;
331
332 len = read(in, buffer);
333 if (len > 0) {
334 bytesRead += len;
335 }
336 }
337
338 hasRead = true;
339 }
340
341 if (pos >= len) {
342 eof = true;
343 }
344
345 return hasRead;
346 }
347
348 /**
349 * Read the under-laying stream into the local buffer.
350 *
351 * @param in
352 * the under-laying {@link InputStream}
353 * @param buffer
354 * the buffer we use in this {@link BufferedInputStream}
355 *
356 * @return the number of bytes read
357 *
358 * @throws IOException
359 * in case of I/O error
360 */
361 protected int read(InputStream in, byte[] buffer) throws IOException {
362 return in.read(buffer);
363 }
364
365 /**
366 * We have more data available in the buffer or we can fetch more.
367 *
368 * @return TRUE if it is the case, FALSE if not
369 */
370 protected boolean hasMoreData() {
371 return !closed && !(eof && pos >= len);
372 }
373
374 /**
375 * Check that the stream was not closed, and throw an {@link IOException} if
376 * it was.
377 *
378 * @throws IOException
379 * if it was closed
380 */
381 protected void checkClose() throws IOException {
382 if (closed) {
383 throw new IOException(
384 "This NextableInputStream was closed, you cannot use it anymore.");
385 }
386 }
387
388 // buffer must be > search
389 static protected boolean startsWith(byte[] search, byte[] buffer, int offset) {
390 boolean same = true;
391 for (int i = 0; i < search.length; i++) {
392 if (search[i] != buffer[offset + i]) {
393 same = false;
394 break;
395 }
396 }
397
398 return same;
399 }
400 }