fix Base64 but breaks compat
[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
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 */
16public class BufferedInputStream extends InputStream {
eeaa5ebc 17 /** The current position in the buffer. */
a26188d3 18 protected int start;
eeaa5ebc 19 /** The index of the last usable position of the buffer. */
a26188d3 20 protected int stop;
eeaa5ebc 21 /** The buffer itself. */
33895a7b 22 protected byte[] buffer;
eeaa5ebc 23 /** An End-Of-File (or buffer, here) marker. */
33895a7b
NR
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;
a26188d3
NR
50 this.start = 0;
51 this.stop = 0;
33895a7b
NR
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;
a26188d3
NR
93 this.start = offset;
94 this.stop = length;
33895a7b
NR
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
eeaa5ebc
NR
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 */
33895a7b
NR
135 public boolean startsWiths(String search) throws IOException {
136 return startsWith(search.getBytes("UTF-8"));
137 }
138
eeaa5ebc
NR
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 */
33895a7b
NR
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
c8ce09c4 170 return StreamUtils.startsWith(search, buffer, start, stop);
33895a7b
NR
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;
028ff7c2 177 len2 = read(in, buffer2, pos2, buffer.length);
33895a7b
NR
178 if (len2 > 0) {
179 bytesRead += len2;
180 }
181
182 // Note: here, len/len2 = INDEX of last good byte
183 len2 += pos2;
184 }
185
c8ce09c4 186 return StreamUtils.startsWith(search, buffer2, pos2, len2);
33895a7b
NR
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).
33895a7b
NR
204 *
205 * @return TRUE if it is
028ff7c2
NR
206 *
207 * @throws IOException
208 * in case of I/O error
33895a7b 209 */
028ff7c2
NR
210 public boolean eof() throws IOException {
211 if (closed) {
212 return true;
213 }
214
215 preRead();
216 return !hasMoreData();
33895a7b
NR
217 }
218
219 @Override
220 public int read() throws IOException {
221 checkClose();
222
223 preRead();
224 if (eof) {
225 return -1;
226 }
227
a26188d3 228 return buffer[start++];
33895a7b
NR
229 }
230
231 @Override
232 public int read(byte[] b) throws IOException {
233 return read(b, 0, b.length);
234 }
235
236 @Override
237 public int read(byte[] b, int boff, int blen) throws IOException {
238 checkClose();
239
240 if (b == null) {
241 throw new NullPointerException();
242 } else if (boff < 0 || blen < 0 || blen > b.length - boff) {
243 throw new IndexOutOfBoundsException();
244 } else if (blen == 0) {
245 return 0;
246 }
247
248 int done = 0;
249 while (hasMoreData() && done < blen) {
250 preRead();
251 if (hasMoreData()) {
f04d5e49 252 int now = Math.min(blen - done, stop - start);
33895a7b 253 if (now > 0) {
a26188d3
NR
254 System.arraycopy(buffer, start, b, boff + done, now);
255 start += now;
33895a7b
NR
256 done += now;
257 }
258 }
259 }
260
261 return done > 0 ? done : -1;
262 }
263
264 @Override
265 public long skip(long n) throws IOException {
266 if (n <= 0) {
267 return 0;
268 }
269
270 long skipped = 0;
271 while (hasMoreData() && n > 0) {
272 preRead();
273
274 long inBuffer = Math.min(n, available());
a26188d3 275 start += inBuffer;
33895a7b
NR
276 n -= inBuffer;
277 skipped += inBuffer;
278 }
279
280 return skipped;
281 }
282
283 @Override
284 public int available() {
285 if (closed) {
286 return 0;
287 }
288
a26188d3 289 return Math.max(0, stop - start);
33895a7b
NR
290 }
291
292 /**
293 * Closes this stream and releases any system resources associated with the
294 * stream.
295 * <p>
296 * Including the under-laying {@link InputStream}.
297 * <p>
298 * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
299 * prior to this one, it will just decrease the internal count of how many
300 * open streams it held and do nothing else. The stream will actually be
301 * closed when you have called {@link BufferedInputStream#close()} once more
302 * than {@link BufferedInputStream#open()}.
303 *
304 * @exception IOException
305 * in case of I/O error
306 */
307 @Override
308 public synchronized void close() throws IOException {
309 close(true);
310 }
311
312 /**
313 * Closes this stream and releases any system resources associated with the
314 * stream.
315 * <p>
316 * Including the under-laying {@link InputStream} if
317 * <tt>incudingSubStream</tt> is true.
318 * <p>
319 * You can call this method multiple times, it will not cause an
320 * {@link IOException} for subsequent calls.
321 * <p>
322 * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
323 * prior to this one, it will just decrease the internal count of how many
324 * open streams it held and do nothing else. The stream will actually be
325 * closed when you have called {@link BufferedInputStream#close()} once more
326 * than {@link BufferedInputStream#open()}.
327 *
eeaa5ebc
NR
328 * @param includingSubStream
329 * also close the under-laying stream
330 *
33895a7b
NR
331 * @exception IOException
332 * in case of I/O error
333 */
334 public synchronized void close(boolean includingSubStream)
335 throws IOException {
336 if (!closed) {
337 if (openCounter > 0) {
338 openCounter--;
339 } else {
340 closed = true;
341 if (includingSubStream && in != null) {
342 in.close();
343 }
344 }
345 }
346 }
347
348 /**
349 * Check if we still have some data in the buffer and, if not, fetch some.
350 *
351 * @return TRUE if we fetched some data, FALSE if there are still some in
352 * the buffer
353 *
354 * @throws IOException
355 * in case of I/O error
356 */
357 protected boolean preRead() throws IOException {
358 boolean hasRead = false;
028ff7c2 359 if (in != null && !eof && start >= stop) {
a26188d3 360 start = 0;
33895a7b
NR
361 if (buffer2 != null) {
362 buffer = buffer2;
a26188d3
NR
363 start = pos2;
364 stop = len2;
33895a7b
NR
365
366 buffer2 = null;
367 pos2 = 0;
368 len2 = 0;
369 } else {
370 buffer = originalBuffer;
371
028ff7c2 372 stop = read(in, buffer, 0, buffer.length);
a26188d3
NR
373 if (stop > 0) {
374 bytesRead += stop;
33895a7b
NR
375 }
376 }
377
378 hasRead = true;
379 }
380
a26188d3 381 if (start >= stop) {
33895a7b
NR
382 eof = true;
383 }
384
385 return hasRead;
386 }
387
388 /**
389 * Read the under-laying stream into the local buffer.
390 *
391 * @param in
392 * the under-laying {@link InputStream}
393 * @param buffer
394 * the buffer we use in this {@link BufferedInputStream}
028ff7c2
NR
395 * @param off
396 * the offset
397 * @param len
398 * the length in bytes
33895a7b
NR
399 *
400 * @return the number of bytes read
401 *
402 * @throws IOException
403 * in case of I/O error
404 */
028ff7c2
NR
405 protected int read(InputStream in, byte[] buffer, int off, int len)
406 throws IOException {
407 return in.read(buffer, off, len);
33895a7b
NR
408 }
409
410 /**
028ff7c2
NR
411 * We have more data available in the buffer <b>or</b> we can, maybe, fetch
412 * more.
33895a7b
NR
413 *
414 * @return TRUE if it is the case, FALSE if not
415 */
416 protected boolean hasMoreData() {
028ff7c2
NR
417 if (closed) {
418 return false;
419 }
420
421 return (start < stop) || !eof;
33895a7b
NR
422 }
423
424 /**
425 * Check that the stream was not closed, and throw an {@link IOException} if
426 * it was.
427 *
428 * @throws IOException
429 * if it was closed
430 */
431 protected void checkClose() throws IOException {
432 if (closed) {
433 throw new IOException(
eeaa5ebc 434 "This BufferedInputStream was closed, you cannot use it anymore.");
33895a7b
NR
435 }
436 }
33895a7b 437}