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}