org.springframework.util.FastByteArrayOutputStream Maven / Gradle / Ivy
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.util.Iterator;
import java.util.LinkedList;
/**
* A speedy alternative to {@link java.io.ByteArrayOutputStream}. Note that
* this variant does not extend {@code ByteArrayOutputStream}, unlike
* its sibling {@link ResizableByteArrayOutputStream}.
*
* Unlike {@link java.io.ByteArrayOutputStream}, this implementation is backed
* by a {@link java.util.LinkedList} of {@code byte[]} instead of 1 constantly
* resizing {@code byte[]}. It does not copy buffers when it gets expanded.
*
*
The initial buffer is only created when the stream is first written.
* There is also no copying of the internal buffer if its contents is extracted
* with the {@link #writeTo(OutputStream)} method.
*
* @author Craig Andrews
* @author Juergen Hoeller
* @since 4.2
* @see #resize
* @see ResizableByteArrayOutputStream
*/
public class FastByteArrayOutputStream extends OutputStream {
private static final int DEFAULT_BLOCK_SIZE = 256;
// The buffers used to store the content bytes
private final LinkedList buffers = new LinkedList();
// The size, in bytes, to use when allocating the first byte[]
private final int initialBlockSize;
// The size, in bytes, to use when allocating the next next byte[]
private int nextBlockSize = 0;
// The number of bytes in previous buffers.
// (The number of bytes in the current buffer is in 'index'.)
private int alreadyBufferedSize = 0;
// The index in the byte[] found at buffers.getLast() to be written next
private int index = 0;
// Is the stream closed?
private boolean closed = false;
/**
* Create a new FastByteArrayOutputStream
* with the default initial capacity of 256 bytes.
*/
public FastByteArrayOutputStream() {
this(DEFAULT_BLOCK_SIZE);
}
/**
* Create a new FastByteArrayOutputStream
* with the specified initial capacity.
* @param initialBlockSize the initial buffer size in bytes
*/
public FastByteArrayOutputStream(int initialBlockSize) {
Assert.isTrue(initialBlockSize > 0, "Initial block size must be greater than 0");
this.initialBlockSize = initialBlockSize;
this.nextBlockSize = initialBlockSize;
}
// Overridden methods
@Override
public void write(int datum) throws IOException {
if (this.closed) {
throw new IOException("Stream closed");
}
else {
if (this.buffers.peekLast() == null || this.buffers.getLast().length == this.index) {
addBuffer(1);
}
// store the byte
this.buffers.getLast()[this.index++] = (byte) datum;
}
}
@Override
public void write(byte[] data, int offset, int length) throws IOException {
if (data == null) {
throw new NullPointerException();
}
else if (offset < 0 || offset + length > data.length || length < 0) {
throw new IndexOutOfBoundsException();
}
else if (this.closed) {
throw new IOException("Stream closed");
}
else {
if (this.buffers.peekLast() == null || this.buffers.getLast().length == this.index) {
addBuffer(length);
}
if (this.index + length > this.buffers.getLast().length) {
int pos = offset;
do {
if (this.index == this.buffers.getLast().length) {
addBuffer(length);
}
int copyLength = this.buffers.getLast().length - this.index;
if (length < copyLength) {
copyLength = length;
}
System.arraycopy(data, pos, this.buffers.getLast(), this.index, copyLength);
pos += copyLength;
this.index += copyLength;
length -= copyLength;
}
while (length > 0);
}
else {
// copy in the sub-array
System.arraycopy(data, offset, this.buffers.getLast(), this.index, length);
this.index += length;
}
}
}
@Override
public void close() {
this.closed = true;
}
/**
* Convert the buffer's contents into a string decoding bytes using the
* platform's default character set. The length of the new String
* is a function of the character set, and hence may not be equal to the
* size of the buffer.
* This method always replaces malformed-input and unmappable-character
* sequences with the default replacement string for the platform's
* default character set. The {@linkplain java.nio.charset.CharsetDecoder}
* class should be used when more control over the decoding process is
* required.
* @return a String decoded from the buffer's contents
*/
@Override
public String toString() {
return new String(toByteArrayUnsafe());
}
// Custom methods
/**
* Return the number of bytes stored in this FastByteArrayOutputStream
.
*/
public int size() {
return (this.alreadyBufferedSize + this.index);
}
/**
* Convert the stream's data to a byte array and return the byte array.
*
Also replaces the internal structures with the byte array to conserve memory:
* if the byte array is being made anyways, mind as well as use it. This approach
* also means that if this method is called twice without any writes in between,
* the second call is a no-op.
*
This method is "unsafe" as it returns the internal buffer.
* Callers should not modify the returned buffer.
* @return the current contents of this output stream, as a byte array.
* @see #size()
* @see #toByteArray()
*/
public byte[] toByteArrayUnsafe() {
int totalSize = size();
if (totalSize == 0) {
return new byte[0];
}
resize(totalSize);
return this.buffers.getFirst();
}
/**
* Creates a newly allocated byte array.
*
Its size is the current
* size of this output stream and the valid contents of the buffer
* have been copied into it.
* @return the current contents of this output stream, as a byte array.
* @see #size()
* @see #toByteArrayUnsafe()
*/
public byte[] toByteArray() {
byte[] bytesUnsafe = toByteArrayUnsafe();
byte[] ret = new byte[bytesUnsafe.length];
System.arraycopy(bytesUnsafe, 0, ret, 0, bytesUnsafe.length);
return ret;
}
/**
* Reset the contents of this FastByteArrayOutputStream
.
* All currently accumulated output in the output stream is discarded.
* The output stream can be used again.
*/
public void reset() {
this.buffers.clear();
this.nextBlockSize = this.initialBlockSize;
this.closed = false;
this.index = 0;
this.alreadyBufferedSize = 0;
}
/**
* Get an {@link InputStream} to retrieve the data in this OutputStream.
*
Note that if any methods are called on the OutputStream
* (including, but not limited to, any of the write methods, {@link #reset()},
* {@link #toByteArray()}, and {@link #toByteArrayUnsafe()}) then the
* {@link java.io.InputStream}'s behavior is undefined.
* @return {@link InputStream} of the contents of this OutputStream
*/
public InputStream getInputStream() {
return new FastByteArrayInputStream(this);
}
/**
* Write the buffers content to the given OutputStream.
* @param out the OutputStream to write to
*/
public void writeTo(OutputStream out) throws IOException {
Iterator it = this.buffers.iterator();
while (it.hasNext()) {
byte[] bytes = it.next();
if (it.hasNext()) {
out.write(bytes, 0, bytes.length);
}
else {
out.write(bytes, 0, this.index);
}
}
}
/**
* Resize the internal buffer size to a specified capacity.
* @param targetCapacity the desired size of the buffer
* @throws IllegalArgumentException if the given capacity is smaller than
* the actual size of the content stored in the buffer already
* @see FastByteArrayOutputStream#size()
*/
public void resize(int targetCapacity) {
Assert.isTrue(targetCapacity >= size(), "New capacity must not be smaller than current size");
if (buffers.peekFirst() == null) {
this.nextBlockSize = targetCapacity - size();
}
else if (size() == targetCapacity && this.buffers.getFirst().length == targetCapacity) {
// do nothing - already at the targetCapacity
}
else {
int totalSize = size();
byte[] data = new byte[targetCapacity];
int pos = 0;
Iterator it = this.buffers.iterator();
while (it.hasNext()) {
byte[] bytes = it.next();
if (it.hasNext()) {
System.arraycopy(bytes, 0, data, pos, bytes.length);
pos += bytes.length;
}
else {
System.arraycopy(bytes, 0, data, pos, this.index);
}
}
this.buffers.clear();
this.buffers.add(data);
this.index = totalSize;
this.alreadyBufferedSize = 0;
}
}
/**
* Create a new buffer and store it in the LinkedList
* Adds a new buffer that can store at least {@code minCapacity} bytes.
*/
private void addBuffer(int minCapacity) {
if (this.buffers.peekLast() != null) {
this.alreadyBufferedSize += this.index;
this.index = 0;
}
if (this.nextBlockSize < minCapacity) {
this.nextBlockSize = nextPowerOf2(minCapacity);
}
this.buffers.add(new byte[this.nextBlockSize]);
this.nextBlockSize *= 2; // block size doubles each time
}
/**
* Get the next power of 2 of a number (ex, the next power of 2 of 119 is 128).
*/
private static int nextPowerOf2(int val) {
val--;
val = (val >> 1) | val;
val = (val >> 2) | val;
val = (val >> 4) | val;
val = (val >> 8) | val;
val = (val >> 16) | val;
val++;
return val;
}
/**
* An implementation of {@link java.io.InputStream} that reads from a given
* FastByteArrayOutputStream
.
*/
private static final class FastByteArrayInputStream extends UpdateMessageDigestInputStream {
private final FastByteArrayOutputStream fastByteArrayOutputStream;
private final Iterator buffersIterator;
private byte[] currentBuffer;
private int currentBufferLength = 0;
private int nextIndexInCurrentBuffer = 0;
private int totalBytesRead = 0;
/**
* Create a new FastByteArrayOutputStreamInputStream
backed
* by the given FastByteArrayOutputStream
.
*/
public FastByteArrayInputStream(FastByteArrayOutputStream fastByteArrayOutputStream) {
this.fastByteArrayOutputStream = fastByteArrayOutputStream;
this.buffersIterator = fastByteArrayOutputStream.buffers.iterator();
if (this.buffersIterator.hasNext()) {
this.currentBuffer = this.buffersIterator.next();
if (this.currentBuffer == fastByteArrayOutputStream.buffers.getLast()) {
this.currentBufferLength = fastByteArrayOutputStream.index;
}
else {
this.currentBufferLength = this.currentBuffer.length;
}
}
}
@Override
public int read() {
if (this.currentBuffer == null) {
// this stream doesn't have any data in it
return -1;
}
else {
if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
this.totalBytesRead++;
return this.currentBuffer[this.nextIndexInCurrentBuffer++];
}
else {
if (this.buffersIterator.hasNext()) {
this.currentBuffer = this.buffersIterator.next();
if (this.currentBuffer == this.fastByteArrayOutputStream.buffers.getLast()) {
this.currentBufferLength = this.fastByteArrayOutputStream.index;
}
else {
this.currentBufferLength = this.currentBuffer.length;
}
this.nextIndexInCurrentBuffer = 0;
}
else {
this.currentBuffer = null;
}
return read();
}
}
}
@Override
public int read(byte[] b) {
return read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) {
if (b == null) {
throw new NullPointerException();
}
else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
}
else if (len == 0) {
return 0;
}
else if (len < 0) {
throw new IllegalArgumentException("len must be 0 or greater: " + len);
}
else if (off < 0) {
throw new IllegalArgumentException("off must be 0 or greater: " + off);
}
else {
if (this.currentBuffer == null) {
// this stream doesn't have any data in it
return 0;
}
else {
if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
int bytesToCopy = Math.min(len, this.currentBufferLength - this.nextIndexInCurrentBuffer);
System.arraycopy(this.currentBuffer, this.nextIndexInCurrentBuffer, b, off, bytesToCopy);
this.totalBytesRead += bytesToCopy;
this.nextIndexInCurrentBuffer += bytesToCopy;
return (bytesToCopy + read(b, off + bytesToCopy, len - bytesToCopy));
}
else {
if (this.buffersIterator.hasNext()) {
this.currentBuffer = this.buffersIterator.next();
if (this.currentBuffer == this.fastByteArrayOutputStream.buffers.getLast()) {
this.currentBufferLength = this.fastByteArrayOutputStream.index;
}
else {
this.currentBufferLength = this.currentBuffer.length;
}
this.nextIndexInCurrentBuffer = 0;
}
else {
this.currentBuffer = null;
}
return read(b, off, len);
}
}
}
}
@Override
public long skip(long n) throws IOException {
if (n > Integer.MAX_VALUE) {
throw new IllegalArgumentException("n exceeds maximum (" + Integer.MAX_VALUE + "): " + n);
}
else if (n == 0) {
return 0;
}
else if (n < 0) {
throw new IllegalArgumentException("n must be 0 or greater: " + n);
}
int len = (int) n;
if (this.currentBuffer == null) {
// this stream doesn't have any data in it
return 0;
}
else {
if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
int bytesToSkip = Math.min(len, this.currentBufferLength - this.nextIndexInCurrentBuffer);
this.totalBytesRead += bytesToSkip;
this.nextIndexInCurrentBuffer += bytesToSkip;
return (bytesToSkip + skip(len - bytesToSkip));
}
else {
if (this.buffersIterator.hasNext()) {
this.currentBuffer = this.buffersIterator.next();
if (this.currentBuffer == this.fastByteArrayOutputStream.buffers.getLast()) {
this.currentBufferLength = this.fastByteArrayOutputStream.index;
}
else {
this.currentBufferLength = this.currentBuffer.length;
}
this.nextIndexInCurrentBuffer = 0;
}
else {
this.currentBuffer = null;
}
return skip(len);
}
}
}
@Override
public int available() {
return (this.fastByteArrayOutputStream.size() - this.totalBytesRead);
}
/**
* Update the message digest with the remaining bytes in this stream.
* @param messageDigest The message digest to update
*/
public void updateMessageDigest(MessageDigest messageDigest) {
updateMessageDigest(messageDigest, available());
}
/**
* Update the message digest with the next len bytes in this stream.
* Avoids creating new byte arrays and use internal buffers for performance.
* @param messageDigest The message digest to update
* @param len how many bytes to read from this stream and use to update the message digest
*/
public void updateMessageDigest(MessageDigest messageDigest, int len) {
if (this.currentBuffer == null) {
// this stream doesn't have any data in it
return;
}
else if (len == 0) {
return;
}
else if (len < 0) {
throw new IllegalArgumentException("len must be 0 or greater: " + len);
}
else {
if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
int bytesToCopy = Math.min(len, this.currentBufferLength - this.nextIndexInCurrentBuffer);
messageDigest.update(this.currentBuffer, this.nextIndexInCurrentBuffer, bytesToCopy);
this.nextIndexInCurrentBuffer += bytesToCopy;
updateMessageDigest(messageDigest, len - bytesToCopy);
}
else {
if (this.buffersIterator.hasNext()) {
this.currentBuffer = this.buffersIterator.next();
if (this.currentBuffer == this.fastByteArrayOutputStream.buffers.getLast()) {
this.currentBufferLength = this.fastByteArrayOutputStream.index;
}
else {
this.currentBufferLength = this.currentBuffer.length;
}
this.nextIndexInCurrentBuffer = 0;
}
else {
this.currentBuffer = null;
}
updateMessageDigest(messageDigest, len);
}
}
}
}
}