/*******************************************************************************
* Copyright 2010 Cees De Groot, Alex Boisvert, Jan Kotek
*
* 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.
******************************************************************************/
package de.mxro.thrd.jdbm2V22.btree;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import de.mxro.thrd.jdbm2V22.RecordListener;
import de.mxro.thrd.jdbm2V22.RecordManager;
import de.mxro.thrd.jdbm2V22.Serializer;
import de.mxro.thrd.jdbm2V22.helper.ComparableComparator;
import de.mxro.thrd.jdbm2V22.helper.JdbmBase;
import de.mxro.thrd.jdbm2V22.helper.Tuple;
import de.mxro.thrd.jdbm2V22.helper.TupleBrowser;
/**
* B+Tree persistent indexing data structure. B+Trees are optimized for
* block-based, random I/O storage because they store multiple keys on
* one tree node (called <code>BPage</code>). In addition, the leaf nodes
* directly contain (inline) the values associated with the keys, allowing a
* single (or sequential) disk read of all the values on the page.
* <p>
* B+Trees are n-airy, yeilding log(N) search cost. They are self-balancing,
* preventing search performance degradation when the size of the tree grows.
* <p>
* Keys and associated values must be <code>Serializable</code> objects. The
* user is responsible to supply a serializable <code>Comparator</code> object
* to be used for the ordering of entries, which are also called <code>Tuple</code>.
* The B+Tree allows traversing the keys in forward and reverse order using a
* TupleBrowser obtained from the browse() methods.
* <p>
* This implementation does not directly support duplicate keys, but it is
* possible to handle duplicates by inlining or referencing an object collection
* as a value.
* <p>
* There is no limit on key size or value size, but it is recommended to keep
* both as small as possible to reduce disk I/O. This is especially true for
* the key size, which impacts all non-leaf <code>BPage</code> objects.
*
* @author <a href="mailto:boisvert@intalio.com">Alex Boisvert</a>
* @version $Id: BTree.java,v 1.6 2005/06/25 23:12:31 doomdark Exp $
*/
public class BTree<K,V>
implements Externalizable, JdbmBase<K,V>
{
private static final long serialVersionUID = 8883213742777032628L;
private static final boolean DEBUG = false;
/**
* Default page size (number of entries per node)
*/
public static final int DEFAULT_SIZE = 32;
/**
* Page manager used to persist changes in BPages
*/
protected transient RecordManager _recman;
/**
* This BTree's record ID in the PageManager.
*/
private transient long _recid;
/**
* Comparator used to index entries.
*/
protected Comparator<K> _comparator;
/**
* Serializer used to serialize index keys (optional)
*/
protected Serializer<K> keySerializer;
/**
* Serializer used to serialize index values (optional)
*/
protected Serializer<V> valueSerializer;
public Serializer<K> getKeySerializer() {
return keySerializer;
}
public void setKeySerializer(Serializer<K> keySerializer) {
this.keySerializer = keySerializer;
}
public Serializer<V> getValueSerializer() {
return valueSerializer;
}
public void setValueSerializer(Serializer<V> valueSerializer) {
this.valueSerializer = valueSerializer;
}
/**
* Height of the B+Tree. This is the number of BPages you have to traverse
* to get to a leaf BPage, starting from the root.
*/
private int _height;
/**
* Recid of the root BPage
*/
private transient long _root;
/**
* Number of entries in each BPage.
*/
protected int _pageSize;
/**
* Total number of entries in the BTree
*/
protected volatile int _entries;
/**
* Serializer used for BPages of this tree
*/
private transient BPage<K,V> _bpageSerializer;
/**
* Listeners which are notified about changes in records
*/
protected List<RecordListener<K,V>> recordListeners = new CopyOnWriteArrayList<RecordListener<K, V>>();
protected ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* No-argument constructor used by serialization.
*/
public BTree()
{
// empty
}
/**
* Create a new persistent BTree, with 16 entries per node.
*
* @param recman Record manager used for persistence.
* @param comparator Comparator used to order index entries
*/
public static <K,V> BTree<K,V> createInstance( RecordManager recman,
Comparator<K> comparator)
throws IOException
{
return createInstance( recman, comparator, null, null, DEFAULT_SIZE );
}
/**
* Create a new persistent BTree, with 16 entries per node.
*
* @param recman Record manager used for persistence.
* @param comparator Comparator used to order index entries
*/
@SuppressWarnings("unchecked")
public static <K extends Comparable,V> BTree<K,V> createInstance( RecordManager recman)
throws IOException
{
BTree<K,V> ret = createInstance( recman, ComparableComparator.INSTANCE, null, null, DEFAULT_SIZE );
return ret;
}
/**
* Create a new persistent BTree, with 16 entries per node.
*
* @param recman Record manager used for persistence.
* @param keySerializer Serializer used to serialize index keys (optional)
* @param valueSerializer Serializer used to serialize index values (optional)
* @param comparator Comparator used to order index entries
*/
public static <K,V> BTree<K,V> createInstance( RecordManager recman,
Comparator<K> comparator,
Serializer<K> keySerializer,
Serializer<V> valueSerializer )
throws IOException
{
return createInstance( recman, comparator, keySerializer,
valueSerializer, DEFAULT_SIZE );
}
/**
* Create a new persistent BTree with the given number of entries per node.
*
* @param recman Record manager used for persistence.
* @param comparator Comparator used to order index entries
* @param keySerializer Serializer used to serialize index keys (optional)
* @param valueSerializer Serializer used to serialize index values (optional)
* @param pageSize Number of entries per page (must be even).
*/
public static <K,V> BTree<K,V> createInstance( RecordManager recman,
Comparator<K> comparator,
Serializer<K> keySerializer,
Serializer<V> valueSerializer,
int pageSize )
throws IOException
{
BTree<K,V> btree;
if ( recman == null ) {
throw new IllegalArgumentException( "Argument 'recman' is null" );
}
if ( comparator == null ) {
throw new IllegalArgumentException( "Argument 'comparator' is null" );
}
if ( ! ( comparator instanceof Serializable ) ) {
throw new IllegalArgumentException( "Argument 'comparator' must be serializable" );
}
// make sure there's an even number of entries per BPage
if ( ( pageSize & 1 ) != 0 ) {
throw new IllegalArgumentException( "Argument 'pageSize' must be even" );
}
btree = new BTree<K,V>();
btree._recman = recman;
btree._comparator = comparator;
btree.keySerializer = keySerializer;
btree.valueSerializer = valueSerializer;
btree._pageSize = pageSize;
btree._bpageSerializer = new BPage<K,V>();
btree._bpageSerializer._btree = btree;
btree._recid = recman.insert( btree );
return btree;
}
/**
* Load a persistent BTree.
*
* @param recman RecordManager used to store the persistent btree
* @param recid Record id of the BTree
*/
@SuppressWarnings("unchecked")
public static <K,V> BTree<K,V> load( RecordManager recman, long recid )
throws IOException
{
BTree<K,V> btree = (BTree<K,V>) recman.fetch( recid );
btree._recid = recid;
btree._recman = recman;
btree._bpageSerializer = new BPage<K,V>();
btree._bpageSerializer._btree = btree;
return btree;
}
/**
* Get the {@link ReadWriteLock} associated with this BTree.
* This should be used with browsing operations to ensure
* consistency.
* @return
*/
public ReadWriteLock getLock() {
return lock;
}
/**
* Insert an entry in the BTree.
* <p>
* The BTree cannot store duplicate entries. An existing entry can be
* replaced using the <code>replace</code> flag. If an entry with the
* same key already exists in the BTree, its value is returned.
*
* @param key Insert key
* @param value Insert value
* @param replace Set to true to replace an existing key-value pair.
* @return Existing value, if any.
*/
public V insert(final K key, final V value,
final boolean replace )
throws IOException
{
if ( key == null ) {
throw new IllegalArgumentException( "Argument 'key' is null" );
}
if ( value == null ) {
throw new IllegalArgumentException( "Argument 'value' is null" );
}
try {
lock.writeLock().lock();
BPage<K,V> rootPage = getRoot();
if ( rootPage == null ) {
// BTree is currently empty, create a new root BPage
if (DEBUG) {
System.out.println( "BTree.insert() new root BPage" );
}
rootPage = new BPage<K,V>( this, key, value );
_root = rootPage._recid;
_height = 1;
_entries = 1;
_recman.update( _recid, this );
//notifi listeners
for(RecordListener<K,V> l : recordListeners){
l.recordInserted(key, value);
}
return null;
} else {
BPage.InsertResult<K,V> insert = rootPage.insert( _height, key, value, replace );
boolean dirty = false;
if ( insert._overflow != null ) {
// current root page overflowed, we replace with a new root page
if ( DEBUG ) {
System.out.println( "BTree.insert() replace root BPage due to overflow" );
}
rootPage = new BPage<K,V>( this, rootPage, insert._overflow );
_root = rootPage._recid;
_height += 1;
dirty = true;
}
if ( insert._existing == null ) {
_entries++;
dirty = true;
}
if ( dirty ) {
_recman.update( _recid, this );
}
//notify listeners
for(RecordListener<K,V> l : recordListeners){
if(insert._existing==null)
l.recordInserted(key, value);
else
l.recordUpdated(key, insert._existing, value);
}
// insert might have returned an existing value
return insert._existing;
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Remove an entry with the given key from the BTree.
*
* @param key Removal key
* @return Value associated with the key, or null if no entry with given
* key existed in the BTree.
*/
public V remove( K key )
throws IOException
{
if ( key == null ) {
throw new IllegalArgumentException( "Argument 'key' is null" );
}
try {
lock.writeLock().lock();
BPage<K,V> rootPage = getRoot();
if ( rootPage == null ) {
return null;
}
boolean dirty = false;
BPage.RemoveResult<K,V> remove = rootPage.remove( _height, key );
if ( remove._underflow && rootPage.isEmpty() ) {
_height -= 1;
dirty = true;
_recman.delete(_root);
if ( _height == 0 ) {
_root = 0;
} else {
_root = rootPage.childBPage( _pageSize-1 )._recid;
}
}
if ( remove._value != null ) {
_entries--;
dirty = true;
}
if ( dirty ) {
_recman.update( _recid, this );
}
if(remove._value!=null)
for(RecordListener<K,V> l : recordListeners)
l.recordRemoved(key,remove._value);
return remove._value;
} finally {
lock.writeLock().unlock();
}
}
/**
* Find the value associated with the given key.
*
* @param key Lookup key.
* @return Value associated with the key, or null if not found.
*/
public V find( K key )
throws IOException
{
if ( key == null ) {
throw new IllegalArgumentException( "Argument 'key' is null" );
}
try {
lock.readLock().lock();
BPage<K,V> rootPage = getRoot();
if ( rootPage == null ) {
return null;
}
return rootPage.findValue( _height, key );
} finally {
lock.readLock().unlock();
}
// Tuple<K,V> tuple = new Tuple<K,V>( null, null );
// TupleBrowser<K,V> browser = rootPage.find( _height, key );
//
// if ( browser.getNext( tuple ) ) {
// // find returns the matching key or the next ordered key, so we must
// // check if we have an exact match
// if ( _comparator.compare( key, tuple.getKey() ) != 0 ) {
// return null;
// } else {
// return tuple.getValue();
// }
// } else {
// return null;
// }
}
/**
* Find the value associated with the given key, or the entry immediately
* following this key in the ordered BTree.
*
* @param key Lookup key.
* @return Value associated with the key, or a greater entry, or null if no
* greater entry was found.
*/
public Tuple<K,V> findGreaterOrEqual( K key )
throws IOException
{
Tuple<K,V> tuple;
TupleBrowser<K,V> browser;
if ( key == null ) {
// there can't be a key greater than or equal to "null"
// because null is considered an infinite key.
return null;
}
tuple = new Tuple<K,V>( null, null );
browser = browse( key );
if ( browser.getNext( tuple ) ) {
return tuple;
} else {
return null;
}
}
/**
* Get a browser initially positioned at the beginning of the BTree.
* <p><b>
* WARNING: �If you make structural modifications to the BTree during
* browsing, you will get inconsistent browing results.
* </b>
*
* @return Browser positionned at the beginning of the BTree.
*/
@SuppressWarnings("unchecked")
public TupleBrowser<K,V> browse()
throws IOException
{
try {
lock.readLock().lock();
BPage<K,V> rootPage = getRoot();
if ( rootPage == null ) {
return EmptyBrowser.INSTANCE;
}
TupleBrowser<K,V> browser = rootPage.findFirst();
return browser;
} finally {
lock.readLock().unlock();
}
}
/**
* Get a browser initially positioned just before the given key.
* <p><b>
* WARNING: �If you make structural modifications to the BTree during
* browsing, you will get inconsistent browing results.
* </b>
*
* @param key Key used to position the browser. If null, the browser
* will be positionned after the last entry of the BTree.
* (Null is considered to be an "infinite" key)
* @return Browser positionned just before the given key.
*/
@SuppressWarnings("unchecked")
public TupleBrowser<K,V> browse( K key )
throws IOException
{
try {
lock.readLock().lock();
BPage<K,V> rootPage = getRoot();
if ( rootPage == null ) {
return EmptyBrowser.INSTANCE;
}
TupleBrowser<K,V> browser = rootPage.find( _height, key );
return browser;
} finally {
lock.readLock().unlock();
}
}
/**
* Return the number of entries (size) of the BTree.
*/
public int size()
{
return _entries;
}
/**
* Return the persistent record identifier of the BTree.
*/
public long getRecid()
{
return _recid;
}
/**
* Return the root BPage, or null if it doesn't exist.
*/
BPage<K,V> getRoot()
throws IOException
{
if ( _root == 0 ) {
return null;
}
BPage<K,V> root = (BPage<K,V>) _recman.fetch( _root, _bpageSerializer );
if (root != null) {
root._recid = _root;
root._btree = this;
}
return root;
}
/**
* Implement Externalizable interface.
*/
@SuppressWarnings("unchecked")
public void readExternal( ObjectInput in )
throws IOException, ClassNotFoundException
{
_comparator = (Comparator<K>) in.readObject();
//serializer is not persistent from 2.0
// _keySerializer = (Serializer<K>) in.readObject();
// _valueSerializer = (Serializer<V>) in.readObject();
_height = in.readInt();
_root = in.readLong();
_pageSize = in.readInt();
_entries = in.readInt();
}
/**
* Implement Externalizable interface.
*/
public void writeExternal( ObjectOutput out )
throws IOException
{
out.writeObject( _comparator );
//serializer is not persistent from 2.0
// out.writeObject( _keySerializer );
// out.writeObject( _valueSerializer );
out.writeInt( _height );
out.writeLong( _root );
out.writeInt( _pageSize );
out.writeInt( _entries );
}
/*
public void assert() throws IOException {
BPage root = getRoot();
if ( root != null ) {
root.assertRecursive( _height );
}
}
*/
/*
public void dump() throws IOException {
BPage root = getRoot();
if ( root != null ) {
root.dumpRecursive( _height, 0 );
}
}
*/
/** PRIVATE INNER CLASS
* Browser returning no element.
*/
static class EmptyBrowser<K,V>
implements TupleBrowser<K,V> {
@SuppressWarnings("unchecked")
static TupleBrowser INSTANCE = new EmptyBrowser();
private EmptyBrowser(){}
public boolean getNext( Tuple<K,V> tuple )
{
return false;
}
public boolean getPrevious( Tuple<K,V> tuple )
{
return false;
}
}
public BTreeSortedMap<K,V> asMap(){
return new BTreeSortedMap<K, V>(this,false);
}
/**
* add RecordListener which is notified about record changes
* @param listener
*/
public void addRecordListener(RecordListener<K,V> listener){
recordListeners.add(listener);
}
/**
* remove RecordListener which is notified about record changes
* @param listener
*/
public void removeRecordListener(RecordListener<K,V> listener){
recordListeners.remove(listener);
}
public RecordManager getRecordManager() {
return _recman;
}
public Comparator<K> getComparator() {
return _comparator;
}
/**
* Deletes all BPages in this BTree, then deletes the tree from the record manager
*/
public void delete()
throws IOException
{
try {
lock.writeLock().lock();
BPage<K,V> rootPage = getRoot();
if (rootPage != null)
rootPage.delete();
_recman.delete(_recid);
} finally {
lock.writeLock().unlock();
}
}
/**
* Used for debugging and testing only. Populates the 'out' list with
* the recids of all child pages in the BTree.
* @param out
* @throws IOException
*/
void dumpChildPageRecIDs(List<Long> out) throws IOException{
BPage<K,V> root = getRoot();
if ( root != null ) {
out.add(root._recid);
root.dumpChildPageRecIDs( out, _height);
}
}
}