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