001: /*
002: * Copyright Aduna (http://www.aduna-software.com/) (c) 1997-2007.
003: *
004: * Licensed under the Aduna BSD-style license.
005: */
006: package org.openrdf.sail.nativerdf.datastore;
007:
008: import java.io.File;
009: import java.io.IOException;
010: import java.io.RandomAccessFile;
011: import java.nio.ByteBuffer;
012: import java.nio.channels.FileChannel;
013: import java.util.Arrays;
014:
015: /**
016: * Class supplying access to an ID file. An ID file maps IDs (integers >= 1)
017: * to file pointers (long integers). There is a direct correlation between IDs
018: * and the position at which the file pointers are stored; the file pointer for
019: * ID X is stored at position 8*X.
020: *
021: * @author Arjohn Kampman
022: */
023: public class IDFile {
024:
025: /*-----------*
026: * Constants *
027: *-----------*/
028:
029: /**
030: * Magic number "Native ID File" to detect whether the file is actually an ID
031: * file. The first three bytes of the file should be equal to this magic
032: * number.
033: */
034: private static final byte[] MAGIC_NUMBER = new byte[] { 'n', 'i',
035: 'f' };
036:
037: /**
038: * File format version, stored as the fourth byte in ID files.
039: */
040: private static final byte FILE_FORMAT_VERSION = 1;
041:
042: /**
043: * The size of the file header in bytes. The file header contains the
044: * following data: magic number (3 bytes) file format version (1 byte) and 4
045: * dummy bytes to align data at 8-byte offsets.
046: */
047: private static final long HEADER_LENGTH = 8;
048:
049: private static final long ITEM_SIZE = 8L;
050:
051: /*-----------*
052: * Variables *
053: *-----------*/
054:
055: private File file;
056:
057: private RandomAccessFile raf;
058:
059: private FileChannel fileChannel;
060:
061: private boolean forceSync;
062:
063: /*--------------*
064: * Constructors *
065: *--------------*/
066:
067: public IDFile(File file) throws IOException {
068: this (file, false);
069: }
070:
071: public IDFile(File file, boolean forceSync) throws IOException {
072: this .file = file;
073: this .forceSync = forceSync;
074:
075: if (!file.exists()) {
076: boolean created = file.createNewFile();
077: if (!created) {
078: throw new IOException("Failed to create file: " + file);
079: }
080: }
081:
082: // Open a read/write channel to the file
083: raf = new RandomAccessFile(file, "rw");
084: fileChannel = raf.getChannel();
085:
086: if (fileChannel.size() == 0L) {
087: // Empty file, write header
088: ByteBuffer buf = ByteBuffer.allocate((int) HEADER_LENGTH);
089: buf.put(MAGIC_NUMBER);
090: buf.put(FILE_FORMAT_VERSION);
091: buf.put(new byte[] { 0, 0, 0, 0 });
092: buf.rewind();
093:
094: fileChannel.write(buf, 0L);
095:
096: sync();
097: } else {
098: // Verify file header
099: ByteBuffer buf = ByteBuffer.allocate((int) HEADER_LENGTH);
100: fileChannel.read(buf, 0L);
101: buf.rewind();
102:
103: if (buf.remaining() < HEADER_LENGTH) {
104: throw new IOException(
105: "File too short to be a compatible ID file");
106: }
107:
108: byte[] magicNumber = new byte[MAGIC_NUMBER.length];
109: buf.get(magicNumber);
110: byte version = buf.get();
111:
112: if (!Arrays.equals(MAGIC_NUMBER, magicNumber)) {
113: throw new IOException(
114: "File doesn't contain compatible ID records");
115: }
116:
117: if (version > FILE_FORMAT_VERSION) {
118: throw new IOException(
119: "Unable to read ID file; it uses a newer file format");
120: } else if (version != FILE_FORMAT_VERSION) {
121: throw new IOException(
122: "Unable to read ID file; invalid file format version: "
123: + version);
124: }
125: }
126:
127: }
128:
129: /*---------*
130: * Methods *
131: *---------*/
132:
133: public final File getFile() {
134: return file;
135: }
136:
137: /**
138: * Gets the largest ID that is stored in this ID file.
139: *
140: * @return The largest ID, or <tt>0</tt> if the file does not contain any
141: * data.
142: * @throws IOException
143: * If an I/O error occurs.
144: */
145: public int getMaxID() throws IOException {
146: return (int) (fileChannel.size() / ITEM_SIZE) - 1;
147: }
148:
149: /**
150: * Stores the offset of a new data entry, returning the ID under which is
151: * stored.
152: */
153: public int storeOffset(long offset) throws IOException {
154: int id = (int) (fileChannel.size() / ITEM_SIZE);
155: setOffset(id, offset);
156: return id;
157: }
158:
159: /**
160: * Sets or updates the stored offset for the specified ID.
161: *
162: * @param id
163: * The ID to set the offset for, must be larger than 0.
164: * @param offset
165: * The (new) offset for the specified ID.
166: */
167: public void setOffset(int id, long offset) throws IOException {
168: assert id > 0 : "id must be larger than 0, is: " + id;
169:
170: ByteBuffer buf = ByteBuffer.allocate(8);
171: buf.putLong(0, offset);
172: fileChannel.write(buf, ITEM_SIZE * id);
173: }
174:
175: /**
176: * Gets the offset of the data entry with the specified ID.
177: *
178: * @param id
179: * The ID to get the offset for, must be larger than 0.
180: * @return The offset for the ID.
181: */
182: public long getOffset(int id) throws IOException {
183: assert id > 0 : "id must be larger than 0, is: " + id;
184:
185: ByteBuffer buf = ByteBuffer.allocate(8);
186: fileChannel.read(buf, ITEM_SIZE * id);
187: return buf.getLong(0);
188: }
189:
190: /**
191: * Discards all stored data.
192: *
193: * @throws IOException
194: * If an I/O error occurred.
195: */
196: public void clear() throws IOException {
197: fileChannel.truncate(HEADER_LENGTH);
198: }
199:
200: /**
201: * Syncs any unstored data to the hash file.
202: */
203: public void sync() throws IOException {
204: if (forceSync) {
205: fileChannel.force(false);
206: }
207: }
208:
209: /**
210: * Closes the ID file, releasing any file locks that it might have.
211: *
212: * @throws IOException
213: */
214: public void close() throws IOException {
215: raf.close();
216: }
217: }
|