Streams: bug fixes
[nikiroo-utils.git] / src / be / nikiroo / utils / streams / BufferedOutputStream.java
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 &gt; 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 &mdash; 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 *
181 * @param includingSubStream
182 * also flush the under-laying stream
183 * @throws IOException
184 * in case of I/O error
185 */
186 protected void flush(boolean includingSubStream) throws IOException {
187 if (stop > start) {
188 out.write(buffer, start, stop - start);
189 bytesWritten += (stop - start);
190 }
191 start = 0;
192 stop = 0;
193
194 if (includingSubStream) {
195 out.flush();
196 }
197 }
198
199 /**
200 * Closes this stream and releases any system resources associated with the
201 * stream.
202 * <p>
203 * Including the under-laying {@link InputStream}.
204 * <p>
205 * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
206 * prior to this one, it will just decrease the internal count of how many
207 * open streams it held and do nothing else. The stream will actually be
208 * closed when you have called {@link BufferedInputStream#close()} once more
209 * than {@link BufferedInputStream#open()}.
210 *
211 * @exception IOException
212 * in case of I/O error
213 */
214 @Override
215 public synchronized void close() throws IOException {
216 close(true);
217 }
218
219 /**
220 * Closes this stream and releases any system resources associated with the
221 * stream.
222 * <p>
223 * Including the under-laying {@link InputStream} if
224 * <tt>incudingSubStream</tt> is true.
225 * <p>
226 * You can call this method multiple times, it will not cause an
227 * {@link IOException} for subsequent calls.
228 * <p>
229 * <b>Note:</b> if you called the {@link BufferedInputStream#open()} method
230 * prior to this one, it will just decrease the internal count of how many
231 * open streams it held and do nothing else. The stream will actually be
232 * closed when you have called {@link BufferedInputStream#close()} once more
233 * than {@link BufferedInputStream#open()}.
234 *
235 * @param includingSubStream
236 * also close the under-laying stream
237 *
238 * @exception IOException
239 * in case of I/O error
240 */
241 public synchronized void close(boolean includingSubStream)
242 throws IOException {
243 if (!closed) {
244 if (openCounter > 0) {
245 openCounter--;
246 } else {
247 closed = true;
248 flush(includingSubStream);
249 if (includingSubStream && out != null) {
250 out.close();
251 }
252 }
253 }
254 }
255 }