/* Copyright (c) 2001-2009, The HSQL Development Group
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the HSQL Development Group nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
/* $Id: ClosableByteArrayOutputStream.java 2946 2009-03-22 17:44:48Z fredt $ */
/**
* @todo - finer-grained synchronization to reduce average
* potential monitor contention
*/
/**
* Provides true Closable semantics ordinarily missing in a
* {@link java.io.ByteArrayOutputStream}.
*
* Accumulates output in a byte array that automatically grows as needed.
*
* Data is retrieved using <tt>toByteArray()</tt>,
* <tt>toByteArrayInputStream()</tt>, <tt>toString()</tt> and
* <tt>toString(encoding)</tt>.
*
* {@link #close() Closing} a <tt>ClosableByteArrayOutputStream</tt> prevents
* further write operations, but all other operations may succeed until after
* the first invocation of {@link #free() free()}.
*
* Freeing a <tt>ClosableByteArrayOutputStream</tt> closes the stream and
* releases the internal buffer, preventing successful invocation of all
* operations, with the exception of <tt>size()<tt>, <tt>close()</tt>,
* <tt>isClosed()</tt>, <tt>free()</tt> and <tt>isFreed()</tt>.
*
* This class is especially useful when an accumulating output stream must be
* handed off to an extenal client under contract that the stream should
* exhibit true Closable behaviour in response both to internally tracked
* events and to client invocation of the <tt>OutputStream.close()</tt> method.
*
* @author boucherb@users
* @version 1.9.0
* @since 1.9.0
*/
public class ClosableByteArrayOutputStream extends OutputStream {
/**
* Data buffer.
*/
protected byte[] buf;
/**
* # of valid bytes in buffer.
*/
protected int count;
/**
* Whether this stream is closed.
*/
protected boolean closed;
/**
* Whether this stream is freed.
*/
protected boolean freed;
/**
* Creates a new output stream.
*
* The buffer capacity is initially 32 bytes, though its size increases
* if necessary.
*/
public ClosableByteArrayOutputStream() {
this(32);
}
/**
* Creates a new output stream with a buffer capacity of the specified
* <tt>size</tt>, in bytes.
*
* @param size the initial size.
* @exception IllegalArgumentException if size is negative.
*/
public ClosableByteArrayOutputStream(int size)
throws IllegalArgumentException {
if (size < 0) {
throw new IllegalArgumentException("Negative initial size: "
+ size); // NOI18N
}
buf = new byte[size];
}
/**
* Writes the specified single byte.
*
* @param b the single byte to be written.
* @throws java.io.IOException if an I/O error occurs.
* In particular, an <tt>IOException</tt> may be thrown
* if this output stream has been {@link #close() closed}.
*/
public synchronized void write(int b) throws IOException {
checkClosed();
int newcount = count + 1;
if (newcount > buf.length) {
buf = copyOf(buf, Math.max(buf.length << 1, newcount));
}
buf[count] = (byte) b;
count = newcount;
}
/**
* Writes the specified portion of the designated octet sequence.
*
* @param b the data.
* @param off the start offset in the data.
* @param len the number of bytes to write.
* @throws java.io.IOException if an I/O error occurs.
* In particular, an <tt>IOException</tt> may be thrown
* if this output stream has been {@link #close() closed}.
*/
public synchronized void write(byte b[], int off,
int len) throws IOException {
checkClosed();
if ((off < 0) || (off > b.length) || (len < 0)
|| ((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
int newcount = count + len;
if (newcount > buf.length) {
buf = copyOf(buf, Math.max(buf.length << 1, newcount));
}
System.arraycopy(b, off, buf, count, len);
count = newcount;
}
/**
* By default, does nothing.
*
* @throws java.io.IOException if an I/O error occurs.
* In particular, an <tt>IOException</tt> may be thrown
* if this output stream has been {@link #close() closed}.
*/
public void flush() throws IOException {
checkClosed();
}
/**
* Writes the complete contents of this stream's accumulated data to the
* specified output stream.
*
* The operation occurs as if by calling <tt>out.write(buf, 0, count)</tt>.
*
* @param out the output stream to which to write the data.
* @throws java.io.IOException if an I/O error occurs.
* In particular, an <tt>IOException</tt> may be thrown
* if this output stream has been {@link #free() freed}.
*/
public synchronized void writeTo(OutputStream out) throws IOException {
checkFreed();
out.write(buf, 0, count);
}
/**
* Returns the current capacity of this stream's data buffer.
*
* @return the length of the internal data array
* @throws java.io.IOException if an I/O error occurs.
* In particular, an <tt>IOException</tt> may be thrown
* if this output stream has been {@link #free() freed}.
*/
public synchronized int capacity() throws IOException {
checkFreed();
return buf.length;
}
/**
* Resets the <tt>count</tt> field of this output stream to zero, so that
* all currently accumulated data is effectively discarded.
*
* Further write operations will reuse the allocated buffer space.
*
* @see #count
* @throws java.io.IOException if an I/O error occurs.
* In particular, an <tt>IOException</tt> may be thrown
* if this output stream has been {@link #close() closed}.
*/
public synchronized void reset() throws IOException {
checkClosed();
count = 0;
}
/**
* Attempts to reduce this stream's capacity to its current size.
*
* If the data buffer is larger than necessary to hold its current sequence
* of bytes, then it may be resized to become more space efficient.
* Calling this method may, but is not required to, affect the value
* returned by a subsequent call to the {@link #capacity()} method.
*
* @throws java.io.IOException if an I/O error occurs.
* In particular, an <tt>IOException</tt> may be thrown
* if this output stream has been {@link #free() freed}.
*/
public synchronized void trimToSize() throws IOException {
checkFreed();
if (buf.length > count) {
buf = copyOf(buf, count);
}
}
/**
* Retrieves a copy of this stream's accumated data, as a byte array.
*
* @return a copy of this stream's accumated data, as a byte array.
* @see #size()
* @throws java.io.IOException if an I/O error occurs.
* In particular, an <tt>IOException</tt> may be thrown
* if this output stream has been {@link #free() freed}.
*/
public synchronized byte[] toByteArray() throws IOException {
checkFreed();
return copyOf(buf, count);
}
/**
* Returns the current size of this stream's accumated data.
*
* @return the value of the <tt>count</tt> field, which is the number
* of valid bytes in this output stream.
* @see #count
* @throws java.io.IOException never
*/
public synchronized int size() throws IOException {
return count;
}
/**
* Sets the size of this stream's accumulated data.
*
* @param newSize the new size
* @throws ArrayIndexOutOfBoundsException if new size is negative
*/
public synchronized void setSize(int newSize) {
if (newSize < 0) {
throw new ArrayIndexOutOfBoundsException(newSize);
} else if (newSize > buf.length) {
buf = copyOf(buf, Math.max(buf.length << 1, newSize));
}
count = newSize;
}
/**
* Performs an effecient (zero-copy) conversion of the data accumulated in
* this output stream to an input stream.
*
* To ensure the future integrity of the resulting input stream, {@link
* #free() free} is invoked upon this output stream as a side-effect.
*
* @return an input stream representing this output stream's accumulated
* data
* @throws java.io.IOException if an I/O error occurs.
* In particular, an <tt>IOException</tt> may be thrown
* if this output stream has been {@link #free() freed}.
*/
public synchronized ByteArrayInputStream toByteArrayInputStream()
throws IOException {
checkFreed();
ByteArrayInputStream inputStream = new ByteArrayInputStream(buf, 0,
count);
free();
return inputStream;
}
/**
* Converts this stream's accumuated data into a string, translating bytes
* into characters according to the platform's default character encoding.
*
* @return String translated from this stream's accumuated data.
* @throws RuntimeException may be thrown if this output stream has been
* {@link #free() freed}.
*/
public synchronized String toString() {
try {
checkFreed();
} catch (IOException ex) {
throw new RuntimeException(ex.toString());
}
return new String(buf, 0, count);
}
/**
* Converts this stream's accumuated data into a string, translating bytes
* into characters according to the specified character encoding.
*
* @return String translated from the buffer's contents.
* @param enc a character-encoding name.
* @throws java.io.IOException may be thrown if this output stream has been
* {@link #free() freed}.
* @throws UnsupportedEncodingException If the named encoding is not
* supported.
*/
public synchronized String toString(String enc)
throws IOException, UnsupportedEncodingException {
checkFreed();
return new String(buf, 0, count, enc);
}
/**
* Closes this object for further writing.
*
* Other operations may continue to succeed until after the first invocation
* of {@link #free() free()}.
*
* @throws java.io.IOException if an I/O error occurs (default: never)
*/
public synchronized void close() throws IOException {
closed = true;
}
/**
* Retrieves whether this stream is closed.
* @return <tt>true</tt> if this stream is closed, else <tt>false</tt>
*/
public synchronized boolean isClosed() {
return closed;
}
/**
* Closes this object and releases the underlying buffer for
* garbage collection.
*
* @throws java.io.IOException if an I/O error occurs while closing
* this stream (default: never).
*/
public synchronized void free() throws IOException {
closed = true;
freed = true;
buf = null;
count = 0;
}
/**
* Retrieves whether this stream is freed.
*
* @return <tt>true</tt> if this stream is freed; else <tt>false</tt>.
*/
public synchronized boolean isFreed() {
return freed;
}
/**
* Tests whether this stream is closed.
*
* @throws java.io.IOException if this stream is closed.
*/
protected synchronized void checkClosed() throws IOException {
if (closed) {
throw new IOException("stream is closed."); // NOI18N
}
}
/**
* Tests whether this stream is freed.
*
* @throws java.io.IOException if this stream is freed.
*/
protected synchronized void checkFreed() throws IOException {
if (freed) {
throw new IOException("stream buffer is freed."); // NOI18N
}
}
/**
* Retrieves a copy of <tt>original</tt> with the given
* <tt>newLength</tt>.
*
* @param original the object to copy
* @param newLength the length of the copy
* @return copy of <tt>original</tt> with the given <tt>newLength</tt>
*/
protected byte[] copyOf(byte[] original, int newLength) {
byte[] copy = new byte[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
}