001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.util;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.OutputStream;
022import java.security.MessageDigest;
023import java.util.Iterator;
024import java.util.LinkedList;
025
026/**
027 * A speedy alternative to {@link java.io.ByteArrayOutputStream}. Note that
028 * this variant does <i>not</i> extend {@code ByteArrayOutputStream}, unlike
029 * its sibling {@link ResizableByteArrayOutputStream}.
030 *
031 * <p>Unlike {@link java.io.ByteArrayOutputStream}, this implementation is backed
032 * by a {@link java.util.LinkedList} of {@code byte[]} instead of 1 constantly
033 * resizing {@code byte[]}. It does not copy buffers when it gets expanded.
034 *
035 * <p>The initial buffer is only created when the stream is first written.
036 * There is also no copying of the internal buffer if its contents is extracted
037 * with the {@link #writeTo(OutputStream)} method.
038 *
039 * @author Craig Andrews
040 * @author Juergen Hoeller
041 * @since 4.2
042 * @see #resize
043 * @see ResizableByteArrayOutputStream
044 */
045public class FastByteArrayOutputStream extends OutputStream {
046
047        private static final int DEFAULT_BLOCK_SIZE = 256;
048
049
050        // The buffers used to store the content bytes
051        private final LinkedList<byte[]> buffers = new LinkedList<byte[]>();
052
053        // The size, in bytes, to use when allocating the first byte[]
054        private final int initialBlockSize;
055
056        // The size, in bytes, to use when allocating the next byte[]
057        private int nextBlockSize = 0;
058
059        // The number of bytes in previous buffers.
060        // (The number of bytes in the current buffer is in 'index'.)
061        private int alreadyBufferedSize = 0;
062
063        // The index in the byte[] found at buffers.getLast() to be written next
064        private int index = 0;
065
066        // Is the stream closed?
067        private boolean closed = false;
068
069
070        /**
071         * Create a new <code>FastByteArrayOutputStream</code>
072         * with the default initial capacity of 256 bytes.
073         */
074        public FastByteArrayOutputStream() {
075                this(DEFAULT_BLOCK_SIZE);
076        }
077
078        /**
079         * Create a new <code>FastByteArrayOutputStream</code>
080         * with the specified initial capacity.
081         * @param initialBlockSize the initial buffer size in bytes
082         */
083        public FastByteArrayOutputStream(int initialBlockSize) {
084                Assert.isTrue(initialBlockSize > 0, "Initial block size must be greater than 0");
085                this.initialBlockSize = initialBlockSize;
086                this.nextBlockSize = initialBlockSize;
087        }
088
089
090        // Overridden methods
091
092        @Override
093        public void write(int datum) throws IOException {
094                if (this.closed) {
095                        throw new IOException("Stream closed");
096                }
097                else {
098                        if (this.buffers.peekLast() == null || this.buffers.getLast().length == this.index) {
099                                addBuffer(1);
100                        }
101                        // store the byte
102                        this.buffers.getLast()[this.index++] = (byte) datum;
103                }
104        }
105
106        @Override
107        public void write(byte[] data, int offset, int length) throws IOException {
108                if (data == null) {
109                        throw new NullPointerException();
110                }
111                else if (offset < 0 || offset + length > data.length || length < 0) {
112                        throw new IndexOutOfBoundsException();
113                }
114                else if (this.closed) {
115                        throw new IOException("Stream closed");
116                }
117                else {
118                        if (this.buffers.peekLast() == null || this.buffers.getLast().length == this.index) {
119                                addBuffer(length);
120                        }
121                        if (this.index + length > this.buffers.getLast().length) {
122                                int pos = offset;
123                                do {
124                                        if (this.index == this.buffers.getLast().length) {
125                                                addBuffer(length);
126                                        }
127                                        int copyLength = this.buffers.getLast().length - this.index;
128                                        if (length < copyLength) {
129                                                copyLength = length;
130                                        }
131                                        System.arraycopy(data, pos, this.buffers.getLast(), this.index, copyLength);
132                                        pos += copyLength;
133                                        this.index += copyLength;
134                                        length -= copyLength;
135                                }
136                                while (length > 0);
137                        }
138                        else {
139                                // copy in the sub-array
140                                System.arraycopy(data, offset, this.buffers.getLast(), this.index, length);
141                                this.index += length;
142                        }
143                }
144        }
145
146        @Override
147        public void close() {
148                this.closed = true;
149        }
150
151        /**
152         * Convert the buffer's contents into a string decoding bytes using the
153         * platform's default character set. The length of the new <tt>String</tt>
154         * is a function of the character set, and hence may not be equal to the
155         * size of the buffer.
156         * <p>This method always replaces malformed-input and unmappable-character
157         * sequences with the default replacement string for the platform's
158         * default character set. The {@linkplain java.nio.charset.CharsetDecoder}
159         * class should be used when more control over the decoding process is
160         * required.
161         * @return a String decoded from the buffer's contents
162         */
163        @Override
164        public String toString() {
165                return new String(toByteArrayUnsafe());
166        }
167
168
169        // Custom methods
170
171        /**
172         * Return the number of bytes stored in this <code>FastByteArrayOutputStream</code>.
173         */
174        public int size() {
175                return (this.alreadyBufferedSize + this.index);
176        }
177
178        /**
179         * Convert the stream's data to a byte array and return the byte array.
180         * <p>Also replaces the internal structures with the byte array to conserve memory:
181         * if the byte array is being made anyways, mind as well as use it. This approach
182         * also means that if this method is called twice without any writes in between,
183         * the second call is a no-op.
184         * <p>This method is "unsafe" as it returns the internal buffer.
185         * Callers should not modify the returned buffer.
186         * @return the current contents of this output stream, as a byte array.
187         * @see #size()
188         * @see #toByteArray()
189         */
190        public byte[] toByteArrayUnsafe() {
191                int totalSize = size();
192                if (totalSize == 0) {
193                        return new byte[0];
194                }
195                resize(totalSize);
196                return this.buffers.getFirst();
197        }
198
199        /**
200         * Creates a newly allocated byte array.
201         * <p>Its size is the current
202         * size of this output stream and the valid contents of the buffer
203         * have been copied into it.</p>
204         * @return the current contents of this output stream, as a byte array.
205         * @see #size()
206         * @see #toByteArrayUnsafe()
207         */
208        public byte[] toByteArray() {
209                byte[] bytesUnsafe = toByteArrayUnsafe();
210                byte[] ret = new byte[bytesUnsafe.length];
211                System.arraycopy(bytesUnsafe, 0, ret, 0, bytesUnsafe.length);
212                return ret;
213        }
214
215        /**
216         * Reset the contents of this <code>FastByteArrayOutputStream</code>.
217         * <p>All currently accumulated output in the output stream is discarded.
218         * The output stream can be used again.
219         */
220        public void reset() {
221                this.buffers.clear();
222                this.nextBlockSize = this.initialBlockSize;
223                this.closed = false;
224                this.index = 0;
225                this.alreadyBufferedSize = 0;
226        }
227
228        /**
229         * Get an {@link InputStream} to retrieve the data in this OutputStream.
230         * <p>Note that if any methods are called on the OutputStream
231         * (including, but not limited to, any of the write methods, {@link #reset()},
232         * {@link #toByteArray()}, and {@link #toByteArrayUnsafe()}) then the
233         * {@link java.io.InputStream}'s behavior is undefined.
234         * @return {@link InputStream} of the contents of this OutputStream
235         */
236        public InputStream getInputStream() {
237                return new FastByteArrayInputStream(this);
238        }
239
240        /**
241         * Write the buffers content to the given OutputStream.
242         * @param out the OutputStream to write to
243         */
244        public void writeTo(OutputStream out) throws IOException {
245                Iterator<byte[]> it = this.buffers.iterator();
246                while (it.hasNext()) {
247                        byte[] bytes = it.next();
248                        if (it.hasNext()) {
249                                out.write(bytes, 0, bytes.length);
250                        }
251                        else {
252                                out.write(bytes, 0, this.index);
253                        }
254                }
255        }
256
257        /**
258         * Resize the internal buffer size to a specified capacity.
259         * @param targetCapacity the desired size of the buffer
260         * @throws IllegalArgumentException if the given capacity is smaller than
261         * the actual size of the content stored in the buffer already
262         * @see FastByteArrayOutputStream#size()
263         */
264        public void resize(int targetCapacity) {
265                Assert.isTrue(targetCapacity >= size(), "New capacity must not be smaller than current size");
266                if (this.buffers.peekFirst() == null) {
267                        this.nextBlockSize = targetCapacity - size();
268                }
269                else if (size() == targetCapacity && this.buffers.getFirst().length == targetCapacity) {
270                        // do nothing - already at the targetCapacity
271                }
272                else {
273                        int totalSize = size();
274                        byte[] data = new byte[targetCapacity];
275                        int pos = 0;
276                        Iterator<byte[]> it = this.buffers.iterator();
277                        while (it.hasNext()) {
278                                byte[] bytes = it.next();
279                                if (it.hasNext()) {
280                                        System.arraycopy(bytes, 0, data, pos, bytes.length);
281                                        pos += bytes.length;
282                                }
283                                else {
284                                        System.arraycopy(bytes, 0, data, pos, this.index);
285                                }
286                        }
287                        this.buffers.clear();
288                        this.buffers.add(data);
289                        this.index = totalSize;
290                        this.alreadyBufferedSize = 0;
291                }
292        }
293
294        /**
295         * Create a new buffer and store it in the LinkedList.
296         * <p>Adds a new buffer that can store at least {@code minCapacity} bytes.
297         */
298        private void addBuffer(int minCapacity) {
299                if (this.buffers.peekLast() != null) {
300                        this.alreadyBufferedSize += this.index;
301                        this.index = 0;
302                }
303                if (this.nextBlockSize < minCapacity) {
304                        this.nextBlockSize = nextPowerOf2(minCapacity);
305                }
306                this.buffers.add(new byte[this.nextBlockSize]);
307                this.nextBlockSize *= 2;  // block size doubles each time
308        }
309
310        /**
311         * Get the next power of 2 of a number (ex, the next power of 2 of 119 is 128).
312         */
313        private static int nextPowerOf2(int val) {
314                val--;
315                val = (val >> 1) | val;
316                val = (val >> 2) | val;
317                val = (val >> 4) | val;
318                val = (val >> 8) | val;
319                val = (val >> 16) | val;
320                val++;
321                return val;
322        }
323
324
325        /**
326         * An implementation of {@link java.io.InputStream} that reads from a given
327         * <code>FastByteArrayOutputStream</code>.
328         */
329        private static final class FastByteArrayInputStream extends UpdateMessageDigestInputStream {
330
331                private final FastByteArrayOutputStream fastByteArrayOutputStream;
332
333                private final Iterator<byte[]> buffersIterator;
334
335                private byte[] currentBuffer;
336
337                private int currentBufferLength = 0;
338
339                private int nextIndexInCurrentBuffer = 0;
340
341                private int totalBytesRead = 0;
342
343                /**
344                 * Create a new <code>FastByteArrayOutputStreamInputStream</code> backed
345                 * by the given <code>FastByteArrayOutputStream</code>.
346                 */
347                public FastByteArrayInputStream(FastByteArrayOutputStream fastByteArrayOutputStream) {
348                        this.fastByteArrayOutputStream = fastByteArrayOutputStream;
349                        this.buffersIterator = fastByteArrayOutputStream.buffers.iterator();
350                        if (this.buffersIterator.hasNext()) {
351                                this.currentBuffer = this.buffersIterator.next();
352                                if (this.currentBuffer == fastByteArrayOutputStream.buffers.getLast()) {
353                                        this.currentBufferLength = fastByteArrayOutputStream.index;
354                                }
355                                else {
356                                        this.currentBufferLength = this.currentBuffer.length;
357                                }
358                        }
359                }
360
361                @Override
362                public int read() {
363                        if (this.currentBuffer == null) {
364                                // This stream doesn't have any data in it...
365                                return -1;
366                        }
367                        else {
368                                if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
369                                        this.totalBytesRead++;
370                                        return this.currentBuffer[this.nextIndexInCurrentBuffer++] & 0xFF;
371                                }
372                                else {
373                                        if (this.buffersIterator.hasNext()) {
374                                                this.currentBuffer = this.buffersIterator.next();
375                                                if (this.currentBuffer == this.fastByteArrayOutputStream.buffers.getLast()) {
376                                                        this.currentBufferLength = this.fastByteArrayOutputStream.index;
377                                                }
378                                                else {
379                                                        this.currentBufferLength = this.currentBuffer.length;
380                                                }
381                                                this.nextIndexInCurrentBuffer = 0;
382                                        }
383                                        else {
384                                                this.currentBuffer = null;
385                                        }
386                                        return read();
387                                }
388                        }
389                }
390
391                @Override
392                public int read(byte[] b) {
393                        return read(b, 0, b.length);
394                }
395
396                @Override
397                public int read(byte[] b, int off, int len) {
398                        if (b == null) {
399                                throw new NullPointerException();
400                        }
401                        else if (off < 0 || len < 0 || len > b.length - off) {
402                                throw new IndexOutOfBoundsException();
403                        }
404                        else if (len == 0) {
405                                return 0;
406                        }
407                        else {
408                                if (this.currentBuffer == null) {
409                                        // This stream doesn't have any data in it...
410                                        return -1;
411                                }
412                                else {
413                                        if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
414                                                int bytesToCopy = Math.min(len, this.currentBufferLength - this.nextIndexInCurrentBuffer);
415                                                System.arraycopy(this.currentBuffer, this.nextIndexInCurrentBuffer, b, off, bytesToCopy);
416                                                this.totalBytesRead += bytesToCopy;
417                                                this.nextIndexInCurrentBuffer += bytesToCopy;
418                                                int remaining = read(b, off + bytesToCopy, len - bytesToCopy);
419                                                return bytesToCopy + Math.max(remaining, 0);
420                                        }
421                                        else {
422                                                if (this.buffersIterator.hasNext()) {
423                                                        this.currentBuffer = this.buffersIterator.next();
424                                                        if (this.currentBuffer == this.fastByteArrayOutputStream.buffers.getLast()) {
425                                                                this.currentBufferLength = this.fastByteArrayOutputStream.index;
426                                                        }
427                                                        else {
428                                                                this.currentBufferLength = this.currentBuffer.length;
429                                                        }
430                                                        this.nextIndexInCurrentBuffer = 0;
431                                                }
432                                                else {
433                                                        this.currentBuffer = null;
434                                                }
435                                                return read(b, off, len);
436                                        }
437                                }
438                        }
439                }
440
441                @Override
442                public long skip(long n) throws IOException {
443                        if (n > Integer.MAX_VALUE) {
444                                throw new IllegalArgumentException("n exceeds maximum (" + Integer.MAX_VALUE + "): " + n);
445                        }
446                        else if (n == 0) {
447                                return 0;
448                        }
449                        else if (n < 0) {
450                                throw new IllegalArgumentException("n must be 0 or greater: " + n);
451                        }
452                        int len = (int) n;
453                        if (this.currentBuffer == null) {
454                                // This stream doesn't have any data in it...
455                                return 0;
456                        }
457                        else {
458                                if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
459                                        int bytesToSkip = Math.min(len, this.currentBufferLength - this.nextIndexInCurrentBuffer);
460                                        this.totalBytesRead += bytesToSkip;
461                                        this.nextIndexInCurrentBuffer += bytesToSkip;
462                                        return (bytesToSkip + skip(len - bytesToSkip));
463                                }
464                                else {
465                                        if (this.buffersIterator.hasNext()) {
466                                                this.currentBuffer = this.buffersIterator.next();
467                                                if (this.currentBuffer == this.fastByteArrayOutputStream.buffers.getLast()) {
468                                                        this.currentBufferLength = this.fastByteArrayOutputStream.index;
469                                                }
470                                                else {
471                                                        this.currentBufferLength = this.currentBuffer.length;
472                                                }
473                                                this.nextIndexInCurrentBuffer = 0;
474                                        }
475                                        else {
476                                                this.currentBuffer = null;
477                                        }
478                                        return skip(len);
479                                }
480                        }
481                }
482
483                @Override
484                public int available() {
485                        return (this.fastByteArrayOutputStream.size() - this.totalBytesRead);
486                }
487
488                /**
489                 * Update the message digest with the remaining bytes in this stream.
490                 * @param messageDigest the message digest to update
491                 */
492                @Override
493                public void updateMessageDigest(MessageDigest messageDigest) {
494                        updateMessageDigest(messageDigest, available());
495                }
496
497                /**
498                 * Update the message digest with the next len bytes in this stream.
499                 * Avoids creating new byte arrays and use internal buffers for performance.
500                 * @param messageDigest the message digest to update
501                 * @param len how many bytes to read from this stream and use to update the message digest
502                 */
503                @Override
504                public void updateMessageDigest(MessageDigest messageDigest, int len) {
505                        if (this.currentBuffer == null) {
506                                // This stream doesn't have any data in it...
507                                return;
508                        }
509                        else if (len == 0) {
510                                return;
511                        }
512                        else if (len < 0) {
513                                throw new IllegalArgumentException("len must be 0 or greater: " + len);
514                        }
515                        else {
516                                if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
517                                        int bytesToCopy = Math.min(len, this.currentBufferLength - this.nextIndexInCurrentBuffer);
518                                        messageDigest.update(this.currentBuffer, this.nextIndexInCurrentBuffer, bytesToCopy);
519                                        this.nextIndexInCurrentBuffer += bytesToCopy;
520                                        updateMessageDigest(messageDigest, len - bytesToCopy);
521                                }
522                                else {
523                                        if (this.buffersIterator.hasNext()) {
524                                                this.currentBuffer = this.buffersIterator.next();
525                                                if (this.currentBuffer == this.fastByteArrayOutputStream.buffers.getLast()) {
526                                                        this.currentBufferLength = this.fastByteArrayOutputStream.index;
527                                                }
528                                                else {
529                                                        this.currentBufferLength = this.currentBuffer.length;
530                                                }
531                                                this.nextIndexInCurrentBuffer = 0;
532                                        }
533                                        else {
534                                                this.currentBuffer = null;
535                                        }
536                                        updateMessageDigest(messageDigest, len);
537                                }
538                        }
539                }
540        }
541
542}