/**
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
*
* 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 Git Development Community 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 THE COPYRIGHT OWNER 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.
*/
package org.n52.wps.io;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
/**
* A fully buffered output stream using local disk storage for large data.
* <p>
* Initially this output stream buffers to memory, like ByteArrayOutputStream
* might do, but it shifts to using an on disk temporary file if the output gets
* too large.
* <p>
* The content of this buffered stream may be sent to another OutputStream only
* after this stream has been properly closed by {@link #close()}.
*/
public class LargeBufferStream extends OutputStream {
private static final int DEFAULT_IN_CORE_LIMIT = 128* 1024 * 1024;
/** Chain of data, if we are still completely in-core; otherwise null. */
private ArrayList<Block> blocks;
/**
* Maximum number of bytes we will permit storing in memory.
* <p>
* When this limit is reached the data will be shifted to a file on disk,
* preventing the JVM heap from growing out of control.
*/
private int inCoreLimit;
/**
* Location of our temporary file if we are on disk; otherwise null.
* <p>
* If we exceeded the {@link #inCoreLimit} we nulled out {@link #blocks} and
* created this file instead. All output goes here through {@link #diskOut}.
*/
private File onDiskFile;
/** If writing to {@link #onDiskFile} this is a buffered stream to it. */
private OutputStream diskOut;
/** Create a new empty temporary buffer. */
public LargeBufferStream() {
inCoreLimit = DEFAULT_IN_CORE_LIMIT;
blocks = new ArrayList<Block>(inCoreLimit / Block.SZ);
blocks.add(new Block());
}
@Override
public void write(final int b) throws IOException {
if (blocks == null) {
diskOut.write(b);
return;
}
Block s = last();
if (s.isFull()) {
if (reachedInCoreLimit()) {
diskOut.write(b);
return;
}
s = new Block();
blocks.add(s);
}
s.buffer[s.count++] = (byte) b;
}
@Override
public void write(final byte[] b, int off, int len) throws IOException {
if (blocks != null) {
while (len > 0) {
Block s = last();
if (s.isFull()) {
if (reachedInCoreLimit())
break;
s = new Block();
blocks.add(s);
}
final int n = Math.min(Block.SZ - s.count, len);
System.arraycopy(b, off, s.buffer, s.count, n);
s.count += n;
len -= n;
off += n;
}
}
if (len > 0)
diskOut.write(b, off, len);
}
private Block last() {
return blocks.get(blocks.size() - 1);
}
private boolean reachedInCoreLimit() throws IOException {
if (blocks.size() * Block.SZ < inCoreLimit)
return false;
onDiskFile = File.createTempFile("jgit_", ".buffer");
diskOut = new FileOutputStream(onDiskFile);
final Block last = blocks.remove(blocks.size() - 1);
for (final Block b : blocks)
diskOut.write(b.buffer, 0, b.count);
blocks = null;
diskOut = new BufferedOutputStream(diskOut, Block.SZ);
diskOut.write(last.buffer, 0, last.count);
return true;
}
public void close() throws IOException {
if (diskOut != null) {
try {
diskOut.close();
} finally {
diskOut = null;
}
}
}
/**
* Obtain the length (in bytes) of the buffer.
* <p>
* The length is only accurate after {@link #close()} has been invoked.
*
* @return total length of the buffer, in bytes.
*/
public long length() {
if (onDiskFile != null)
return onDiskFile.length();
final Block last = last();
return ((long) blocks.size()) * Block.SZ - (Block.SZ - last.count);
}
/**
* Send this buffer to an output stream.
* <p>
* This method may only be invoked after {@link #close()} has completed
* normally, to ensure all data is completely transferred.
*
* @param os
* stream to send this buffer's complete content to.
* @param pm
* if not null progress updates are sent here. Caller should
* initialize the task and the number of work units to
* <code>{@link #length()}/1024</code>.
* @throws IOException
* an error occurred reading from a temporary file on the local
* system, or writing to the output stream.
*/
public void writeTo(final OutputStream os)
throws IOException {
if (blocks != null) {
// Everything is in core so we can stream directly to the output.
//
for (final Block b : blocks) {
os.write(b.buffer, 0, b.count);
}
} else {
// Reopen the temporary file and copy the contents.
//
final FileInputStream in = new FileInputStream(onDiskFile);
try {
int cnt;
final byte[] buf = new byte[Block.SZ];
while ((cnt = in.read(buf)) >= 0) {
os.write(buf, 0, cnt);
}
} finally {
in.close();
}
}
}
/** Clear this buffer so it has no data, and cannot be used again. */
public void destroy() {
blocks = null;
if (diskOut != null) {
try {
diskOut.close();
} catch (IOException err) {
// We shouldn't encounter an error closing the file.
} finally {
diskOut = null;
}
}
if (onDiskFile != null) {
if (!onDiskFile.delete())
onDiskFile.deleteOnExit();
onDiskFile = null;
}
}
private static class Block {
static final int SZ = 8 * 1024 * 1024;
final byte[] buffer = new byte[SZ];
int count;
boolean isFull() {
return count == SZ;
}
}
}