package org.basex.io.random;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
import org.basex.data.MetaData;
import org.basex.io.IO;
import org.basex.io.in.DataInput;
import org.basex.io.out.DataOutput;
import org.basex.util.Array;
import org.basex.util.BitArray;
import org.basex.util.Util;
/**
* This class stores the table on disk and reads it block-wise.
*
* @author BaseX Team 2005-12, BSD License
* @author Christian Gruen
* @author Tim Petrowsky
*/
public final class TableDiskAccess extends TableAccess {
/** Buffer manager. */
private final Buffers bm = new Buffers();
/** File storing all blocks. */
private final RandomAccessFile file;
/** Filename prefix. */
private final String pref;
/** FirstPre values (sorted ascending; length={@link #allBlocks}). */
private int[] fpres;
/** Index array storing BlockNumbers (length={@link #allBlocks}). */
private int[] pages;
/** Bitmap storing free (=0) and occupied (=1) pages. */
private final BitArray pagemap;
/** Pre value of the first entry in the current block. */
private int fpre = -1;
/** First pre value of the next block. */
private int npre = -1;
/** Number of blocks in the data file (including unused). */
private int allBlocks;
/** Number of entries in the index (used blocks). */
private int blocks;
/** Index of the current block number in the {@link #pages} array. */
private int index = -1;
/**
* Constructor.
* @param md meta data
* @param pf file prefix
* @throws IOException I/O exception
*/
public TableDiskAccess(final MetaData md, final String pf)
throws IOException {
super(md);
pref = pf;
// read meta and index data
final DataInput in = new DataInput(meta.dbfile(pf + 'i'));
allBlocks = in.readNum();
blocks = in.readNum();
fpres = in.readNums();
pages = in.readNums();
final int psize = in.readNum();
// check if the page map has been stored
if(psize == 0) {
// init the map with empty pages
pagemap = new BitArray(allBlocks);
for(final int p : pages) pagemap.set(p);
dirty = true;
} else {
pagemap = new BitArray(in.readLongs(psize), allBlocks);
}
in.close();
// initialize data file
file = new RandomAccessFile(meta.dbfile(pf).file(), "rw");
readIndex(0);
}
@Override
public synchronized void flush() throws IOException {
for(final Buffer b : bm.all()) if(b.dirty) writeBlock(b);
if(!dirty) return;
final DataOutput out = new DataOutput(meta.dbfile(pref + 'i'));
out.writeNum(allBlocks);
out.writeNum(blocks);
// due to legacy issues, allBlocks is written several times
out.writeNum(allBlocks);
for(int a = 0; a < allBlocks; a++) out.writeNum(fpres[a]);
out.writeNum(allBlocks);
for(int a = 0; a < allBlocks; a++) out.writeNum(pages[a]);
//out.writeNums(fpres);
//out.writeNums(pages);
out.writeLongs(pagemap.toArray());
out.close();
dirty = false;
}
@Override
public synchronized void close() throws IOException {
flush();
file.close();
}
@Override
public synchronized int read1(final int pre, final int off) {
final int o = off + cursor(pre);
final byte[] b = bm.current().data;
return b[o] & 0xFF;
}
@Override
public synchronized int read2(final int pre, final int off) {
final int o = off + cursor(pre);
final byte[] b = bm.current().data;
return ((b[o] & 0xFF) << 8) + (b[o + 1] & 0xFF);
}
@Override
public synchronized int read4(final int pre, final int off) {
final int o = off + cursor(pre);
final byte[] b = bm.current().data;
return ((b[o] & 0xFF) << 24) + ((b[o + 1] & 0xFF) << 16) +
((b[o + 2] & 0xFF) << 8) + (b[o + 3] & 0xFF);
}
@Override
public synchronized long read5(final int pre, final int off) {
final int o = off + cursor(pre);
final byte[] b = bm.current().data;
return ((long) (b[o] & 0xFF) << 32) + ((long) (b[o + 1] & 0xFF) << 24) +
((b[o + 2] & 0xFF) << 16) + ((b[o + 3] & 0xFF) << 8) + (b[o + 4] & 0xFF);
}
@Override
public void write1(final int pre, final int off, final int v) {
final int o = off + cursor(pre);
final Buffer bf = bm.current();
final byte[] b = bf.data;
b[o] = (byte) v;
bf.dirty = true;
}
@Override
public void write2(final int pre, final int off, final int v) {
final int o = off + cursor(pre);
final Buffer bf = bm.current();
final byte[] b = bf.data;
b[o] = (byte) (v >>> 8);
b[o + 1] = (byte) v;
bf.dirty = true;
}
@Override
public void write4(final int pre, final int off, final int v) {
final int o = off + cursor(pre);
final Buffer bf = bm.current();
final byte[] b = bf.data;
b[o] = (byte) (v >>> 24);
b[o + 1] = (byte) (v >>> 16);
b[o + 2] = (byte) (v >>> 8);
b[o + 3] = (byte) v;
bf.dirty = true;
}
@Override
public void write5(final int pre, final int off, final long v) {
final int o = off + cursor(pre);
final Buffer bf = bm.current();
final byte[] b = bf.data;
b[o] = (byte) (v >>> 32);
b[o + 1] = (byte) (v >>> 24);
b[o + 2] = (byte) (v >>> 16);
b[o + 3] = (byte) (v >>> 8);
b[o + 4] = (byte) v;
bf.dirty = true;
}
@Override
protected void copy(final byte[] entries, final int pre, final int last) {
for(int o = 0, i = pre; i < last; ++i, o += IO.NODESIZE) {
final int off = cursor(i);
final Buffer bf = bm.current();
System.arraycopy(entries, o, bf.data, off, IO.NODESIZE);
bf.dirty = true;
}
}
@Override
public void delete(final int pre, final int nr) {
if(nr == 0) return;
dirty = true;
// get first block
cursor(pre);
// some useful variables to make code more readable
int from = pre - fpre;
final int last = pre + nr;
// check if all entries are in current block: handle and return
if(last - 1 < npre) {
final Buffer bf = bm.current();
copy(bf.data, from + nr, bf.data, from, npre - last);
updatePre(nr);
// if whole block was deleted, remove it from the index
if(npre == fpre) {
// mark the block as empty
pagemap.clear(pages[index]);
Array.move(fpres, index + 1, -1, blocks - index - 1);
Array.move(pages, index + 1, -1, blocks - index - 1);
--blocks;
readIndex(index);
}
return;
}
// handle blocks whose entries are to be deleted entirely
// first count them
int unused = 0;
while(npre < last) {
if(from == 0) {
++unused;
// mark the blocks as empty; range clear cannot be used because the
// blocks may not be consecutive
pagemap.clear(pages[index]);
}
setIndex(index + 1);
from = 0;
}
// if the last block is empty, clear the corresponding bit
readBlock(pages[index]);
final Buffer bf = bm.current();
if(npre == last) {
pagemap.clear((int) bf.pos);
++unused;
if(index < blocks - 1) readIndex(index + 1);
else ++index;
} else {
// delete entries at beginning of current (last) block
copy(bf.data, last - fpre, bf.data, 0, npre - last);
}
// now remove them from the index
if(unused > 0) {
Array.move(fpres, index, -unused, blocks - index);
Array.move(pages, index, -unused, blocks - index);
blocks -= unused;
index -= unused;
}
// update index entry for this block
fpres[index] = pre;
fpre = pre;
updatePre(nr);
}
@Override
public void insert(final int pre, final byte[] entries) {
if(entries.length == 0) return;
dirty = true;
// go to the block and find the offset within the block where the new
// records will be inserted
final int split = cursor(pre - 1) + IO.NODESIZE;
// number of records to be inserted
final int nr = entries.length >>> IO.NODEPOWER;
// number of bytes occupied by old records in the current block
final int nold = npre - fpre << IO.NODEPOWER;
// number of bytes occupied by old records which will be moved at the end
final int moved = nold - split;
// special case: all entries fit in the current block
Buffer bf = bm.current();
if(nold + entries.length <= IO.BLOCKSIZE) {
System.arraycopy(bf.data, split, bf.data, split + entries.length, moved);
System.arraycopy(entries, 0, bf.data, split, entries.length);
bf.dirty = true;
// increment first pre-values of blocks after the last modified block
for(int i = index + 1; i < blocks; ++i) fpres[i] += nr;
// update cached variables (fpre is not changed)
npre += nr;
meta.size += nr;
return;
}
// append old entries at the end of the new entries
// [DP] Storage: the following can be optimized to avoid copying arrays
final byte[] all = new byte[entries.length + moved];
System.arraycopy(entries, 0, all, 0, entries.length);
System.arraycopy(bf.data, split, all, entries.length, moved);
// fill in the current block with new entries
// number of bytes which can fit in the first block
int n = bf.data.length - split;
if(n > 0) {
System.arraycopy(all, 0, bf.data, split, n);
bf.dirty = true;
}
int neededBlocks = (all.length - n) / IO.BLOCKSIZE;
// number of bytes which don't fill one block completely
final int remain = (all.length - n) % IO.BLOCKSIZE;
if(remain > 0) {
// check if the last entries can fit in the block after the current one
if(index + 1 < blocks) {
final int o = occSpace(index + 1) << IO.NODEPOWER;
if(remain <= IO.BLOCKSIZE - o) {
// copy the last records
readIndex(index + 1);
bf = bm.current();
System.arraycopy(bf.data, 0, bf.data, remain, o);
System.arraycopy(all, all.length - remain, bf.data, 0, remain);
bf.dirty = true;
// reduce the pre value, since it will be later incremented with nr
fpres[index] -= remain >>> IO.NODEPOWER;
// go back to the previous block
readIndex(index - 1);
} else {
// there is not enough space in the block - allocate a new one
++neededBlocks;
}
} else {
// this is the last block - allocate a new one
++neededBlocks;
}
}
// number of expected blocks:
// existing blocks + needed block - empty blocks
final int expBlocks = allBlocks + neededBlocks - (allBlocks - blocks);
if(expBlocks > fpres.length) {
// resize directory arrays if existing ones are too small
final int ns = Math.max(fpres.length << 1, expBlocks);
fpres = Arrays.copyOf(fpres, ns);
pages = Arrays.copyOf(pages, ns);
}
// make place for the blocks where the new entries will be written
Array.move(fpres, index + 1, neededBlocks, blocks - index - 1);
Array.move(pages, index + 1, neededBlocks, blocks - index - 1);
// write the all remaining entries
while(neededBlocks-- > 0) {
freeBlock();
n += write(all, n);
fpres[index] = fpres[index - 1] + IO.ENTRIES;
pages[index] = (int) bm.current().pos;
}
// increment first pre-values of blocks after the last modified block
for(int i = index + 1; i < blocks; ++i) fpres[i] += nr;
meta.size += nr;
// update cached variables
fpre = fpres[index];
npre = index + 1 < blocks && fpres[index + 1] < meta.size ?
fpres[index + 1] : meta.size;
}
// PRIVATE METHODS ==========================================================
/**
* Searches for the block containing the entry for that pre. then it
* reads the block and returns it's offset inside the block.
* @param pre pre of the entry to search for
* @return offset of the entry in currentBlock
*/
private int cursor(final int pre) {
int fp = fpre;
int np = npre;
if(pre < fp || pre >= np) {
final int last = blocks - 1;
int l = 0;
int h = last;
int m = index;
while(l <= h) {
if(pre < fp) h = m - 1;
else if(pre >= np) l = m + 1;
else break;
m = h + l >>> 1;
fp = fpres[m];
np = m == last ? meta.size : fpres[m + 1];
}
if(l > h) Util.notexpected("Data Access out of bounds [pre:" + pre +
", indexSize:" + blocks + ", access:" + l + " > " + h + ']');
readIndex(m);
}
return pre - fpre << IO.NODEPOWER;
}
/**
* Update the index pointers.
* @param i new index
*/
private void setIndex(final int i) {
index = i;
fpre = fpres[i];
npre = i + 1 >= blocks ? meta.size : fpres[i + 1];
}
/**
* Updates the index pointers and fetches the requested block.
* @param i index of the block to fetch
*/
private void readIndex(final int i) {
setIndex(i);
readBlock(pages[i]);
}
/**
* Reads a block from disk.
* @param b block to fetch
*/
private void readBlock(final int b) {
if(!bm.cursor(b)) return;
final Buffer bf = bm.current();
try {
if(bf.dirty) writeBlock(bf);
bf.pos = b;
if(b >= allBlocks) {
allBlocks = b + 1;
} else {
file.seek(bf.pos * IO.BLOCKSIZE);
file.readFully(bf.data);
}
} catch(final IOException ex) {
Util.stack(ex);
}
}
/**
* Moves the cursor to a free block (either new or existing empty one).
*/
private void freeBlock() {
final int b = pagemap.nextClearBit(0);
pagemap.set(b);
readBlock(b);
++blocks;
++index;
}
/**
* Writes the specified block to disk and resets the dirty flag.
* @param bf buffer to write
* @throws IOException I/O exception
*/
private void writeBlock(final Buffer bf) throws IOException {
file.seek(bf.pos * IO.BLOCKSIZE);
file.write(bf.data);
bf.dirty = false;
}
/**
* Updates the firstPre index entries.
* @param nr number of entries to move
*/
private void updatePre(final int nr) {
// update index entries for all following blocks and reduce counter
for(int i = index + 1; i < blocks; ++i) fpres[i] -= nr;
meta.size -= nr;
npre = index + 1 < blocks && fpres[index + 1] < meta.size ? fpres[index + 1]
: meta.size;
}
/**
* Convenience method for copying blocks.
* @param s source array
* @param sp source position
* @param d destination array
* @param dp destination position
* @param l source length
*/
private void copy(final byte[] s, final int sp, final byte[] d,
final int dp, final int l) {
System.arraycopy(s, sp << IO.NODEPOWER, d, dp << IO.NODEPOWER,
l << IO.NODEPOWER);
bm.current().dirty = true;
}
/**
* Fill the current buffer with bytes from the specified array from the
* specified offset.
* @param s source array
* @param o offset from the beginning of the array
* @return number of written bytes
*/
private int write(final byte[] s, final int o) {
final Buffer bf = bm.current();
final int len = Math.min(bf.data.length, s.length - o);
System.arraycopy(s, o, bf.data, 0, len);
bf.dirty = true;
return len;
}
/**
* Calculate the occupied space in a block.
* @param i index of the block
* @return occupied space in number of records
*/
private int occSpace(final int i) {
return (i + 1 < blocks ? fpres[i + 1] : meta.size) - fpres[i];
}
}