/*
* Copyright 2011 Future Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* PAGE_HEADER: flag(2), count(2), left(4), right(4), right-child(4), upper(4),
* first_freeblock (2), first_record_pos(2), number_of_fragmented_freebytes(2),
* +---------------+---------+---------------+-----------------+
* |PAGE_HEADER(26)|Slots(8n)| unalloc space | Records |
* +---------------+---------+---------------+-----------------+
*
* Slots and Records are managed in sorted order.
*
* Record:
* +---------------------+----------------------------------+-----+-------+
* | data length(varlen) | key length or key itself(varlen) | Key | Value |
* +---------------------+----------------------------------+-----+-------+
* |<-------------------------- Record Length --------------------------->|
*
* if (record.keyLength > page.size * page.maxPayloadFraction)
* *record has overflow page*
*
* Record which has overflow page:
* +---------------------+----------------------------------+-----+-------+-------------------------+
* | data length(varlen) | key length or key itself(varlen) | Key | Value | overflow page number(4) |
* +---------------------+----------------------------------+-----+-------+-------------------------+
* |<-------------------------- Record Length ----------------------------------------------------->|
*
* Record Length := varlen + varlen + data length + key length : key length <= page.size * page.maxPayloadFraction
* or varlen + varlen + data length + key length + 4 : key length > page.size * page.maxPayloadFraction
*
* Slot:
* +-----------+------------------+
* | Offset(2) | Record Length(2) |
* +-----------+------------------+
*
* free block header
* +-------------------+-------------------------------------------+
* | next block pos(2) | length of this block(2) (excludes header) |
* +-------------------+-------------------------------------------+
*
*/
package org.krakenapps.btree;
import java.nio.ByteBuffer;
public class Page {
// (flag, count), left, right, right-child, upper
// (first_freeblock, first_recore_pos), (number_of_fragmented_freebytes, )
public static final int PAGE_HEADER_SIZE = 26;
public static final int SLOT_SIZE = 4;
// record = record header + key + value
// record header: key length(2) + overflow(1) + non-unique(1)
//
@Deprecated
public static final int RECORD_HEADER_SIZE = 4;
private boolean dirty;
private int number;
private Schema schema;
private ByteBuffer bb;
private byte[] data;
public Page(Schema schema) {
this(0, schema, null);
}
public Page(int number, Schema schema, byte[] data) {
this.number = number;
this.schema = schema;
if (data != null)
this.data = data;
else
this.data = new byte[schema.getPageSize()];
this.bb = ByteBuffer.wrap(this.data);
}
public boolean isDirty() {
return dirty;
}
public void clearDirty() {
this.dirty = false;
}
public int getNumber() {
return number;
}
public Schema getSchema() {
return schema;
}
public byte[] getData() {
return data;
}
public boolean insert(RowKey key, RowEntry value) {
byte[] valueBytes = value.getBytes();
int keyLength = key.getBytes().length;
int valueLength = valueBytes.length;
int recordLength = getRecordHeaderSize(keyLength, valueLength) + keyLength + valueLength;
if (!checkFreeSpace(recordLength))
return false;
int before = findSlotBefore(key);
int slot = before + 1;
// calculate new offset
int offset = 0;
if (before >= 0) {
int pos = getSlotPosition(before);
int beforeOffset = bb.getShort(pos) & 0xFFFF;
int beforeLength = bb.getShort(pos + 2) & 0xFFFF;
offset = beforeOffset + beforeLength;
}
// move existing data
// order: last to first
// move record one by one
// record is already ordered by key.
for (int i = getRecordCount(); i > slot; i--) {
copyRecord(recordLength, i - 1, i);
}
// write slot metadata
int pos = getSlotPosition(slot);
bb.putShort(pos, (short) offset);
bb.putShort(pos + 2, (short) recordLength);
// write record
int initialRecordPos;
int recordPos = getPhysicalOffset(offset, recordLength);
initialRecordPos = recordPos;
bb.position(recordPos);
recordPos += encodeVarNumber(bb, recordPos, valueLength);
recordPos += encodeVarNumber(bb, recordPos, keyLength);
recordPos += encodeBytes(bb, recordPos, key.getBytes());
recordPos += encodeBytes(bb, recordPos, valueBytes);
assert initialRecordPos - recordPos != recordLength : "buffer overflow";
setRecordCount(getRecordCount() + 1);
return true;
}
public boolean delete(int slot) {
int recordCount = getRecordCount();
if (slot >= recordCount || slot < 0)
return false;
// if it is not last slot, move data
if (slot != recordCount - 1) {
int pos = getSlotPosition(slot);
int length = bb.getShort(pos + 2) & 0xFFFF;
for (int i = slot; i < recordCount - 1; i++) {
copyRecord(-length, i + 1, i);
}
}
setRecordCount(recordCount - 1);
return true;
}
private boolean checkFreeSpace(int recordLength) {
return getFreeSpace() >= (SLOT_SIZE + recordLength);
}
public int getFreeSpace() {
int count = getRecordCount();
if (count == 0)
return schema.getPageSize() - PAGE_HEADER_SIZE;
int pos = getSlotPosition(count - 1);
int offset = bb.getShort(pos) & 0xFFFF;
int length = bb.getShort(pos + 2) & 0xFFFF;
int phyOffset = getPhysicalOffset(offset, length);
return phyOffset - (PAGE_HEADER_SIZE + SLOT_SIZE * count);
}
private int getPhysicalOffset(int offset, int length) {
return schema.getPageSize() - offset - length;
}
private void copyRecord(int move, int from, int to) {
int fromSlotPos = getSlotPosition(from);
int offset = bb.getShort(fromSlotPos) & 0xFFFF;
int length = bb.getShort(fromSlotPos + 2) & 0xFFFF;
int newOffset = offset + move;
// move slot metadata
int toSlotPos = getSlotPosition(to);
bb.putShort(toSlotPos, (short) newOffset);
bb.putShort(toSlotPos + 2, (short) length);
// move slot data
int phyNewOffset = getPhysicalOffset(newOffset, length);
int phyOffset = getPhysicalOffset(offset, length);
int i = 0;
try {
for (; i < length; i++)
data[i + phyNewOffset] = data[i + phyOffset];
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* returns first matching key (used in index node search)
*/
public int findSlot(RowKey searchKey) {
int count = getRecordCount();
// Since there is no duplicated key in index, you can just return first
// matching key.
for (int slot = 0; slot < count; slot++)
if (getKey(slot).compareTo(searchKey) == 0)
return slot;
return -1;
}
public int findSlotBefore(RowKey searchKey) {
int slot = 0;
int count = getRecordCount();
while (slot < count && getKey(slot).compareTo(searchKey) < 0)
slot++;
return slot - 1;
}
public RowKey getKey(int slot) {
int recordCount = getRecordCount();
if (slot >= recordCount || slot < 0)
return null;
int pos = getSlotPosition(slot);
int offset = bb.getShort(pos) & 0xFFFF;
int length = bb.getShort(pos + 2) & 0xFFFF;
int phyOffset = getPhysicalOffset(offset, length);
int valueLength = getVarInt(bb, phyOffset);
int keyLength = getVarInt(bb, phyOffset + NumberEncoder.lengthOf(valueLength));
byte[] key = new byte[keyLength];
bb.position(phyOffset + NumberEncoder.lengthOf(keyLength) + NumberEncoder.lengthOf(valueLength));
bb.get(key);
return schema.getRowKeyFactory().newKey(key);
}
public RowEntry getValue(int slot) {
if (slot >= getRecordCount() || slot < 0)
return null;
int pos = getSlotPosition(slot);
int offset = bb.getShort(pos) & 0xFFFF;
int length = bb.getShort(pos + 2) & 0xFFFF;
int phyOffset = getPhysicalOffset(offset, length);
int valueLength = getVarInt(bb, phyOffset);
int keyLength = getVarInt(bb, phyOffset + NumberEncoder.lengthOf(valueLength));
if (valueLength < 0)
throw new IllegalStateException("corrupted value length: " + valueLength);
byte[] b = new byte[valueLength];
int valuePosition = phyOffset + NumberEncoder.lengthOf(valueLength) + NumberEncoder.lengthOf(keyLength)
+ keyLength;
bb.position(valuePosition);
bb.get(b);
return schema.getRowValueFactory().newValue(b);
}
private static int getVarInt(ByteBuffer bb, int phyOffset) {
return (int) NumberEncoder.decode(ByteBuffer.wrap(bb.array(), phyOffset, bb.limit() - phyOffset));
}
// private static long getVarLong(ByteBuffer bb, int phyOffset) {
// return NumberEncoder.decode(ByteBuffer.wrap(bb.array(), phyOffset,
// bb.limit() - phyOffset));
// }
//
// private static short getVarShort(ByteBuffer bb, int phyOffset) {
// return (short) NumberEncoder.decode(ByteBuffer.wrap(bb.array(),
// phyOffset, bb.limit() - phyOffset));
// }
private int getSlotPosition(int slot) {
// slot size = payload offset (2) + length (2)
return PAGE_HEADER_SIZE + slot * SLOT_SIZE;
}
//
// page header
//
public boolean leaf() {
return (bb.getShort(0) & PageType.LEAF) != 0;
}
public boolean intKey() {
return (bb.getShort(0) & PageType.INTKEY) != 0;
}
public boolean overflowPage() {
return (bb.getShort(0) & PageType.OVERFLOW) != 0;
}
public short getFlag() {
return bb.getShort(0);
}
public boolean getFlag(short type) {
return (bb.getShort(0) & type) != 0;
}
public void setFlag(short type, boolean flag) {
if (flag)
bb.putShort(0, (short) (getFlag() | type));
else
bb.putShort(0, (short) (getFlag() & ~type));
}
public void clearAllFlag() {
bb.putShort(0, (short) 0);
}
public void setFlag(short flag) {
bb.putShort(0, flag);
}
public int getRecordCount() {
return bb.getShort(2);
}
private void setRecordCount(int count) {
bb.putShort(2, (short) count);
dirty = true;
}
public int getLeftPage() {
return bb.getInt(4);
}
public void setLeftPage(int pageNumber) {
bb.putInt(4, pageNumber);
dirty = true;
}
public int getRightPage() {
return bb.getInt(8);
}
public void setRightPage(int pageNumber) {
bb.putInt(8, pageNumber);
dirty = true;
}
public int getRightChildPage() {
return bb.getInt(12);
}
public void setRightChildPage(int pageNumber) {
bb.putInt(12, pageNumber);
dirty = true;
}
public int getUpperPage() {
return bb.getInt(16);
}
public void setUpperPage(int pageNumber) {
bb.putInt(16, pageNumber);
dirty = true;
}
@Override
public String toString() {
return "Page " + getNumber();
}
private int encodeBytes(ByteBuffer bb, int recordPos, byte[] array) {
int encodedLen = array.length;
ByteBuffer.wrap(bb.array(), recordPos, encodedLen).put(array);
return encodedLen;
}
private int encodeVarNumber(ByteBuffer bb, int recordPos, int valueLength) {
int encodedLen = NumberEncoder.lengthOf(valueLength);
NumberEncoder.encode(ByteBuffer.wrap(bb.array(), recordPos, encodedLen), valueLength);
return encodedLen;
}
private int getRecordHeaderSize(int keyLength, int valueLength) {
return NumberEncoder.lengthOf(keyLength) + NumberEncoder.lengthOf(valueLength);
}
}