1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.codehaus.plexus.archiver.zip;
18
19 import java.io.ByteArrayInputStream;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.io.OutputStream;
23 import java.io.SequenceInputStream;
24 import java.io.UnsupportedEncodingException;
25 import java.nio.charset.Charset;
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.List;
29
30 import org.apache.commons.io.input.ClosedInputStream;
31
32 /**
33 * This class implements an output stream in which the data is
34 * written into a byte array. The buffer automatically grows as data
35 * is written to it.
36 * <p>
37 * The data can be retrieved using <code>toByteArray()</code> and
38 * <code>toString()</code>.
39 * <p>
40 * Closing a {@code ByteArrayOutputStream} has no effect. The methods in
41 * this class can be called after the stream has been closed without
42 * generating an {@code IOException}.
43 * <p>
44 * This is an alternative implementation of the {@link java.io.ByteArrayOutputStream}
45 * class. The original implementation only allocates 32 bytes at the beginning.
46 * As this class is designed for heavy duty it starts at 1024 bytes. In contrast
47 * to the original it doesn't reallocate the whole memory block but allocates
48 * additional buffers. This way no buffers need to be garbage collected and
49 * the contents don't have to be copied to the new buffer. This class is
50 * designed to behave exactly like the original. The only exception is the
51 * deprecated toString(int) method that has been ignored.
52 */
53 public class ByteArrayOutputStream extends OutputStream {
54
55 /**
56 * A singleton empty byte array.
57 */
58 private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
59
60 /**
61 * Maximum size of a single buffer (16MB) to prevent excessive memory allocation.
62 * When buffers need to grow beyond this size, additional buffers are created instead.
63 */
64 private static final int MAX_BUFFER_SIZE = 16 * 1024 * 1024;
65
66 /**
67 * The list of buffers, which grows and never reduces.
68 */
69 private final List<byte[]> buffers = new ArrayList<byte[]>();
70
71 /**
72 * The index of the current buffer.
73 */
74 private int currentBufferIndex;
75
76 /**
77 * The total count of bytes in all the filled buffers.
78 */
79 private int filledBufferSum;
80
81 /**
82 * The current buffer.
83 */
84 private byte[] currentBuffer;
85
86 /**
87 * The total count of bytes written.
88 */
89 private int count;
90
91 /**
92 * Flag to indicate if the buffers can be reused after reset
93 */
94 private boolean reuseBuffers = true;
95
96 /**
97 * Creates a new byte array output stream. The buffer capacity is
98 * initially 1024 bytes, though its size increases if necessary.
99 */
100 public ByteArrayOutputStream() {
101 this(1024);
102 }
103
104 /**
105 * Creates a new byte array output stream, with a buffer capacity of
106 * the specified size, in bytes.
107 *
108 * @param size the initial size
109 *
110 * @throws IllegalArgumentException if size is negative
111 */
112 public ByteArrayOutputStream(final int size) {
113 if (size < 0) {
114 throw new IllegalArgumentException("Negative initial size: " + size);
115 }
116 synchronized (this) {
117 needNewBuffer(size);
118 }
119 }
120
121 /**
122 * Makes a new buffer available either by allocating
123 * a new one or re-cycling an existing one.
124 *
125 * @param newcount the size of the buffer if one is created
126 */
127 private void needNewBuffer(final int newcount) {
128 if (currentBufferIndex < buffers.size() - 1) {
129 // Recycling old buffer
130 filledBufferSum += currentBuffer.length;
131
132 currentBufferIndex++;
133 currentBuffer = buffers.get(currentBufferIndex);
134 } else {
135 // Creating new buffer
136 int newBufferSize;
137 if (currentBuffer == null) {
138 newBufferSize = newcount;
139 filledBufferSum = 0;
140 } else {
141 newBufferSize = Math.max(currentBuffer.length << 1, newcount - filledBufferSum);
142 filledBufferSum += currentBuffer.length;
143 }
144
145 // Cap the buffer size to prevent excessive memory allocation
146 newBufferSize = Math.min(newBufferSize, MAX_BUFFER_SIZE);
147
148 currentBufferIndex++;
149 currentBuffer = new byte[newBufferSize];
150 buffers.add(currentBuffer);
151 }
152 }
153
154 /**
155 * Write the bytes to byte array.
156 *
157 * @param b the bytes to write
158 * @param off The start offset
159 * @param len The number of bytes to write
160 */
161 @Override
162 public void write(final byte[] b, final int off, final int len) {
163 if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) {
164 throw new IndexOutOfBoundsException();
165 } else if (len == 0) {
166 return;
167 }
168 synchronized (this) {
169 final int newcount = count + len;
170 int remaining = len;
171 int inBufferPos = count - filledBufferSum;
172 while (remaining > 0) {
173 final int part = Math.min(remaining, currentBuffer.length - inBufferPos);
174 System.arraycopy(b, off + len - remaining, currentBuffer, inBufferPos, part);
175 remaining -= part;
176 if (remaining > 0) {
177 needNewBuffer(newcount);
178 inBufferPos = 0;
179 }
180 }
181 count = newcount;
182 }
183 }
184
185 /**
186 * Write a byte to byte array.
187 *
188 * @param b the byte to write
189 */
190 @Override
191 public synchronized void write(final int b) {
192 int inBufferPos = count - filledBufferSum;
193 if (inBufferPos == currentBuffer.length) {
194 needNewBuffer(count + 1);
195 inBufferPos = 0;
196 }
197 currentBuffer[inBufferPos] = (byte) b;
198 count++;
199 }
200
201 /**
202 * Writes the entire contents of the specified input stream to this
203 * byte stream. Bytes from the input stream are read directly into the
204 * internal buffers of this streams.
205 *
206 * @param in the input stream to read from
207 *
208 * @return total number of bytes read from the input stream
209 * (and written to this stream)
210 *
211 * @throws java.io.IOException if an I/O error occurs while reading the input stream
212 * @since 1.4
213 */
214 public synchronized int write(final InputStream in) throws IOException {
215 int readCount = 0;
216 int inBufferPos = count - filledBufferSum;
217 int n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos);
218 while (n != -1) {
219 readCount += n;
220 inBufferPos += n;
221 count += n;
222 if (inBufferPos == currentBuffer.length) {
223 needNewBuffer(currentBuffer.length);
224 inBufferPos = 0;
225 }
226 n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos);
227 }
228 return readCount;
229 }
230
231 /**
232 * Return the current size of the byte array.
233 *
234 * @return the current size of the byte array
235 */
236 public synchronized int size() {
237 return count;
238 }
239
240 /**
241 * Closing a {@code ByteArrayOutputStream} has no effect. The methods in
242 * this class can be called after the stream has been closed without
243 * generating an {@code IOException}.
244 *
245 * @throws java.io.IOException never (this method should not declare this exception
246 * but it has to now due to backwards compatibility)
247 */
248 @Override
249 public void close() throws IOException {
250 // nop
251 }
252
253 /**
254 * @see java.io.ByteArrayOutputStream#reset()
255 */
256 public synchronized void reset() {
257 count = 0;
258 filledBufferSum = 0;
259 currentBufferIndex = 0;
260 if (reuseBuffers) {
261 currentBuffer = buffers.get(currentBufferIndex);
262 } else {
263 // Throw away old buffers
264 currentBuffer = null;
265 int size = buffers.get(0).length;
266 buffers.clear();
267 needNewBuffer(size);
268 reuseBuffers = true;
269 }
270 }
271
272 /**
273 * Writes the entire contents of this byte stream to the
274 * specified output stream.
275 *
276 * @param out the output stream to write to
277 *
278 * @throws java.io.IOException if an I/O error occurs, such as if the stream is closed
279 * @see java.io.ByteArrayOutputStream#writeTo(java.io.OutputStream)
280 */
281 public synchronized void writeTo(final OutputStream out) throws IOException {
282 int remaining = count;
283 for (final byte[] buf : buffers) {
284 final int c = Math.min(buf.length, remaining);
285 out.write(buf, 0, c);
286 remaining -= c;
287 if (remaining == 0) {
288 break;
289 }
290 }
291 }
292
293 /**
294 * Fetches entire contents of an <code>InputStream</code> and represent
295 * same data as result InputStream.
296 * <p>
297 * This method is useful where,
298 * <ul>
299 * <li>Source InputStream is slow.</li>
300 * <li>It has network resources associated, so we cannot keep it open for
301 * long time.</li>
302 * <li>It has network timeout associated.</li>
303 * </ul>
304 * It can be used in favor of {@link #toByteArray()}, since it
305 * avoids unnecessary allocation and copy of byte[].<br>
306 * This method buffers the input internally, so there is no need to use a
307 * <code>BufferedInputStream</code>.
308 *
309 * @param input Stream to be fully buffered.
310 *
311 * @return A fully buffered stream.
312 *
313 * @throws java.io.IOException if an I/O error occurs
314 * @since 2.0
315 */
316 public static InputStream toBufferedInputStream(final InputStream input) throws IOException {
317 return toBufferedInputStream(input, 1024);
318 }
319
320 /**
321 * Fetches entire contents of an <code>InputStream</code> and represent
322 * same data as result InputStream.
323 * <p>
324 * This method is useful where,
325 * <ul>
326 * <li>Source InputStream is slow.</li>
327 * <li>It has network resources associated, so we cannot keep it open for
328 * long time.</li>
329 * <li>It has network timeout associated.</li>
330 * </ul>
331 * It can be used in favor of {@link #toByteArray()}, since it
332 * avoids unnecessary allocation and copy of byte[].<br>
333 * This method buffers the input internally, so there is no need to use a
334 * <code>BufferedInputStream</code>.
335 *
336 * @param input Stream to be fully buffered.
337 * @param size the initial buffer size
338 *
339 * @return A fully buffered stream.
340 *
341 * @throws java.io.IOException if an I/O error occurs
342 * @since 2.5
343 */
344 public static InputStream toBufferedInputStream(final InputStream input, int size) throws IOException {
345 // It does not matter if a ByteArrayOutputStream is not closed as close() is a no-op
346 @SuppressWarnings("resource")
347 final ByteArrayOutputStream output = new ByteArrayOutputStream(size);
348 output.write(input);
349 return output.toInputStream();
350 }
351
352 /**
353 * Gets the current contents of this byte stream as a Input Stream. The
354 * returned stream is backed by buffers of <code>this</code> stream,
355 * avoiding memory allocation and copy, thus saving space and time.<br>
356 *
357 * @return the current contents of this output stream.
358 *
359 * @see java.io.ByteArrayOutputStream#toByteArray()
360 * @see #reset()
361 * @since 2.5
362 */
363 public synchronized InputStream toInputStream() {
364 int remaining = count;
365 if (remaining == 0) {
366 return new ClosedInputStream();
367 }
368 final List<ByteArrayInputStream> list = new ArrayList<ByteArrayInputStream>(buffers.size());
369 for (final byte[] buf : buffers) {
370 final int c = Math.min(buf.length, remaining);
371 list.add(new ByteArrayInputStream(buf, 0, c));
372 remaining -= c;
373 if (remaining == 0) {
374 break;
375 }
376 }
377 reuseBuffers = false;
378 return new SequenceInputStream(Collections.enumeration(list));
379 }
380
381 /**
382 * Gets the curent contents of this byte stream as a byte array.
383 * The result is independent of this stream.
384 *
385 * @return the current contents of this output stream, as a byte array
386 *
387 * @see java.io.ByteArrayOutputStream#toByteArray()
388 */
389 public synchronized byte[] toByteArray() {
390 int remaining = count;
391 if (remaining == 0) {
392 return EMPTY_BYTE_ARRAY;
393 }
394 final byte newbuf[] = new byte[remaining];
395 int pos = 0;
396 for (final byte[] buf : buffers) {
397 final int c = Math.min(buf.length, remaining);
398 System.arraycopy(buf, 0, newbuf, pos, c);
399 pos += c;
400 remaining -= c;
401 if (remaining == 0) {
402 break;
403 }
404 }
405 return newbuf;
406 }
407
408 /**
409 * Gets the curent contents of this byte stream as a string
410 * using the platform default charset.
411 *
412 * @return the contents of the byte array as a String
413 *
414 * @see java.io.ByteArrayOutputStream#toString()
415 * @deprecated 2.5 use {@link #toString(String)} instead
416 */
417 @Override
418 @Deprecated
419 public String toString() {
420 // make explicit the use of the default charset
421 return new String(toByteArray(), Charset.defaultCharset());
422 }
423
424 /**
425 * Gets the curent contents of this byte stream as a string
426 * using the specified encoding.
427 *
428 * @param enc the name of the character encoding
429 *
430 * @return the string converted from the byte array
431 *
432 * @throws java.io.UnsupportedEncodingException if the encoding is not supported
433 * @see java.io.ByteArrayOutputStream#toString(String)
434 */
435 public String toString(final String enc) throws UnsupportedEncodingException {
436 return new String(toByteArray(), enc);
437 }
438
439 /**
440 * Gets the curent contents of this byte stream as a string
441 * using the specified encoding.
442 *
443 * @param charset the character encoding
444 *
445 * @return the string converted from the byte array
446 *
447 * @see java.io.ByteArrayOutputStream#toString(String)
448 * @since 2.5
449 */
450 public String toString(final Charset charset) {
451 return new String(toByteArray(), charset);
452 }
453 }