/*
* Copyright Aduna (http://www.aduna-software.com/) (c) 1997-2007.
*
* Licensed under the Aduna BSD-style license.
*/
package org.openrdf.sail.nativerdf.datastore;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.NoSuchElementException;
/**
* Class supplying access to a data file. A data file stores data sequentially.
* Each entry starts with the entry's length (4 bytes), followed by the data
* itself. File offsets are used to identify entries.
*
* @author Arjohn Kampman
*/
public class DataFile {
/*-----------*
* Constants *
*-----------*/
/**
* Magic number "Native Data File" to detect whether the file is actually a
* data file. The first three bytes of the file should be equal to this magic
* number.
*/
private static final byte[] MAGIC_NUMBER = new byte[] { 'n', 'd', 'f' };
/**
* File format version, stored as the fourth byte in data files.
*/
private static final byte FILE_FORMAT_VERSION = 1;
private static final long HEADER_LENGTH = MAGIC_NUMBER.length + 1;
/*-----------*
* Variables *
*-----------*/
private final File file;
private final RandomAccessFile raf;
private final FileChannel fileChannel;
private final boolean forceSync;
/*--------------*
* Constructors *
*--------------*/
public DataFile(File file)
throws IOException
{
this(file, false);
}
public DataFile(File file, boolean forceSync)
throws IOException
{
this.file = file;
this.forceSync = forceSync;
if (!file.exists()) {
boolean created = file.createNewFile();
if (!created) {
throw new IOException("Failed to create file: " + file);
}
}
// Open a read/write channel to the file
raf = new RandomAccessFile(file, "rw");
fileChannel = raf.getChannel();
if (fileChannel.size() == 0L) {
// Empty file, write header
ByteBuffer buf = ByteBuffer.allocate((int)HEADER_LENGTH);
buf.put(MAGIC_NUMBER);
buf.put(FILE_FORMAT_VERSION);
buf.rewind();
fileChannel.write(buf, 0L);
sync();
}
else {
// Verify file header
ByteBuffer buf = ByteBuffer.allocate((int)HEADER_LENGTH);
fileChannel.read(buf, 0L);
buf.rewind();
if (buf.remaining() < HEADER_LENGTH) {
throw new IOException("File too short to be a compatible data file");
}
byte[] magicNumber = new byte[MAGIC_NUMBER.length];
buf.get(magicNumber);
byte version = buf.get();
if (!Arrays.equals(MAGIC_NUMBER, magicNumber)) {
throw new IOException("File doesn't contain compatible data records");
}
if (version > FILE_FORMAT_VERSION) {
throw new IOException("Unable to read data file; it uses a newer file format");
}
else if (version != FILE_FORMAT_VERSION) {
throw new IOException("Unable to read data file; invalid file format version: " + version);
}
}
}
/*---------*
* Methods *
*---------*/
public File getFile() {
return file;
}
/**
* Stores the specified data and returns the byte-offset at which it has been
* stored.
*
* @param data
* The data to store, must not be <tt>null</tt>.
* @return The byte-offset in the file at which the data was stored.
*/
public long storeData(byte[] data)
throws IOException
{
assert data != null : "data must not be null";
long offset = fileChannel.size();
ByteBuffer buf = ByteBuffer.allocate(data.length + 4);
buf.putInt(data.length);
buf.put(data);
buf.rewind();
fileChannel.write(buf, offset);
return offset;
}
/**
* Gets the data that is stored at the specified offset.
*
* @param offset
* An offset in the data file, must be larger than 0.
* @return The data that was found on the specified offset.
* @exception IOException
* If an I/O error occurred.
*/
public byte[] getData(long offset)
throws IOException
{
assert offset > 0 : "offset must be larger than 0, is: " + offset;
// TODO: maybe get more data in one go is more efficient?
ByteBuffer buf = ByteBuffer.allocate(4);
fileChannel.read(buf, offset);
int dataLength = buf.getInt(0);
byte[] data = new byte[dataLength];
buf = ByteBuffer.wrap(data);
fileChannel.read(buf, offset + 4L);
return data;
}
/**
* Discards all stored data.
*
* @throws IOException
* If an I/O error occurred.
*/
public void clear()
throws IOException
{
fileChannel.truncate(HEADER_LENGTH);
}
/**
* Syncs any unstored data to the hash file.
*/
public void sync()
throws IOException
{
if (forceSync) {
fileChannel.force(false);
}
}
/**
* Closes the data file, releasing any file locks that it might have.
*
* @throws IOException
*/
public void close()
throws IOException
{
raf.close();
}
/**
* Gets an iterator that can be used to iterate over all stored data.
*
* @return a DataIterator.
*/
public DataIterator iterator() {
return new DataIterator();
}
/**
* An iterator that iterates over the data that is stored in a data file.
*/
public class DataIterator {
private long position = HEADER_LENGTH;
public boolean hasNext()
throws IOException
{
return position < fileChannel.size();
}
public byte[] next()
throws IOException
{
if (!hasNext()) {
throw new NoSuchElementException();
}
byte[] data = getData(position);
position += (4 + data.length);
return data;
}
}
}