001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.util.io;
018
019import java.io.IOException;
020import java.io.OutputStream;
021import java.io.UnsupportedEncodingException;
022import java.util.List;
023
024/**
025 * This class implements an output stream in which the data is written into a byte array. The buffer
026 * automatically grows as data is written to it.
027 * <p>
028 * The data can be retrieved using <code>toByteArray()</code> and <code>toString()</code>.
029 * <p>
030 * Closing a <tt>ByteArrayOutputStream</tt> has no effect. The methods in this class can be called
031 * after the stream has been closed without generating an <tt>IOException</tt>.
032 * <p>
033 * This is an alternative implementation of the java.io.ByteArrayOutputStream class. The original
034 * implementation only allocates 32 bytes at the beginning. As this class is designed for heavy duty
035 * it starts at 1024 bytes. In contrast to the original it doesn't reallocate the whole memory block
036 * but allocates additional buffers. This way no buffers need to be garbage collected and the
037 * contents don't have to be copied to the new buffer. This class is designed to behave exactly like
038 * the original. The only exception is the deprecated toString(int) method that has been ignored.
039 * 
040 * @author <a href="mailto:jeremias@apache.org">Jeremias Maerki</a>
041 * @version $Id$
042 */
043public class ByteArrayOutputStream extends OutputStream
044{
045        private final List<byte[]> buffers = new java.util.ArrayList<>();
046        private int count;
047        private byte[] currentBuffer;
048        private int currentBufferIndex;
049        private int filledBufferSum;
050
051        /**
052         * Creates a new byte array output stream. The buffer capacity is initially 1024 bytes, though
053         * its size increases if necessary.
054         */
055        public ByteArrayOutputStream()
056        {
057                this(1024);
058        }
059
060        /**
061         * Creates a new byte array output stream, with a buffer capacity of the specified size, in
062         * bytes.
063         * 
064         * @param size
065         *            the initial size.
066         * @exception IllegalArgumentException
067         *                if size is negative.
068         */
069        public ByteArrayOutputStream(final int size)
070        {
071                if (size < 0)
072                {
073                        throw new IllegalArgumentException("Negative initial size: " + size);
074                }
075                needNewBuffer(size);
076        }
077
078        /**
079         * Closing a <tt>ByteArrayOutputStream</tt> has no effect. The methods in this class can be
080         * called after the stream has been closed without generating an <tt>IOException</tt>.
081         * 
082         * @throws IOException
083         *             in case an I/O error occurs
084         */
085        @Override
086        public void close() throws IOException
087        {
088                // nop
089        }
090
091        /**
092         * @see java.io.ByteArrayOutputStream#reset()
093         */
094        public synchronized void reset()
095        {
096                count = 0;
097                filledBufferSum = 0;
098                currentBufferIndex = 0;
099                currentBuffer = getBuffer(currentBufferIndex);
100        }
101
102        /**
103         * Gets the size.
104         * 
105         * @return the size
106         */
107        public int size()
108        {
109                return count;
110        }
111
112        /**
113         * Writes to a byte array.
114         * 
115         * @return this is a byte array
116         */
117        public synchronized byte[] toByteArray()
118        {
119                int remaining = count;
120                int pos = 0;
121                byte newbuf[] = new byte[count];
122                for (int i = 0; i < buffers.size(); i++)
123                {
124                        byte[] buf = getBuffer(i);
125                        int c = Math.min(buf.length, remaining);
126                        System.arraycopy(buf, 0, newbuf, pos, c);
127                        pos += c;
128                        remaining -= c;
129                        if (remaining == 0)
130                        {
131                                break;
132                        }
133                }
134                return newbuf;
135        }
136
137        /**
138         * @see java.lang.Object#toString()
139         */
140        @Override
141        public String toString()
142        {
143                return new String(toByteArray());
144        }
145
146        /**
147         * This as a string using the provided encoding.
148         * 
149         * @param enc
150         *            the encoding to use
151         * @return This as a string using the provided encoding
152         * @throws UnsupportedEncodingException
153         */
154        public String toString(final String enc) throws UnsupportedEncodingException
155        {
156                return new String(toByteArray(), enc);
157        }
158
159        /**
160         * @see java.io.OutputStream#write(byte[], int, int)
161         */
162        @Override
163        public synchronized void write(final byte[] b, final int off, final int len)
164        {
165                if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) ||
166                        ((off + len) < 0))
167                {
168                        throw new IndexOutOfBoundsException();
169                }
170                else if (len == 0)
171                {
172                        return;
173                }
174                int newcount = count + len;
175                int remaining = len;
176                int inBufferPos = count - filledBufferSum;
177                while (remaining > 0)
178                {
179                        int part = Math.min(remaining, currentBuffer.length - inBufferPos);
180                        System.arraycopy(b, off + len - remaining, currentBuffer, inBufferPos, part);
181                        remaining -= part;
182                        if (remaining > 0)
183                        {
184                                needNewBuffer(newcount);
185                                inBufferPos = 0;
186                        }
187                }
188                count = newcount;
189        }
190
191        /**
192         * Calls the write(byte[]) method.
193         * 
194         * @see java.io.OutputStream#write(int)
195         */
196        @Override
197        public synchronized void write(final int b)
198        {
199                write(new byte[] { (byte)b }, 0, 1);
200        }
201
202        /**
203         * Write to the given output stream.
204         * 
205         * @param out
206         *            the output stream to write to
207         * @throws IOException
208         * @see java.io.ByteArrayOutputStream#writeTo(OutputStream)
209         */
210        public synchronized void writeTo(final OutputStream out) throws IOException
211        {
212                int remaining = count;
213                for (int i = 0; i < buffers.size(); i++)
214                {
215                        byte[] buf = getBuffer(i);
216                        int c = Math.min(buf.length, remaining);
217                        out.write(buf, 0, c);
218                        remaining -= c;
219                        if (remaining == 0)
220                        {
221                                break;
222                        }
223                }
224        }
225
226        private byte[] getBuffer(final int index)
227        {
228                return buffers.get(index);
229        }
230
231        private void needNewBuffer(final int newcount)
232        {
233                if (currentBufferIndex < buffers.size() - 1)
234                {
235                        // Recycling old buffer
236                        filledBufferSum += currentBuffer.length;
237
238                        currentBufferIndex++;
239                        currentBuffer = getBuffer(currentBufferIndex);
240                }
241                else
242                {
243                        // Creating new buffer
244                        int newBufferSize;
245                        if (currentBuffer == null)
246                        {
247                                newBufferSize = newcount;
248                                filledBufferSum = 0;
249                        }
250                        else
251                        {
252                                newBufferSize = Math.max(currentBuffer.length << 1, newcount - filledBufferSum);
253                                filledBufferSum += currentBuffer.length;
254                        }
255
256                        currentBufferIndex++;
257                        currentBuffer = new byte[newBufferSize];
258                        buffers.add(currentBuffer);
259                }
260        }
261
262}