| 1 | package be.nikiroo.utils.streams; |
| 2 | |
| 3 | import java.io.IOException; |
| 4 | import java.io.InputStream; |
| 5 | import java.io.OutputStream; |
| 6 | |
| 7 | /** |
| 8 | * A simple {@link OutputStream} 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 OutputStream}s with special operation modes, and to give some default |
| 12 | * methods. |
| 13 | * |
| 14 | * @author niki |
| 15 | */ |
| 16 | public class BufferedOutputStream extends OutputStream { |
| 17 | /** The current position in the buffer. */ |
| 18 | protected int start; |
| 19 | /** The index of the last usable position of the buffer. */ |
| 20 | protected int stop; |
| 21 | /** The buffer itself. */ |
| 22 | protected byte[] buffer; |
| 23 | /** An End-Of-File (or buffer, here) marker. */ |
| 24 | protected boolean eof; |
| 25 | /** The actual under-laying stream. */ |
| 26 | protected OutputStream out; |
| 27 | /** The number of bytes written to the under-laying stream. */ |
| 28 | protected long bytesWritten; |
| 29 | /** |
| 30 | * Can bypass the flush process for big writes (will directly write to the |
| 31 | * under-laying buffer if the array to write is > the internal buffer |
| 32 | * size). |
| 33 | * <p> |
| 34 | * By default, this is true. |
| 35 | */ |
| 36 | protected boolean bypassFlush = true; |
| 37 | |
| 38 | private boolean closed; |
| 39 | private int openCounter; |
| 40 | private byte[] b1; |
| 41 | |
| 42 | /** |
| 43 | * Create a new {@link BufferedInputStream} that wraps the given |
| 44 | * {@link InputStream}. |
| 45 | * |
| 46 | * @param out |
| 47 | * the {@link OutputStream} to wrap |
| 48 | */ |
| 49 | public BufferedOutputStream(OutputStream out) { |
| 50 | this.out = out; |
| 51 | |
| 52 | this.buffer = new byte[4096]; |
| 53 | this.b1 = new byte[1]; |
| 54 | this.start = 0; |
| 55 | this.stop = 0; |
| 56 | } |
| 57 | |
| 58 | @Override |
| 59 | public void write(int b) throws IOException { |
| 60 | b1[0] = (byte) b; |
| 61 | write(b1, 0, 1); |
| 62 | } |
| 63 | |
| 64 | @Override |
| 65 | public void write(byte[] b) throws IOException { |
| 66 | write(b, 0, b.length); |
| 67 | } |
| 68 | |
| 69 | @Override |
| 70 | public void write(byte[] source, int sourceOffset, int sourceLength) |
| 71 | throws IOException { |
| 72 | |
| 73 | checkClose(); |
| 74 | |
| 75 | if (source == null) { |
| 76 | throw new NullPointerException(); |
| 77 | } else if ((sourceOffset < 0) || (sourceOffset > source.length) |
| 78 | || (sourceLength < 0) |
| 79 | || ((sourceOffset + sourceLength) > source.length) |
| 80 | || ((sourceOffset + sourceLength) < 0)) { |
| 81 | throw new IndexOutOfBoundsException(); |
| 82 | } else if (sourceLength == 0) { |
| 83 | return; |
| 84 | } |
| 85 | |
| 86 | if (bypassFlush && sourceLength >= buffer.length) { |
| 87 | /* |
| 88 | * If the request length exceeds the size of the output buffer, |
| 89 | * flush the output buffer and then write the data directly. In this |
| 90 | * way buffered streams will cascade harmlessly. |
| 91 | */ |
| 92 | flush(false); |
| 93 | out.write(source, sourceOffset, sourceLength); |
| 94 | bytesWritten += (sourceLength - sourceOffset); |
| 95 | return; |
| 96 | } |
| 97 | |
| 98 | int done = 0; |
| 99 | while (done < sourceLength) { |
| 100 | if (available() <= 0) { |
| 101 | flush(false); |
| 102 | } |
| 103 | |
| 104 | int now = Math.min(sourceLength - done, available()); |
| 105 | System.arraycopy(source, sourceOffset + done, buffer, stop, now); |
| 106 | stop += now; |
| 107 | done += now; |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | /** |
| 112 | * The available space in the buffer. |
| 113 | * |
| 114 | * @return the space in bytes |
| 115 | */ |
| 116 | private int available() { |
| 117 | if (closed) { |
| 118 | return 0; |
| 119 | } |
| 120 | |
| 121 | return Math.max(0, buffer.length - stop - 1); |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * The number of bytes written to the under-laying {@link OutputStream}. |
| 126 | * |
| 127 | * @return the number of bytes |
| 128 | */ |
| 129 | public long getBytesWritten() { |
| 130 | return bytesWritten; |
| 131 | } |
| 132 | |
| 133 | /** |
| 134 | * Return this very same {@link BufferedInputStream}, but keep a counter of |
| 135 | * how many streams were open this way. When calling |
| 136 | * {@link BufferedInputStream#close()}, decrease this counter if it is not |
| 137 | * already zero instead of actually closing the stream. |
| 138 | * <p> |
| 139 | * You are now responsible for it — you <b>must</b> close it. |
| 140 | * <p> |
| 141 | * This method allows you to use a wrapping stream around this one and still |
| 142 | * close the wrapping stream. |
| 143 | * |
| 144 | * @return the same stream, but you are now responsible for closing it |
| 145 | * |
| 146 | * @throws IOException |
| 147 | * in case of I/O error or if the stream is closed |
| 148 | */ |
| 149 | public synchronized OutputStream open() throws IOException { |
| 150 | checkClose(); |
| 151 | openCounter++; |
| 152 | return this; |
| 153 | } |
| 154 | |
| 155 | /** |
| 156 | * Check that the stream was not closed, and throw an {@link IOException} if |
| 157 | * it was. |
| 158 | * |
| 159 | * @throws IOException |
| 160 | * if it was closed |
| 161 | */ |
| 162 | protected void checkClose() throws IOException { |
| 163 | if (closed) { |
| 164 | throw new IOException( |
| 165 | "This BufferedInputStream was closed, you cannot use it anymore."); |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | @Override |
| 170 | public void flush() throws IOException { |
| 171 | flush(true); |
| 172 | } |
| 173 | |
| 174 | /** |
| 175 | * Flush the {@link BufferedOutputStream}, write the current buffered data |
| 176 | * to (and optionally also flush) the under-laying stream. |
| 177 | * <p> |
| 178 | * If {@link BufferedOutputStream#bypassFlush} is false, all writes to the |
| 179 | * under-laying stream are done in this method. |
| 180 | * <p> |
| 181 | * This can be used if you want to write some data in the under-laying |
| 182 | * stream yourself (in that case, flush this {@link BufferedOutputStream} |
| 183 | * with or without flushing the under-laying stream, then you can write to |
| 184 | * the under-laying stream). |
| 185 | * |
| 186 | * @param includingSubStream |
| 187 | * also flush the under-laying stream |
| 188 | * @throws IOException |
| 189 | * in case of I/O error |
| 190 | */ |
| 191 | public void flush(boolean includingSubStream) throws IOException { |
| 192 | if (stop > start) { |
| 193 | out.write(buffer, start, stop - start); |
| 194 | bytesWritten += (stop - start); |
| 195 | } |
| 196 | start = 0; |
| 197 | stop = 0; |
| 198 | |
| 199 | if (includingSubStream) { |
| 200 | out.flush(); |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | /** |
| 205 | * Closes this stream and releases any system resources associated with the |
| 206 | * stream. |
| 207 | * <p> |
| 208 | * Including the under-laying {@link InputStream}. |
| 209 | * <p> |
| 210 | * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method |
| 211 | * prior to this one, it will just decrease the internal count of how many |
| 212 | * open streams it held and do nothing else. The stream will actually be |
| 213 | * closed when you have called {@link BufferedInputStream#close()} once more |
| 214 | * than {@link BufferedInputStream#open()}. |
| 215 | * |
| 216 | * @exception IOException |
| 217 | * in case of I/O error |
| 218 | */ |
| 219 | @Override |
| 220 | public synchronized void close() throws IOException { |
| 221 | close(true); |
| 222 | } |
| 223 | |
| 224 | /** |
| 225 | * Closes this stream and releases any system resources associated with the |
| 226 | * stream. |
| 227 | * <p> |
| 228 | * Including the under-laying {@link InputStream} if |
| 229 | * <tt>incudingSubStream</tt> is true. |
| 230 | * <p> |
| 231 | * You can call this method multiple times, it will not cause an |
| 232 | * {@link IOException} for subsequent calls. |
| 233 | * <p> |
| 234 | * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method |
| 235 | * prior to this one, it will just decrease the internal count of how many |
| 236 | * open streams it held and do nothing else. The stream will actually be |
| 237 | * closed when you have called {@link BufferedInputStream#close()} once more |
| 238 | * than {@link BufferedInputStream#open()}. |
| 239 | * |
| 240 | * @param includingSubStream |
| 241 | * also close the under-laying stream |
| 242 | * |
| 243 | * @exception IOException |
| 244 | * in case of I/O error |
| 245 | */ |
| 246 | public synchronized void close(boolean includingSubStream) |
| 247 | throws IOException { |
| 248 | if (!closed) { |
| 249 | if (openCounter > 0) { |
| 250 | openCounter--; |
| 251 | } else { |
| 252 | closed = true; |
| 253 | flush(includingSubStream); |
| 254 | if (includingSubStream && out != null) { |
| 255 | out.close(); |
| 256 | } |
| 257 | } |
| 258 | } |
| 259 | } |
| 260 | } |