/*
* Copyright (C) 2008 Laurent Caillette
*
* This program 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.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.novelang.rendering.buffer;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import com.google.common.base.Preconditions;
import org.apache.commons.io.IOUtils;
import org.novelang.logger.Logger;
import org.novelang.logger.LoggerFactory;
import org.novelang.outfit.TemporaryFileService;
/**
* An {@code OutputStream } that keeps all bytes in memory, or in a temporary files if going a
* given amount.
* <p>
* This class is not thread-safe.
*
* @author Laurent Caillette
*/
public class CisternOutputStream extends OutputStream {
private static final Logger LOGGER = LoggerFactory.getLogger( CisternOutputStream.class ) ;
private byte[] bytes ;
private int byteCount ;
private final TemporaryFileService.FileSupplier fileSupplier ;
private File file = null ;
private FileOutputStream fileOutputStream = null;
/**
* Constructor.
*
* @param fileSupplier a non-null object.
* @param maximumHeapMemorySizeInBytes maximum amount of bytes to keep in heap memory.
*/
public CisternOutputStream(
final TemporaryFileService.FileSupplier fileSupplier,
final int maximumHeapMemorySizeInBytes
) {
this.fileSupplier = Preconditions.checkNotNull( fileSupplier ) ;
bytes = new byte[ maximumHeapMemorySizeInBytes ] ;
byteCount = 0 ;
}
/**
* Copies buffer's content to the given {@code OutputStream}.
*
* @param target a non-null object.
* @throws IllegalStateException if already released.
*/
public void copy( final OutputStream target ) throws IOException {
checkState() ;
if( file == null ) {
target.write( bytes, 0, byteCount ) ;
} else {
fileOutputStream.flush() ;
final FileInputStream fileInputStream = new FileInputStream( file ) ;
try {
// No need to buffer, says the documentation.
IOUtils.copy( fileInputStream, target ) ;
} finally {
fileInputStream.close() ;
}
}
}
private void checkState() {
if( bytes == null && file == null ) {
throw new IllegalStateException( "Already released" ) ;
}
}
/**
* Returns true if adding given byte count overflows the heap memory buffer.
*/
private boolean overflows( final int additionalByteCount ) {
return byteCount + additionalByteCount > bytes.length ;
}
@SuppressWarnings( { "IOResourceOpenedButNotSafelyClosed" } )
private void switchToFile() throws IOException {
Preconditions.checkState( file == null ) ;
Preconditions.checkState( fileOutputStream == null ) ;
file = fileSupplier.get() ;
fileOutputStream = new FileOutputStream( file ) ;
fileOutputStream.write( bytes, 0, byteCount ) ;
LOGGER.debug( "Wrote ", byteCount, " bytes into '", file.getAbsolutePath(), "'." ) ;
bytes = null ;
}
// ============
// OutputStream
// ============
@Override
public void write( final int someByte ) throws IOException {
checkState() ;
if( file == null ) {
if( overflows( 1 ) ) {
switchToFile() ;
} else {
bytes[ byteCount ++ ] = ( byte ) someByte ;
return ;
}
}
fileOutputStream.write( someByte ) ;
}
@Override
public void write( final byte[] bytes, final int offset, final int length ) throws IOException {
checkState() ;
if( file == null ) {
if( overflows( length ) ) {
switchToFile() ;
} else {
for( int i = 0 ; i < length ; i ++ ) {
this.bytes[ byteCount ++ ] = bytes[ offset + i ] ;
}
return ;
}
}
fileOutputStream.write( bytes, offset, length ) ;
}
@Override
public void flush() throws IOException {
checkState() ;
if( fileOutputStream != null ) {
fileOutputStream.flush() ;
}
}
public boolean isOpen() {
return bytes != null || file != null ;
}
/**
* Releases underlying resources.
*
* @throws IllegalStateException if already released.
*/
@Override
public void close() throws IOException {
checkState() ;
bytes = null ;
byteCount = -1 ;
if( fileOutputStream != null ) {
fileOutputStream.close() ;
fileOutputStream = null ;
}
if( file != null ) {
file.delete() ;
file = null ;
}
}
}