/*
* Copyright (C) 2014 Jörg Prante
*
* 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.xbib.io.compress.lzf;
import java.io.IOException;
import java.io.OutputStream;
/**
* Decorator {@link java.io.OutputStream} implementation that will compress output using
* LZF compression algorithm, given uncompressed input to write. Its counterpart
* is {@link LZFInputStream}; although in some ways
* {@link LZFCompressingInputStream} can be seen as the opposite.
*/
public class LZFOutputStream extends OutputStream {
private final ChunkEncoder _encoder;
private final BufferRecycler _recycler;
protected final OutputStream _outputStream;
protected byte[] _outputBuffer;
protected int _position = 0;
/**
* Configuration setting that governs whether basic 'flush()' should first
* complete a block or not. <p> Default value is 'true'
*/
protected boolean _cfgFinishBlockOnFlush = true;
/**
* Flag that indicates if we have already called '_outputStream.close()' (to
* avoid calling it multiple times)
*/
protected boolean _outputStreamClosed;
/*
// Construction, configuration
*/
public LZFOutputStream(final OutputStream outputStream) {
this(outputStream, LZFChunk.MAX_CHUNK_LEN);
}
public LZFOutputStream(final OutputStream outputStream, int bufsize) {
_encoder = new ChunkEncoder(bufsize);
_recycler = BufferRecycler.instance();
_outputStream = outputStream;
_outputBuffer = _recycler.allocOutputBuffer(bufsize);
_outputStreamClosed = false;
}
/**
* Method for defining whether call to {@link #flush} will also complete
* current block (similar to calling {@link #finishBlock()}) or not.
*/
public LZFOutputStream setFinishBlockOnFlush(boolean b) {
_cfgFinishBlockOnFlush = b;
return this;
}
/*
// OutputStream impl
*/
@Override
public void write(final int singleByte) throws IOException {
checkNotClosed();
if (_position >= _outputBuffer.length) {
writeCompressedBlock();
}
_outputBuffer[_position++] = (byte) singleByte;
}
@Override
public void write(final byte[] buffer, int offset, int length) throws IOException {
checkNotClosed();
final int BUFFER_LEN = _outputBuffer.length;
// simple case first: buffering only (for trivially short writes)
int free = BUFFER_LEN - _position;
if (free >= length) {
System.arraycopy(buffer, offset, _outputBuffer, _position, length);
_position += length;
return;
}
// otherwise, copy whatever we can, flush
System.arraycopy(buffer, offset, _outputBuffer, _position, free);
offset += free;
length -= free;
_position += free;
writeCompressedBlock();
// then write intermediate full block, if any, without copying:
while (length >= BUFFER_LEN) {
_encoder.encodeAndWriteChunk(buffer, offset, BUFFER_LEN, _outputStream);
offset += BUFFER_LEN;
length -= BUFFER_LEN;
}
// and finally, copy leftovers in buffer, if any
if (length > 0) {
System.arraycopy(buffer, offset, _outputBuffer, 0, length);
}
_position = length;
}
@Override
public void flush() throws IOException {
checkNotClosed();
if (_cfgFinishBlockOnFlush && _position > 0) {
writeCompressedBlock();
}
_outputStream.flush();
}
@Override
public void close() throws IOException {
if (!_outputStreamClosed) {
if (_position > 0) {
writeCompressedBlock();
}
_outputStream.flush();
_encoder.close();
byte[] buf = _outputBuffer;
if (buf != null) {
_outputBuffer = null;
_recycler.releaseOutputBuffer(buf);
}
_outputStreamClosed = true;
_outputStream.close();
}
}
/**
* Method that can be used to find underlying {@link java.io.OutputStream} that we
* write encoded LZF encoded data into, after compressing it. Will never
* return null; although underlying stream may be closed (if this stream has
* been closed).
*/
public OutputStream getUnderlyingOutputStream() {
return _outputStream;
}
/**
* Accessor for checking whether call to "flush()" will first finish the
* current block or not
*/
public boolean getFinishBlockOnFlush() {
return _cfgFinishBlockOnFlush;
}
/**
* Method that can be used to force completion of the current block, which
* means that all buffered data will be compressed into an LZF block. This
* typically results in lower compression ratio as larger blocks compress
* better; but may be necessary for network connections to ensure timely
* sending of data.
*/
public LZFOutputStream finishBlock() throws IOException {
checkNotClosed();
if (_position > 0) {
writeCompressedBlock();
}
return this;
}
/**
* Compress and write the current block to the OutputStream
*/
protected void writeCompressedBlock() throws IOException {
int left = _position;
_position = 0;
int offset = 0;
do {
int chunkLen = Math.min(LZFChunk.MAX_CHUNK_LEN, left);
_encoder.encodeAndWriteChunk(_outputBuffer, offset, chunkLen, _outputStream);
offset += chunkLen;
left -= chunkLen;
} while (left > 0);
}
protected void checkNotClosed() throws IOException {
if (_outputStreamClosed) {
throw new IOException(getClass().getName() + " already closed");
}
}
}