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.apache.commons.io.output;
18 
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.OutputStream;
22 import java.io.UnsupportedEncodingException;
23 import java.util.ArrayList;
24 import java.util.List;
25 
26 /**
27  * This class implements an output stream in which the data is
28  * written into a byte array. The buffer automatically grows as data
29  * is written to it.
30  * <p>
31  * The data can be retrieved using <code>toByteArray()</code> and
32  * <code>toString()</code>.
33  * <p>
34  * Closing a <tt>ByteArrayOutputStream</tt> has no effect. The methods in
35  * this class can be called after the stream has been closed without
36  * generating an <tt>IOException</tt>.
37  * <p>
38  * This is an alternative implementation of the java.io.ByteArrayOutputStream
39  * class. The original implementation only allocates 32 bytes at the beginning.
40  * As this class is designed for heavy duty it starts at 1024 bytes. In contrast
41  * to the original it doesn't reallocate the whole memory block but allocates
42  * additional buffers. This way no buffers need to be garbage collected and
43  * the contents don't have to be copied to the new buffer. This class is
44  * designed to behave exactly like the original. The only exception is the
45  * deprecated toString(int) method that has been ignored.
46  *
47  * @author <a href="mailto:jeremias@apache.org">Jeremias Maerki</a>
48  * @author Holger Hoffstatte
49  * @version $Id: ByteArrayOutputStream.java 610010 2008-01-08 14:50:59Z niallp $
50  */
51 public class ByteArrayOutputStream extends OutputStream {
52 
53     /** A singleton empty byte array. */
54     private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
55 
56     /** The list of buffers, which grows and never reduces. */
57     private List<byte[]> buffers = new ArrayList<byte[]>();
58     /** The index of the current buffer. */
59     private int currentBufferIndex;
60     /** The total count of bytes in all the filled buffers. */
61     private int filledBufferSum;
62     /** The current buffer. */
63     private byte[] currentBuffer;
64     /** The total count of bytes written. */
65     private int count;
66 
67     /**
68      * Creates a new byte array output stream. The buffer capacity is
69      * initially 1024 bytes, though its size increases if necessary.
70      */
ByteArrayOutputStream()71     public ByteArrayOutputStream() {
72         this(1024);
73     }
74 
75     /**
76      * Creates a new byte array output stream, with a buffer capacity of
77      * the specified size, in bytes.
78      *
79      * @param size  the initial size
80      * @throws IllegalArgumentException if size is negative
81      */
ByteArrayOutputStream(int size)82     public ByteArrayOutputStream(int size) {
83         if (size < 0) {
84             throw new IllegalArgumentException(
85                 "Negative initial size: " + size);
86         }
87         needNewBuffer(size);
88     }
89 
90     /**
91      * Return the appropriate <code>byte[]</code> buffer
92      * specified by index.
93      *
94      * @param index  the index of the buffer required
95      * @return the buffer
96      */
getBuffer(int index)97     private byte[] getBuffer(int index) {
98         return buffers.get(index);
99     }
100 
101     /**
102      * Makes a new buffer available either by allocating
103      * a new one or re-cycling an existing one.
104      *
105      * @param newcount  the size of the buffer if one is created
106      */
needNewBuffer(int newcount)107     private void needNewBuffer(int newcount) {
108         if (currentBufferIndex < buffers.size() - 1) {
109             //Recycling old buffer
110             filledBufferSum += currentBuffer.length;
111 
112             currentBufferIndex++;
113             currentBuffer = getBuffer(currentBufferIndex);
114         } else {
115             //Creating new buffer
116             int newBufferSize;
117             if (currentBuffer == null) {
118                 newBufferSize = newcount;
119                 filledBufferSum = 0;
120             } else {
121                 newBufferSize = Math.max(
122                     currentBuffer.length << 1,
123                     newcount - filledBufferSum);
124                 filledBufferSum += currentBuffer.length;
125             }
126 
127             currentBufferIndex++;
128             currentBuffer = new byte[newBufferSize];
129             buffers.add(currentBuffer);
130         }
131     }
132 
133     /**
134      * Write the bytes to byte array.
135      * @param b the bytes to write
136      * @param off The start offset
137      * @param len The number of bytes to write
138      */
139     @Override
write(byte[] b, int off, int len)140     public void write(byte[] b, int off, int len) {
141         if ((off < 0)
142                 || (off > b.length)
143                 || (len < 0)
144                 || ((off + len) > b.length)
145                 || ((off + len) < 0)) {
146             throw new IndexOutOfBoundsException();
147         } else if (len == 0) {
148             return;
149         }
150         synchronized (this) {
151             int newcount = count + len;
152             int remaining = len;
153             int inBufferPos = count - filledBufferSum;
154             while (remaining > 0) {
155                 int part = Math.min(remaining, currentBuffer.length - inBufferPos);
156                 System.arraycopy(b, off + len - remaining, currentBuffer, inBufferPos, part);
157                 remaining -= part;
158                 if (remaining > 0) {
159                     needNewBuffer(newcount);
160                     inBufferPos = 0;
161                 }
162             }
163             count = newcount;
164         }
165     }
166 
167     /**
168      * Write a byte to byte array.
169      * @param b the byte to write
170      */
171     @Override
write(int b)172     public synchronized void write(int b) {
173         int inBufferPos = count - filledBufferSum;
174         if (inBufferPos == currentBuffer.length) {
175             needNewBuffer(count + 1);
176             inBufferPos = 0;
177         }
178         currentBuffer[inBufferPos] = (byte) b;
179         count++;
180     }
181 
182     /**
183      * Writes the entire contents of the specified input stream to this
184      * byte stream. Bytes from the input stream are read directly into the
185      * internal buffers of this streams.
186      *
187      * @param in the input stream to read from
188      * @return total number of bytes read from the input stream
189      *         (and written to this stream)
190      * @throws IOException if an I/O error occurs while reading the input stream
191      * @since Commons IO 1.4
192      */
write(InputStream in)193     public synchronized int write(InputStream in) throws IOException {
194         int readCount = 0;
195         int inBufferPos = count - filledBufferSum;
196         int n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos);
197         while (n != -1) {
198             readCount += n;
199             inBufferPos += n;
200             count += n;
201             if (inBufferPos == currentBuffer.length) {
202                 needNewBuffer(currentBuffer.length);
203                 inBufferPos = 0;
204             }
205             n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos);
206         }
207         return readCount;
208     }
209 
210     /**
211      * Return the current size of the byte array.
212      * @return the current size of the byte array
213      */
size()214     public synchronized int size() {
215         return count;
216     }
217 
218     /**
219      * Closing a <tt>ByteArrayOutputStream</tt> has no effect. The methods in
220      * this class can be called after the stream has been closed without
221      * generating an <tt>IOException</tt>.
222      *
223      * @throws IOException never (this method should not declare this exception
224      * but it has to now due to backwards compatability)
225      */
226     @Override
close()227     public void close() throws IOException {
228         //nop
229     }
230 
231     /**
232      * @see java.io.ByteArrayOutputStream#reset()
233      */
reset()234     public synchronized void reset() {
235         count = 0;
236         filledBufferSum = 0;
237         currentBufferIndex = 0;
238         currentBuffer = getBuffer(currentBufferIndex);
239     }
240 
241     /**
242      * Writes the entire contents of this byte stream to the
243      * specified output stream.
244      *
245      * @param out  the output stream to write to
246      * @throws IOException if an I/O error occurs, such as if the stream is closed
247      * @see java.io.ByteArrayOutputStream#writeTo(OutputStream)
248      */
writeTo(OutputStream out)249     public synchronized void writeTo(OutputStream out) throws IOException {
250         int remaining = count;
251         for (int i = 0; i < buffers.size(); i++) {
252             byte[] buf = getBuffer(i);
253             int c = Math.min(buf.length, remaining);
254             out.write(buf, 0, c);
255             remaining -= c;
256             if (remaining == 0) {
257                 break;
258             }
259         }
260     }
261 
262     /**
263      * Gets the curent contents of this byte stream as a byte array.
264      * The result is independent of this stream.
265      *
266      * @return the current contents of this output stream, as a byte array
267      * @see java.io.ByteArrayOutputStream#toByteArray()
268      */
toByteArray()269     public synchronized byte[] toByteArray() {
270         int remaining = count;
271         if (remaining == 0) {
272             return EMPTY_BYTE_ARRAY;
273         }
274         byte newbuf[] = new byte[remaining];
275         int pos = 0;
276         for (int i = 0; i < buffers.size(); i++) {
277             byte[] buf = getBuffer(i);
278             int c = Math.min(buf.length, remaining);
279             System.arraycopy(buf, 0, newbuf, pos, c);
280             pos += c;
281             remaining -= c;
282             if (remaining == 0) {
283                 break;
284             }
285         }
286         return newbuf;
287     }
288 
289     /**
290      * Gets the curent contents of this byte stream as a string.
291      * @return the contents of the byte array as a String
292      * @see java.io.ByteArrayOutputStream#toString()
293      */
294     @Override
toString()295     public String toString() {
296         return new String(toByteArray());
297     }
298 
299     /**
300      * Gets the curent contents of this byte stream as a string
301      * using the specified encoding.
302      *
303      * @param enc  the name of the character encoding
304      * @return the string converted from the byte array
305      * @throws UnsupportedEncodingException if the encoding is not supported
306      * @see java.io.ByteArrayOutputStream#toString(String)
307      */
toString(String enc)308     public String toString(String enc) throws UnsupportedEncodingException {
309         return new String(toByteArray(), enc);
310     }
311 
312 }
313