001/*
002 * Copyright 2002-2020 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.ArrayDeque;
024import java.util.Deque;
025import java.util.Iterator;
026
027import org.springframework.lang.Nullable;
028
029/**
030 * A speedy alternative to {@link java.io.ByteArrayOutputStream}. Note that
031 * this variant does <i>not</i> extend {@code ByteArrayOutputStream}, unlike
032 * its sibling {@link ResizableByteArrayOutputStream}.
033 *
034 * <p>Unlike {@link java.io.ByteArrayOutputStream}, this implementation is backed
035 * by an {@link java.util.ArrayDeque} of {@code byte[]} instead of 1 constantly
036 * resizing {@code byte[]}. It does not copy buffers when it gets expanded.
037 *
038 * <p>The initial buffer is only created when the stream is first written.
039 * There is also no copying of the internal buffer if its contents is extracted
040 * with the {@link #writeTo(OutputStream)} method.
041 *
042 * @author Craig Andrews
043 * @author Juergen Hoeller
044 * @since 4.2
045 * @see #resize
046 * @see ResizableByteArrayOutputStream
047 */
048public class FastByteArrayOutputStream extends OutputStream {
049
050        private static final int DEFAULT_BLOCK_SIZE = 256;
051
052
053        // The buffers used to store the content bytes
054        private final Deque<byte[]> buffers = new ArrayDeque<>();
055
056        // The size, in bytes, to use when allocating the first byte[]
057        private final int initialBlockSize;
058
059        // The size, in bytes, to use when allocating the next byte[]
060        private int nextBlockSize = 0;
061
062        // The number of bytes in previous buffers.
063        // (The number of bytes in the current buffer is in 'index'.)
064        private int alreadyBufferedSize = 0;
065
066        // The index in the byte[] found at buffers.getLast() to be written next
067        private int index = 0;
068
069        // Is the stream closed?
070        private boolean closed = false;
071
072
073        /**
074         * Create a new <code>FastByteArrayOutputStream</code>
075         * with the default initial capacity of 256 bytes.
076         */
077        public FastByteArrayOutputStream() {
078                this(DEFAULT_BLOCK_SIZE);
079        }
080
081        /**
082         * Create a new <code>FastByteArrayOutputStream</code>
083         * with the specified initial capacity.
084         * @param initialBlockSize the initial buffer size in bytes
085         */
086        public FastByteArrayOutputStream(int initialBlockSize) {
087                Assert.isTrue(initialBlockSize > 0, "Initial block size must be greater than 0");
088                this.initialBlockSize = initialBlockSize;
089                this.nextBlockSize = initialBlockSize;
090        }
091
092
093        // Overridden methods
094
095        @Override
096        public void write(int datum) throws IOException {
097                if (this.closed) {
098                        throw new IOException("Stream closed");
099                }
100                else {
101                        if (this.buffers.peekLast() == null || this.buffers.getLast().length == this.index) {
102                                addBuffer(1);
103                        }
104                        // store the byte
105                        this.buffers.getLast()[this.index++] = (byte) datum;
106                }
107        }
108
109        @Override
110        public void write(byte[] data, int offset, int length) throws IOException {
111                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                return bytesUnsafe.clone();
211        }
212
213        /**
214         * Reset the contents of this <code>FastByteArrayOutputStream</code>.
215         * <p>All currently accumulated output in the output stream is discarded.
216         * The output stream can be used again.
217         */
218        public void reset() {
219                this.buffers.clear();
220                this.nextBlockSize = this.initialBlockSize;
221                this.closed = false;
222                this.index = 0;
223                this.alreadyBufferedSize = 0;
224        }
225
226        /**
227         * Get an {@link InputStream} to retrieve the data in this OutputStream.
228         * <p>Note that if any methods are called on the OutputStream
229         * (including, but not limited to, any of the write methods, {@link #reset()},
230         * {@link #toByteArray()}, and {@link #toByteArrayUnsafe()}) then the
231         * {@link java.io.InputStream}'s behavior is undefined.
232         * @return {@link InputStream} of the contents of this OutputStream
233         */
234        public InputStream getInputStream() {
235                return new FastByteArrayInputStream(this);
236        }
237
238        /**
239         * Write the buffers content to the given OutputStream.
240         * @param out the OutputStream to write to
241         */
242        public void writeTo(OutputStream out) throws IOException {
243                Iterator<byte[]> it = this.buffers.iterator();
244                while (it.hasNext()) {
245                        byte[] bytes = it.next();
246                        if (it.hasNext()) {
247                                out.write(bytes, 0, bytes.length);
248                        }
249                        else {
250                                out.write(bytes, 0, this.index);
251                        }
252                }
253        }
254
255        /**
256         * Resize the internal buffer size to a specified capacity.
257         * @param targetCapacity the desired size of the buffer
258         * @throws IllegalArgumentException if the given capacity is smaller than
259         * the actual size of the content stored in the buffer already
260         * @see FastByteArrayOutputStream#size()
261         */
262        public void resize(int targetCapacity) {
263                Assert.isTrue(targetCapacity >= size(), "New capacity must not be smaller than current size");
264                if (this.buffers.peekFirst() == null) {
265                        this.nextBlockSize = targetCapacity - size();
266                }
267                else if (size() == targetCapacity && this.buffers.getFirst().length == targetCapacity) {
268                        // do nothing - already at the targetCapacity
269                }
270                else {
271                        int totalSize = size();
272                        byte[] data = new byte[targetCapacity];
273                        int pos = 0;
274                        Iterator<byte[]> it = this.buffers.iterator();
275                        while (it.hasNext()) {
276                                byte[] bytes = it.next();
277                                if (it.hasNext()) {
278                                        System.arraycopy(bytes, 0, data, pos, bytes.length);
279                                        pos += bytes.length;
280                                }
281                                else {
282                                        System.arraycopy(bytes, 0, data, pos, this.index);
283                                }
284                        }
285                        this.buffers.clear();
286                        this.buffers.add(data);
287                        this.index = totalSize;
288                        this.alreadyBufferedSize = 0;
289                }
290        }
291
292        /**
293         * Create a new buffer and store it in the ArrayDeque.
294         * <p>Adds a new buffer that can store at least {@code minCapacity} bytes.
295         */
296        private void addBuffer(int minCapacity) {
297                if (this.buffers.peekLast() != null) {
298                        this.alreadyBufferedSize += this.index;
299                        this.index = 0;
300                }
301                if (this.nextBlockSize < minCapacity) {
302                        this.nextBlockSize = nextPowerOf2(minCapacity);
303                }
304                this.buffers.add(new byte[this.nextBlockSize]);
305                this.nextBlockSize *= 2;  // block size doubles each time
306        }
307
308        /**
309         * Get the next power of 2 of a number (ex, the next power of 2 of 119 is 128).
310         */
311        private static int nextPowerOf2(int val) {
312                val--;
313                val = (val >> 1) | val;
314                val = (val >> 2) | val;
315                val = (val >> 4) | val;
316                val = (val >> 8) | val;
317                val = (val >> 16) | val;
318                val++;
319                return val;
320        }
321
322
323        /**
324         * An implementation of {@link java.io.InputStream} that reads from a given
325         * <code>FastByteArrayOutputStream</code>.
326         */
327        private static final class FastByteArrayInputStream extends UpdateMessageDigestInputStream {
328
329                private final FastByteArrayOutputStream fastByteArrayOutputStream;
330
331                private final Iterator<byte[]> buffersIterator;
332
333                @Nullable
334                private byte[] currentBuffer;
335
336                private int currentBufferLength = 0;
337
338                private int nextIndexInCurrentBuffer = 0;
339
340                private int totalBytesRead = 0;
341
342                /**
343                 * Create a new <code>FastByteArrayOutputStreamInputStream</code> backed
344                 * by the given <code>FastByteArrayOutputStream</code>.
345                 */
346                public FastByteArrayInputStream(FastByteArrayOutputStream fastByteArrayOutputStream) {
347                        this.fastByteArrayOutputStream = fastByteArrayOutputStream;
348                        this.buffersIterator = fastByteArrayOutputStream.buffers.iterator();
349                        if (this.buffersIterator.hasNext()) {
350                                this.currentBuffer = this.buffersIterator.next();
351                                if (this.currentBuffer == fastByteArrayOutputStream.buffers.getLast()) {
352                                        this.currentBufferLength = fastByteArrayOutputStream.index;
353                                }
354                                else {
355                                        this.currentBufferLength = (this.currentBuffer != null ? this.currentBuffer.length : 0);
356                                }
357                        }
358                }
359
360                @Override
361                public int read() {
362                        if (this.currentBuffer == null) {
363                                // This stream doesn't have any data in it...
364                                return -1;
365                        }
366                        else {
367                                if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
368                                        this.totalBytesRead++;
369                                        return this.currentBuffer[this.nextIndexInCurrentBuffer++] & 0xFF;
370                                }
371                                else {
372                                        if (this.buffersIterator.hasNext()) {
373                                                this.currentBuffer = this.buffersIterator.next();
374                                                updateCurrentBufferLength();
375                                                this.nextIndexInCurrentBuffer = 0;
376                                        }
377                                        else {
378                                                this.currentBuffer = null;
379                                        }
380                                        return read();
381                                }
382                        }
383                }
384
385                @Override
386                public int read(byte[] b) {
387                        return read(b, 0, b.length);
388                }
389
390                @Override
391                public int read(byte[] b, int off, int len) {
392                        if (off < 0 || len < 0 || len > b.length - off) {
393                                throw new IndexOutOfBoundsException();
394                        }
395                        else if (len == 0) {
396                                return 0;
397                        }
398                        else {
399                                if (this.currentBuffer == null) {
400                                        // This stream doesn't have any data in it...
401                                        return -1;
402                                }
403                                else {
404                                        if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
405                                                int bytesToCopy = Math.min(len, this.currentBufferLength - this.nextIndexInCurrentBuffer);
406                                                System.arraycopy(this.currentBuffer, this.nextIndexInCurrentBuffer, b, off, bytesToCopy);
407                                                this.totalBytesRead += bytesToCopy;
408                                                this.nextIndexInCurrentBuffer += bytesToCopy;
409                                                int remaining = read(b, off + bytesToCopy, len - bytesToCopy);
410                                                return bytesToCopy + Math.max(remaining, 0);
411                                        }
412                                        else {
413                                                if (this.buffersIterator.hasNext()) {
414                                                        this.currentBuffer = this.buffersIterator.next();
415                                                        updateCurrentBufferLength();
416                                                        this.nextIndexInCurrentBuffer = 0;
417                                                }
418                                                else {
419                                                        this.currentBuffer = null;
420                                                }
421                                                return read(b, off, len);
422                                        }
423                                }
424                        }
425                }
426
427                @Override
428                public long skip(long n) throws IOException {
429                        if (n > Integer.MAX_VALUE) {
430                                throw new IllegalArgumentException("n exceeds maximum (" + Integer.MAX_VALUE + "): " + n);
431                        }
432                        else if (n == 0) {
433                                return 0;
434                        }
435                        else if (n < 0) {
436                                throw new IllegalArgumentException("n must be 0 or greater: " + n);
437                        }
438                        int len = (int) n;
439                        if (this.currentBuffer == null) {
440                                // This stream doesn't have any data in it...
441                                return 0;
442                        }
443                        else {
444                                if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
445                                        int bytesToSkip = Math.min(len, this.currentBufferLength - this.nextIndexInCurrentBuffer);
446                                        this.totalBytesRead += bytesToSkip;
447                                        this.nextIndexInCurrentBuffer += bytesToSkip;
448                                        return (bytesToSkip + skip(len - bytesToSkip));
449                                }
450                                else {
451                                        if (this.buffersIterator.hasNext()) {
452                                                this.currentBuffer = this.buffersIterator.next();
453                                                updateCurrentBufferLength();
454                                                this.nextIndexInCurrentBuffer = 0;
455                                        }
456                                        else {
457                                                this.currentBuffer = null;
458                                        }
459                                        return skip(len);
460                                }
461                        }
462                }
463
464                @Override
465                public int available() {
466                        return (this.fastByteArrayOutputStream.size() - this.totalBytesRead);
467                }
468
469                /**
470                 * Update the message digest with the remaining bytes in this stream.
471                 * @param messageDigest the message digest to update
472                 */
473                @Override
474                public void updateMessageDigest(MessageDigest messageDigest) {
475                        updateMessageDigest(messageDigest, available());
476                }
477
478                /**
479                 * Update the message digest with the next len bytes in this stream.
480                 * Avoids creating new byte arrays and use internal buffers for performance.
481                 * @param messageDigest the message digest to update
482                 * @param len how many bytes to read from this stream and use to update the message digest
483                 */
484                @Override
485                public void updateMessageDigest(MessageDigest messageDigest, int len) {
486                        if (this.currentBuffer == null) {
487                                // This stream doesn't have any data in it...
488                                return;
489                        }
490                        else if (len == 0) {
491                                return;
492                        }
493                        else if (len < 0) {
494                                throw new IllegalArgumentException("len must be 0 or greater: " + len);
495                        }
496                        else {
497                                if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
498                                        int bytesToCopy = Math.min(len, this.currentBufferLength - this.nextIndexInCurrentBuffer);
499                                        messageDigest.update(this.currentBuffer, this.nextIndexInCurrentBuffer, bytesToCopy);
500                                        this.nextIndexInCurrentBuffer += bytesToCopy;
501                                        updateMessageDigest(messageDigest, len - bytesToCopy);
502                                }
503                                else {
504                                        if (this.buffersIterator.hasNext()) {
505                                                this.currentBuffer = this.buffersIterator.next();
506                                                updateCurrentBufferLength();
507                                                this.nextIndexInCurrentBuffer = 0;
508                                        }
509                                        else {
510                                                this.currentBuffer = null;
511                                        }
512                                        updateMessageDigest(messageDigest, len);
513                                }
514                        }
515                }
516
517                private void updateCurrentBufferLength() {
518                        if (this.currentBuffer == this.fastByteArrayOutputStream.buffers.getLast()) {
519                                this.currentBufferLength = this.fastByteArrayOutputStream.index;
520                        }
521                        else {
522                                this.currentBufferLength = (this.currentBuffer != null ? this.currentBuffer.length : 0);
523                        }
524                }
525        }
526
527}