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}