Merge commit '7ce18848c8327967ce27b90abf2e280953530b5f'
[nikiroo-utils.git] / streams / BufferedInputStream.java
CommitLineData
8e76f6ab 1package be.nikiroo.utils.streams;
33895a7b
NR
2
3import java.io.IOException;
4import java.io.InputStream;
5import java.util.Arrays;
6
f8147a0e
NR
7import be.nikiroo.utils.StringUtils;
8
33895a7b
NR
9/**
10 * A simple {@link InputStream} that is buffered with a bytes array.
11 * <p>
12 * It is mostly intended to be used as a base class to create new
13 * {@link InputStream}s with special operation modes, and to give some default
14 * methods.
15 *
16 * @author niki
17 */
18public class BufferedInputStream extends InputStream {
7194ac50
NR
19 /**
20 * The size of the internal buffer (can be different if you pass your own
21 * buffer, of course).
22 * <p>
23 * A second buffer of twice the size can sometimes be created as needed for
24 * the {@link BufferedInputStream#startsWith(byte[])} search operation.
25 */
26 static private final int BUFFER_SIZE = 4096;
27
eeaa5ebc 28 /** The current position in the buffer. */
a26188d3 29 protected int start;
eeaa5ebc 30 /** The index of the last usable position of the buffer. */
a26188d3 31 protected int stop;
eeaa5ebc 32 /** The buffer itself. */
33895a7b 33 protected byte[] buffer;
7194ac50 34 /** An End-Of-File (or {@link InputStream}, here) marker. */
33895a7b
NR
35 protected boolean eof;
36
37 private boolean closed;
38 private InputStream in;
39 private int openCounter;
40
41 // special use, prefetched next buffer
42 private byte[] buffer2;
43 private int pos2;
44 private int len2;
45 private byte[] originalBuffer;
46
47 private long bytesRead;
48
49 /**
50 * Create a new {@link BufferedInputStream} that wraps the given
51 * {@link InputStream}.
52 *
53 * @param in
54 * the {@link InputStream} to wrap
55 */
56 public BufferedInputStream(InputStream in) {
57 this.in = in;
58
7194ac50 59 this.buffer = new byte[BUFFER_SIZE];
33895a7b 60 this.originalBuffer = this.buffer;
a26188d3
NR
61 this.start = 0;
62 this.stop = 0;
33895a7b
NR
63 }
64
65 /**
66 * Create a new {@link BufferedInputStream} that wraps the given bytes array
67 * as a data source.
68 *
69 * @param in
70 * the array to wrap, cannot be NULL
71 */
72 public BufferedInputStream(byte[] in) {
73 this(in, 0, in.length);
74 }
75
76 /**
77 * Create a new {@link BufferedInputStream} that wraps the given bytes array
78 * as a data source.
79 *
80 * @param in
81 * the array to wrap, cannot be NULL
82 * @param offset
83 * the offset to start the reading at
84 * @param length
85 * the number of bytes to take into account in the array,
86 * starting from the offset
87 *
88 * @throws NullPointerException
89 * if the array is NULL
90 * @throws IndexOutOfBoundsException
91 * if the offset and length do not correspond to the given array
92 */
93 public BufferedInputStream(byte[] in, int offset, int length) {
94 if (in == null) {
95 throw new NullPointerException();
96 } else if (offset < 0 || length < 0 || length > in.length - offset) {
97 throw new IndexOutOfBoundsException();
98 }
99
100 this.in = null;
101
102 this.buffer = in;
103 this.originalBuffer = this.buffer;
a26188d3
NR
104 this.start = offset;
105 this.stop = length;
33895a7b
NR
106 }
107
dce410fe
NR
108 /**
109 * The internal buffer size (can be useful to know for search methods).
110 *
111 * @return the size of the internal buffer, in bytes.
112 */
113 public int getInternalBufferSize() {
114 return originalBuffer.length;
115 }
116
33895a7b
NR
117 /**
118 * Return this very same {@link BufferedInputStream}, but keep a counter of
119 * how many streams were open this way. When calling
120 * {@link BufferedInputStream#close()}, decrease this counter if it is not
121 * already zero instead of actually closing the stream.
122 * <p>
123 * You are now responsible for it &mdash; you <b>must</b> close it.
124 * <p>
125 * This method allows you to use a wrapping stream around this one and still
126 * close the wrapping stream.
127 *
128 * @return the same stream, but you are now responsible for closing it
129 *
130 * @throws IOException
131 * in case of I/O error or if the stream is closed
132 */
133 public synchronized InputStream open() throws IOException {
134 checkClose();
135 openCounter++;
136 return this;
137 }
138
dce410fe
NR
139 /**
140 * Check if the current content (until eof) is equal to the given search
141 * term.
142 * <p>
143 * Note: the search term size <b>must</b> be smaller or equal the internal
144 * buffer size.
145 *
146 * @param search
147 * the term to search for
148 *
149 * @return TRUE if the content that will be read starts with it
150 *
151 * @throws IOException
152 * in case of I/O error or if the size of the search term is
153 * greater than the internal buffer
154 */
155 public boolean is(String search) throws IOException {
156 return is(StringUtils.getBytes(search));
157 }
158
159 /**
160 * Check if the current content (until eof) is equal to the given search
161 * term.
162 * <p>
163 * Note: the search term size <b>must</b> be smaller or equal the internal
164 * buffer size.
165 *
166 * @param search
167 * the term to search for
168 *
169 * @return TRUE if the content that will be read starts with it
170 *
171 * @throws IOException
172 * in case of I/O error or if the size of the search term is
173 * greater than the internal buffer
174 */
175 public boolean is(byte[] search) throws IOException {
176 if (startsWith(search)) {
d251f3dd 177 return (stop - start) == search.length;
dce410fe
NR
178 }
179
180 return false;
181 }
182
eeaa5ebc
NR
183 /**
184 * Check if the current content (what will be read next) starts with the
185 * given search term.
186 * <p>
187 * Note: the search term size <b>must</b> be smaller or equal the internal
188 * buffer size.
189 *
190 * @param search
191 * the term to search for
192 *
193 * @return TRUE if the content that will be read starts with it
194 *
195 * @throws IOException
196 * in case of I/O error or if the size of the search term is
197 * greater than the internal buffer
198 */
d251f3dd 199 public boolean startsWith(String search) throws IOException {
f8147a0e 200 return startsWith(StringUtils.getBytes(search));
33895a7b
NR
201 }
202
eeaa5ebc
NR
203 /**
204 * Check if the current content (what will be read next) starts with the
205 * given search term.
206 * <p>
d251f3dd
NR
207 * An empty string will always return true (unless the stream is closed,
208 * which would throw an {@link IOException}).
209 * <p>
eeaa5ebc
NR
210 * Note: the search term size <b>must</b> be smaller or equal the internal
211 * buffer size.
212 *
213 * @param search
214 * the term to search for
215 *
216 * @return TRUE if the content that will be read starts with it
217 *
218 * @throws IOException
219 * in case of I/O error or if the size of the search term is
220 * greater than the internal buffer
221 */
33895a7b
NR
222 public boolean startsWith(byte[] search) throws IOException {
223 if (search.length > originalBuffer.length) {
224 throw new IOException(
225 "This stream does not support searching for more than "
226 + buffer.length + " bytes");
227 }
228
229 checkClose();
230
231 if (available() < search.length) {
232 preRead();
233 }
234
235 if (available() >= search.length) {
236 // Easy path
c8ce09c4 237 return StreamUtils.startsWith(search, buffer, start, stop);
d251f3dd 238 } else if (in != null && !eof) {
33895a7b
NR
239 // Harder path
240 if (buffer2 == null && buffer.length == originalBuffer.length) {
241 buffer2 = Arrays.copyOf(buffer, buffer.length * 2);
242
243 pos2 = buffer.length;
028ff7c2 244 len2 = read(in, buffer2, pos2, buffer.length);
33895a7b
NR
245 if (len2 > 0) {
246 bytesRead += len2;
247 }
248
249 // Note: here, len/len2 = INDEX of last good byte
250 len2 += pos2;
251 }
252
c8ce09c4 253 return StreamUtils.startsWith(search, buffer2, pos2, len2);
33895a7b
NR
254 }
255
256 return false;
257 }
258
259 /**
260 * The number of bytes read from the under-laying {@link InputStream}.
261 *
262 * @return the number of bytes
263 */
264 public long getBytesRead() {
265 return bytesRead;
266 }
267
268 /**
d2219aa0 269 * Check if this stream is spent (no more data to read or to
33895a7b 270 * process).
33895a7b
NR
271 *
272 * @return TRUE if it is
028ff7c2
NR
273 *
274 * @throws IOException
275 * in case of I/O error
33895a7b 276 */
028ff7c2
NR
277 public boolean eof() throws IOException {
278 if (closed) {
279 return true;
280 }
281
282 preRead();
283 return !hasMoreData();
33895a7b
NR
284 }
285
d251f3dd
NR
286 /**
287 * Read the whole {@link InputStream} until the end and return the number of
288 * bytes read.
289 *
290 * @return the number of bytes read
291 *
292 * @throws IOException
293 * in case of I/O error
294 */
295 public long end() throws IOException {
296 long skipped = 0;
297 while (hasMoreData()) {
298 skipped += skip(buffer.length);
299 }
300
301 return skipped;
302 }
303
33895a7b
NR
304 @Override
305 public int read() throws IOException {
306 checkClose();
307
308 preRead();
309 if (eof) {
310 return -1;
311 }
312
a26188d3 313 return buffer[start++];
33895a7b
NR
314 }
315
316 @Override
317 public int read(byte[] b) throws IOException {
318 return read(b, 0, b.length);
319 }
320
321 @Override
322 public int read(byte[] b, int boff, int blen) throws IOException {
323 checkClose();
324
325 if (b == null) {
326 throw new NullPointerException();
327 } else if (boff < 0 || blen < 0 || blen > b.length - boff) {
328 throw new IndexOutOfBoundsException();
329 } else if (blen == 0) {
330 return 0;
331 }
332
333 int done = 0;
334 while (hasMoreData() && done < blen) {
335 preRead();
336 if (hasMoreData()) {
f04d5e49 337 int now = Math.min(blen - done, stop - start);
33895a7b 338 if (now > 0) {
a26188d3
NR
339 System.arraycopy(buffer, start, b, boff + done, now);
340 start += now;
33895a7b
NR
341 done += now;
342 }
343 }
344 }
345
346 return done > 0 ? done : -1;
347 }
348
349 @Override
350 public long skip(long n) throws IOException {
351 if (n <= 0) {
352 return 0;
353 }
354
355 long skipped = 0;
356 while (hasMoreData() && n > 0) {
357 preRead();
358
359 long inBuffer = Math.min(n, available());
a26188d3 360 start += inBuffer;
33895a7b
NR
361 n -= inBuffer;
362 skipped += inBuffer;
363 }
364
365 return skipped;
366 }
367
368 @Override
369 public int available() {
370 if (closed) {
371 return 0;
372 }
373
a26188d3 374 return Math.max(0, stop - start);
33895a7b
NR
375 }
376
377 /**
378 * Closes this stream and releases any system resources associated with the
379 * stream.
380 * <p>
381 * Including the under-laying {@link InputStream}.
382 * <p>
383 * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
384 * prior to this one, it will just decrease the internal count of how many
385 * open streams it held and do nothing else. The stream will actually be
386 * closed when you have called {@link BufferedInputStream#close()} once more
387 * than {@link BufferedInputStream#open()}.
388 *
389 * @exception IOException
390 * in case of I/O error
391 */
392 @Override
393 public synchronized void close() throws IOException {
394 close(true);
395 }
396
397 /**
398 * Closes this stream and releases any system resources associated with the
399 * stream.
400 * <p>
401 * Including the under-laying {@link InputStream} if
402 * <tt>incudingSubStream</tt> is true.
403 * <p>
404 * You can call this method multiple times, it will not cause an
405 * {@link IOException} for subsequent calls.
406 * <p>
407 * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
408 * prior to this one, it will just decrease the internal count of how many
409 * open streams it held and do nothing else. The stream will actually be
410 * closed when you have called {@link BufferedInputStream#close()} once more
411 * than {@link BufferedInputStream#open()}.
412 *
eeaa5ebc
NR
413 * @param includingSubStream
414 * also close the under-laying stream
415 *
33895a7b
NR
416 * @exception IOException
417 * in case of I/O error
418 */
419 public synchronized void close(boolean includingSubStream)
420 throws IOException {
421 if (!closed) {
422 if (openCounter > 0) {
423 openCounter--;
424 } else {
425 closed = true;
426 if (includingSubStream && in != null) {
427 in.close();
428 }
429 }
430 }
431 }
432
433 /**
434 * Check if we still have some data in the buffer and, if not, fetch some.
435 *
436 * @return TRUE if we fetched some data, FALSE if there are still some in
437 * the buffer
438 *
439 * @throws IOException
440 * in case of I/O error
441 */
442 protected boolean preRead() throws IOException {
443 boolean hasRead = false;
028ff7c2 444 if (in != null && !eof && start >= stop) {
a26188d3 445 start = 0;
33895a7b
NR
446 if (buffer2 != null) {
447 buffer = buffer2;
a26188d3
NR
448 start = pos2;
449 stop = len2;
33895a7b
NR
450
451 buffer2 = null;
452 pos2 = 0;
453 len2 = 0;
454 } else {
455 buffer = originalBuffer;
456
028ff7c2 457 stop = read(in, buffer, 0, buffer.length);
a26188d3
NR
458 if (stop > 0) {
459 bytesRead += stop;
33895a7b
NR
460 }
461 }
462
463 hasRead = true;
464 }
465
a26188d3 466 if (start >= stop) {
33895a7b
NR
467 eof = true;
468 }
469
470 return hasRead;
471 }
472
473 /**
474 * Read the under-laying stream into the local buffer.
475 *
476 * @param in
477 * the under-laying {@link InputStream}
478 * @param buffer
479 * the buffer we use in this {@link BufferedInputStream}
028ff7c2
NR
480 * @param off
481 * the offset
482 * @param len
483 * the length in bytes
33895a7b
NR
484 *
485 * @return the number of bytes read
486 *
487 * @throws IOException
488 * in case of I/O error
489 */
028ff7c2
NR
490 protected int read(InputStream in, byte[] buffer, int off, int len)
491 throws IOException {
492 return in.read(buffer, off, len);
33895a7b
NR
493 }
494
495 /**
028ff7c2
NR
496 * We have more data available in the buffer <b>or</b> we can, maybe, fetch
497 * more.
33895a7b
NR
498 *
499 * @return TRUE if it is the case, FALSE if not
500 */
501 protected boolean hasMoreData() {
028ff7c2
NR
502 if (closed) {
503 return false;
504 }
505
506 return (start < stop) || !eof;
33895a7b
NR
507 }
508
509 /**
510 * Check that the stream was not closed, and throw an {@link IOException} if
511 * it was.
512 *
513 * @throws IOException
514 * if it was closed
515 */
516 protected void checkClose() throws IOException {
517 if (closed) {
518 throw new IOException(
eeaa5ebc 519 "This BufferedInputStream was closed, you cannot use it anymore.");
33895a7b
NR
520 }
521 }
33895a7b 522}