jDoc + code cleanup
[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 /** The current position in the buffer. */
18 protected int pos;
19 /** The index of the last usable position of the buffer. */
20 protected int len;
21 /** The buffer itself. */
22 protected byte[] buffer;
23 /** An End-Of-File (or buffer, here) marker. */
24 protected boolean eof;
25
26 private boolean closed;
27 private InputStream in;
28 private int openCounter;
29
30 // special use, prefetched next buffer
31 private byte[] buffer2;
32 private int pos2;
33 private int len2;
34 private byte[] originalBuffer;
35
36 private long bytesRead;
37
38 /**
39 * Create a new {@link BufferedInputStream} that wraps the given
40 * {@link InputStream}.
41 *
42 * @param in
43 * the {@link InputStream} to wrap
44 */
45 public BufferedInputStream(InputStream in) {
46 this.in = in;
47
48 this.buffer = new byte[4096];
49 this.originalBuffer = this.buffer;
50 this.pos = 0;
51 this.len = 0;
52 }
53
54 /**
55 * Create a new {@link BufferedInputStream} that wraps the given bytes array
56 * as a data source.
57 *
58 * @param in
59 * the array to wrap, cannot be NULL
60 */
61 public BufferedInputStream(byte[] in) {
62 this(in, 0, in.length);
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 * @param offset
72 * the offset to start the reading at
73 * @param length
74 * the number of bytes to take into account in the array,
75 * starting from the offset
76 *
77 * @throws NullPointerException
78 * if the array is NULL
79 * @throws IndexOutOfBoundsException
80 * if the offset and length do not correspond to the given array
81 */
82 public BufferedInputStream(byte[] in, int offset, int length) {
83 if (in == null) {
84 throw new NullPointerException();
85 } else if (offset < 0 || length < 0 || length > in.length - offset) {
86 throw new IndexOutOfBoundsException();
87 }
88
89 this.in = null;
90
91 this.buffer = in;
92 this.originalBuffer = this.buffer;
93 this.pos = offset;
94 this.len = length;
95 }
96
97 /**
98 * Return this very same {@link BufferedInputStream}, but keep a counter of
99 * how many streams were open this way. When calling
100 * {@link BufferedInputStream#close()}, decrease this counter if it is not
101 * already zero instead of actually closing the stream.
102 * <p>
103 * You are now responsible for it &mdash; you <b>must</b> close it.
104 * <p>
105 * This method allows you to use a wrapping stream around this one and still
106 * close the wrapping stream.
107 *
108 * @return the same stream, but you are now responsible for closing it
109 *
110 * @throws IOException
111 * in case of I/O error or if the stream is closed
112 */
113 public synchronized InputStream open() throws IOException {
114 checkClose();
115 openCounter++;
116 return this;
117 }
118
119 /**
120 * Check if the current content (what will be read next) starts with the
121 * given search term.
122 * <p>
123 * Note: the search term size <b>must</b> be smaller or equal the internal
124 * buffer size.
125 *
126 * @param search
127 * the term to search for
128 *
129 * @return TRUE if the content that will be read starts with it
130 *
131 * @throws IOException
132 * in case of I/O error or if the size of the search term is
133 * greater than the internal buffer
134 */
135 public boolean startsWiths(String search) throws IOException {
136 return startsWith(search.getBytes("UTF-8"));
137 }
138
139 /**
140 * Check if the current content (what will be read next) starts with the
141 * given search 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 startsWith(byte[] search) throws IOException {
156 if (search.length > originalBuffer.length) {
157 throw new IOException(
158 "This stream does not support searching for more than "
159 + buffer.length + " bytes");
160 }
161
162 checkClose();
163
164 if (available() < search.length) {
165 preRead();
166 }
167
168 if (available() >= search.length) {
169 // Easy path
170 return startsWith(search, buffer, pos, len);
171 } else if (!eof) {
172 // Harder path
173 if (buffer2 == null && buffer.length == originalBuffer.length) {
174 buffer2 = Arrays.copyOf(buffer, buffer.length * 2);
175
176 pos2 = buffer.length;
177 len2 = in.read(buffer2, pos2, buffer.length);
178 if (len2 > 0) {
179 bytesRead += len2;
180 }
181
182 // Note: here, len/len2 = INDEX of last good byte
183 len2 += pos2;
184 }
185
186 return startsWith(search, buffer2, pos2, len2);
187 }
188
189 return false;
190 }
191
192 /**
193 * The number of bytes read from the under-laying {@link InputStream}.
194 *
195 * @return the number of bytes
196 */
197 public long getBytesRead() {
198 return bytesRead;
199 }
200
201 /**
202 * Check if this stream is totally spent (no more data to read or to
203 * process).
204 * <p>
205 * Note: an empty stream that is still not started will return FALSE, as we
206 * don't know yet if it is empty.
207 *
208 * @return TRUE if it is
209 */
210 public boolean eof() {
211 return closed || (len < 0 && !hasMoreData());
212 }
213
214 @Override
215 public int read() throws IOException {
216 checkClose();
217
218 preRead();
219 if (eof) {
220 return -1;
221 }
222
223 return buffer[pos++];
224 }
225
226 @Override
227 public int read(byte[] b) throws IOException {
228 return read(b, 0, b.length);
229 }
230
231 @Override
232 public int read(byte[] b, int boff, int blen) throws IOException {
233 checkClose();
234
235 if (b == null) {
236 throw new NullPointerException();
237 } else if (boff < 0 || blen < 0 || blen > b.length - boff) {
238 throw new IndexOutOfBoundsException();
239 } else if (blen == 0) {
240 return 0;
241 }
242
243 int done = 0;
244 while (hasMoreData() && done < blen) {
245 preRead();
246 if (hasMoreData()) {
247 int now = Math.min(blen, len) - pos;
248 if (now > 0) {
249 System.arraycopy(buffer, pos, b, boff, now);
250 pos += now;
251 done += now;
252 }
253 }
254 }
255
256 return done > 0 ? done : -1;
257 }
258
259 @Override
260 public long skip(long n) throws IOException {
261 if (n <= 0) {
262 return 0;
263 }
264
265 long skipped = 0;
266 while (hasMoreData() && n > 0) {
267 preRead();
268
269 long inBuffer = Math.min(n, available());
270 pos += inBuffer;
271 n -= inBuffer;
272 skipped += inBuffer;
273 }
274
275 return skipped;
276 }
277
278 @Override
279 public int available() {
280 if (closed) {
281 return 0;
282 }
283
284 return Math.max(0, len - pos);
285 }
286
287 /**
288 * Closes this stream and releases any system resources associated with the
289 * stream.
290 * <p>
291 * Including the under-laying {@link InputStream}.
292 * <p>
293 * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
294 * prior to this one, it will just decrease the internal count of how many
295 * open streams it held and do nothing else. The stream will actually be
296 * closed when you have called {@link BufferedInputStream#close()} once more
297 * than {@link BufferedInputStream#open()}.
298 *
299 * @exception IOException
300 * in case of I/O error
301 */
302 @Override
303 public synchronized void close() throws IOException {
304 close(true);
305 }
306
307 /**
308 * Closes this stream and releases any system resources associated with the
309 * stream.
310 * <p>
311 * Including the under-laying {@link InputStream} if
312 * <tt>incudingSubStream</tt> is true.
313 * <p>
314 * You can call this method multiple times, it will not cause an
315 * {@link IOException} for subsequent calls.
316 * <p>
317 * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
318 * prior to this one, it will just decrease the internal count of how many
319 * open streams it held and do nothing else. The stream will actually be
320 * closed when you have called {@link BufferedInputStream#close()} once more
321 * than {@link BufferedInputStream#open()}.
322 *
323 * @param includingSubStream
324 * also close the under-laying stream
325 *
326 * @exception IOException
327 * in case of I/O error
328 */
329 public synchronized void close(boolean includingSubStream)
330 throws IOException {
331 if (!closed) {
332 if (openCounter > 0) {
333 openCounter--;
334 } else {
335 closed = true;
336 if (includingSubStream && in != null) {
337 in.close();
338 }
339 }
340 }
341 }
342
343 /**
344 * Check if we still have some data in the buffer and, if not, fetch some.
345 *
346 * @return TRUE if we fetched some data, FALSE if there are still some in
347 * the buffer
348 *
349 * @throws IOException
350 * in case of I/O error
351 */
352 protected boolean preRead() throws IOException {
353 boolean hasRead = false;
354 if (!eof && in != null && pos >= len) {
355 pos = 0;
356 if (buffer2 != null) {
357 buffer = buffer2;
358 pos = pos2;
359 len = len2;
360
361 buffer2 = null;
362 pos2 = 0;
363 len2 = 0;
364 } else {
365 buffer = originalBuffer;
366
367 len = read(in, buffer);
368 if (len > 0) {
369 bytesRead += len;
370 }
371 }
372
373 hasRead = true;
374 }
375
376 if (pos >= len) {
377 eof = true;
378 }
379
380 return hasRead;
381 }
382
383 /**
384 * Read the under-laying stream into the local buffer.
385 *
386 * @param in
387 * the under-laying {@link InputStream}
388 * @param buffer
389 * the buffer we use in this {@link BufferedInputStream}
390 *
391 * @return the number of bytes read
392 *
393 * @throws IOException
394 * in case of I/O error
395 */
396 protected int read(InputStream in, byte[] buffer) throws IOException {
397 return in.read(buffer);
398 }
399
400 /**
401 * We have more data available in the buffer or we can fetch more.
402 *
403 * @return TRUE if it is the case, FALSE if not
404 */
405 protected boolean hasMoreData() {
406 return !closed && !(eof && pos >= len);
407 }
408
409 /**
410 * Check that the stream was not closed, and throw an {@link IOException} if
411 * it was.
412 *
413 * @throws IOException
414 * if it was closed
415 */
416 protected void checkClose() throws IOException {
417 if (closed) {
418 throw new IOException(
419 "This BufferedInputStream was closed, you cannot use it anymore.");
420 }
421 }
422
423 /**
424 * Check if the buffer starts with the given search term (given as an array,
425 * a start position and a end position).
426 * <p>
427 * Note: the parameter <tt>len</tt> is the <b>index</b> of the last
428 * position, <b>not</b> the length.
429 * <p>
430 * Note: the search term size <b>must</b> be smaller or equal the internal
431 * buffer size.
432 *
433 * @param search
434 * the term to search for
435 * @param buffer
436 * the buffer to look into
437 * @param offset
438 * the offset at which to start the search
439 * @param len
440 * the maximum index of the data to check (this is <b>not</b> a
441 * length, but an index)
442 *
443 * @return TRUE if the search content is present at the given location and
444 * does not exceed the <tt>len</tt> index
445 */
446 static protected boolean startsWith(byte[] search, byte[] buffer,
447 int offset, int len) {
448
449 // Check if there even is enough space for it
450 if (search.length > (len - offset)) {
451 return false;
452 }
453
454 boolean same = true;
455 for (int i = 0; i < search.length; i++) {
456 if (search[i] != buffer[offset + i]) {
457 same = false;
458 break;
459 }
460 }
461
462 return same;
463 }
464 }