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