/*-
* Copyright (C) 2006-2009 Erik Larsson
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.catacombae.hfs;
import java.util.LinkedList;
import java.util.List;
import org.catacombae.hfs.types.hfscommon.CommonBTHeaderNode;
import org.catacombae.hfs.types.hfscommon.CommonBTHeaderRecord;
import org.catacombae.hfs.types.hfscommon.CommonBTIndexRecord;
import org.catacombae.hfs.types.hfscommon.CommonBTKey;
import org.catacombae.hfs.types.hfscommon.CommonBTKeyedNode;
import org.catacombae.hfs.types.hfscommon.CommonBTKeyedRecord;
import org.catacombae.hfs.types.hfscommon.CommonBTLeafRecord;
import org.catacombae.hfs.types.hfscommon.CommonBTNode;
import org.catacombae.hfs.types.hfscommon.CommonBTNodeDescriptor;
import org.catacombae.hfs.types.hfscommon.CommonBTNodeDescriptor.NodeType;
import org.catacombae.hfs.types.hfscommon.CommonHFSVolumeHeader;
import org.catacombae.io.Readable;
import org.catacombae.io.ReadableRandomAccessStream;
import org.catacombae.io.RuntimeIOException;
/**
* @author <a href="http://www.catacombae.org/" target="_top">Erik Larsson</a>
*/
public abstract class BTreeFile<K extends CommonBTKey<K>,
L extends CommonBTLeafRecord<K>>
{
final HFSVolume vol;
BTreeFile(HFSVolume vol) {
this.vol = vol;
}
abstract class BTreeFileSession {
final CommonHFSVolumeHeader header;
final CommonBTNodeDescriptor btnd;
final CommonBTHeaderRecord bthr;
final ReadableRandomAccessStream btreeStream;
public BTreeFileSession() {
this.header = vol.getVolumeHeader();
//header.print(System.err, " ");
this.btreeStream = getBTreeStream(header);
this.btreeStream.seek(0);
this.btnd = readNodeDescriptor(this.btreeStream);
//this.btnd.print(System.err, " ");
if(btnd.getNodeType() != NodeType.HEADER) {
throw new RuntimeIOException("Invalid node type for header " +
"node.");
}
this.bthr = readHeaderRecord(this.btreeStream);
//this.bthr.print(System.err, " ");
}
public final void close() {
this.btreeStream.close();
}
protected abstract ReadableRandomAccessStream getBTreeStream(
CommonHFSVolumeHeader header);
}
protected <R extends CommonBTKeyedRecord<K>> R findLEKey(
CommonBTKeyedNode<R> indexNode, K searchKey)
{
/*
* Algorithm:
* input: Key searchKey
* variables: Key greatestMatchingKey
* For each n : records
* If n.key <= searchKey && n.key > greatestMatchingKey
* greatestMatchingKey = n.key
*/
R largestMatchingRecord = null;
//System.err.println("findLEKey(): Entering loop...");
for(R record : indexNode.getBTKeyedRecords()) {
K recordKey = record.getKey();
//System.err.print("findLEKey(): Processing record");
//if(recordKey instanceof CommonHFSExtentKey)
// System.err.print(" with key " + getDebugString((CommonHFSExtentKey)recordKey));
//System.err.print("...");
if(recordKey.compareTo(searchKey) <= 0 &&
(largestMatchingRecord == null ||
recordKey.compareTo(largestMatchingRecord.getKey()) > 0)) {
largestMatchingRecord = record;
//System.err.print("match!");
}
//else
// System.err.print("no match.");
//System.err.println();
}
//System.err.println("findLEKey(): Returning...");
return largestMatchingRecord;
}
/**
* Find records with keys <code>k</code> in the range
* <code>minKeyInclusive</code> <= <code>k</code> <
* <code>maxKeyExclusive</code> that exist in <code>keyedNode</code>.<br>
*
* If no matching records are found, then the record with the largest key
* that is less than <code>minKeyInclusive</code> (if any such record
* exists) is returned in <code>result</code>. If no such record exists,
* nothing is added to <code>result</code>.
*
* @param <R> The type of the records that we operate on.
*
* @param keyedNode
* <b>(in)</b> The keyed node to search.
* @param minKeyInclusive
* <b>(in)</b> The smallest key in the range (inclusive).
* @param maxKeyExclusive
* <b>(in)</b> The largest key in the range (exclusive).
* @param strict
* <b>(in)</b> If <code>false</code>, then the record before the first
* match is always included in the result. This is appropriate when
* searching index nodes, but not for leaf nodes.
*
* @return
* A {@link java.util.List} of records.
*/
protected <R extends CommonBTKeyedRecord<K>> List<R> findLEKeys(
CommonBTKeyedNode<R> keyedNode, K minKeyInclusive,
K maxKeyExclusive, boolean strict)
{
final LinkedList<R> result = new LinkedList<R>();
findLEKeys(keyedNode, minKeyInclusive, maxKeyExclusive, strict, result);
return result;
}
/**
* Find records with keys <code>k</code> in the range
* <code>minKeyInclusive</code> <= <code>k</code> <
* <code>maxKeyExclusive</code>) that exist in <code>keyedNode</code>.<br>
*
* If no matching records are found, then the record with the largest key
* that is less than <code>minKeyInclusive</code> (if any such record
* exists) is returned in <code>result</code> and the function returns
* <code>false</code>. If no such record exists, nothing is added to
* <code>result</code> (and <code>false</code> is still returned).
*
* @param <R> The type of the records that we operate on.
*
* @param keyedNode
* <b>(in)</b> The keyed node to search.
* @param minKeyInclusive
* <b>(in)</b> The smallest key in the range (inclusive).
* @param maxKeyExclusive
* <b>(in)</b> The largest key in the range (exclusive).
* @param strict
* <b>(in)</b> If <code>false</code>, then the record before the first
* match is always included in the result. This is appropriate when
* searching index nodes, but not for leaf nodes.
* @param result
* <b>(out)</b> A {@link java.util.LinkedList} that will receive the
* matching keys.
*
* @return
* <code>true</code> if at least one key matching the specified
* conditions was found, and <code>false</code> otherwise.
*/
protected <R extends CommonBTKeyedRecord<K>> boolean findLEKeys(
CommonBTKeyedNode<R> keyedNode, K minKeyInclusive,
K maxKeyExclusive, boolean strict, LinkedList<R> result)
{
boolean found = false;
K largestLEKey = null;
R largestLERecord = null;
/* TODO: Iteration could be optimized to binary search since keys are
* (supposed to be) ordered. */
for(R record : keyedNode.getBTKeyedRecords()) {
K key = record.getKey();
if(key.compareTo(minKeyInclusive) < 0) {
if(largestLEKey == null ||
key.compareTo(largestLEKey) > 0)
{
largestLEKey = key;
largestLERecord = record;
}
}
else if(key.compareTo(maxKeyExclusive) < 0) {
if(result != null) {
result.addLast(record);
}
found = true;
}
}
if(largestLEKey != null && (!found || !strict)) {
if(result != null) {
result.addFirst(largestLERecord);
}
}
return found;
}
protected CommonBTHeaderNode createCommonBTHeaderNode(byte[] currentNodeData,
int offset, int nodeSize) {
return vol.createCommonBTHeaderNode(currentNodeData, offset, nodeSize);
}
protected abstract CommonBTKeyedNode<? extends CommonBTIndexRecord<K>>
createIndexNode(byte[] nodeData, int offset, int nodeSize);
protected abstract CommonBTKeyedNode<L> createLeafNode(byte[] nodeData,
int offset, int nodeSize);
protected CommonBTNodeDescriptor readNodeDescriptor(Readable rd) {
return vol.readNodeDescriptor(rd);
}
protected CommonBTHeaderRecord readHeaderRecord(Readable rd) {
return vol.readHeaderRecord(rd);
}
protected CommonBTNodeDescriptor createCommonBTNodeDescriptor(
byte[] currentNodeData, int offset) {
return vol.createCommonBTNodeDescriptor(currentNodeData, offset);
}
public HFSVolume getVolume() {
return vol;
}
protected abstract BTreeFileSession openSession();
/**
* Returns the root node of the B-tree file. If it does not exist
* <code>null</code> is returned. The B-tree file will have no meaningful
* content if there is no root node.
*
* @return the B-tree root node of the B-tree file.
*/
public CommonBTNode getRootNode() {
BTreeFileSession ses = openSession();
try {
long rootNode = ses.bthr.getRootNodeNumber();
if(rootNode == 0) {
// There is no index node, or other content. So the node we
// seek does not exist. Return null.
return null;
}
else if(rootNode < 0 || rootNode > Integer.MAX_VALUE * 2L) {
throw new RuntimeException("Internal error - rootNode out of " +
"range: " + rootNode);
}
else {
return getNode(rootNode);
}
} finally {
ses.close();
}
}
public long getRootNodeNumber() {
BTreeFileSession ses = openSession();
try {
long rootNodeNumber = ses.bthr.getRootNodeNumber();
return rootNodeNumber;
} finally {
ses.close();
}
}
/**
* Returns the requested node in the B-tree file. If the requested node is
* not a header, index or leaf node, <code>null</code> is returned because
* they are the only ones that are implemented at the moment.<br>
*
* @param nodeNumber the node number of the requested node.
* @return the requested node if it exists and has type header, index node
* or leaf node, or <code>null</code> otherwise.
*/
public CommonBTNode getNode(long nodeNumber) {
BTreeFileSession ses = openSession();
try {
final String METHOD = "getNode";
final int nodeSize = ses.bthr.getNodeSize();
byte[] nodeData = new byte[nodeSize];
try {
ses.btreeStream.seek(nodeNumber * nodeSize);
ses.btreeStream.readFully(nodeData);
} catch(RuntimeException e) {
System.err.println("RuntimeException in " + METHOD + ". " +
"Printing additional information:");
System.err.println(" nodeNumber=" + nodeNumber);
System.err.println(" nodeSize=" + nodeSize);
System.err.println(" init.btreeStream.length()=" +
ses.btreeStream.length());
System.err.println(" (currentNodeNumber * nodeSize)=" +
(nodeNumber * nodeSize));
throw e;
}
CommonBTNodeDescriptor nodeDescriptor =
createCommonBTNodeDescriptor(nodeData, 0);
if(nodeDescriptor.getNodeType() == NodeType.HEADER)
return createCommonBTHeaderNode(nodeData, 0, nodeSize);
else if(nodeDescriptor.getNodeType() == NodeType.INDEX)
return createIndexNode(nodeData, 0, nodeSize);
else if(nodeDescriptor.getNodeType() == NodeType.LEAF)
return createLeafNode(nodeData, 0, nodeSize);
else
return null;
} finally {
ses.close();
}
}
/**
* Get a record from the B* tree with the specified key.<br>
*
* If none is found, the method returns <code>null</code>.<br>
* Tis method should execute in <code>O(log n)</code> time, where
* <code>n</code> is the number of elements in the tree.
*
* @param searchKey the key of the record that we are looking for.
*
* @return the requested record, if any, or <code>null</code> if no such
* record was found.
*/
public L getRecord(K searchKey) {
BTreeFileSession ses = openSession();
try {
final int nodeSize = ses.bthr.getNodeSize();
long currentNodeOffset = ses.bthr.getRootNodeNumber() * nodeSize;
byte[] currentNodeData = new byte[nodeSize];
ses.btreeStream.seek(currentNodeOffset);
ses.btreeStream.readFully(currentNodeData);
CommonBTNodeDescriptor nodeDescriptor =
createCommonBTNodeDescriptor(currentNodeData, 0);
/* Search down through the layers of indices (O(log n) steps, where
* n is the size of the tree) */
while(nodeDescriptor.getNodeType() == NodeType.INDEX) {
CommonBTKeyedNode<? extends CommonBTIndexRecord<K>>
currentNode =
createIndexNode(currentNodeData, 0, nodeSize);
CommonBTIndexRecord<K> matchingRecord =
findLEKey(currentNode, searchKey);
if(matchingRecord == null) {
return null;
}
currentNodeOffset = matchingRecord.getIndex() * nodeSize;
ses.btreeStream.seek(currentNodeOffset);
ses.btreeStream.readFully(currentNodeData);
nodeDescriptor =
createCommonBTNodeDescriptor(currentNodeData, 0);
}
/* Leaf node reached. Find record. */
if(nodeDescriptor.getNodeType() == NodeType.LEAF) {
CommonBTKeyedNode<L> leaf =
createLeafNode(currentNodeData, 0, nodeSize);
for(L rec : leaf.getBTRecords()) {
if(rec.getKey().compareTo(searchKey) == 0) {
return rec;
}
}
return null;
}
else {
throw new RuntimeException("Expected leaf node. Found other " +
"kind: " + nodeDescriptor.getNodeType());
}
} finally {
ses.close();
}
}
}