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 */
17package org.apache.commons.io.output;
18
19import java.io.IOException;
20import java.io.InputStream;
21import java.io.OutputStream;
22import java.io.UnsupportedEncodingException;
23import java.util.ArrayList;
24import 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 */
51public 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     */
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     */
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     */
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     */
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
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
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     */
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     */
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
227    public void close() throws IOException {
228        //nop
229    }
230
231    /**
232     * @see java.io.ByteArrayOutputStream#reset()
233     */
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     */
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     */
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
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     */
308    public String toString(String enc) throws UnsupportedEncodingException {
309        return new String(toByteArray(), enc);
310    }
311
312}
313