package hep.io.sio;
import hep.io.xdr.XDRBufferedRandomAccessFile;
import hep.io.xdr.XDRDataOutput;
import hep.io.xdr.XDROutputStream;
import hep.io.xdr.XDRRandomAccessFile;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.DataOutput;
import java.io.File;
import java.io.Flushable;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.DeflaterOutputStream;
/**
* Class for writing out SIO files. Includes limited support for rewriting
* existing records, which is useful for supporting record based random access.
* @author tonyj
*/
public class SIOWriter implements Closeable, Flushable {
private XDRDataOutput xdr;
//FIXME: This is inherantly single threaded, not ideal for random access
private SIOByteArrayOutputStream blockBytes = new SIOByteArrayOutputStream();
private SIOByteArrayOutputStream recordBytes = new SIOByteArrayOutputStream();
private SIOOutputStream block = new SIOOutputStream(blockBytes);
private XDROutputStream record = new XDROutputStream(recordBytes);
private DeflaterOutputStream compressor;
private final static int recordFrame = 0xabadcafe;
private final static int blockFrame = 0xdeadbeef;
private String blockName;
private int blockVersion;
private String recordName;
private boolean recordCompress = false;
private static int bufferSize = Integer.getInteger("hep.io.sio.BufferSize", 32768).intValue();
private int originalLength; // Gives length of original record when a rewrite is in progress
private long originalPosition; // Gives position of next record when a rewrite is in progress
/** Creates an SIOWriter which writes to an OutputStream. The resulting SIOWriter
will not support random access.
* @param out The output stream to write to
* @throws IOException If an error occurs
*/
public SIOWriter(OutputStream out) throws IOException {
this.xdr = new XDROutputStream(out);
}
/** Creates an SIOWriter which writes to a file. The resulting SIOWriter will support
* random access.
* @param file The file to write to
* @throws IOException If an errors occurs.
* @since 2.1
*/
public SIOWriter(String file) throws IOException {
this.xdr = new XDRBufferedRandomAccessFile(file, false, bufferSize);
}
/** Creates an SIOWriter which writes to a file. The resulting SIOWriter will support
* random access.
* @param file The file to write to
* @throws IOException If an error occurs
* @since 2.1
*/
public SIOWriter(File file) throws IOException {
this.xdr = new XDRBufferedRandomAccessFile(file, false, bufferSize);
}
/**
* Create a new record.
* @param name The name if the newly created record.
* @param compress <code>true</code> if the record should be compressed.
* @return The position of the record in the file. This position can be used to
* rewrite the record later if the writer supports random access.
* @throws IOException If an error occurs
* @see #rewriteRecord(long, boolean)
* @since 2.1
*/
public long createRecord(String name, boolean compress) throws IOException {
flushRecord();
prepareRecord(name, compress);
return getPosition();
}
/**
* Rewrite a record previously created with createRecord or reserveSpaceForRecord. The
* new record must be no bigger than the existing record or reserved space at the specified
* position.
* @param position The position in the file to rewrite the record. The position must
* correspond to an existing record or reserved space for a record.
* @param compress If the record should be compressed. Since predicting the size
* of compressed records is difficult it is not recommended to rewrite compressed
* records.
* @throws IOException If an error occurs, or if the specified position does not
* correspond to a record, or if the new record will not fit in the reserved space.
* @see #reserveSpaceForRecord(java.lang.String, int)
* @since 2.1
*/
public void rewriteRecord(long position, boolean compress) throws IOException {
flushRecord();
XDRRandomAccessFile raf = checkRandomAccess();
originalPosition = raf.getFilePointer();
raf.seek(position);
int headerLength = raf.readInt();
// Now read the record header for the next record
int frame = raf.readInt();
if (frame != recordFrame) {
throw new IOException("Framing error");
}
int control = raf.readInt();
if ((control & 0xfffc) != 0) {
throw new IOException("Bad control word");
}
int compressedLength = raf.readInt();
int uncompressedLength = raf.readInt();
int l = raf.readInt();
if (l > headerLength - raf.getFilePointer() + originalPosition) {
throw new IOException("Record name is insane");
}
String originalName = raf.readString(l);
originalLength = compressedLength;
prepareRecord(originalName,compress);
raf.seek(position);
}
/**
* Reserve space for writing a record in the future.
* @param name The name of the record for which the space is reserved
* @param size The size in bytes to reserve for the record
* @return The position in the file of the reserved space
* @throws IOException If an errors occurs or if the writer does not support random access.
* @see #rewriteRecord(long, boolean)
* @since 2.1
*/
public long reserveSpaceForRecord(String name, int size) throws IOException {
flushRecord();
XDRRandomAccessFile raf = checkRandomAccess();
long recordPosition = raf.getFilePointer();
writeRecordHeader(name, 2, size, size);
raf.seek(raf.getFilePointer() + size);
xdr.pad();
return recordPosition;
}
/**
* Add a block within the current record
* @param name The name of the block to be created
* @param major The major version number of the created block
* @param minor The minor version number of the created block
* @return The stream to be used to write the content of the block
* @throws IOException If an errors occurs, or it no current record exists.
*/
public SIOOutputStream createBlock(String name, int major, int minor) throws IOException {
if (recordName == null) {
throw new IOException("No record currently exists");
}
flushBlock();
blockName = name;
blockVersion = (major << 16) + minor;
return block;
}
private void flushBlock() throws IOException {
if (blockName == null) {
return;
}
block.flush();
int blockLength = pad(blockBytes.size()) + 16 + pad(blockName.length());
record.writeInt(blockLength);
record.writeInt(blockFrame);
record.writeInt(blockVersion);
record.writeString(blockName);
blockBytes.writeTo((DataOutput) record);
blockBytes.reset();
blockName = null;
}
private void flushRecord() throws IOException {
if (recordName == null) {
return;
}
try {
flushBlock();
block.clear();
record.flush();
if (recordCompress) {
compressor.finish();
}
if (originalPosition != 0) {
// We are rewriting a record, we need to check if the new record fits
if (recordBytes.size() > originalLength) {
throw new IOException("Rewritten record does not fit");
}
}
writeRecordHeader(recordName,recordCompress ? 1 : 0,
originalPosition != 0 ? originalLength : recordBytes.size(),
(int) record.getBytesWritten());
recordBytes.writeTo(xdr);
recordBytes.reset();
xdr.pad();
} finally {
recordName = null;
if (originalPosition != 0) {
checkRandomAccess().seek(originalPosition);
originalPosition = 0;
originalLength = 0;
}
}
}
public void close() throws IOException {
flushRecord();
if (xdr instanceof XDRRandomAccessFile)
{
XDRRandomAccessFile raf = (XDRRandomAccessFile) xdr;
raf.setLength(raf.getFilePointer());
}
xdr.close();
}
public void flush() throws IOException {
flushRecord();
xdr.flush();
}
/**
* Test of the writer supports random access
* @return <code>true</code> if random access is supported
* @since 2.1
*/
public boolean isRandomAccess() {
return xdr instanceof XDRRandomAccessFile;
}
private XDRRandomAccessFile checkRandomAccess() throws IOException {
if (!isRandomAccess()) {
throw new IOException("File does not support random access");
}
return (XDRRandomAccessFile) xdr;
}
private long getPosition() throws IOException {
if (xdr instanceof XDRRandomAccessFile) {
return ((XDRRandomAccessFile) xdr).getFilePointer();
} else {
return ((XDROutputStream) xdr).getBytesWritten();
}
}
private int pad(int size) {
int r = size % 4;
if (r == 0) {
return size;
}
return size + 4 - r;
}
private void prepareRecord(String name, boolean compress) {
recordName = name;
if (compress) {
compressor = new DeflaterOutputStream(recordBytes);
record = new XDROutputStream(compressor);
} else {
compressor = null;
record = new XDROutputStream(recordBytes);
}
recordCompress = compress;
}
private static class SIOByteArrayOutputStream extends ByteArrayOutputStream {
void writeTo(DataOutput out) throws IOException {
out.write(buf, 0, count);
}
}
private void writeRecordHeader(String name, int mode, int compressedSize, int uncompressedSize) throws IOException {
int headerLength = 24 + pad(name.length());
xdr.writeInt(headerLength);
xdr.writeInt(recordFrame);
xdr.writeInt(mode);
xdr.writeInt(compressedSize);
xdr.writeInt(uncompressedSize);
xdr.writeString(name);
xdr.pad();
}
}