| 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 | } |