You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by el...@apache.org on 2012/02/02 13:38:42 UTC

svn commit: r1239581 [8/9] - in /directory/apacheds/trunk/jdbm2: ./ src/ src/etc/ src/examples/ src/main/ src/main/java/ src/main/java/jdbm/ src/main/java/jdbm/btree/ src/main/java/jdbm/helper/ src/main/java/jdbm/htree/ src/main/java/jdbm/recman/ src/s...

Added: directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/RecordFile.java
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/RecordFile.java?rev=1239581&view=auto
==============================================================================
--- directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/RecordFile.java (added)
+++ directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/RecordFile.java Thu Feb  2 12:38:39 2012
@@ -0,0 +1,604 @@
+/**
+ * JDBM LICENSE v1.00
+ *
+ * Redistribution and use of this software and associated documentation
+ * ("Software"), with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain copyright
+ *    statements and notices.  Redistributions must also contain a
+ *    copy of this document.
+ *
+ * 2. Redistributions in binary form must reproduce the
+ *    above copyright notice, this list of conditions and the
+ *    following disclaimer in the documentation and/or other
+ *    materials provided with the distribution.
+ *
+ * 3. The name "JDBM" must not be used to endorse or promote
+ *    products derived from this Software without prior written
+ *    permission of Cees de Groot.  For written permission,
+ *    please contact cg@cdegroot.com.
+ *
+ * 4. Products derived from this Software may not be called "JDBM"
+ *    nor may "JDBM" appear in their names without prior written
+ *    permission of Cees de Groot.
+ *
+ * 5. Due credit should be given to the JDBM Project
+ *    (http://jdbm.sourceforge.net/).
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE JDBM PROJECT AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+ * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
+ * CEES DE GROOT OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Copyright 2000 (C) Cees de Groot. All Rights Reserved.
+ * Contributions are Copyright (C) 2000 by their associated contributors.
+ *
+ * $Id: RecordFile.java,v 1.6 2005/06/25 23:12:32 doomdark Exp $
+ */
+package jdbm.recman;
+
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+import org.apache.directory.server.i18n.I18n;
+
+
+/**
+ *  This class represents a random access file as a set of fixed size
+ *  records. Each record has a physical record number, and records are
+ *  cached in order to improve access.
+ * <p>
+ *  The set of dirty records on the in-use list constitutes a transaction.
+ *  Later on, we will send these records to some recovery thingy.
+ */
+public final class RecordFile 
+{
+    private TransactionManager transactionManager;
+
+    // state transitions: free -> inUse -> dirty -> inTxn -> free
+    // free is a cache, thus a FIFO. The rest are hashes.
+    /** The list of free pages */
+    private final LinkedList<BlockIo> free = new LinkedList<BlockIo>();
+    
+    /** The map of pages being currently used */
+    private final HashMap<Long,BlockIo> inUse = new HashMap<Long,BlockIo>();
+    
+    /** The map of dirty pages (page being modified) */
+    private final HashMap<Long,BlockIo> dirty = new HashMap<Long,BlockIo>();
+    
+    /** The map of page in a transaction */
+    private final HashMap<Long,BlockIo> inTxn = new HashMap<Long,BlockIo>();
+
+    /** A flag set if transactions is disabled. Default to false */
+    private boolean transactionsDisabled = false;
+
+    /** The length of a single block. */
+    public final static int BLOCK_SIZE = 4096;
+
+    /** The extension of a record file */
+    final static String EXTENSION = ".db";
+
+    /** A block of clean data to wipe clean pages. */
+    final static byte[] cleanData = new byte[BLOCK_SIZE];
+
+    /** The underlying file */
+    private RandomAccessFile file;
+    
+    /** The file name */
+    private final String fileName;
+
+    
+    /**
+     * Creates a new object on the indicated filename. The file is
+     * opened in read/write mode.
+     *
+     * @param fileName the name of the file to open or create, without
+     *        an extension.
+     * @throws IOException whenever the creation of the underlying
+     *         RandomAccessFile throws it.
+     */
+    RecordFile( String fileName ) throws IOException 
+    {
+        this.fileName = fileName;
+        file = new RandomAccessFile( fileName + EXTENSION, "rw" );
+    }
+
+
+    /**
+     * @return The TransactionManager if the transaction system is enabled.
+     * @throws IOException If we can't create a TransactionManager
+     */
+    TransactionManager getTxnMgr() throws IOException
+    {
+        if ( transactionsDisabled )
+        {
+            throw new IllegalStateException( "Transactions are disabled." );
+        }
+        
+        if ( transactionManager == null )
+        {
+            transactionManager = new TransactionManager( this );
+        }
+        
+        return transactionManager;
+    }
+
+
+    /**
+     * @return the file name.
+     */
+    String getFileName() 
+    {
+        return fileName;
+    }
+
+    
+    /**
+     * Disables transactions: doesn't sync and doesn't use the
+     * transaction manager.
+     */
+    void disableTransactions() 
+    {
+        transactionsDisabled = true;
+    }
+
+    
+    /**
+     * Gets a block from the file. The returned byte array is the in-memory 
+     * copy of the record, and thus can be written (and subsequently released 
+     * with a dirty flag in order to write the block back).
+     *
+     * @param blockId The record number to retrieve.
+     */
+     BlockIo get( long blockId ) throws IOException 
+     {
+         // try in transaction list, dirty list, free list
+         BlockIo blockIo = inTxn.get( blockId );
+         
+         if ( blockIo != null ) 
+         {
+             inTxn.remove( blockId );
+             inUse.put( blockId, blockIo );
+             
+             return blockIo;
+         }
+         
+         blockIo = dirty.get( blockId );
+         
+         if ( blockIo != null ) 
+         {
+             dirty.remove( blockId );
+             inUse.put( blockId, blockIo );
+             
+             return blockIo;
+         }
+         
+         for ( Iterator<BlockIo> iterator = free.iterator(); iterator.hasNext(); ) 
+         {
+             BlockIo cur = iterator.next();
+             
+             if ( cur.getBlockId() == blockId ) 
+             {
+                 blockIo = cur;
+                 iterator.remove();
+                 inUse.put( blockId, blockIo );
+                 
+                 return blockIo;
+             }
+         }
+
+         // sanity check: can't be on in use list
+         if ( inUse.get( blockId ) != null ) 
+         {
+             throw new Error( I18n.err( I18n.ERR_554, blockId ) );
+         }
+
+         // get a new node and read it from the file
+         blockIo = getNewBlockIo( blockId );
+         long offset = blockId * BLOCK_SIZE;
+         long fileLength = file.length();
+         
+         if ( ( fileLength > 0 ) && ( offset <= fileLength ) ) 
+         {
+             read( file, offset, blockIo.getData(), BLOCK_SIZE );
+         } 
+         
+         inUse.put( blockId, blockIo );
+         blockIo.setClean();
+         
+         return blockIo;
+     }
+
+
+    /**
+     * Releases a block.
+     *
+     * @param blockId The record number to release.
+     * @param isDirty If true, the block was modified since the get().
+     */
+    void release( long blockId, boolean isDirty ) throws IOException 
+    {
+        BlockIo blockIo = inUse.get( blockId );
+        
+        if ( blockIo == null )
+        {
+            throw new IOException( I18n.err( I18n.ERR_555, blockId ) );
+        }
+        
+        if ( ! blockIo.isDirty() && isDirty )
+        {
+            blockIo.setDirty();
+        }
+            
+        release( blockIo );
+    }
+
+    
+    /**
+     * Releases a block.
+     *
+     * @param block The block to release.
+     */
+    void release( BlockIo block ) 
+    {
+        inUse.remove( block.getBlockId() );
+        
+        if ( block.isDirty() ) 
+        {
+            // System.out.println( "Dirty: " + key + block );
+            dirty.put( block.getBlockId(), block );
+        } 
+        else 
+        {
+            if ( ! transactionsDisabled && block.isInTransaction() ) 
+            {
+                inTxn.put( block.getBlockId(), block );
+            } 
+            else 
+            {
+                free.add( block );
+            }
+        }
+    }
+    
+
+    /**
+     * Discards a block (will not write the block even if it's dirty)
+     *
+     * @param block The block to discard.
+     */
+    void discard( BlockIo block ) 
+    {
+        inUse.remove( block.getBlockId() );
+
+        // note: block not added to free list on purpose, because
+        //       it's considered invalid
+    }
+
+    
+    /**
+     * Commits the current transaction by flushing all dirty buffers to disk.
+     */
+    void commit() throws IOException 
+    {
+        // debugging...
+        if ( ! inUse.isEmpty() && inUse.size() > 1 ) 
+        {
+            showList( inUse.values().iterator() );
+            throw new Error( I18n.err( I18n.ERR_556, inUse.size() ) );
+        }
+
+        //  System.out.println("committing...");
+
+        if ( dirty.size() == 0 ) 
+        {
+            // if no dirty blocks, skip commit process
+            return;
+        }
+
+        
+        if ( ! transactionsDisabled ) 
+        {
+            getTxnMgr().start();
+        }
+
+        
+        for ( BlockIo blockIo : dirty.values() ) 
+        {
+            // System.out.println("node " + node + " map size now " + dirty.size());
+            if ( transactionsDisabled ) 
+            {
+                sync( blockIo );
+                blockIo.setClean();
+                free.add( blockIo );
+            }
+            else 
+            {
+                getTxnMgr().add( blockIo );
+                inTxn.put( blockIo.getBlockId(), blockIo );
+            }
+        }
+        
+        dirty.clear();
+
+        if ( ! transactionsDisabled ) 
+        {
+            getTxnMgr().commit();
+        }
+    }
+
+    
+    /**
+     * Rollback the current transaction by discarding all dirty buffers
+     */
+    void rollback() throws IOException 
+    {
+        // debugging...
+        if ( ! inUse.isEmpty() ) 
+        {
+            showList( inUse.values().iterator() );
+            throw new Error( I18n.err( I18n.ERR_557, inUse.size() ) );
+        }
+    
+        //  System.out.println("rollback...");
+        dirty.clear();
+
+        if ( ! transactionsDisabled ) 
+        {
+            getTxnMgr().synchronizeLogFromDisk();
+        }
+
+        if ( ! inTxn.isEmpty() ) 
+        {
+            showList( inTxn.values().iterator() );
+            throw new Error( I18n.err( I18n.ERR_558, inTxn.size() ) );
+        }
+    }
+
+    
+    /**
+     * Commits and closes file.
+     */
+    void close() throws IOException 
+    {
+        if ( ! dirty.isEmpty() ) 
+        {
+            commit();
+        }
+        
+        if( ! transactionsDisabled )
+        {
+            getTxnMgr().shutdown();
+        }
+
+        if ( ! inTxn.isEmpty() ) 
+        {
+            showList( inTxn.values().iterator() );
+            throw new Error( I18n.err( I18n.ERR_559 ) );
+        }
+
+        // these actually ain't that bad in a production release
+        if ( ! dirty.isEmpty() ) 
+        {
+            System.out.println( "ERROR: dirty blocks at close time" );
+            showList( dirty.values().iterator() );
+            throw new Error( I18n.err( I18n.ERR_560 ) );
+        }
+        
+        if ( ! inUse.isEmpty() ) 
+        {
+            System.out.println( "ERROR: inUse blocks at close time" );
+            showList( inUse.values().iterator() );
+            throw new Error( I18n.err( I18n.ERR_561 ) );
+        }
+
+        // debugging stuff to keep an eye on the free list
+        // System.out.println("Free list size:" + free.size());
+        file.close();
+        file = null;
+    }
+
+
+    /**
+     * Force closing the file and underlying transaction manager.
+     * Used for testing purposed only.
+     */
+    void forceClose() throws IOException 
+    {
+        if ( ! transactionsDisabled ) 
+        {
+            getTxnMgr().forceClose();
+        }
+        file.close();
+    }
+
+    
+    /**
+     * Prints contents of a list
+     */
+    private void showList( Iterator<BlockIo> i ) 
+    {
+        int cnt = 0;
+        while ( i.hasNext() ) 
+        {
+            System.out.println( "elem " + cnt + ": " + i.next() );
+            cnt++;
+        }
+    }
+
+
+    /**
+     * Returns a new BlockIo. The BlockIo is retrieved (and removed) from the 
+     * released list or created new.
+     */
+    private BlockIo getNewBlockIo( long blockId ) throws IOException 
+    {
+        BlockIo blockIo = null;
+
+        if ( ! free.isEmpty() ) 
+        {
+            blockIo = ( BlockIo ) free.removeFirst();
+            blockIo.setBlockId( blockId );
+        }
+        
+        if ( blockIo == null )
+        {
+            blockIo = new BlockIo( blockId, new byte[BLOCK_SIZE] );
+        }
+        
+        blockIo.setView( null );
+        
+        return blockIo;
+    }
+    
+
+    /**
+     * Synchronizes a BlockIo to disk. This is called by the transaction manager's
+     * synchronization code.
+     * 
+     * @param blockIo The blocIo to write on disk
+     * @exception IOException If we have a problem while trying to write the blockIo to disk
+     */
+    void sync( BlockIo blockIo ) throws IOException 
+    {
+        byte[] data = blockIo.getData();
+        
+        if ( data != null ) 
+        {
+            // Write the data to disk now.
+            long offset = blockIo.getBlockId() * BLOCK_SIZE;
+            file.seek( offset );
+            file.write( data );
+        }
+    }
+
+    
+    /**
+     * Releases a node from the transaction list, if it was sitting there.
+     *
+     * @param recycle true if block data can be reused
+     */
+    void releaseFromTransaction( BlockIo node, boolean recycle ) throws IOException 
+    {
+        if ( ( inTxn.remove( node.getBlockId() ) != null ) && recycle ) 
+        {
+            free.add( node );
+        }
+    }
+    
+
+    /**
+     * Synchronizes the file.
+     */
+    void sync() throws IOException 
+    {
+        file.getFD().sync();
+    }
+
+
+    /**
+     * Utility method: Read a block from a RandomAccessFile
+     */
+    private static void read( RandomAccessFile file, long offset, byte[] buffer, int nBytes ) throws IOException 
+    {
+        file.seek( offset );
+        int remaining = nBytes;
+        int pos = 0;
+        while ( remaining > 0 ) 
+        {
+            int read = file.read( buffer, pos, remaining );
+            if ( read == -1 ) 
+            {
+                System.arraycopy( cleanData, 0, buffer, pos, remaining );
+                break;
+            }
+            remaining -= read;
+            pos += read;
+        }
+    }
+    
+    
+    /**
+     * {@inheritDoc}
+     */
+    public String toString()
+    {
+        StringBuilder sb = new StringBuilder();
+        
+        sb.append( "RecordFile<" ).append( fileName ).append( ", " );
+        
+        // The file size
+        sb.append( "size : " );
+        
+        try
+        {
+            sb.append( file.length() ).append( "bytes" );
+        }
+        catch ( IOException ioe )
+        {
+            sb.append( "unknown" );
+        }
+        
+        // Transactions
+        if ( transactionsDisabled )
+        {
+            sb.append( "(noTx)" );
+        }
+        else
+        {
+            sb.append( "(Tx)" );
+        }
+        
+        // Dump the free blocks
+        sb.append( "\n    Free blockIo : " ).append( free.size() );
+                
+        for ( BlockIo blockIo : free )
+        {
+            sb.append( "\n         " );
+            sb.append( blockIo );
+        }
+        
+        // Dump the inUse blocks
+        sb.append( "\n    InUse blockIo : " ).append( inUse.size() );
+        
+        for ( BlockIo blockIo : inUse.values() )
+        {
+            sb.append( "\n         " );
+            sb.append( blockIo );
+        }
+        
+        // Dump the dirty blocks
+        sb.append( "\n    Dirty blockIo : " ).append( dirty.size() );
+        
+        for ( BlockIo blockIo : dirty.values() )
+        {
+            sb.append( "\n         " );
+            sb.append( blockIo );
+        }
+        
+        // Dump the inTxn blocks
+        sb.append( "\n    InTxn blockIo : " ).append( inTxn.size() );
+        
+        for ( BlockIo blockIo : inTxn.values() )
+        {
+            sb.append( "\n         " );
+            sb.append( blockIo );
+        }
+
+        
+        return sb.toString();
+    }
+}

Added: directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/RecordHeader.java
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/RecordHeader.java?rev=1239581&view=auto
==============================================================================
--- directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/RecordHeader.java (added)
+++ directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/RecordHeader.java Thu Feb  2 12:38:39 2012
@@ -0,0 +1,124 @@
+/**
+ * JDBM LICENSE v1.00
+ *
+ * Redistribution and use of this software and associated documentation
+ * ("Software"), with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain copyright
+ *    statements and notices.  Redistributions must also contain a
+ *    copy of this document.
+ *
+ * 2. Redistributions in binary form must reproduce the
+ *    above copyright notice, this list of conditions and the
+ *    following disclaimer in the documentation and/or other
+ *    materials provided with the distribution.
+ *
+ * 3. The name "JDBM" must not be used to endorse or promote
+ *    products derived from this Software without prior written
+ *    permission of Cees de Groot.  For written permission,
+ *    please contact cg@cdegroot.com.
+ *
+ * 4. Products derived from this Software may not be called "JDBM"
+ *    nor may "JDBM" appear in their names without prior written
+ *    permission of Cees de Groot. 
+ *
+ * 5. Due credit should be given to the JDBM Project
+ *    (http://jdbm.sourceforge.net/).
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE JDBM PROJECT AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+ * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
+ * CEES DE GROOT OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Copyright 2000 (C) Cees de Groot. All Rights Reserved.
+ * Contributions are Copyright (C) 2000 by their associated contributors.
+ *
+ * $Id: RecordHeader.java,v 1.1 2000/05/06 00:00:31 boisvert Exp $
+ */
+package jdbm.recman;
+
+
+import org.apache.directory.server.i18n.I18n;
+
+
+/**
+ * The data that comes at the start of a record of data. It stores both the 
+ * current size and the available size for the record - the latter can be 
+ * bigger than the former, which allows the record to grow without needing to 
+ * be moved and which allows the system to put small records in larger free 
+ * spots.
+ */
+class RecordHeader 
+{
+    // offsets
+    private static final short O_CURRENTSIZE = 0; // int currentSize
+    private static final short O_AVAILABLESIZE = Magic.SZ_INT; // int availableSize
+    static final int SIZE = O_AVAILABLESIZE + Magic.SZ_INT;
+    
+    // my block and the position within the block
+    private BlockIo block;
+    private short pos;
+
+    
+    /**
+     * Constructs a record header from the indicated data starting at the 
+     * indicated position.
+     */
+    RecordHeader( BlockIo block, short pos ) 
+    {
+        this.block = block;
+        this.pos = pos;
+        
+        if ( pos > ( RecordFile.BLOCK_SIZE - SIZE ) )
+        {
+            throw new Error( I18n.err( I18n.ERR_562, block.getBlockId(), pos ) );
+        }
+    }
+
+    
+    /** Returns the current size */
+    int getCurrentSize() 
+    {
+        return block.readInt( pos + O_CURRENTSIZE );
+    }
+    
+    
+    /** Sets the current size */
+    void setCurrentSize( int value ) 
+    {
+        block.writeInt( pos + O_CURRENTSIZE, value );
+    }
+    
+    
+    /** Returns the available size */
+    int getAvailableSize() 
+    {
+        return block.readInt( pos + O_AVAILABLESIZE );
+    }
+    
+    
+    /** Sets the available size */
+    void setAvailableSize( int value ) 
+    {
+        block.writeInt( pos + O_AVAILABLESIZE, value );
+    }
+
+    
+    // overrides java.lang.Object
+    public String toString() 
+    {
+        return "RH( " + block.getBlockId() + " : " + pos 
+            + ", avl = " + getAvailableSize()
+            + ", cur = " + getCurrentSize() 
+            + " )";
+    }
+}
\ No newline at end of file

Added: directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/SnapshotRecordManager.java
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/SnapshotRecordManager.java?rev=1239581&view=auto
==============================================================================
--- directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/SnapshotRecordManager.java (added)
+++ directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/SnapshotRecordManager.java Thu Feb  2 12:38:39 2012
@@ -0,0 +1,694 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 jdbm.recman;
+
+import java.io.IOException;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import jdbm.ActionRecordManager;
+import jdbm.RecordManager;
+import jdbm.helper.ActionContext;
+import jdbm.helper.ActionVersioning;
+import jdbm.helper.CacheEvictionException;
+import jdbm.helper.DefaultSerializer;
+import jdbm.helper.EntryIO;
+import jdbm.helper.LRUCache;
+import jdbm.helper.Serializer;
+
+import org.apache.directory.server.i18n.I18n;
+
+
+/**
+ * 
+ * TODO SnapshotRecordManager.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public class SnapshotRecordManager implements ActionRecordManager
+{
+    /** Wrapped RecordManager */
+    protected RecordManager recordManager;
+    
+    /** Per thread action context */
+    private static final ThreadLocal < ActionContext > actionContextVar = 
+         new ThreadLocal < ActionContext > () 
+         {
+             @Override 
+             protected ActionContext initialValue()
+             {
+                 return null;
+             }
+        };
+     
+    /** Used for keeping track of actions versions */
+    ActionVersioning versioning = new ActionVersioning();
+    
+    /** Versioned cache */
+    LRUCache<Long, Object> versionedCache;
+    
+    /** Passed to cache as IO callback */
+    RecordIO recordIO = new RecordIO();
+    
+    /** Lock used to serialize write actions and some management operatins */
+    Lock bigLock = new ReentrantLock();
+
+    /**
+     * Construct a SanshotRecordManager wrapping another RecordManager
+     *
+     * @param recordManager Wrapped RecordManager
+     */
+    public SnapshotRecordManager( RecordManager recordManager, int size)
+    {
+        if ( recordManager == null ) 
+        {
+            throw new IllegalArgumentException( I18n.err( I18n.ERR_517 ) );
+        }
+
+        this.recordManager = recordManager;
+        
+        versionedCache = new LRUCache<Long ,Object>(recordIO, size);
+    }
+    
+    
+    /**
+     * {@inheritDoc}
+     */     
+     public ActionContext beginAction( boolean readOnly , String whoStarted )
+     {
+         ActionContext actionContext = new ActionContext();
+         ActionVersioning.Version version;
+         
+         if ( readOnly )
+         {
+             version = versioning.beginReadAction();
+         }
+         else
+         {
+             bigLock.lock();
+             version = versioning.beginWriteAction();
+         }
+         
+         actionContext.beginAction( readOnly, version, whoStarted );
+         setCurrentActionContext( actionContext );
+         
+         return actionContext;
+     }
+     
+     /**
+      * {@inheritDoc}
+      */
+     public void setCurrentActionContext( ActionContext context )
+     {
+         ActionContext actionContext = actionContextVar.get();
+         
+         if ( actionContext != null )
+         {
+             throw new IllegalStateException( "Action Context Not Null: " + actionContext.getWhoStarted() );
+         }
+
+         actionContextVar.set( context );
+     }
+     
+     
+     /**
+      * {@inheritDoc}
+      */
+     public void unsetCurrentActionContext( ActionContext context )
+     {
+         ActionContext actionContext = actionContextVar.get();
+         
+         if ( actionContext != context )
+         {
+             throw new IllegalStateException( "Trying to end action context not set in the thread context variable" + context + 
+                     " " + actionContext );
+         }
+
+         actionContextVar.set( null );
+     }
+     
+     
+     /**
+      * {@inheritDoc}
+      */
+     public void endAction( ActionContext actionContext )
+     {
+         ActionVersioning.Version minVersion = null;
+         
+         if ( actionContext.isReadOnlyAction() )
+         {
+             ActionVersioning.Version version = actionContext.getVersion(); 
+             minVersion = versioning.endReadAction( version );
+             actionContext.endAction();
+         }
+         else if ( actionContext.isWriteAction() )
+         {
+             minVersion = versioning.endWriteAction();
+             actionContext.endAction();
+             bigLock.unlock();
+         }
+         else
+         {
+             throw new IllegalStateException( " Wrong action type " + actionContext );
+         }
+         
+         unsetCurrentActionContext( actionContext );
+         
+         if ( minVersion != null )
+         {
+             versionedCache.advanceMinReadVersion( minVersion.getVersion() );
+         }
+     }
+     
+     
+     /**
+      * {@inheritDoc}
+      */
+     public void abortAction( ActionContext actionContext )
+     {
+         ActionVersioning.Version minVersion = null;
+         
+         if ( actionContext.isReadOnlyAction() )
+         {
+             ActionVersioning.Version version = actionContext.getVersion(); 
+             minVersion = versioning.endReadAction( version );
+             actionContext.endAction();
+         }
+         else if ( actionContext.isWriteAction() )
+         {
+             /*
+              *  Do not let versioning know that write action is complete,
+              *  so that the readers wont see the effect of the aborted
+              *  txn. The sensible thing to do would be to have the underling
+              *  record manager expose a abort action interface. When that lacks.
+              *  the right thing for the upper layer to do would is to rollback whatever 
+              *  is part of what JDBM calls a txn.
+              */
+             
+             actionContext.endAction();
+             bigLock.unlock();
+         }
+         else
+         {
+             throw new IllegalStateException( "Wrong action context type " + actionContext );
+         }
+         
+         unsetCurrentActionContext( actionContext );
+         
+         if ( minVersion != null )
+         {
+             versionedCache.advanceMinReadVersion( minVersion.getVersion() );
+         }
+     }
+     
+         
+    /**
+     * Get the underlying Record Manager.
+     *
+     * @return underlying RecordManager
+     */
+    public RecordManager getRecordManager()
+    {
+        return recordManager;
+    }
+
+    
+    /**
+     * Inserts a new record using a custom serializer.
+     *
+     * @param obj the object for the new record.
+     * @return the rowid for the new record.
+     * @throws IOException when one of the underlying I/O operations fails.
+     */
+    public long insert( Object obj ) throws IOException
+    {
+        return insert( obj, DefaultSerializer.INSTANCE );
+    }
+        
+        
+    /**
+     * Inserts a new record using a custom serializer.
+     *
+     * @param obj the object for the new record.
+     * @param serializer a custom serializer
+     * @return the rowid for the new record.
+     * @throws IOException when one of the underlying I/O operations fails.
+     */
+    public long insert( Object obj, Serializer serializer ) throws IOException
+    {
+        checkIfClosed();
+        
+        ActionContext actionContext = actionContextVar.get();
+        boolean startedAction = false;
+        boolean abortedAction = false;
+        
+        if ( actionContext == null )
+        {
+            actionContext = beginAction( false, "insert missing action" );
+            startedAction = true;
+        }
+        
+        long recid = 0;
+        
+        try
+        {
+            recid = recordManager.insert( obj, serializer );
+            
+            versionedCache.put( Long.valueOf( recid ), obj, actionContext.getVersion().getVersion(),
+                serializer, false );
+        } 
+        catch ( IOException e )
+        {
+            if ( startedAction )
+            {
+                abortAction( actionContext );
+                abortedAction = true;
+            }
+            
+            throw e;
+        }
+        catch ( CacheEvictionException except ) 
+        {
+            if ( startedAction )
+            {
+                abortAction( actionContext );
+                abortedAction = true;
+            }
+            
+            throw new IOException( except.getLocalizedMessage() );
+        }       
+        finally
+        {
+            if ( startedAction && !abortedAction )
+            {
+                endAction( actionContext );
+            }
+        }
+        
+        return recid;
+    }
+
+
+    /**
+     * Deletes a record.
+     *
+     * @param recid the rowid for the record that should be deleted.
+     * @throws IOException when one of the underlying I/O operations fails.
+     */
+    public void delete( long recid ) throws IOException
+    {
+        checkIfClosed();
+        
+        ActionContext actionContext = actionContextVar.get();
+        boolean startedAction = false;
+        boolean abortedAction = false;
+        
+        if ( actionContext == null )
+        {
+            actionContext = beginAction( false, "delete missing action" );
+            startedAction = true;
+        }
+        
+        // Update the cache
+        try 
+        {
+            versionedCache.put( Long.valueOf( recid ), null, actionContext.getVersion().getVersion(),
+                null, false );
+        }
+        catch ( IOException e )
+        {
+            if ( startedAction )
+            {
+                abortAction( actionContext );
+                abortedAction = true;
+            }
+            
+            throw e;
+        }
+        catch ( CacheEvictionException except ) 
+        {
+            if ( startedAction )
+            {
+                abortAction( actionContext );
+                abortedAction = true;
+            }
+            
+            throw new IOException( except.getLocalizedMessage() );
+        }
+        finally
+        {
+            if ( startedAction && !abortedAction )
+            {
+                endAction( actionContext );
+            }
+        }
+    }
+
+
+    /**
+     * Updates a record using standard Java serialization.
+     *
+     * @param recid the recid for the record that is to be updated.
+     * @param obj the new object for the record.
+     * @throws IOException when one of the underlying I/O operations fails.
+     */
+    public void update( long recid, Object obj ) throws IOException
+    {
+        update( recid, obj, DefaultSerializer.INSTANCE );
+    }
+    
+
+    /**
+     * Updates a record using a custom serializer.
+     *
+     * @param recid the recid for the record that is to be updated.
+     * @param obj the new object for the record.
+     * @param serializer a custom serializer
+     * @throws IOException when one of the underlying I/O operations fails.
+     */
+    public void update( long recid, Object obj, Serializer serializer ) throws IOException
+    {
+        checkIfClosed();
+        ActionContext actionContext = actionContextVar.get();
+        boolean startedAction = false;
+        boolean abortedAction = false;
+        
+        if ( actionContext == null )
+        {
+            actionContext = beginAction( false, "update missing action" );
+            startedAction = true;
+        }
+
+        try 
+        {
+           versionedCache.put( Long.valueOf( recid ), obj, actionContext.getVersion().getVersion(),
+               serializer, recid < 0 );       
+        }
+        catch ( IOException e )
+        {
+            if ( startedAction )
+            {
+                abortAction( actionContext );
+                abortedAction = true;
+            }
+            
+            throw e;
+        }
+        catch ( CacheEvictionException except ) 
+        {
+            if ( startedAction )
+            {
+                abortAction( actionContext );
+                abortedAction = true;
+            }
+            
+            throw new IOException( except.getLocalizedMessage() );
+        }       
+        finally
+        {
+            if ( startedAction && !abortedAction )
+            {
+                endAction ( actionContext );
+            }
+        }
+    }
+
+
+    /**
+     * Fetches a record using standard Java serialization.
+     *
+     * @param recid the recid for the record that must be fetched.
+     * @return the object contained in the record.
+     * @throws IOException when one of the underlying I/O operations fails.
+     */
+    public Object fetch( long recid ) throws IOException
+    {
+        return fetch( recid, DefaultSerializer.INSTANCE );
+    }
+
+        
+    /**
+     * Fetches a record using a custom serializer.
+     *
+     * @param recid the recid for the record that must be fetched.
+     * @param serializer a custom serializer
+     * @return the object contained in the record.
+     * @throws IOException when one of the underlying I/O operations fails.
+     */
+    public Object fetch( long recid, Serializer serializer ) throws IOException
+    {
+        checkIfClosed();
+        Object obj;
+        ActionContext actionContext = actionContextVar.get();
+        
+        boolean startedAction = false;
+        boolean abortedAction = false;
+        
+        if ( actionContext == null )
+        {
+            actionContext = beginAction( false, "fetch missing action" );
+            startedAction = true;
+        }
+        
+        try 
+        {
+           obj = versionedCache.get( Long.valueOf( recid ), actionContext.getVersion().getVersion(),
+               serializer, recid < 0 );
+        } 
+        catch ( IOException e )
+        {
+            if ( startedAction )
+            {
+                abortAction( actionContext );
+                abortedAction = true;
+            }
+            
+            throw e;
+        }
+        finally
+        {
+            if ( startedAction && !abortedAction )
+            {
+                endAction( actionContext );
+            }
+        }
+        
+        return obj;
+    }
+
+
+    /**
+     * Closes the record manager.
+     *
+     * @throws IOException when one of the underlying I/O operations fails.
+     */
+    public void close() throws IOException
+    {
+        checkIfClosed();
+
+        // Maybe quiesce all actions ..( not really required)
+        recordManager.close();
+        recordManager = null;
+        versionedCache = null;
+        versioning = null;
+    }
+
+
+    /**
+     * Returns the number of slots available for "root" rowids. These slots
+     * can be used to store special rowids, like rowids that point to
+     * other rowids. Root rowids are useful for bootstrapping access to
+     * a set of data.
+     */
+    public int getRootCount()
+    {
+        checkIfClosed();
+
+        return recordManager.getRootCount();
+    }
+
+
+    /**
+     * Returns the indicated root rowid.
+     *
+     * @see #getRootCount
+     */
+    public long getRoot( int id ) throws IOException
+    {
+        bigLock.lock();
+        
+        try
+        {
+            checkIfClosed();
+            return recordManager.getRoot( id );
+        }
+        finally
+        {
+            bigLock.unlock();
+        }
+    }
+
+
+    /**
+     * Sets the indicated root rowid.
+     *
+     * @see #getRootCount
+     */
+    public void setRoot( int id, long rowid ) throws IOException
+    {
+        bigLock.lock();
+        
+        try
+        {
+            checkIfClosed();
+
+            recordManager.setRoot( id, rowid );
+        }
+        finally
+        {
+            bigLock.unlock();
+        }
+    }
+
+
+    /**
+     * Commit (make persistent) all changes since beginning of transaction.
+     */
+    public void commit() throws IOException
+    {
+        bigLock.lock();
+        
+        try
+        {
+            checkIfClosed();
+        
+            recordManager.commit();
+        }
+        finally
+        {
+            bigLock.unlock();
+        }
+    }
+
+
+    /**
+     * Rollback (cancel) all changes since beginning of transaction.
+     */
+    public void rollback() throws IOException
+    {
+      // TODO handle this by quiecesing all actions and throwing away the cache contents
+    }
+
+
+    /**
+     * Obtain the record id of a named object. Returns 0 if named object
+     * doesn't exist.
+     */
+    public long getNamedObject( String name ) throws IOException
+    {
+        bigLock.lock();
+        
+        try
+        {
+            checkIfClosed();
+
+            return recordManager.getNamedObject( name );
+        }
+        finally
+        {
+            bigLock.unlock();
+        }
+    }
+
+
+    /**
+     * Set the record id of a named object.
+     */
+    public void setNamedObject( String name, long recid ) throws IOException
+    {
+        bigLock.lock();
+        
+        try
+        {
+            checkIfClosed();
+
+            recordManager.setNamedObject( name, recid );
+        }
+        finally
+        {
+            bigLock.unlock();
+        }
+    }
+    
+    @Override
+    public String toString()
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.append( "SnapshotRecordManager: " );
+        sb.append( "(lruCache:" ).append( versionedCache );
+        sb.append( ")\n" );
+        
+        return sb.toString();
+    }
+
+
+    /**
+     * Check if RecordManager has been closed.  If so, throw an IllegalStateException
+     */
+    private void checkIfClosed() throws IllegalStateException
+    {
+        if ( recordManager == null ) 
+        {
+            throw new IllegalStateException( I18n.err( I18n.ERR_538 ) );
+        }
+    }
+   
+    
+    private class RecordIO implements EntryIO<Long, Object>
+    {
+        public Object read( Long key, Serializer serializer) throws IOException
+        {
+            // Meta objects are kept in memory only
+            if ( key < 0 )
+            {
+                return null;
+            }
+            
+            return recordManager.fetch( key.longValue(), serializer );
+        }
+        
+        public void write( Long key, Object value, Serializer serializer ) throws IOException
+        {
+            if ( key < 0 )
+            {
+                return;
+            }
+            
+            if ( value != null )
+            {
+                recordManager.update( key.longValue(), value , serializer );
+            }
+            else
+            {
+                recordManager.delete( key.longValue() );
+            }
+        }
+    }
+}

Added: directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/TransactionManager.java
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/TransactionManager.java?rev=1239581&view=auto
==============================================================================
--- directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/TransactionManager.java (added)
+++ directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/TransactionManager.java Thu Feb  2 12:38:39 2012
@@ -0,0 +1,411 @@
+/**
+ * JDBM LICENSE v1.00
+ *
+ * Redistribution and use of this software and associated documentation
+ * ("Software"), with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain copyright
+ *    statements and notices.  Redistributions must also contain a
+ *    copy of this document.
+ *
+ * 2. Redistributions in binary form must reproduce the
+ *    above copyright notice, this list of conditions and the
+ *    following disclaimer in the documentation and/or other
+ *    materials provided with the distribution.
+ *
+ * 3. The name "JDBM" must not be used to endorse or promote
+ *    products derived from this Software without prior written
+ *    permission of Cees de Groot.  For written permission,
+ *    please contact cg@cdegroot.com.
+ *
+ * 4. Products derived from this Software may not be called "JDBM"
+ *    nor may "JDBM" appear in their names without prior written
+ *    permission of Cees de Groot.
+ *
+ * 5. Due credit should be given to the JDBM Project
+ *    (http://jdbm.sourceforge.net/).
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE JDBM PROJECT AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+ * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
+ * CEES DE GROOT OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Copyright 2000 (C) Cees de Groot. All Rights Reserved.
+ * Contributions are Copyright (C) 2000 by their associated contributors.
+ *
+ * $Id: TransactionManager.java,v 1.7 2005/06/25 23:12:32 doomdark Exp $
+ */
+
+package jdbm.recman;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.directory.server.i18n.I18n;
+
+/**
+ *  This class manages the transaction log that belongs to every
+ *  {@link RecordFile}. The transaction log is either clean, or
+ *  in progress. In the latter case, the transaction manager
+ *  takes care of a roll forward.
+ *<p>
+ *  Implementation note: this is a proof-of-concept implementation
+ *  which hasn't been optimized for speed. For instance, all sorts
+ *  of streams are created for every transaction.
+ */
+// TODO: Handle the case where we are recovering lg9 and lg0, were we
+// should start with lg9 instead of lg0!
+
+public final class TransactionManager {
+    private RecordFile owner;
+
+    // streams for transaction log.
+    private FileOutputStream fos;
+    private ObjectOutputStream oos;
+
+    /** 
+     * By default, we keep 10 transactions in the log file before
+     * synchronizing it with the main database file.
+     */
+    static final int DEFAULT_TXNS_IN_LOG = 10;
+
+    /** 
+     * Maximum number of transactions before the log file is
+     * synchronized with the main database file.
+     */
+    private int _maxTxns = DEFAULT_TXNS_IN_LOG;
+
+    /**
+     * In-core copy of transactions. We could read everything back from
+     * the log file, but the RecordFile needs to keep the dirty blocks in
+     * core anyway, so we might as well point to them and spare us a lot
+     * of hassle.
+     */
+    private ArrayList[] txns = new ArrayList[DEFAULT_TXNS_IN_LOG];
+    private int curTxn = -1;
+
+    /** Extension of a log file. */
+    static final String extension = ".lg";
+
+    /**
+     *  Instantiates a transaction manager instance. If recovery
+     *  needs to be performed, it is done.
+     *
+     *  @param owner the RecordFile instance that owns this transaction mgr.
+     */
+    TransactionManager(RecordFile owner) throws IOException {
+        this.owner = owner;
+        recover();
+        open();
+    }
+
+    
+    /**
+     * Synchronize log file data with the main database file.
+     * <p>
+     * After this call, the main database file is guaranteed to be 
+     * consistent and guaranteed to be the only file needed for 
+     * backup purposes.
+     */
+    public void synchronizeLog()
+        throws IOException
+    {
+        synchronizeLogFromMemory();
+    }
+
+    
+    /**
+     * Set the maximum number of transactions to record in
+     * the log (and keep in memory) before the log is
+     * synchronized with the main database file.
+     * <p>
+     * This method must be called while there are no
+     * pending transactions in the log.
+     */
+    public void setMaximumTransactionsInLog( int maxTxns )
+        throws IOException
+    {
+        if ( maxTxns <= 0 ) {
+            throw new IllegalArgumentException( I18n.err( I18n.ERR_563 ) );
+        }
+        if ( curTxn != -1 ) {
+            throw new IllegalStateException( I18n.err( I18n.ERR_564 ) );
+        }
+        _maxTxns = maxTxns;
+        txns = new ArrayList[ maxTxns ];
+    }
+
+    
+    /** Builds logfile name  */
+    private String makeLogName() {
+        return owner.getFileName() + extension;
+    }
+
+
+    /** Synchs in-core transactions to data file and opens a fresh log */
+    private void synchronizeLogFromMemory() throws IOException {
+        close();
+
+        TreeSet blockList = new TreeSet( new BlockIoComparator() );
+
+        for (int i = 0; i < _maxTxns; i++) {
+            if (txns[i] == null)
+                continue;
+            // Add each block to the blockList, replacing the old copy of this
+            // block if necessary, thus avoiding writing the same block twice
+            for (Iterator k = txns[i].iterator(); k.hasNext(); ) {
+                BlockIo block = (BlockIo)k.next();
+                if ( blockList.contains( block ) ) {
+                    block.decrementTransactionCount();
+                }
+                else {
+                    blockList.add( block );
+                }
+            }
+
+            txns[i] = null;
+        }
+        // Write the blocks from the blockList to disk
+        synchronizeBlocks(blockList.iterator(), true);
+
+        owner.sync();
+        open();
+    }
+
+
+    /** Opens the log file */
+    private void open() throws IOException {
+        fos = new FileOutputStream(makeLogName());
+        oos = new ObjectOutputStream(fos);
+        oos.writeShort(Magic.LOGFILE_HEADER);
+        oos.flush();
+        curTxn = -1;
+    }
+
+    /** Startup recovery on all files */
+    private void recover() throws IOException {
+        String logName = makeLogName();
+        File logFile = new File(logName);
+        if (!logFile.exists())
+            return;
+        if (logFile.length() == 0) {
+            logFile.delete();
+            return;
+        }
+
+        FileInputStream fis = new FileInputStream(logFile);
+        ObjectInputStream ois = new ObjectInputStream(fis);
+
+        try {
+            if (ois.readShort() != Magic.LOGFILE_HEADER) {
+                ois.close();
+                throw new Error( I18n.err( I18n.ERR_565 ) );
+            }
+        } catch (IOException e) {
+            // corrupted/empty logfile
+            ois.close();
+            logFile.delete();
+            return;
+        }
+
+        while (true) {
+            ArrayList blocks = null;
+            try {
+                blocks = (ArrayList) ois.readObject();
+            } catch (ClassNotFoundException e) {
+                ois.close();
+                throw new Error( I18n.err( I18n.ERR_566, e ) );
+            } catch (IOException e) {
+                // corrupted logfile, ignore rest of transactions
+                break;
+            }
+            synchronizeBlocks(blocks.iterator(), false);
+
+            // ObjectInputStream must match exactly each
+            // ObjectOutputStream created during writes
+            try {
+                ois = new ObjectInputStream(fis);
+            } catch (IOException e) {
+                // corrupted logfile, ignore rest of transactions
+                break;
+            }
+        }
+        owner.sync();
+        ois.close();
+        logFile.delete();
+    }
+
+    /** Synchronizes the indicated blocks with the owner. */
+    private void synchronizeBlocks(Iterator blockIterator, boolean fromCore)
+    throws IOException {
+        // write block vector elements to the data file.
+        while ( blockIterator.hasNext() ) {
+            BlockIo cur = (BlockIo)blockIterator.next();
+            owner.sync(cur);
+            if (fromCore) {
+                cur.decrementTransactionCount();
+                if (!cur.isInTransaction()) {
+                    owner.releaseFromTransaction(cur, true);
+                }
+            }
+        }
+    }
+
+
+    /** Set clean flag on the blocks. */
+    private void setClean(ArrayList blocks)
+    throws IOException {
+        for (Iterator k = blocks.iterator(); k.hasNext(); ) {
+            BlockIo cur = (BlockIo) k.next();
+            cur.setClean();
+        }
+    }
+
+    /** Discards the indicated blocks and notify the owner. */
+    private void discardBlocks(ArrayList blocks)
+    throws IOException {
+        for (Iterator k = blocks.iterator(); k.hasNext(); ) {
+            BlockIo cur = (BlockIo) k.next();
+            cur.decrementTransactionCount();
+            if (!cur.isInTransaction()) {
+                owner.releaseFromTransaction(cur, false);
+            }
+        }
+    }
+
+    /**
+     *  Starts a transaction. This can block if all slots have been filled
+     *  with full transactions, waiting for the synchronization thread to
+     *  clean out slots.
+     */
+    void start() throws IOException {
+        curTxn++;
+        if (curTxn == _maxTxns) {
+            synchronizeLogFromMemory();
+            curTxn = 0;
+        }
+        txns[curTxn] = new ArrayList();
+    }
+
+    /**
+     *  Indicates the block is part of the transaction.
+     */
+    void add(BlockIo block) throws IOException {
+        block.incrementTransactionCount();
+        txns[curTxn].add(block);
+    }
+
+    /**
+     *  Commits the transaction to the log file.
+     */
+    void commit() throws IOException {
+        oos.writeObject(txns[curTxn]);
+        sync();
+
+        // set clean flag to indicate blocks have been written to log
+        setClean(txns[curTxn]);
+
+        // reset ObjectOutputStream in order to store
+        // newer states of BlockIo
+        oos = new ObjectOutputStream(fos);
+        oos.reset();
+    }
+
+    /** Flushes and syncs */
+    private void sync() throws IOException {
+        oos.flush();
+        fos.flush();
+        fos.getFD().sync();
+    }
+
+    /**
+     *  Shutdowns the transaction manager. Resynchronizes outstanding
+     *  logs.
+     */
+    void shutdown() throws IOException {
+        synchronizeLogFromMemory();
+        close();
+    }
+
+    /**
+     *  Closes open files.
+     */
+    private void close() throws IOException {
+        sync();
+        oos.close();
+        fos.close();
+        oos = null;
+        fos = null;
+    }
+
+    /**
+     * Force closing the file without synchronizing pending transaction data.
+     * Used for testing purposes only.
+     */
+    void forceClose() throws IOException {
+        oos.close();
+        fos.close();
+        oos = null;
+        fos = null;
+    }
+
+    /**
+     * Use the disk-based transaction log to synchronize the data file.
+     * Outstanding memory logs are discarded because they are believed
+     * to be inconsistent.
+     */
+    void synchronizeLogFromDisk() throws IOException {
+        close();
+
+        for ( int i=0; i < _maxTxns; i++ ) {
+            if (txns[i] == null)
+                continue;
+            discardBlocks(txns[i]);
+            txns[i] = null;
+        }
+
+        recover();
+        open();
+    }
+
+
+    /** INNER CLASS.
+     *  Comparator class for use by the tree set used to store the blocks
+     *  to write for this transaction.  The BlockIo objects are ordered by
+     *  their blockIds.
+     */
+    public static class BlockIoComparator
+        implements Comparator
+    {
+
+        public int compare( Object o1, Object o2 ) {
+            BlockIo block1 = (BlockIo)o1;
+            BlockIo block2 = (BlockIo)o2;
+            int result = 0;
+            if ( block1.getBlockId() == block2.getBlockId() ) {
+                result = 0;
+            }
+            else if ( block1.getBlockId() < block2.getBlockId() ) {
+                result = -1;
+            }
+            else {
+                result = 1;
+            }
+            return result;
+        }
+
+        public boolean equals(Object obj) {
+            return super.equals(obj);
+        }
+    } // class BlockIOComparator
+
+}

Added: directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/TranslationPage.java
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/TranslationPage.java?rev=1239581&view=auto
==============================================================================
--- directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/TranslationPage.java (added)
+++ directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/TranslationPage.java Thu Feb  2 12:38:39 2012
@@ -0,0 +1,136 @@
+/**
+ * JDBM LICENSE v1.00
+ *
+ * Redistribution and use of this software and associated documentation
+ * ("Software"), with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain copyright
+ *    statements and notices.  Redistributions must also contain a
+ *    copy of this document.
+ *
+ * 2. Redistributions in binary form must reproduce the
+ *    above copyright notice, this list of conditions and the
+ *    following disclaimer in the documentation and/or other
+ *    materials provided with the distribution.
+ *
+ * 3. The name "JDBM" must not be used to endorse or promote
+ *    products derived from this Software without prior written
+ *    permission of Cees de Groot.  For written permission,
+ *    please contact cg@cdegroot.com.
+ *
+ * 4. Products derived from this Software may not be called "JDBM"
+ *    nor may "JDBM" appear in their names without prior written
+ *    permission of Cees de Groot. 
+ *
+ * 5. Due credit should be given to the JDBM Project
+ *    (http://jdbm.sourceforge.net/).
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE JDBM PROJECT AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+ * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
+ * CEES DE GROOT OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Copyright 2000 (C) Cees de Groot. All Rights Reserved.
+ * Contributions are Copyright (C) 2000 by their associated contributors.
+ *
+ * $Id: TranslationPage.java,v 1.1 2000/05/06 00:00:31 boisvert Exp $
+ */
+package jdbm.recman;
+
+
+
+/**
+ * Class describing a page that holds translations from physical rowids
+ * to logical rowids. In fact, the page just holds physical rowids - the
+ * page's block is the block for the logical rowid, the offset serve
+ * as offset for the rowids.
+ */
+final class TranslationPage extends PageHeader 
+{
+    /** Offset of the PageHeader */
+    static final short O_TRANS = PageHeader.SIZE; // short count
+    
+    /** Number of PhysicalRowId in this page */
+    static final short ELEMS_PER_PAGE = ( RecordFile.BLOCK_SIZE - O_TRANS ) / PhysicalRowId.SIZE;
+    
+    /** The table of PhysicalRowId */
+    final PhysicalRowId[] slots = new PhysicalRowId[ELEMS_PER_PAGE];
+
+    
+    /**
+     * Constructs a data page view from the indicated block.
+     */
+    TranslationPage( BlockIo blockIo ) 
+    {
+        super( blockIo );
+    }
+    
+
+    /**
+     * Factory method to create or return a data page for the indicated block.
+     */
+    static TranslationPage getTranslationPageView( BlockIo blockIo ) 
+    {
+        BlockView view = blockIo.getView();
+        
+        if ( ( view != null ) && view instanceof TranslationPage )
+        {
+            return ( TranslationPage ) view;
+        }
+        else
+        {
+            return new TranslationPage( blockIo );
+        }
+    }
+    
+
+    /** Returns the value of the indicated rowid on the page */
+    PhysicalRowId get( short offset ) 
+    {
+        int slot = ( offset - O_TRANS ) / PhysicalRowId.SIZE;
+        
+        if ( slots[slot] == null )
+        {
+            slots[slot] = new PhysicalRowId( blockIo, offset );
+        }
+        
+        return slots[slot];
+    }
+    
+    
+    /**
+     * {@inheritDoc}
+     */
+    public String toString() 
+    {
+        StringBuilder sb = new StringBuilder();
+        
+        sb.append( "TranslationPage ( " );
+        
+        // The blockIO
+        sb.append( super.toString() ).append( ", " );
+        
+        // Dump the Physical row id
+        for ( int i = 0; i < ELEMS_PER_PAGE; i++ )
+        {
+            if ( slots[i] != null )
+            {
+                sb.append( ", [" ).append( i ).append( "]=<" ).
+                append( slots[i].getBlock() ).append( ", " ).
+                append( slots[i].getOffset() ).append( ">" );
+            }
+        }
+        
+        sb.append( ")" );
+        return sb.toString();
+    }
+}

Added: directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/package.html
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/package.html?rev=1239581&view=auto
==============================================================================
--- directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/package.html (added)
+++ directory/apacheds/trunk/jdbm2/src/main/java/jdbm/recman/package.html Thu Feb  2 12:38:39 2012
@@ -0,0 +1,12 @@
+<!-- $Id: package.html,v 1.1 2001/05/19 16:01:33 boisvert Exp $ -->
+<html>
+  <body>
+    <p>Core classes for managing persistent objects and processing transactions.</p>
+
+    <dl>
+      <dt><b>Version: </b></dt><dd>$Revision: 1.1 $ $Date: 2001/05/19 16:01:33 $</dd>
+      <dt><b>Author: </b></dt><dd><a href="mailto:boisvert@intalio.com">Alex Boisvert</a></dd>
+    </dl>
+
+  </body>
+</html>

Added: directory/apacheds/trunk/jdbm2/src/site/site.xml
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/jdbm2/src/site/site.xml?rev=1239581&view=auto
==============================================================================
--- directory/apacheds/trunk/jdbm2/src/site/site.xml (added)
+++ directory/apacheds/trunk/jdbm2/src/site/site.xml Thu Feb  2 12:38:39 2012
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You 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.
+-->
+<!--
+  @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+-->
+<project name="${project.name}">
+  <body>
+    <menu ref="parent" />
+    <menu ref="reports" />
+  </body>
+</project>
\ No newline at end of file

Added: directory/apacheds/trunk/jdbm2/src/test/java/jdbm/btree/TestBTree.java
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/jdbm2/src/test/java/jdbm/btree/TestBTree.java?rev=1239581&view=auto
==============================================================================
--- directory/apacheds/trunk/jdbm2/src/test/java/jdbm/btree/TestBTree.java (added)
+++ directory/apacheds/trunk/jdbm2/src/test/java/jdbm/btree/TestBTree.java Thu Feb  2 12:38:39 2012
@@ -0,0 +1,685 @@
+/**
+ * JDBM LICENSE v1.00
+ *
+ * Redistribution and use of this software and associated documentation
+ * ("Software"), with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain copyright
+ *    statements and notices.  Redistributions must also contain a
+ *    copy of this document.
+ *
+ * 2. Redistributions in binary form must reproduce the
+ *    above copyright notice, this list of conditions and the
+ *    following disclaimer in the documentation and/or other
+ *    materials provided with the distribution.
+ *
+ * 3. The name "JDBM" must not be used to endorse or promote
+ *    products derived from this Software without prior written
+ *    permission of Cees de Groot.  For written permission,
+ *    please contact cg@cdegroot.com.
+ *
+ * 4. Products derived from this Software may not be called "JDBM"
+ *    nor may "JDBM" appear in their names without prior written
+ *    permission of Cees de Groot.
+ *
+ * 5. Due credit should be given to the JDBM Project
+ *    (http://jdbm.sourceforge.net/).
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE JDBM PROJECT AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+ * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
+ * CEES DE GROOT OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Copyright 2000 (C) Cees de Groot. All Rights Reserved.
+ * Contributions are Copyright (C) 2000 by their associated contributors.
+ *
+ */
+
+package jdbm.btree;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+import jdbm.RecordManager;
+import jdbm.RecordManagerFactory;
+import jdbm.helper.ByteArrayComparator;
+import jdbm.helper.StringComparator;
+import jdbm.helper.Tuple;
+import jdbm.helper.TupleBrowser;
+
+import com.mycila.junit.concurrent.Concurrency;
+import com.mycila.junit.concurrent.ConcurrentJunitRunner;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+
+
+/**
+ *  This class contains all Unit tests for {@link BTree}.
+ *
+ *  @author <a href="mailto:boisvert@exoffice.com">Alex Boisvert</a>
+ */
+@RunWith(ConcurrentJunitRunner.class)
+@Concurrency()
+public class TestBTree
+{
+    @Rule
+    public TemporaryFolder folder = new TemporaryFolder();
+
+    static final boolean DEBUG = false;
+
+    // the number of threads to be started in the synchronization test
+    static final int THREAD_NUMBER = 5;
+
+    // the size of the content of the maps for the synchronization
+    // test. Beware that THREAD_NUMBER * THREAD_CONTENT_COUNT < Integer.MAX_VALUE.
+    static final int THREAD_CONTENT_SIZE = 150;
+
+    // for how long should the threads run.
+    static final int THREAD_RUNTIME = 10 * 1000;
+
+
+    private String getTemporaryFile( String name ) throws IOException
+    {
+        String file = folder.newFile( name ).getAbsolutePath();
+        return file;
+    }
+
+
+    //----------------------------------------------------------------------
+    /**
+     *  Basic tests
+     */
+    @Test
+    public void testBasics() throws IOException
+    {
+        RecordManager recman;
+        BTree<byte[], byte[]> tree;
+        byte[] test0 = "test0".getBytes();
+        byte[] test1 = "test1".getBytes();
+        byte[] test2 = "test2".getBytes();
+        byte[] test3 = "test3".getBytes();
+        byte[] value1 = "value1".getBytes();
+        byte[] value2 = "value2".getBytes();
+
+        recman = RecordManagerFactory.createRecordManager( getTemporaryFile( "testBasics" ) );
+        tree = new BTree<byte[], byte[]>( recman, new ByteArrayComparator() );
+
+        tree.insert( test1, value1, false );
+        tree.insert( test2, value2, false );
+        byte[] result = tree.find( test0 );
+
+        assertNull( result );
+
+        result = tree.find( test1 );
+
+        assertNotNull( result );
+        assertEquals( 0, ByteArrayComparator.compareByteArray( result, value1 ) );
+
+        result = tree.find( test2 );
+
+        assertNotNull( result );
+        assertEquals( 0, ByteArrayComparator.compareByteArray( result, value2 ) );
+
+        result = tree.find( test3 );
+        assertNull( result );
+
+        recman.close();
+    }
+
+
+    /**
+     *  Basic tests, just use the simple test possibilities of junit (cdaller)
+     */
+    @Test
+    public void testBasics2() throws IOException
+    {
+        RecordManager recman;
+        BTree<byte[], byte[]> tree;
+        byte[] test0 = "test0".getBytes();
+        byte[] test1 = "test1".getBytes();
+        byte[] test2 = "test2".getBytes();
+        byte[] test3 = "test3".getBytes();
+        byte[] value1 = "value1".getBytes();
+        byte[] value2 = "value2".getBytes();
+
+        recman = RecordManagerFactory.createRecordManager( getTemporaryFile( "testBasics2" ) );
+        tree = new BTree<byte[], byte[]>( recman, new ByteArrayComparator() );
+
+        tree.insert( test1, value1, false );
+        tree.insert( test2, value2, false );
+
+        assertEquals( null, tree.find( test0 ) );
+        assertEquals( 0, ByteArrayComparator.compareByteArray( value1, ( byte[] ) tree.find( test1 ) ) );
+        assertEquals( 0, ByteArrayComparator.compareByteArray( value2, ( byte[] ) tree.find( test2 ) ) );
+        assertEquals( null, ( byte[] ) tree.find( test3 ) );
+
+        recman.close();
+    }
+
+
+    /**
+     *  Test what happens after the recmanager has been closed but the
+     *  btree is accessed. WHAT SHOULD HAPPEN???????????
+     * (cdaller)
+     */
+    @Test
+    public void testClose() throws IOException
+    {
+        RecordManager recman;
+        BTree<byte[], byte[]> tree;
+        byte[] test0 = "test0".getBytes();
+        byte[] test1 = "test1".getBytes();
+        byte[] test2 = "test2".getBytes();
+        byte[] test3 = "test3".getBytes();
+        byte[] value1 = "value1".getBytes();
+        byte[] value2 = "value2".getBytes();
+
+        recman = RecordManagerFactory.createRecordManager( getTemporaryFile( "testClose" ) );
+        tree = new BTree<byte[], byte[]>( recman, new ByteArrayComparator() );
+
+        tree.insert( test1, value1, false );
+        tree.insert( test2, value2, false );
+
+        assertEquals( null, tree.find( test0 ) );
+        assertEquals( 0, ByteArrayComparator.compareByteArray( value1, ( byte[] ) tree.find( test1 ) ) );
+        assertEquals( 0, ByteArrayComparator.compareByteArray( value2, ( byte[] ) tree.find( test2 ) ) );
+        assertEquals( null, ( byte[] ) tree.find( test3 ) );
+
+        recman.close();
+
+        try
+        {
+            tree.browse();
+            fail( "Should throw an IllegalStateException on access on not opened btree" );
+        }
+        catch ( IllegalStateException except )
+        {
+            // expected
+        }
+
+        try
+        {
+            tree.find( test0 );
+            fail( "Should throw an IllegalStateException on access on not opened btree" );
+        }
+        catch ( IllegalStateException except )
+        {
+            // expected
+        }
+
+        try
+        {
+            tree.findGreaterOrEqual( test0 );
+            fail( "Should throw an IllegalStateException on access on not opened btree" );
+        }
+        catch ( IllegalStateException except )
+        {
+            // expected
+        }
+
+        try
+        {
+            tree.insert( test2, value2, false );
+            fail( "Should throw an IllegalStateException on access on not opened btree" );
+        }
+        catch ( IllegalStateException except )
+        {
+            // expected
+        }
+
+        try
+        {
+            tree.remove( test0 );
+            fail( "Should throw an IllegalStateException on access on not opened btree" );
+        }
+        catch ( IllegalStateException except )
+        {
+            // expected
+        }
+    }
+
+
+    /**
+     *  Test to insert different objects into one btree. (cdaller)
+     */
+    @Test
+    public void testInsert() throws IOException
+    {
+        RecordManager recman;
+        BTree<String, Object> tree;
+
+        recman = RecordManagerFactory.createRecordManager( getTemporaryFile( "testInsert" ) );
+        tree = new BTree<String, Object>( recman, new StringComparator() );
+
+        // insert different objects and retrieve them
+        tree.insert( "test1", "value1", false );
+        tree.insert( "test2", "value2", false );
+        tree.insert( "one", Integer.valueOf( 1 ), false );
+        tree.insert( "two", Long.valueOf( 2 ), false );
+        tree.insert( "myownobject", new ObjectStore( Integer.valueOf( 234 ) ), false );
+
+        assertEquals( "value2", tree.find( "test2" ) );
+        assertEquals( "value1", tree.find( "test1" ) );
+        assertEquals( Integer.valueOf( 1 ), tree.find( "one" ) );
+        assertEquals( Long.valueOf( 2 ), tree.find( "two" ) );
+
+        // what happens here? must not be replaced, does it return anything?
+        // probably yes!
+        assertEquals( "value1", tree.insert( "test1", "value11", false ) );
+        assertEquals( "value1", tree.find( "test1" ) ); // still the old value?
+        assertEquals( "value1", tree.insert( "test1", "value11", true ) );
+        assertEquals( "value11", tree.find( "test1" ) ); // now the new value!
+
+        ObjectStore expectedObj = new ObjectStore( Integer.valueOf( 234 ) );
+        ObjectStore btreeObj = ( ObjectStore ) tree.find( "myownobject" );
+
+        assertEquals( expectedObj, btreeObj );
+
+        recman.close();
+    }
+
+
+    /**
+     *  Test to insert many objects into one btree
+     */
+    @Test
+    public void testInsertMany() throws IOException
+    {
+        BTree<String, String> tree;
+
+        RecordManager recordManager = RecordManagerFactory.createRecordManager( getTemporaryFile( "testInsertMany" ) );
+        tree = new BTree<String, String>( recordManager, new StringComparator() );
+        tree.setPageSize( 4 );
+
+        // insert different objects and retrieve them
+        tree.insert( "test1", "value1", false );
+        tree.insert( "test2", "value2", false );
+        tree.insert( "test3", "value3", false );
+        tree.insert( "test4", "value4", false );
+        tree.insert( "test5", "value5", false );
+        tree.insert( "test6", "value6", false );
+
+        assertEquals( "value2", tree.find( "test2" ) );
+        assertEquals( "value1", tree.find( "test1" ) );
+
+        recordManager.close();
+    }
+
+
+    /**
+     *  Test to remove  objects from the btree. (cdaller)
+     */
+    @Test
+    public void testRemove() throws IOException
+    {
+        RecordManager recman;
+        BTree<String, Object> tree;
+
+        recman = RecordManagerFactory.createRecordManager( getTemporaryFile( "testRemove" ) );
+        tree = new BTree<String, Object>( recman, new StringComparator() );
+
+        tree.insert( "test1", "value1", false );
+        tree.insert( "test2", "value2", false );
+
+        assertEquals( "value1", tree.find( "test1" ) );
+        assertEquals( "value2", tree.find( "test2" ) );
+
+        tree.remove( "test1" );
+
+        assertEquals( null, tree.find( "test1" ) );
+        assertEquals( "value2", tree.find( "test2" ) );
+
+        tree.remove( "test2" );
+
+        assertEquals( null, tree.find( "test2" ) );
+
+        int iterations = 1000;
+
+        for ( int count = 0; count < iterations; count++ )
+        {
+            tree.insert( "num" + count, Integer.valueOf( count ), false );
+        }
+
+        assertEquals( iterations, tree.size() );
+
+        for ( int count = 0; count < iterations; count++ )
+        {
+            assertEquals( Integer.valueOf( count ), tree.find( "num" + count ) );
+        }
+
+        for ( int count = 0; count < iterations; count++ )
+        {
+            tree.remove( "num" + count );
+        }
+
+        assertEquals( 0, tree.size() );
+
+        recman.close();
+    }
+
+
+    /**
+     *  Test to find differents objects in the btree. (cdaller)
+     */
+    @Test
+    public void testFind() throws IOException
+    {
+        RecordManager recman;
+        BTree<String, String> tree;
+
+        recman = RecordManagerFactory.createRecordManager( getTemporaryFile( "testFind" ) );
+        tree = new BTree<String, String>( recman, new StringComparator() );
+
+        tree.insert( "test1", "value1", false );
+        tree.insert( "test2", "value2", false );
+
+        Object value = tree.find( "test1" );
+
+        assertTrue( value instanceof String );
+        assertEquals( "value1", value );
+
+        tree.insert( "", "Empty String as key", false );
+
+        assertEquals( "Empty String as key", tree.find( "" ) );
+        assertEquals( null, tree.find( "someoneelse" ) );
+
+        recman.close();
+    }
+
+
+    /**
+     *  Test to insert, retrieve and remove a large amount of data. (cdaller)
+     */
+    @Test
+    public void testLargeDataAmount() throws IOException
+    {
+        RecordManager recman;
+        BTree<String, Object> tree;
+
+        recman = RecordManagerFactory.createRecordManager( getTemporaryFile( "testLargeDataAmount" ) );
+
+        // recman = new jdbm.recman.BaseRecordManager( "test" );
+        tree = new BTree<String, Object>( recman, new StringComparator() );
+        int iterations = 10000;
+
+        // insert data
+        for ( int count = 0; count < iterations; count++ )
+        {
+            assertEquals( null, tree.insert( "num" + count, Integer.valueOf( count ), false ) );
+        }
+
+        // find data
+        for ( int count = 0; count < iterations; count++ )
+        {
+            assertEquals( Integer.valueOf( count ), tree.find( "num" + count ) );
+        }
+
+        // delete data
+        for ( int count = 0; count < iterations; count++ )
+        {
+            assertEquals( Integer.valueOf( count ), tree.remove( "num" + count ) );
+        }
+
+        assertEquals( 0, tree.size() );
+
+        recman.close();
+    }
+
+
+    /**
+     * Test access from multiple threads. Assertions only work, when the
+     * run() method is overridden and the exceptions of the threads are
+     * added to the resultset of the TestCase. see run() and
+     * handleException().
+     */
+    @Test
+    public void testMultithreadAccess() throws IOException, InterruptedException
+    {
+        RecordManager recman;
+        BTree<String, Integer> tree;
+
+        recman = RecordManagerFactory.createRecordManager( getTemporaryFile( "testMultithreadAccess" ) );
+        tree = new BTree<String, Integer>( recman, new StringComparator() );
+        TestThread<String, Integer>[] threadPool = ( TestThread<String, Integer>[] ) new TestThread[THREAD_NUMBER];
+        String name;
+        Map<String, Integer> content;
+
+        // create content for the tree, different content for different threads!
+        for ( int threadCount = 0; threadCount < THREAD_NUMBER; threadCount++ )
+        {
+            name = "thread" + threadCount;
+            content = new TreeMap<String, Integer>();
+
+            for ( int contentCount = 0; contentCount < THREAD_CONTENT_SIZE; contentCount++ )
+            {
+                // guarantee, that keys and values do not overleap,
+                // otherwise one thread removes some keys/values of
+                // other threads!
+                content.put( name + "_" + contentCount,
+                    Integer.valueOf( threadCount * THREAD_CONTENT_SIZE + contentCount ) );
+            }
+
+            threadPool[threadCount] = new TestThread<String, Integer>( name, tree, content );
+            threadPool[threadCount].start();
+        }
+
+        Thread.sleep( THREAD_RUNTIME );
+
+        // stop threads:
+        for ( int threadCount = 0; threadCount < THREAD_NUMBER; threadCount++ )
+        {
+            threadPool[threadCount].setStop();
+        }
+
+        // wait until the threads really stop:
+        try
+        {
+            for ( int threadCount = 0; threadCount < THREAD_NUMBER; threadCount++ )
+            {
+                threadPool[threadCount].join();
+            }
+        }
+        catch ( InterruptedException ignore )
+        {
+            ignore.printStackTrace();
+        }
+
+        recman.close();
+    }
+
+
+    /**
+     *  Helper method to 'simulate' the methods of an entry set of the btree.
+     */
+    protected boolean containsValue( Object value, BTree btree ) throws IOException
+    {
+        // we must synchronize on the BTree while browsing
+        synchronized ( btree )
+        {
+            TupleBrowser browser = btree.browse();
+            Tuple tuple = new Tuple();
+
+            while ( browser.getNext( tuple ) )
+            {
+                if ( tuple.getValue().equals( value ) )
+                {
+                    return ( true );
+                }
+            }
+        }
+
+        return false;
+    }
+
+
+    /**
+     *  Helper method to 'simulate' the methods of an entry set of the btree.
+     */
+    protected static boolean contains( Map.Entry entry, BTree btree ) throws IOException
+    {
+        Object tree_obj = btree.find( entry.getKey() );
+
+        if ( tree_obj == null )
+        {
+            // can't distinguish, if value is null or not found!!!!!!
+            return ( entry.getValue() == null );
+        }
+
+        return ( tree_obj.equals( entry.getValue() ) );
+    }
+
+    /**
+     * Inner class for testing puroposes only (multithreaded access)
+     */
+    class TestThread<K, V> extends Thread
+    {
+        Map<K, V> content;
+        BTree<K, V> btree;
+        volatile boolean stop = true;
+        int THREAD_SLEEP_TIME = 50; // in ms
+        String name;
+
+
+        TestThread( String name, BTree<K, V> btree, Map<K, V> content )
+        {
+            this.content = content;
+            this.btree = btree;
+            this.name = name;
+        }
+
+
+        public void setStop()
+        {
+            stop = true;
+        }
+
+
+        private void action() throws IOException
+        {
+            Iterator<Map.Entry<K, V>> iterator = content.entrySet().iterator();
+            Map.Entry<K, V> entry;
+
+            while ( iterator.hasNext() )
+            {
+                entry = iterator.next();
+                assertEquals( null, btree.insert( entry.getKey(), entry.getValue(), false ) );
+            }
+
+            // as other threads are filling the btree as well, the size
+            // of the btree is unknown (but must be at least the size of
+            // the content map)
+            assertTrue( content.size() <= btree.size() );
+            iterator = content.entrySet().iterator();
+
+            while ( iterator.hasNext() )
+            {
+                entry = iterator.next();
+                assertEquals( entry.getValue(), btree.find( entry.getKey() ) );
+                assertTrue( contains( entry, btree ) );
+
+                assertNotNull( btree.find( entry.getKey() ) );
+
+                assertTrue( containsValue( entry.getValue(), btree ) );
+            }
+
+            iterator = content.entrySet().iterator();
+            K key;
+
+            while ( iterator.hasNext() )
+            {
+                key = iterator.next().getKey();
+                btree.remove( key );
+                assertNull( btree.find( key ) );
+            }
+        }
+
+
+        public void run()
+        {
+            try
+            {
+                while ( !stop )
+                {
+                    action();
+
+                    try
+                    {
+                        Thread.sleep( THREAD_SLEEP_TIME );
+                    }
+                    catch ( InterruptedException except )
+                    {
+                        except.printStackTrace();
+                    }
+                }
+            }
+            catch ( Throwable t )
+            {
+            }
+        }
+    } // end of class TestThread
+}
+
+/**
+ * class for testing purposes only (store as value in btree) not
+ * implemented as inner class, as this prevents Serialization if
+ * outer class is not Serializable.
+ */
+class ObjectStore implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 
+     */
+    Object content;
+
+
+    public ObjectStore( Object content )
+    {
+        this.content = content;
+    }
+
+
+    Object getContent()
+    {
+        return content;
+    }
+
+
+    public boolean equals( Object obj )
+    {
+        if ( !( obj instanceof ObjectStore ) )
+        {
+            return false;
+        }
+
+        return content.equals( ( ( ObjectStore ) obj ).getContent() );
+    }
+
+
+    public String toString()
+    {
+        return ( "TestObject {content='" + content + "'}" );
+    }
+} // TestObject