code cleanup + BufInputStream.is()
[nikiroo-utils.git] / src / be / nikiroo / utils / 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)) {
177 return stop == search.length;
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 */
33895a7b 199 public boolean startsWiths(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>
207 * Note: the search term size <b>must</b> be smaller or equal the internal
208 * buffer size.
209 *
210 * @param search
211 * the term to search for
212 *
213 * @return TRUE if the content that will be read starts with it
214 *
215 * @throws IOException
216 * in case of I/O error or if the size of the search term is
217 * greater than the internal buffer
218 */
33895a7b
NR
219 public boolean startsWith(byte[] search) throws IOException {
220 if (search.length > originalBuffer.length) {
221 throw new IOException(
222 "This stream does not support searching for more than "
223 + buffer.length + " bytes");
224 }
225
226 checkClose();
227
228 if (available() < search.length) {
229 preRead();
230 }
231
232 if (available() >= search.length) {
233 // Easy path
c8ce09c4 234 return StreamUtils.startsWith(search, buffer, start, stop);
33895a7b
NR
235 } else if (!eof) {
236 // Harder path
237 if (buffer2 == null && buffer.length == originalBuffer.length) {
238 buffer2 = Arrays.copyOf(buffer, buffer.length * 2);
239
240 pos2 = buffer.length;
028ff7c2 241 len2 = read(in, buffer2, pos2, buffer.length);
33895a7b
NR
242 if (len2 > 0) {
243 bytesRead += len2;
244 }
245
246 // Note: here, len/len2 = INDEX of last good byte
247 len2 += pos2;
248 }
249
c8ce09c4 250 return StreamUtils.startsWith(search, buffer2, pos2, len2);
33895a7b
NR
251 }
252
253 return false;
254 }
255
256 /**
257 * The number of bytes read from the under-laying {@link InputStream}.
258 *
259 * @return the number of bytes
260 */
261 public long getBytesRead() {
262 return bytesRead;
263 }
264
265 /**
266 * Check if this stream is totally spent (no more data to read or to
267 * process).
33895a7b
NR
268 *
269 * @return TRUE if it is
028ff7c2
NR
270 *
271 * @throws IOException
272 * in case of I/O error
33895a7b 273 */
028ff7c2
NR
274 public boolean eof() throws IOException {
275 if (closed) {
276 return true;
277 }
278
279 preRead();
280 return !hasMoreData();
33895a7b
NR
281 }
282
283 @Override
284 public int read() throws IOException {
285 checkClose();
286
287 preRead();
288 if (eof) {
289 return -1;
290 }
291
a26188d3 292 return buffer[start++];
33895a7b
NR
293 }
294
295 @Override
296 public int read(byte[] b) throws IOException {
297 return read(b, 0, b.length);
298 }
299
300 @Override
301 public int read(byte[] b, int boff, int blen) throws IOException {
302 checkClose();
303
304 if (b == null) {
305 throw new NullPointerException();
306 } else if (boff < 0 || blen < 0 || blen > b.length - boff) {
307 throw new IndexOutOfBoundsException();
308 } else if (blen == 0) {
309 return 0;
310 }
311
312 int done = 0;
313 while (hasMoreData() && done < blen) {
314 preRead();
315 if (hasMoreData()) {
f04d5e49 316 int now = Math.min(blen - done, stop - start);
33895a7b 317 if (now > 0) {
a26188d3
NR
318 System.arraycopy(buffer, start, b, boff + done, now);
319 start += now;
33895a7b
NR
320 done += now;
321 }
322 }
323 }
324
325 return done > 0 ? done : -1;
326 }
327
328 @Override
329 public long skip(long n) throws IOException {
330 if (n <= 0) {
331 return 0;
332 }
333
334 long skipped = 0;
335 while (hasMoreData() && n > 0) {
336 preRead();
337
338 long inBuffer = Math.min(n, available());
a26188d3 339 start += inBuffer;
33895a7b
NR
340 n -= inBuffer;
341 skipped += inBuffer;
342 }
343
344 return skipped;
345 }
346
347 @Override
348 public int available() {
349 if (closed) {
350 return 0;
351 }
352
a26188d3 353 return Math.max(0, stop - start);
33895a7b
NR
354 }
355
356 /**
357 * Closes this stream and releases any system resources associated with the
358 * stream.
359 * <p>
360 * Including the under-laying {@link InputStream}.
361 * <p>
362 * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
363 * prior to this one, it will just decrease the internal count of how many
364 * open streams it held and do nothing else. The stream will actually be
365 * closed when you have called {@link BufferedInputStream#close()} once more
366 * than {@link BufferedInputStream#open()}.
367 *
368 * @exception IOException
369 * in case of I/O error
370 */
371 @Override
372 public synchronized void close() throws IOException {
373 close(true);
374 }
375
376 /**
377 * Closes this stream and releases any system resources associated with the
378 * stream.
379 * <p>
380 * Including the under-laying {@link InputStream} if
381 * <tt>incudingSubStream</tt> is true.
382 * <p>
383 * You can call this method multiple times, it will not cause an
384 * {@link IOException} for subsequent calls.
385 * <p>
386 * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
387 * prior to this one, it will just decrease the internal count of how many
388 * open streams it held and do nothing else. The stream will actually be
389 * closed when you have called {@link BufferedInputStream#close()} once more
390 * than {@link BufferedInputStream#open()}.
391 *
eeaa5ebc
NR
392 * @param includingSubStream
393 * also close the under-laying stream
394 *
33895a7b
NR
395 * @exception IOException
396 * in case of I/O error
397 */
398 public synchronized void close(boolean includingSubStream)
399 throws IOException {
400 if (!closed) {
401 if (openCounter > 0) {
402 openCounter--;
403 } else {
404 closed = true;
405 if (includingSubStream && in != null) {
406 in.close();
407 }
408 }
409 }
410 }
411
412 /**
413 * Check if we still have some data in the buffer and, if not, fetch some.
414 *
415 * @return TRUE if we fetched some data, FALSE if there are still some in
416 * the buffer
417 *
418 * @throws IOException
419 * in case of I/O error
420 */
421 protected boolean preRead() throws IOException {
422 boolean hasRead = false;
028ff7c2 423 if (in != null && !eof && start >= stop) {
a26188d3 424 start = 0;
33895a7b
NR
425 if (buffer2 != null) {
426 buffer = buffer2;
a26188d3
NR
427 start = pos2;
428 stop = len2;
33895a7b
NR
429
430 buffer2 = null;
431 pos2 = 0;
432 len2 = 0;
433 } else {
434 buffer = originalBuffer;
435
028ff7c2 436 stop = read(in, buffer, 0, buffer.length);
a26188d3
NR
437 if (stop > 0) {
438 bytesRead += stop;
33895a7b
NR
439 }
440 }
441
442 hasRead = true;
443 }
444
a26188d3 445 if (start >= stop) {
33895a7b
NR
446 eof = true;
447 }
448
449 return hasRead;
450 }
451
452 /**
453 * Read the under-laying stream into the local buffer.
454 *
455 * @param in
456 * the under-laying {@link InputStream}
457 * @param buffer
458 * the buffer we use in this {@link BufferedInputStream}
028ff7c2
NR
459 * @param off
460 * the offset
461 * @param len
462 * the length in bytes
33895a7b
NR
463 *
464 * @return the number of bytes read
465 *
466 * @throws IOException
467 * in case of I/O error
468 */
028ff7c2
NR
469 protected int read(InputStream in, byte[] buffer, int off, int len)
470 throws IOException {
471 return in.read(buffer, off, len);
33895a7b
NR
472 }
473
474 /**
028ff7c2
NR
475 * We have more data available in the buffer <b>or</b> we can, maybe, fetch
476 * more.
33895a7b
NR
477 *
478 * @return TRUE if it is the case, FALSE if not
479 */
480 protected boolean hasMoreData() {
028ff7c2
NR
481 if (closed) {
482 return false;
483 }
484
485 return (start < stop) || !eof;
33895a7b
NR
486 }
487
488 /**
489 * Check that the stream was not closed, and throw an {@link IOException} if
490 * it was.
491 *
492 * @throws IOException
493 * if it was closed
494 */
495 protected void checkClose() throws IOException {
496 if (closed) {
497 throw new IOException(
eeaa5ebc 498 "This BufferedInputStream was closed, you cannot use it anymore.");
33895a7b
NR
499 }
500 }
33895a7b 501}