This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-io.git
The following commit(s) were added to refs/heads/master by this push: new c2c2709 Refactor ByteArrayOutputStream into synchronized and non-synchronized versions (#108) c2c2709 is described below commit c2c2709cf7a24498292e65d2e058d7b4e7da9edb Author: Adam Retter <adam.ret...@googlemail.com> AuthorDate: Wed Apr 8 02:15:54 2020 +0200 Refactor ByteArrayOutputStream into synchronized and non-synchronized versions (#108) * Split ByteArrayOutputStream into synchronized and non-synchronized versions * Improve the test coverage of AbstractByteArrayOutputStream and sub-classes * Address review comments by @aherbert * Address review comments by @garydgregory * Address further review comments by @garydgregory * Remove </p> tags, breaks the javadoc build * Address review comments by @aherbert * Improve coverage of tests --- pom.xml | 4 + ...eam.java => AbstractByteArrayOutputStream.java} | 235 ++++++++--------- .../commons/io/output/ByteArrayOutputStream.java | 292 ++------------------- .../UnsynchronizedByteArrayOutputStream.java | 163 ++++++++++++ .../io/output/ByteArrayOutputStreamTestCase.java | 211 +++++++++++++-- 5 files changed, 482 insertions(+), 423 deletions(-) diff --git a/pom.xml b/pom.xml index 8e1c921..076916f 100644 --- a/pom.xml +++ b/pom.xml @@ -209,6 +209,10 @@ file comparators, endian transformation classes, and much more. <email>alban.peignier at free.fr</email> </contributor> <contributor> + <name>Adam Retter</name> + <organization>Evolved Binary</organization> + </contributor> + <contributor> <name>Ian Springer</name> </contributor> <contributor> diff --git a/src/main/java/org/apache/commons/io/output/ByteArrayOutputStream.java b/src/main/java/org/apache/commons/io/output/AbstractByteArrayOutputStream.java similarity index 64% copy from src/main/java/org/apache/commons/io/output/ByteArrayOutputStream.java copy to src/main/java/org/apache/commons/io/output/AbstractByteArrayOutputStream.java index c84721a..03d5127 100644 --- a/src/main/java/org/apache/commons/io/output/ByteArrayOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/AbstractByteArrayOutputStream.java @@ -16,11 +16,11 @@ */ package org.apache.commons.io.output; -import static org.apache.commons.io.IOUtils.EOF; +import org.apache.commons.io.input.ClosedInputStream; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.io.InputStream; +import java.io.IOException; import java.io.OutputStream; import java.io.SequenceInputStream; import java.io.UnsupportedEncodingException; @@ -29,30 +29,35 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.apache.commons.io.input.ClosedInputStream; +import static org.apache.commons.io.IOUtils.EOF; /** - * This class implements an output stream in which the data is - * written into a byte array. The buffer automatically grows as data + * This is the base class for implementing an output stream in which the data + * is written into a byte array. The buffer automatically grows as data * is written to it. * <p> * The data can be retrieved using <code>toByteArray()</code> and * <code>toString()</code>. - * <p> - * Closing a {@code ByteArrayOutputStream} has no effect. The methods in + * Closing an {@code AbstractByteArrayOutputStream} has no effect. The methods in * this class can be called after the stream has been closed without * generating an {@code IOException}. + * </p> * <p> - * This is an alternative implementation of the {@link java.io.ByteArrayOutputStream} - * class. The original implementation only allocates 32 bytes at the beginning. - * As this class is designed for heavy duty it starts at 1024 bytes. In contrast - * to the original it doesn't reallocate the whole memory block but allocates - * additional buffers. This way no buffers need to be garbage collected and - * the contents don't have to be copied to the new buffer. This class is - * designed to behave exactly like the original. The only exception is the - * deprecated toString(int) method that has been ignored. + * This is the base for an alternative implementation of the + * {@link java.io.ByteArrayOutputStream} class. The original implementation + * only allocates 32 bytes at the beginning. As this class is designed for + * heavy duty it starts at 1024 bytes. In contrast to the original it doesn't + * reallocate the whole memory block but allocates additional buffers. This + * way no buffers need to be garbage collected and the contents don't have + * to be copied to the new buffer. This class is designed to behave exactly + * like the original. The only exception is the deprecated + * {@link java.io.ByteArrayOutputStream#toString(int)} method that has been + * ignored. + * </p> + * + * @since 2.7 */ -public class ByteArrayOutputStream extends OutputStream { +public abstract class AbstractByteArrayOutputStream extends OutputStream { static final int DEFAULT_SIZE = 1024; @@ -68,42 +73,17 @@ public class ByteArrayOutputStream extends OutputStream { /** The current buffer. */ private byte[] currentBuffer; /** The total count of bytes written. */ - private int count; + protected int count; /** Flag to indicate if the buffers can be reused after reset */ private boolean reuseBuffers = true; /** - * Creates a new byte array output stream. The buffer capacity is - * initially 1024 bytes, though its size increases if necessary. - */ - public ByteArrayOutputStream() { - this(DEFAULT_SIZE); - } - - /** - * Creates a new byte array output stream, with a buffer capacity of - * the specified size, in bytes. - * - * @param size the initial size - * @throws IllegalArgumentException if size is negative - */ - public ByteArrayOutputStream(final int size) { - if (size < 0) { - throw new IllegalArgumentException( - "Negative initial size: " + size); - } - synchronized (this) { - needNewBuffer(size); - } - } - - /** * Makes a new buffer available either by allocating * a new one or re-cycling an existing one. * * @param newcount the size of the buffer if one is created */ - private void needNewBuffer(final int newcount) { + protected void needNewBuffer(final int newcount) { if (currentBufferIndex < buffers.size() - 1) { //Recycling old buffer filledBufferSum += currentBuffer.length; @@ -130,37 +110,34 @@ public class ByteArrayOutputStream extends OutputStream { } /** - * Write the bytes to byte array. + * Writes the bytes to the byte array. * @param b the bytes to write * @param off The start offset * @param len The number of bytes to write */ @Override - public void write(final byte[] b, final int off, final int len) { - if ((off < 0) - || (off > b.length) - || (len < 0) - || ((off + len) > b.length) - || ((off + len) < 0)) { - throw new IndexOutOfBoundsException(); - } else if (len == 0) { - return; - } - synchronized (this) { - final int newcount = count + len; - int remaining = len; - int inBufferPos = count - filledBufferSum; - while (remaining > 0) { - final int part = Math.min(remaining, currentBuffer.length - inBufferPos); - System.arraycopy(b, off + len - remaining, currentBuffer, inBufferPos, part); - remaining -= part; - if (remaining > 0) { - needNewBuffer(newcount); - inBufferPos = 0; - } + public abstract void write(final byte[] b, final int off, final int len); + + /** + * Writes the bytes to the byte array. + * @param b the bytes to write + * @param off The start offset + * @param len The number of bytes to write + */ + protected void writeImpl(final byte[] b, final int off, final int len) { + final int newcount = count + len; + int remaining = len; + int inBufferPos = count - filledBufferSum; + while (remaining > 0) { + final int part = Math.min(remaining, currentBuffer.length - inBufferPos); + System.arraycopy(b, off + len - remaining, currentBuffer, inBufferPos, part); + remaining -= part; + if (remaining > 0) { + needNewBuffer(newcount); + inBufferPos = 0; } - count = newcount; } + count = newcount; } /** @@ -168,7 +145,13 @@ public class ByteArrayOutputStream extends OutputStream { * @param b the byte to write */ @Override - public synchronized void write(final int b) { + public abstract void write(final int b); + + /** + * Write a byte to byte array. + * @param b the byte to write + */ + protected void writeImpl(final int b) { int inBufferPos = count - filledBufferSum; if (inBufferPos == currentBuffer.length) { needNewBuffer(count + 1); @@ -178,6 +161,7 @@ public class ByteArrayOutputStream extends OutputStream { count++; } + /** * Writes the entire contents of the specified input stream to this * byte stream. Bytes from the input stream are read directly into the @@ -189,7 +173,20 @@ public class ByteArrayOutputStream extends OutputStream { * @throws IOException if an I/O error occurs while reading the input stream * @since 1.4 */ - public synchronized int write(final InputStream in) throws IOException { + public abstract int write(final InputStream in) throws IOException; + + /** + * Writes the entire contents of the specified input stream to this + * byte stream. Bytes from the input stream are read directly into the + * internal buffers of this streams. + * + * @param in the input stream to read from + * @return total number of bytes read from the input stream + * (and written to this stream) + * @throws IOException if an I/O error occurs while reading the input stream + * @since 2.7 + */ + protected int writeImpl(final InputStream in) throws IOException { int readCount = 0; int inBufferPos = count - filledBufferSum; int n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos); @@ -207,12 +204,11 @@ public class ByteArrayOutputStream extends OutputStream { } /** - * Return the current size of the byte array. + * Returns the current size of the byte array. + * * @return the current size of the byte array */ - public synchronized int size() { - return count; - } + public abstract int size(); /** * Closing a {@code ByteArrayOutputStream} has no effect. The methods in @@ -230,7 +226,12 @@ public class ByteArrayOutputStream extends OutputStream { /** * @see java.io.ByteArrayOutputStream#reset() */ - public synchronized void reset() { + public abstract void reset(); + + /** + * @see java.io.ByteArrayOutputStream#reset() + */ + protected void resetImpl() { count = 0; filledBufferSum = 0; currentBufferIndex = 0; @@ -254,7 +255,17 @@ public class ByteArrayOutputStream extends OutputStream { * @throws IOException if an I/O error occurs, such as if the stream is closed * @see java.io.ByteArrayOutputStream#writeTo(OutputStream) */ - public synchronized void writeTo(final OutputStream out) throws IOException { + public abstract void writeTo(final OutputStream out) throws IOException; + + /** + * Writes the entire contents of this byte stream to the + * specified output stream. + * + * @param out the output stream to write to + * @throws IOException if an I/O error occurs, such as if the stream is closed + * @see java.io.ByteArrayOutputStream#writeTo(OutputStream) + */ + protected void writeToImpl(final OutputStream out) throws IOException { int remaining = count; for (final byte[] buf : buffers) { final int c = Math.min(buf.length, remaining); @@ -267,61 +278,16 @@ public class ByteArrayOutputStream extends OutputStream { } /** - * Fetches entire contents of an <code>InputStream</code> and represent - * same data as result InputStream. - * <p> - * This method is useful where, - * <ul> - * <li>Source InputStream is slow.</li> - * <li>It has network resources associated, so we cannot keep it open for - * long time.</li> - * <li>It has network timeout associated.</li> - * </ul> - * It can be used in favor of {@link #toByteArray()}, since it - * avoids unnecessary allocation and copy of byte[].<br> - * This method buffers the input internally, so there is no need to use a - * <code>BufferedInputStream</code>. - * - * @param input Stream to be fully buffered. - * @return A fully buffered stream. - * @throws IOException if an I/O error occurs - * @since 2.0 - */ - public static InputStream toBufferedInputStream(final InputStream input) - throws IOException { - return toBufferedInputStream(input, 1024); - } - - /** - * Fetches entire contents of an <code>InputStream</code> and represent - * same data as result InputStream. - * <p> - * This method is useful where, - * <ul> - * <li>Source InputStream is slow.</li> - * <li>It has network resources associated, so we cannot keep it open for - * long time.</li> - * <li>It has network timeout associated.</li> - * </ul> - * It can be used in favor of {@link #toByteArray()}, since it - * avoids unnecessary allocation and copy of byte[].<br> - * This method buffers the input internally, so there is no need to use a - * <code>BufferedInputStream</code>. + * Gets the current contents of this byte stream as a Input Stream. The + * returned stream is backed by buffers of <code>this</code> stream, + * avoiding memory allocation and copy, thus saving space and time.<br> * - * @param input Stream to be fully buffered. - * @param size the initial buffer size - * @return A fully buffered stream. - * @throws IOException if an I/O error occurs + * @return the current contents of this output stream. + * @see java.io.ByteArrayOutputStream#toByteArray() + * @see #reset() * @since 2.5 */ - public static InputStream toBufferedInputStream(final InputStream input, final int size) - throws IOException { - // It does not matter if a ByteArrayOutputStream is not closed as close() is a no-op - @SuppressWarnings("resource") - final ByteArrayOutputStream output = new ByteArrayOutputStream(size); - output.write(input); - return output.toInputStream(); - } + public abstract InputStream toInputStream(); /** * Gets the current contents of this byte stream as a Input Stream. The @@ -331,9 +297,9 @@ public class ByteArrayOutputStream extends OutputStream { * @return the current contents of this output stream. * @see java.io.ByteArrayOutputStream#toByteArray() * @see #reset() - * @since 2.5 + * @since 2.7 */ - public synchronized InputStream toInputStream() { + protected InputStream toInputStreamImpl() { int remaining = count; if (remaining == 0) { return new ClosedInputStream(); @@ -358,7 +324,16 @@ public class ByteArrayOutputStream extends OutputStream { * @return the current contents of this output stream, as a byte array * @see java.io.ByteArrayOutputStream#toByteArray() */ - public synchronized byte[] toByteArray() { + public abstract byte[] toByteArray(); + + /** + * Gets the current contents of this byte stream as a byte array. + * The result is independent of this stream. + * + * @return the current contents of this output stream, as a byte array + * @see java.io.ByteArrayOutputStream#toByteArray() + */ + protected byte[] toByteArrayImpl() { int remaining = count; if (remaining == 0) { return EMPTY_BYTE_ARRAY; diff --git a/src/main/java/org/apache/commons/io/output/ByteArrayOutputStream.java b/src/main/java/org/apache/commons/io/output/ByteArrayOutputStream.java index c84721a..ecd47f3 100644 --- a/src/main/java/org/apache/commons/io/output/ByteArrayOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/ByteArrayOutputStream.java @@ -16,61 +16,17 @@ */ package org.apache.commons.io.output; -import static org.apache.commons.io.IOUtils.EOF; - -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.SequenceInputStream; -import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.apache.commons.io.input.ClosedInputStream; /** - * This class implements an output stream in which the data is - * written into a byte array. The buffer automatically grows as data - * is written to it. - * <p> - * The data can be retrieved using <code>toByteArray()</code> and - * <code>toString()</code>. - * <p> - * Closing a {@code ByteArrayOutputStream} has no effect. The methods in - * this class can be called after the stream has been closed without - * generating an {@code IOException}. - * <p> - * This is an alternative implementation of the {@link java.io.ByteArrayOutputStream} - * class. The original implementation only allocates 32 bytes at the beginning. - * As this class is designed for heavy duty it starts at 1024 bytes. In contrast - * to the original it doesn't reallocate the whole memory block but allocates - * additional buffers. This way no buffers need to be garbage collected and - * the contents don't have to be copied to the new buffer. This class is - * designed to behave exactly like the original. The only exception is the - * deprecated toString(int) method that has been ignored. + * Implements a ThreadSafe version of + * {@link AbstractByteArrayOutputStream} using instance + * synchronisation. */ -public class ByteArrayOutputStream extends OutputStream { - - static final int DEFAULT_SIZE = 1024; - - /** A singleton empty byte array. */ - private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; - - /** The list of buffers, which grows and never reduces. */ - private final List<byte[]> buffers = new ArrayList<>(); - /** The index of the current buffer. */ - private int currentBufferIndex; - /** The total count of bytes in all the filled buffers. */ - private int filledBufferSum; - /** The current buffer. */ - private byte[] currentBuffer; - /** The total count of bytes written. */ - private int count; - /** Flag to indicate if the buffers can be reused after reset */ - private boolean reuseBuffers = true; +//@ThreadSafe +public class ByteArrayOutputStream extends AbstractByteArrayOutputStream { /** * Creates a new byte array output stream. The buffer capacity is @@ -97,44 +53,6 @@ public class ByteArrayOutputStream extends OutputStream { } } - /** - * Makes a new buffer available either by allocating - * a new one or re-cycling an existing one. - * - * @param newcount the size of the buffer if one is created - */ - private void needNewBuffer(final int newcount) { - if (currentBufferIndex < buffers.size() - 1) { - //Recycling old buffer - filledBufferSum += currentBuffer.length; - - currentBufferIndex++; - currentBuffer = buffers.get(currentBufferIndex); - } else { - //Creating new buffer - int newBufferSize; - if (currentBuffer == null) { - newBufferSize = newcount; - filledBufferSum = 0; - } else { - newBufferSize = Math.max( - currentBuffer.length << 1, - newcount - filledBufferSum); - filledBufferSum += currentBuffer.length; - } - - currentBufferIndex++; - currentBuffer = new byte[newBufferSize]; - buffers.add(currentBuffer); - } - } - - /** - * Write the bytes to byte array. - * @param b the bytes to write - * @param off The start offset - * @param len The number of bytes to write - */ @Override public void write(final byte[] b, final int off, final int len) { if ((off < 0) @@ -147,123 +65,36 @@ public class ByteArrayOutputStream extends OutputStream { return; } synchronized (this) { - final int newcount = count + len; - int remaining = len; - int inBufferPos = count - filledBufferSum; - while (remaining > 0) { - final int part = Math.min(remaining, currentBuffer.length - inBufferPos); - System.arraycopy(b, off + len - remaining, currentBuffer, inBufferPos, part); - remaining -= part; - if (remaining > 0) { - needNewBuffer(newcount); - inBufferPos = 0; - } - } - count = newcount; + writeImpl(b, off, len); } } - /** - * Write a byte to byte array. - * @param b the byte to write - */ @Override public synchronized void write(final int b) { - int inBufferPos = count - filledBufferSum; - if (inBufferPos == currentBuffer.length) { - needNewBuffer(count + 1); - inBufferPos = 0; - } - currentBuffer[inBufferPos] = (byte) b; - count++; + writeImpl(b); } - /** - * Writes the entire contents of the specified input stream to this - * byte stream. Bytes from the input stream are read directly into the - * internal buffers of this streams. - * - * @param in the input stream to read from - * @return total number of bytes read from the input stream - * (and written to this stream) - * @throws IOException if an I/O error occurs while reading the input stream - * @since 1.4 - */ + @Override public synchronized int write(final InputStream in) throws IOException { - int readCount = 0; - int inBufferPos = count - filledBufferSum; - int n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos); - while (n != EOF) { - readCount += n; - inBufferPos += n; - count += n; - if (inBufferPos == currentBuffer.length) { - needNewBuffer(currentBuffer.length); - inBufferPos = 0; - } - n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos); - } - return readCount; + return writeImpl(in); } - /** - * Return the current size of the byte array. - * @return the current size of the byte array - */ + @Override public synchronized int size() { return count; } /** - * Closing a {@code ByteArrayOutputStream} has no effect. The methods in - * this class can be called after the stream has been closed without - * generating an {@code IOException}. - * - * @throws IOException never (this method should not declare this exception - * but it has to now due to backwards compatibility) - */ - @Override - public void close() throws IOException { - //nop - } - - /** * @see java.io.ByteArrayOutputStream#reset() */ + @Override public synchronized void reset() { - count = 0; - filledBufferSum = 0; - currentBufferIndex = 0; - if (reuseBuffers) { - currentBuffer = buffers.get(currentBufferIndex); - } else { - //Throw away old buffers - currentBuffer = null; - final int size = buffers.get(0).length; - buffers.clear(); - needNewBuffer(size); - reuseBuffers = true; - } + resetImpl(); } - /** - * Writes the entire contents of this byte stream to the - * specified output stream. - * - * @param out the output stream to write to - * @throws IOException if an I/O error occurs, such as if the stream is closed - * @see java.io.ByteArrayOutputStream#writeTo(OutputStream) - */ + @Override public synchronized void writeTo(final OutputStream out) throws IOException { - int remaining = count; - for (final byte[] buf : buffers) { - final int c = Math.min(buf.length, remaining); - out.write(buf, 0, c); - remaining -= c; - if (remaining == 0) { - break; - } - } + writeToImpl(out); } /** @@ -271,6 +102,7 @@ public class ByteArrayOutputStream extends OutputStream { * same data as result InputStream. * <p> * This method is useful where, + * </p> * <ul> * <li>Source InputStream is slow.</li> * <li>It has network resources associated, so we cannot keep it open for @@ -297,6 +129,7 @@ public class ByteArrayOutputStream extends OutputStream { * same data as result InputStream. * <p> * This method is useful where, + * </p> * <ul> * <li>Source InputStream is slow.</li> * <li>It has network resources associated, so we cannot keep it open for @@ -323,98 +156,13 @@ public class ByteArrayOutputStream extends OutputStream { return output.toInputStream(); } - /** - * Gets the current contents of this byte stream as a Input Stream. The - * returned stream is backed by buffers of <code>this</code> stream, - * avoiding memory allocation and copy, thus saving space and time.<br> - * - * @return the current contents of this output stream. - * @see java.io.ByteArrayOutputStream#toByteArray() - * @see #reset() - * @since 2.5 - */ + @Override public synchronized InputStream toInputStream() { - int remaining = count; - if (remaining == 0) { - return new ClosedInputStream(); - } - final List<ByteArrayInputStream> list = new ArrayList<>(buffers.size()); - for (final byte[] buf : buffers) { - final int c = Math.min(buf.length, remaining); - list.add(new ByteArrayInputStream(buf, 0, c)); - remaining -= c; - if (remaining == 0) { - break; - } - } - reuseBuffers = false; - return new SequenceInputStream(Collections.enumeration(list)); + return toInputStreamImpl(); } - /** - * Gets the current contents of this byte stream as a byte array. - * The result is independent of this stream. - * - * @return the current contents of this output stream, as a byte array - * @see java.io.ByteArrayOutputStream#toByteArray() - */ - public synchronized byte[] toByteArray() { - int remaining = count; - if (remaining == 0) { - return EMPTY_BYTE_ARRAY; - } - final byte newbuf[] = new byte[remaining]; - int pos = 0; - for (final byte[] buf : buffers) { - final int c = Math.min(buf.length, remaining); - System.arraycopy(buf, 0, newbuf, pos, c); - pos += c; - remaining -= c; - if (remaining == 0) { - break; - } - } - return newbuf; - } - - /** - * Gets the current contents of this byte stream as a string - * using the platform default charset. - * @return the contents of the byte array as a String - * @see java.io.ByteArrayOutputStream#toString() - * @deprecated 2.5 use {@link #toString(String)} instead - */ @Override - @Deprecated - public String toString() { - // make explicit the use of the default charset - return new String(toByteArray(), Charset.defaultCharset()); - } - - /** - * Gets the current contents of this byte stream as a string - * using the specified encoding. - * - * @param enc the name of the character encoding - * @return the string converted from the byte array - * @throws UnsupportedEncodingException if the encoding is not supported - * @see java.io.ByteArrayOutputStream#toString(String) - */ - public String toString(final String enc) throws UnsupportedEncodingException { - return new String(toByteArray(), enc); - } - - /** - * Gets the current contents of this byte stream as a string - * using the specified encoding. - * - * @param charset the character encoding - * @return the string converted from the byte array - * @see java.io.ByteArrayOutputStream#toString(String) - * @since 2.5 - */ - public String toString(final Charset charset) { - return new String(toByteArray(), charset); + public synchronized byte[] toByteArray() { + return toByteArrayImpl(); } - } diff --git a/src/main/java/org/apache/commons/io/output/UnsynchronizedByteArrayOutputStream.java b/src/main/java/org/apache/commons/io/output/UnsynchronizedByteArrayOutputStream.java new file mode 100644 index 0000000..d138ea6 --- /dev/null +++ b/src/main/java/org/apache/commons/io/output/UnsynchronizedByteArrayOutputStream.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.io.output; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Implements a version of {@link AbstractByteArrayOutputStream} + * <b>without</b> any concurrent thread safety. + * + * @since 2.7 + */ +//@NotThreadSafe +public final class UnsynchronizedByteArrayOutputStream extends AbstractByteArrayOutputStream { + + /** + * Creates a new byte array output stream. The buffer capacity is + * initially 1024 bytes, though its size increases if necessary. + */ + public UnsynchronizedByteArrayOutputStream() { + this(DEFAULT_SIZE); + } + + /** + * Creates a new byte array output stream, with a buffer capacity of + * the specified size, in bytes. + * + * @param size the initial size + * @throws IllegalArgumentException if size is negative + */ + public UnsynchronizedByteArrayOutputStream(final int size) { + if (size < 0) { + throw new IllegalArgumentException( + "Negative initial size: " + size); + } + needNewBuffer(size); + } + + @Override + public void write(final byte[] b, final int off, final int len) { + if ((off < 0) + || (off > b.length) + || (len < 0) + || ((off + len) > b.length) + || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + writeImpl(b, off, len); + } + + @Override + public void write(final int b) { + writeImpl(b); + } + + @Override + public int write(final InputStream in) throws IOException { + return writeImpl(in); + } + + @Override + public int size() { + return count; + } + + /** + * @see java.io.ByteArrayOutputStream#reset() + */ + @Override + public void reset() { + resetImpl(); + } + + @Override + public void writeTo(final OutputStream out) throws IOException { + writeToImpl(out); + } + + /** + * Fetches entire contents of an <code>InputStream</code> and represent + * same data as result InputStream. + * <p> + * This method is useful where, + * </p> + * <ul> + * <li>Source InputStream is slow.</li> + * <li>It has network resources associated, so we cannot keep it open for + * long time.</li> + * <li>It has network timeout associated.</li> + * </ul> + * It can be used in favor of {@link #toByteArray()}, since it + * avoids unnecessary allocation and copy of byte[].<br> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedInputStream</code>. + * + * @param input Stream to be fully buffered. + * @return A fully buffered stream. + * @throws IOException if an I/O error occurs + */ + public static InputStream toBufferedInputStream(final InputStream input) + throws IOException { + return toBufferedInputStream(input, 1024); + } + + /** + * Fetches entire contents of an <code>InputStream</code> and represent + * same data as result InputStream. + * <p> + * This method is useful where, + * </p> + * <ul> + * <li>Source InputStream is slow.</li> + * <li>It has network resources associated, so we cannot keep it open for + * long time.</li> + * <li>It has network timeout associated.</li> + * </ul> + * It can be used in favor of {@link #toByteArray()}, since it + * avoids unnecessary allocation and copy of byte[].<br> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedInputStream</code>. + * + * @param input Stream to be fully buffered. + * @param size the initial buffer size + * @return A fully buffered stream. + * @throws IOException if an I/O error occurs + */ + public static InputStream toBufferedInputStream(final InputStream input, final int size) + throws IOException { + // It does not matter if a ByteArrayOutputStream is not closed as close() is a no-op + @SuppressWarnings("resource") + final UnsynchronizedByteArrayOutputStream output = new UnsynchronizedByteArrayOutputStream(size); + output.write(input); + return output.toInputStream(); + } + + @Override + public InputStream toInputStream() { + return toInputStreamImpl(); + } + + @Override + public byte[] toByteArray() { + return toByteArrayImpl(); + } +} diff --git a/src/test/java/org/apache/commons/io/output/ByteArrayOutputStreamTestCase.java b/src/test/java/org/apache/commons/io/output/ByteArrayOutputStreamTestCase.java index 037e112..917a993 100644 --- a/src/test/java/org/apache/commons/io/output/ByteArrayOutputStreamTestCase.java +++ b/src/test/java/org/apache/commons/io/output/ByteArrayOutputStreamTestCase.java @@ -16,19 +16,26 @@ */ package org.apache.commons.io.output; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.fail; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.stream.Stream; import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.Test; +import org.apache.commons.io.input.ClosedInputStream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; /** - * Basic unit tests for the alternative ByteArrayOutputStream implementation. + * Basic unit tests for the alternative ByteArrayOutputStream implementations. */ public class ByteArrayOutputStreamTestCase { @@ -41,7 +48,7 @@ public class ByteArrayOutputStreamTestCase { } } - private int writeData(final ByteArrayOutputStream baout, + private int writeData(final AbstractByteArrayOutputStream baout, final java.io.ByteArrayOutputStream ref, final int count) { if (count > DATA.length) { @@ -58,7 +65,7 @@ public class ByteArrayOutputStreamTestCase { } } - private int writeData(final ByteArrayOutputStream baout, + private int writeData(final AbstractByteArrayOutputStream baout, final java.io.ByteArrayOutputStream ref, final int[] instructions) { int written = 0; @@ -87,7 +94,7 @@ public class ByteArrayOutputStreamTestCase { } private void checkStreams( - final ByteArrayOutputStream actual, + final AbstractByteArrayOutputStream actual, final java.io.ByteArrayOutputStream expected) { assertEquals(expected.size(), actual.size(), "Sizes are not equal"); final byte[] buf = actual.toByteArray(); @@ -95,9 +102,114 @@ public class ByteArrayOutputStreamTestCase { checkByteArrays(buf, refbuf); } - @Test - public void testToInputStream() throws IOException { - final ByteArrayOutputStream baout = new ByteArrayOutputStream(); + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("baosFactories") + public void testWriteZero(final String baosName, final BAOSFactory baosFactory) { + final AbstractByteArrayOutputStream baout = baosFactory.instance(); + baout.write(new byte[0], 0, 0); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("baosFactories") + public void testInvalidWriteOffsetUnder(final String baosName, final BAOSFactory baosFactory) { + final AbstractByteArrayOutputStream baout = baosFactory.instance(); + assertThrows(IndexOutOfBoundsException.class, () -> + baout.write(null, -1, 0) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("baosFactories") + public void testInvalidWriteOffsetOver(final String baosName, final BAOSFactory baosFactory) { + final AbstractByteArrayOutputStream baout = baosFactory.instance(); + assertThrows(IndexOutOfBoundsException.class, () -> + baout.write(new byte[0], 1, 0) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("baosFactories") + public void testInvalidWriteLenUnder(final String baosName, final BAOSFactory baosFactory) { + final AbstractByteArrayOutputStream baout = baosFactory.instance(); + assertThrows(IndexOutOfBoundsException.class, () -> + baout.write(new byte[1], 0, -1) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("baosFactories") + public void testInvalidWriteOffsetAndLenUnder(final String baosName, final BAOSFactory baosFactory) { + final AbstractByteArrayOutputStream baout = baosFactory.instance(); + assertThrows(IndexOutOfBoundsException.class, () -> + baout.write(new byte[1], 1, -2) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("baosFactories") + public void testInvalidWriteOffsetAndLenOver(final String baosName, final BAOSFactory baosFactory) { + final AbstractByteArrayOutputStream baout = baosFactory.instance(); + assertThrows(IndexOutOfBoundsException.class, () -> + baout.write(new byte[1], 0, 2) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("baosFactories") + public void testInvalidParameterizedConstruction(final String baosName, final BAOSFactory baosFactory) { + assertThrows(IllegalArgumentException.class, () -> + baosFactory.instance(-1) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("baosFactories") + public void testToInputStreamEmpty(final String baosName, final BAOSFactory baosFactory) throws IOException { + final AbstractByteArrayOutputStream baout = baosFactory.instance(); + + //Get data before more writes + final InputStream in = baout.toInputStream(); + assertEquals(0, in.available()); + assertTrue(in instanceof ClosedInputStream); + + in.close(); + baout.close(); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("toBufferedInputStreamFunctionFactories") + public void testToBufferedInputStreamEmpty(final String baosName, final IOFunction<InputStream, InputStream> toBufferedInputStreamFunction) throws IOException { + final ByteArrayInputStream bain = new ByteArrayInputStream(new byte[0]); + assertEquals(0, bain.available()); + + final InputStream buffered = toBufferedInputStreamFunction.apply(bain); + assertEquals(0, buffered.available()); + + buffered.close(); + bain.close(); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("toBufferedInputStreamFunctionFactories") + public void testToBufferedInputStream(final String baosName, final IOFunction<InputStream, InputStream> toBufferedInputStreamFunction) throws IOException { + final byte data[] = {(byte)0xCA, (byte)0xFE, (byte)0xBA, (byte)0xBE}; + + final ByteArrayInputStream bain = new ByteArrayInputStream(data); + assertEquals(data.length, bain.available()); + + final InputStream buffered = toBufferedInputStreamFunction.apply(bain); + assertEquals(data.length, buffered.available()); + + assertArrayEquals(data, IOUtils.toByteArray(buffered)); + + buffered.close(); + bain.close(); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("baosFactories") + public void testToInputStream(final String baosName, final BAOSFactory baosFactory) throws IOException { + final AbstractByteArrayOutputStream baout = baosFactory.instance(); final java.io.ByteArrayOutputStream ref = new java.io.ByteArrayOutputStream(); //Write 8224 bytes @@ -127,10 +239,11 @@ public class ByteArrayOutputStreamTestCase { in.close(); } - @Test - public void testToInputStreamWithReset() throws IOException { + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("baosFactories") + public void testToInputStreamWithReset(final String baosName, final BAOSFactory baosFactory) throws IOException { //Make sure reset() do not destroy InputStream returned from toInputStream() - final ByteArrayOutputStream baout = new ByteArrayOutputStream(); + final AbstractByteArrayOutputStream baout = baosFactory.instance(); final java.io.ByteArrayOutputStream ref = new java.io.ByteArrayOutputStream(); //Write 8224 bytes @@ -162,13 +275,14 @@ public class ByteArrayOutputStreamTestCase { in.close(); } - @Test - public void testStream() throws Exception { + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("baosFactories") + public void testStream(final String baosName, final BAOSFactory baosFactory) throws Exception { int written; //The ByteArrayOutputStream is initialized with 32 bytes to match //the original more closely for this test. - final ByteArrayOutputStream baout = new ByteArrayOutputStream(32); + final AbstractByteArrayOutputStream baout = baosFactory.instance(32); final java.io.ByteArrayOutputStream ref = new java.io.ByteArrayOutputStream(); //First three writes @@ -198,7 +312,7 @@ public class ByteArrayOutputStreamTestCase { //Write the commons Byte[]OutputStream to a java.io.Byte[]OutputStream //and vice-versa to test the writeTo() method. - final ByteArrayOutputStream baout1 = new ByteArrayOutputStream(32); + final AbstractByteArrayOutputStream baout1 = baosFactory.instance(32); ref.writeTo(baout1); final java.io.ByteArrayOutputStream ref1 = new java.io.ByteArrayOutputStream(); baout.writeTo(ref1); @@ -211,13 +325,68 @@ public class ByteArrayOutputStreamTestCase { //Make sure that empty ByteArrayOutputStreams really don't create garbage //on toByteArray() - final ByteArrayOutputStream baos1 = new ByteArrayOutputStream(); - final ByteArrayOutputStream baos2 = new ByteArrayOutputStream(); + final AbstractByteArrayOutputStream baos1 = baosFactory.instance(); + final AbstractByteArrayOutputStream baos2 = baosFactory.instance(); assertSame(baos1.toByteArray(), baos2.toByteArray()); baos1.close(); baos2.close(); baout.close(); baout1.close(); } + + private static Stream<Arguments> baosFactories() { + return Stream.of( + Arguments.of(ByteArrayOutputStream.class.getSimpleName(), new ByteArrayOutputStreamFactory()), + Arguments.of(UnsynchronizedByteArrayOutputStream.class.getSimpleName(), new UnsynchronizedByteArrayOutputStreamFactory()) + ); + } + + private static class ByteArrayOutputStreamFactory implements BAOSFactory { + @Override + public AbstractByteArrayOutputStream instance() { + return new ByteArrayOutputStream(); + } + + @Override + public AbstractByteArrayOutputStream instance(final int size) { + return new ByteArrayOutputStream(size); + } + } + + private static class UnsynchronizedByteArrayOutputStreamFactory implements BAOSFactory { + @Override + public AbstractByteArrayOutputStream instance() { + return new UnsynchronizedByteArrayOutputStream(); + } + + @Override + public AbstractByteArrayOutputStream instance(final int size) { + return new UnsynchronizedByteArrayOutputStream(size); + } + } + + private interface BAOSFactory<T extends AbstractByteArrayOutputStream> { + AbstractByteArrayOutputStream instance(); + AbstractByteArrayOutputStream instance(final int size); + } + + private static Stream<Arguments> toBufferedInputStreamFunctionFactories() { + final IOFunction<InputStream, InputStream> syncBaosToBufferedInputStream = ByteArrayOutputStream::toBufferedInputStream; + final IOFunction<InputStream, InputStream> syncBaosToBufferedInputStreamWithSize = is -> ByteArrayOutputStream.toBufferedInputStream(is, 1024); + final IOFunction<InputStream, InputStream> unSyncBaosToBufferedInputStream = UnsynchronizedByteArrayOutputStream::toBufferedInputStream; + final IOFunction<InputStream, InputStream> unSyncBaosToBufferedInputStreamWithSize = is -> UnsynchronizedByteArrayOutputStream.toBufferedInputStream(is, 1024); + + return Stream.of( + Arguments.of("ByteArrayOutputStream.toBufferedInputStream(InputStream)", syncBaosToBufferedInputStream), + Arguments.of("ByteArrayOutputStream.toBufferedInputStream(InputStream, int)", syncBaosToBufferedInputStreamWithSize), + Arguments.of("UnsynchronizedByteArrayOutputStream.toBufferedInputStream(InputStream)", unSyncBaosToBufferedInputStream), + Arguments.of("UnsynchronizedByteArrayOutputStream.toBufferedInputStream(InputStream, int)", unSyncBaosToBufferedInputStreamWithSize) + ); + } + + @FunctionalInterface + private interface IOFunction<T, R> { + R apply(final T t) throws IOException; + } }