Simple NextableInputStream
[nikiroo-utils.git] / src / be / nikiroo / utils / NextableInputStream.java
1 package be.nikiroo.utils;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5
6 /**
7 * This {@link InputStream} can be separated into sub-streams (you can process
8 * it as a normal {@link InputStream} but, when it is spent, you can call
9 * {@link NextableInputStream#next()} on it to unlock new data).
10 * <p>
11 * The separation in sub-streams is done via {@link NextableInputStreamStep}.
12 *
13 * @author niki
14 */
15 public class NextableInputStream extends InputStream {
16 private NextableInputStreamStep step;
17 private boolean stopped;
18
19 private InputStream in;
20 private boolean eof;
21 private int pos = 0;
22 private int len = 0;
23 private byte[] buffer = new byte[4096];
24
25 /**
26 * Create a new {@link NextableInputStream} that wraps the given
27 * {@link InputStream}.
28 *
29 * @param in
30 * the {@link InputStream} to wrap
31 * @param step
32 * how to separate it into sub-streams (can be NULL, but in that
33 * case it will behave as a normal {@link InputStream})
34 */
35 public NextableInputStream(InputStream in, NextableInputStreamStep step) {
36 this.in = in;
37 this.step = step;
38 }
39
40 /**
41 * Unblock the processing of the next sub-stream.
42 * <p>
43 * It can only be called when the "current" stream is spent (i.e., you must
44 * first process the stream until it is spent).
45 * <p>
46 * We consider that when the under-laying {@link InputStream} is also spent,
47 * we cannot have a next sub-stream (it will thus return FALSE).
48 * <p>
49 * {@link IOException}s can happen when we have no data available in the
50 * buffer; in that case, we fetch more data to know if we can have a next
51 * sub-stream or not.
52 *
53 * @return TRUE if we unblocked the next sub-stream, FALSE if not
54 *
55 * @throws IOException
56 * in case of I/O error
57 */
58 public boolean next() throws IOException {
59 if (!hasMoreData() && stopped) {
60 len = step.getResumeLen();
61 pos += step.getResumeSkip();
62 eof = false;
63
64 if (!preRead()) {
65 checkBuffer(false);
66 }
67
68 // consider that if EOF, there is no next
69 return hasMoreData();
70 }
71
72 return false;
73 }
74
75 @Override
76 public int read() throws IOException {
77 preRead();
78 if (eof) {
79 return -1;
80 }
81
82 return buffer[pos++];
83 }
84
85 @Override
86 public int read(byte[] b) throws IOException {
87 return read(b, 0, b.length);
88 }
89
90 @Override
91 public int read(byte[] b, int boff, int blen) throws IOException {
92 if (b == null) {
93 throw new NullPointerException();
94 } else if (boff < 0 || blen < 0 || blen > b.length - boff) {
95 throw new IndexOutOfBoundsException();
96 } else if (blen == 0) {
97 return 0;
98 }
99
100 int done = 0;
101 while (hasMoreData() && done < blen) {
102 preRead();
103 if (hasMoreData()) {
104 for (int i = pos; i < blen && i < len; i++) {
105 b[boff + done] = buffer[i];
106 pos++;
107 done++;
108 }
109 }
110 }
111
112 return done > 0 ? done : -1;
113 }
114
115 @Override
116 public int available() throws IOException {
117 return Math.max(0, len - pos);
118 }
119
120 /**
121 * Check if we still have some data in the buffer and, if not, fetch some.
122 *
123 * @return TRUE if we fetched some data, FALSE if there are still some in
124 * the buffer
125 *
126 * @throws IOException
127 * in case of I/O error
128 */
129 private boolean preRead() throws IOException {
130 boolean hasRead = false;
131 if (!eof && in != null && pos >= len && !stopped) {
132 pos = 0;
133 len = in.read(buffer);
134 checkBuffer(true);
135 hasRead = true;
136 }
137
138 if (pos >= len) {
139 eof = true;
140 }
141
142 return hasRead;
143 }
144
145 /**
146 * We have more data available in the buffer or we can fetch more.
147 *
148 * @return TRUE if it is the case, FALSE if not
149 */
150 private boolean hasMoreData() {
151 return !(eof && pos >= len);
152 }
153
154 /**
155 * Check that the buffer didn't overshot to the next item, and fix
156 * {@link NextableInputStream#len} if needed.
157 * <p>
158 * If {@link NextableInputStream#len} is fixed,
159 * {@link NextableInputStream#eof} and {@link NextableInputStream#stopped}
160 * are set to TRUE.
161 *
162 * @param newBuffer
163 * we changed the buffer, we need to clear some information in
164 * the {@link NextableInputStreamStep}
165 */
166 private void checkBuffer(boolean newBuffer) {
167 if (step != null) {
168 if (newBuffer) {
169 step.clearBuffer();
170 }
171
172 int stopAt = step.stop(buffer, pos, len);
173 if (stopAt >= 0) {
174 len = stopAt;
175 eof = true;
176 stopped = true;
177 }
178 }
179 }
180 }