/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.io;
import java.io.IOException;
/**
* BufferedRandomOutputStream is a buffered output stream for {@link RandomAccessOutputStream} which, unlike a regular
* <code>java.io.BufferedOutputStream</code>, makes it safe to seek in the underlying <code>RandomAccessOutputStream</code>.
*
* <p>This class uses {@link BufferPool} to create the internal buffer, to avoid excessive memory allocation and
* garbage collection. The buffer is released when this stream is closed.</p>
*
* @author Maxence Bernard
*/
public class BufferedRandomOutputStream extends RandomAccessOutputStream {
/** The underlying random access output stream */
private RandomAccessOutputStream raos;
/** The buffer where written bytes are accumulated before being sent to the underlying output stream */
private byte buffer[];
/** The current number of bytes waiting to be flushed to the underlying output stream */
private int count;
/** The default buffer size if none is specified */
public final static int DEFAULT_BUFFER_SIZE = 65536;
/**
* Creates a new <code>BufferedRandomOutputStream</code> on top of the given {@link RandomAccessOutputStream}.
* An internal buffer of {@link #DEFAULT_BUFFER_SIZE} bytes is created.
*
* @param raos the underlying RandomAccessOutputStream used by this buffered output stream
*/
public BufferedRandomOutputStream(RandomAccessOutputStream raos) {
this(raos, DEFAULT_BUFFER_SIZE);
}
/**
* Creates a new <code>BufferedRandomOutputStream</code> on top of the given {@link RandomAccessOutputStream}.
* An internal buffer of the specified size is created.
*
* @param raos the underlying RandomAccessOutputStream used by this buffered output stream
* @param size size of the buffer in bytes
*/
public BufferedRandomOutputStream(RandomAccessOutputStream raos, int size) {
this.raos = raos;
this.buffer = BufferPool.getByteArray(size);
}
/**
* Flushes the internal buffer.
*
* @throws IOException if an error occurs
*/
private void flushBuffer() throws IOException {
if (count > 0) {
raos.write(buffer, 0, count);
count = 0;
}
}
/////////////////////////////////////////////
// RandomAccessOutputStream implementation //
/////////////////////////////////////////////
/**
* Writes the specified byte to this buffered output stream.
*
* @param b the byte to be written
* @throws IOException if an I/O error occurs
*/
@Override
public synchronized void write(int b) throws IOException {
if (count >= buffer.length)
flushBuffer();
buffer[count++] = (byte)b;
}
/**
* Writes the specified byte array to this buffered output stream.
*
* @param b the bytes to be written
* @throws IOException if an I/O error occurs
*/
@Override
public synchronized void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
/**
* Writes <code>len</code> bytes from the specified byte array starting at offset <code>off</code> to this
* buffered output stream.
*
* <p>Usually this method stores bytes from the given array into this
* stream's buffer, flushing the buffer to the underlying output stream as
* needed. However, if the requested data length is equal or larger than this stream's
* buffer, then this method will flush the buffer and write the
* bytes directly to the underlying output stream. Thus redundant
* <code>RandomBufferedOutputStream</code>s will not copy data unnecessarily.</p>
*
* @param b the data.
* @param off the start offset in the data.
* @param len the number of bytes to write.
* @throws IOException if an I/O error occurs.
*/
@Override
public synchronized void write(byte b[], int off, int len) throws IOException {
if (len >= buffer.length) {
/* If the request length exceeds the size of the output buffer,
flush the output buffer and then write the data directly.
In this way buffered streams will cascade harmlessly. */
flushBuffer();
raos.write(b, off, len);
return;
}
if (len > buffer.length - count)
flushBuffer();
System.arraycopy(b, off, buffer, count, len);
count += len;
}
/**
* Flushes this buffered output stream. This forces any buffered
* output bytes to be written out to the underlying output stream.
*
* @throws IOException if an I/O error occurs.
*/
@Override
public synchronized void flush() throws IOException {
flushBuffer();
raos.flush();
}
public synchronized long getOffset() throws IOException {
// Add the buffered byte count
return raos.getOffset() + count;
}
public synchronized void seek(long offset) throws IOException {
// Flush any buffered bytes before seeking, otherwise buffered bytes would be written at the wrong offset
flush();
raos.seek(offset);
}
public synchronized long getLength() throws IOException {
// Anticipate if the file is to be expanded by the bytes awaiting in the buffer
return Math.max(raos.getLength(), getOffset());
}
@Override
public synchronized void setLength(long newLength) throws IOException {
// Flush before changing the file's length, otherwise the behavior of setLength() would be modified, especially
// when truncating the file
flush();
raos.setLength(newLength);
}
////////////////////////
// Overridden methods //
////////////////////////
/**
* This method is overridden to release the internal buffer when this stream is closed.
*/
@Override
public synchronized void close() throws IOException {
if(buffer!=null) { // buffer is null if close() was already called
try {
flush();
}
catch(IOException e) {
// Continue anyway
}
// Release the buffer
BufferPool.releaseByteArray(buffer);
buffer = null;
}
raos.close();
}
/**
* This method is overridden to release the internal buffer if {@link #close()} has not been called, to avoid any
* memory leak.
*/
@Override
protected void finalize() throws Throwable {
// If this stream hasn't been closed, release the buffer before finalizing the object
if(buffer!=null)
BufferPool.releaseByteArray(buffer);
super.finalize();
}
}