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