/*
 *  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 org.apache.directory.server.core.partition.impl.btree.jdbm;


import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.csn.CsnFactory;
import org.apache.directory.api.ldap.model.cursor.Cursor;
import org.apache.directory.api.ldap.model.cursor.CursorException;
import org.apache.directory.api.ldap.model.cursor.Tuple;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Value;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapOtherException;
import org.apache.directory.api.ldap.model.exception.LdapSchemaViolationException;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.util.exception.MultiException;
import org.apache.directory.server.constants.ApacheSchemaConstants;
import org.apache.directory.server.core.api.DnFactory;
import org.apache.directory.server.core.api.entry.ClonedServerEntry;
import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
import org.apache.directory.server.core.api.interceptor.context.OperationContext;
import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
import org.apache.directory.server.core.api.partition.Partition;
import org.apache.directory.server.core.api.partition.PartitionReadTxn;
import org.apache.directory.server.core.api.partition.PartitionTxn;
import org.apache.directory.server.core.api.partition.PartitionWriteTxn;
import org.apache.directory.server.core.partition.impl.btree.AbstractBTreePartition;
import org.apache.directory.server.i18n.I18n;
import org.apache.directory.server.xdbm.Index;
import org.apache.directory.server.xdbm.ParentIdAndRdn;
import org.apache.directory.server.xdbm.search.impl.CursorBuilder;
import org.apache.directory.server.xdbm.search.impl.DefaultOptimizer;
import org.apache.directory.server.xdbm.search.impl.DefaultSearchEngine;
import org.apache.directory.server.xdbm.search.impl.EvaluatorBuilder;
import org.apache.directory.server.xdbm.search.impl.NoOpOptimizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import jdbm.RecordManager;
import jdbm.helper.MRU;
import jdbm.recman.BaseRecordManager;
import jdbm.recman.CacheRecordManager;
import jdbm.recman.TransactionManager;


/**
 * A {@link Partition} that stores entries in
 * <a href="http://jdbm.sourceforge.net/">JDBM</a> database.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class JdbmPartition extends AbstractBTreePartition
{
    /** static logger */
    private static final Logger LOG = LoggerFactory.getLogger( JdbmPartition.class );

    private static final String JDBM_DB_FILE_EXTN = ".db";

    private static final FilenameFilter DB_FILTER = new FilenameFilter()
    {
        @Override
        public boolean accept( File dir, String name )
        {
            // really important to filter master.db and master.lg files
            return name.endsWith( JDBM_DB_FILE_EXTN ) && !name.startsWith( "master." );
        }
    };

    /** the JDBM record manager used by this database */
    private RecordManager recMan;

    /** the entry cache */
    private Cache< String, Entry > entryCache;


    /**
     * Creates a store based on JDBM B+Trees.
     * 
     * @param schemaManager The SchemaManager instance
     * @param dnFactory The DN factory instance
     */
    public JdbmPartition( SchemaManager schemaManager, DnFactory dnFactory )
    {
        super( schemaManager, dnFactory );

        // Initialize the cache size
        if ( cacheSize < 0 )
        {
            cacheSize = DEFAULT_CACHE_SIZE;
            LOG.debug( "Using the default entry cache size of {} for {} partition", cacheSize, id );
        }
        else
        {
            LOG.debug( "Using the custom configured cache size of {} for {} partition", cacheSize, id );
        }
    }
    
    
    /**
     * Rebuild the indexes 
     */
    private int rebuildIndexes( PartitionTxn partitionTxn ) throws LdapException, IOException
    {
        Cursor<Tuple<String, Entry>> cursor = getMasterTable().cursor();

        int masterTableCount = 0;
        int repaired = 0;

        System.out.println( "Re-building indices..." );

        boolean ctxEntryLoaded = false;

        try
        {
            while ( cursor.next() )
            {
                masterTableCount++;
                Tuple<String, Entry> tuple = cursor.get();
                String id = tuple.getKey();

                Entry entry = tuple.getValue();
                
                // Start with the RdnIndex
                String parentId = entry.get( ApacheSchemaConstants.ENTRY_PARENT_ID_OID ).getString();
                System.out.println( "Read entry " + entry.getDn() + " with ID " + id + " and parent ID " + parentId );

                Dn dn = entry.getDn();
                
                ParentIdAndRdn parentIdAndRdn = null;

                // context entry may have more than one RDN
                if ( !ctxEntryLoaded && getSuffixDn().getName().startsWith( dn.getName() ) )
                {
                    // If the read entry is the context entry, inject a tuple that have one or more RDNs
                    parentIdAndRdn = new ParentIdAndRdn( parentId, getSuffixDn().getRdns() );
                    ctxEntryLoaded = true;
                }
                else
                {
                    parentIdAndRdn = new ParentIdAndRdn( parentId, dn.getRdn() );
                }

                // Inject the parentIdAndRdn in the rdnIndex
                rdnIdx.add( partitionTxn, parentIdAndRdn, id );
                
                // Process the ObjectClass index
                // Update the ObjectClass index
                Attribute objectClass = entry.get( objectClassAT );

                if ( objectClass == null )
                {
                    String msg = I18n.err( I18n.ERR_217, dn, entry );
                    ResultCodeEnum rc = ResultCodeEnum.OBJECT_CLASS_VIOLATION;
                    throw new LdapSchemaViolationException( rc, msg );
                }

                for ( Value value : objectClass )
                {
                    String valueStr = value.getString();

                    if ( valueStr.equals( SchemaConstants.TOP_OC ) )
                    {
                        continue;
                    }

                    objectClassIdx.add( partitionTxn, valueStr, id );
                }
                
                // The Alias indexes
                if ( objectClass.contains( SchemaConstants.ALIAS_OC ) )
                {
                    Attribute aliasAttr = entry.get( aliasedObjectNameAT );
                    addAliasIndices( partitionTxn, id, dn, new Dn( schemaManager, aliasAttr.getString() ) );
                }
                
                // The entryCSN index
                // Update the EntryCsn index
                Attribute entryCsn = entry.get( entryCsnAT );

                if ( entryCsn == null )
                {
                    String msg = I18n.err( I18n.ERR_219, dn, entry );
                    throw new LdapSchemaViolationException( ResultCodeEnum.OBJECT_CLASS_VIOLATION, msg );
                }

                entryCsnIdx.add( partitionTxn, entryCsn.getString(), id );

                // The AdministrativeRole index
                // Update the AdministrativeRole index, if needed
                if ( entry.containsAttribute( administrativeRoleAT ) )
                {
                    // We may have more than one role
                    Attribute adminRoles = entry.get( administrativeRoleAT );

                    for ( Value value : adminRoles )
                    {
                        adminRoleIdx.add( partitionTxn, value.getString(), id );
                    }

                    // Adds only those attributes that are indexed
                    presenceIdx.add( partitionTxn, administrativeRoleAT.getOid(), id );
                }

                // And the user indexess
                // Now work on the user defined userIndices
                for ( Attribute attribute : entry )
                {
                    AttributeType attributeType = attribute.getAttributeType();
                    String attributeOid = attributeType.getOid();

                    if ( hasUserIndexOn( attributeType ) )
                    {
                        Index<Object, String> idx = ( Index<Object, String> ) getUserIndex( attributeType );

                        // here lookup by attributeId is OK since we got attributeId from
                        // the entry via the enumeration - it's in there as is for sure

                        for ( Value value : attribute )
                        {
                            idx.add( partitionTxn, value.getString(), id );
                        }

                        // Adds only those attributes that are indexed
                        presenceIdx.add( partitionTxn, attributeOid, id );
                    }
                }
            }
            
        }
        catch ( Exception e )
        {
            System.out.println( "Exiting after fetching entries " + repaired );
            throw new LdapOtherException( e.getMessage(), e );
        }
        finally
        {
            cursor.close();
        }
        
        return masterTableCount;
    }
    
    
    /**
     * Update the children and descendant counters in the RDN index
     */
    private void updateRdnIndexCounters( PartitionTxn partitionTxn ) throws LdapException, IOException
    {
        Cursor<Tuple<String, Entry>> cursor = getMasterTable().cursor();

        System.out.println( "Updating the RDN index counters..." );

        try
        {
            while ( cursor.next() )
            {
                Tuple<String, Entry> tuple = cursor.get();

                Entry entry = tuple.getValue();

                // Update the parent's nbChildren and nbDescendants values
                // Start with the RdnIndex
                String parentId = entry.get( ApacheSchemaConstants.ENTRY_PARENT_ID_OID ).getString();
                
                if ( parentId != Partition.ROOT_ID )
                {
                    updateRdnIdx( partitionTxn, parentId, ADD_CHILD, 0 );
                }
            }
        }
        catch ( Exception e )
        {
            System.out.println( "Exiting, wasn't able to update the RDN index counters" );
            throw new LdapOtherException( e.getMessage(), e );
        }
        finally
        {
            cursor.close();
        }
    }
    
    
    /**
     * {@inheritDoc}
     */
    @Override
    protected void doRepair() throws LdapException
    {
        BaseRecordManager base;

        try
        {
            base = new BaseRecordManager( getPartitionPath().getPath() );
            TransactionManager transactionManager = base.getTransactionManager();
            transactionManager.setMaximumTransactionsInLog( 2000 );
        }
        catch ( IOException ioe )
        {
            throw new LdapOtherException( ioe.getMessage(), ioe );
        }

        // Find the underlying directories
        File partitionDir = new File( getPartitionPath() );
        
        // get the names of the db files
        List<String> indexDbFileNameList = Arrays.asList( partitionDir.list( DB_FILTER ) );

        // then add all index objects to a list
        List<String> allIndices = new ArrayList<>();

        try
        {
            // Iterate on the declared indexes, deleting the old ones
            for ( Index<?, String> index : getIndexedAttributes() )
            {
                // Index won't be initialized at this time, so lookup AT registry to get the OID
                AttributeType indexAT = schemaManager.lookupAttributeTypeRegistry( index.getAttributeId() );
                String oid = indexAT.getOid();
                allIndices.add( oid );
                
                // take the part after removing .db from the
                String name = oid + JDBM_DB_FILE_EXTN;
                
                // if the name doesn't exist in the list of index DB files
                // this is a new index and we need to build it
                if ( indexDbFileNameList.contains( name ) )
                {
                    ( ( JdbmIndex<?> ) index ).close( null );
                    
                    File indexFile = new File( partitionDir, name );
                    indexFile.delete();
                    
                    // Recreate the index
                    ( ( JdbmIndex<?> ) index ).init( base, schemaManager, indexAT );
                }
            }
            // Ok, now, rebuild the indexes.
            int masterTableCount = rebuildIndexes( null );
            
            // Now that the RdnIndex has been rebuilt, we have to update the nbChildren and nbDescendants values
            // We loop again on the MasterTable 
            updateRdnIndexCounters( null );

            // Flush the indexes on disk
            sync();

            System.out.println( "Total entries present in the partition " + masterTableCount );
            System.out.println( "Repair complete" );
        }
        catch ( IOException ioe )
        {
            throw new LdapOtherException( ioe.getMessage(), ioe );
        }
    }


    @Override
    protected void doInit() throws LdapException
    {
        if ( !initialized )
        {
            BaseRecordManager base;

            // setup optimizer and registries for parent
            if ( !optimizerEnabled )
            {
                setOptimizer( new NoOpOptimizer() );
            }
            else
            {
                setOptimizer( new DefaultOptimizer( this ) );
            }

            EvaluatorBuilder evaluatorBuilder = new EvaluatorBuilder( this, schemaManager );
            CursorBuilder cursorBuilder = new CursorBuilder( this, evaluatorBuilder );

            setSearchEngine( new DefaultSearchEngine( this, cursorBuilder, evaluatorBuilder, getOptimizer() ) );

            // Create the underlying directories (only if needed)
            File partitionDir = new File( getPartitionPath() );
            
            if ( !partitionDir.exists() && !partitionDir.mkdirs() )
            {
                throw new LdapOtherException( I18n.err( I18n.ERR_112_COULD_NOT_CREATE_DIRECTORY, partitionDir ) );
            }

            // First, check if the file storing the data exists
            String path = partitionDir.getPath() + File.separator + id;

            try
            {
                base = new BaseRecordManager( path );
                TransactionManager transactionManager = base.getTransactionManager();
                transactionManager.setMaximumTransactionsInLog( 2000 );
                
                // prevent the OOM when more than 50k users are loaded at a stretch
                // adding this system property to make it configurable till JDBM gets replaced by Mavibot
                String cacheSizeVal = System.getProperty( "jdbm.recman.cache.size", "100" );
                
                int recCacheSize = Integer.parseInt( cacheSizeVal );
                
                LOG.info( "Setting CacheRecondManager's cache size to {}", recCacheSize );
                
                recMan = new CacheRecordManager( base, new MRU( recCacheSize ) );
            }
            catch ( IOException ioe )
            {
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }

            // Iterate on the declared indexes
            List<String> allIndices = new ArrayList<>();
            List<Index<?, String>> indexToBuild = new ArrayList<>();

            for ( Index<?, String> index : getIndexedAttributes() )
            {
                String oid = schemaManager.lookupAttributeTypeRegistry( index.getAttributeId() ).getOid();
                allIndices.add( oid );
                
                // if the name doesn't exist in the database
                // this is a new index and we need to build it
                try
                {
                    // Check the forward index only (we suppose we never will add a reverse index later on)
                    String forwardIndex = oid + "_forward";
                    
                    if ( recMan.getNamedObject( forwardIndex ) == 0 )
                    {
                        // The index does not exist in the database, we need to build it
                        indexToBuild.add( index );
                    }
                }
                catch ( IOException ioe )
                {
                    throw new LdapOtherException( ioe.getMessage(), ioe );
                }
            }

            /*
            // get all index db files first
            File[] allIndexDbFiles = partitionDir.listFiles( DB_FILTER );

            // get the names of the db files also
            List<String> indexDbFileNameList = Arrays.asList( partitionDir.list( DB_FILTER ) );

            // then add all index objects to a list
            List<String> allIndices = new ArrayList<>();

            List<Index<?, String>> indexToBuild = new ArrayList<>();
            
            // Iterate on the declared indexes
            for ( Index<?, String> index : getIndexedAttributes() )
            {
                // Index won't be initialized at this time, so lookup AT registry to get the OID
                String oid = schemaManager.lookupAttributeTypeRegistry( index.getAttributeId() ).getOid();
                allIndices.add( oid );
                
                // take the part after removing .db from the
                String name = oid + JDBM_DB_FILE_EXTN;
                
                // if the name doesn't exist in the list of index DB files
                // this is a new index and we need to build it
                if ( !indexDbFileNameList.contains( name ) )
                {
                    indexToBuild.add( index );
                }
            }
            */

            // Initialize the indexes
            super.doInit();

            if ( cacheSize < 0 )
            {
                cacheSize = DEFAULT_CACHE_SIZE;
                LOG.debug( "Using the default entry cache size of {} for {} partition", cacheSize, id );
            }
            else
            {
                LOG.debug( "Using the custom configured cache size of {} for {} partition", cacheSize, id );
            }

            // Create the master table (the table containing all the entries)
            try
            {
                master = new JdbmMasterTable( recMan, schemaManager );
            }
            catch ( IOException ioe )
            {
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }

            if ( !indexToBuild.isEmpty() )
            {
                buildUserIndex( beginReadTransaction(), indexToBuild );
            }

            entryCache = Caffeine.newBuilder().maximumSize( cacheSize ).build();

            // Initialization of the context entry
            if ( ( suffixDn != null ) && ( contextEntry != null ) )
            {
                Dn contextEntryDn = contextEntry.getDn();

                // Checking if the context entry DN is schema aware
                if ( !contextEntryDn.isSchemaAware() )
                {
                    contextEntryDn = new Dn( schemaManager, contextEntryDn );
                }

                // We're only adding the entry if the two DNs are equal
                if ( suffixDn.equals( contextEntryDn ) )
                {
                    // Looking for the current context entry
                    Entry suffixEntry;
                    LookupOperationContext lookupContext = new LookupOperationContext( null, suffixDn );
                    lookupContext.setPartition( this );
                    
                    try ( PartitionTxn partitionTxn = beginReadTransaction() )
                    {
                        lookupContext.setTransaction( partitionTxn );
                        suffixEntry = lookup( lookupContext );
                    }
                    catch ( IOException ioe )
                    {
                        throw new LdapOtherException( ioe.getMessage(), ioe );
                    }

                    // We're only adding the context entry if it doesn't already exist
                    if ( suffixEntry == null )
                    {
                        // Checking of the context entry is schema aware
                        if ( !contextEntry.isSchemaAware() )
                        {
                            // Making the context entry schema aware
                            contextEntry = new DefaultEntry( schemaManager, contextEntry );
                        }

                        // Adding the 'entryCsn' attribute
                        if ( contextEntry.get( SchemaConstants.ENTRY_CSN_AT ) == null )
                        {
                            contextEntry.add( SchemaConstants.ENTRY_CSN_AT, new CsnFactory( 0 ).newInstance()
                                .toString() );
                        }

                        // Adding the 'entryUuid' attribute
                        if ( contextEntry.get( SchemaConstants.ENTRY_UUID_AT ) == null )
                        {
                            String uuid = UUID.randomUUID().toString();
                            contextEntry.add( SchemaConstants.ENTRY_UUID_AT, uuid );
                        }

                        // And add this entry to the underlying partition
                        PartitionTxn partitionTxn = null;
                        AddOperationContext addContext = new AddOperationContext( null, contextEntry );
                        
                        try
                        {
                            partitionTxn = beginWriteTransaction();
                            addContext.setTransaction( partitionTxn );

                            add( addContext );
                            partitionTxn.commit();
                        }
                        catch ( LdapException le )
                        {
                            if ( partitionTxn != null )
                            {
                                try
                                { 
                                    partitionTxn.abort();
                                }
                                catch ( IOException ioe )
                                {
                                    throw new LdapOtherException( ioe.getMessage(), ioe );
                                }
                            }
                            
                            throw le;
                        }
                        catch ( IOException ioe )
                        {
                            try
                            { 
                                partitionTxn.abort();
                            }
                            catch ( IOException ioe2 )
                            {
                                throw new LdapOtherException( ioe2.getMessage(), ioe2 );
                            }

                            throw new LdapOtherException( ioe.getMessage(), ioe );
                        }
                    }
                }
            }

            // We are done !
            initialized = true;
        }
    }


    /**
     * {@inheritDoc}}
     */
    public String getDefaultId()
    {
        return Partition.DEFAULT_ID;
    }


    /**
     * {@inheritDoc}
     */
    public String getRootId()
    {
        return Partition.ROOT_ID;
    }


    /**
     * This method is called when the synch thread is waking up, to write
     * the modified data.
     * 
     * @throws LdapException on failures to sync database files to disk
     */
    @Override
    public synchronized void sync() throws LdapException
    {
        if ( !initialized )
        {
            return;
        }
        
        try
        {
            // Commit
            recMan.commit();
    
            // And flush the journal
            BaseRecordManager baseRecordManager = null;
    
            if ( recMan instanceof CacheRecordManager )
            {
                baseRecordManager = ( ( BaseRecordManager ) ( ( CacheRecordManager ) recMan ).getRecordManager() );
            }
            else
            {
                baseRecordManager = ( ( BaseRecordManager ) recMan );
            }
    
            baseRecordManager.getTransactionManager().synchronizeLog();
        }
        catch ( IOException ioe )
        {
            throw new LdapOtherException( ioe.getMessage(), ioe );
        }
    }


    /**
     * Builds user defined indexes on a attributes by browsing all the entries present in master db
     * 
     * Note: if the given list of indices contains any system index that will be skipped.
     * 
     * WARN: MUST be called after calling super.doInit()
     * 
     * @param indices then selected indexes that need to be built
     * @throws Exception in case of any problems while building the index
     */
    private void buildUserIndex( PartitionTxn partitionTxn, List<Index<?, String>> indices ) throws LdapException
    {
        try
        {
            Cursor<Tuple<String, Entry>> cursor = master.cursor();
            cursor.beforeFirst();
    
            while ( cursor.next() )
            {
                for ( Index index : indices )
                {
                    AttributeType atType = index.getAttribute();
    
                    String attributeOid = index.getAttribute().getOid();
    
                    if ( systemIndices.get( attributeOid ) != null )
                    {
                        // skipping building of the system index
                        continue;
                    }
                    
                    LOG.info( "building the index for attribute type {}", atType );
    
                    Tuple<String, Entry> tuple = cursor.get();
    
                    String id = tuple.getKey();
                    Entry entry = tuple.getValue();
    
                    Attribute entryAttr = entry.get( atType );
    
                    if ( entryAttr != null )
                    {
                        for ( Value value : entryAttr )
                        {
                            index.add( partitionTxn, value.getString(), id );
                        }
    
                        // Adds only those attributes that are indexed
                        presenceIdx.add( partitionTxn, attributeOid, id );
                    }
                }
            }
    
            cursor.close();
        }
        catch ( CursorException | IOException e )
        {
            throw new LdapOtherException( e.getMessage(), e );
        }
    }


    /**
     * removes any unused/removed attribute index files present under the partition's
     * working directory
     */
    private void deleteUnusedIndexFiles( List<String> allIndices, File[] dbFiles )
    {
        for ( File file : dbFiles )
        {
            String name = file.getName();
            // take the part after removing .db from the
            name = name.substring( 0, name.lastIndexOf( JDBM_DB_FILE_EXTN ) );

            if ( systemIndices.get( name ) != null )
            {
                // do not delete the system index file
                continue;
            }

            // remove the file if not found in the list of names of indices
            if ( !allIndices.contains( name ) )
            {
                boolean deleted = file.delete();

                if ( deleted )
                {
                    LOG.info( "Deleted unused index file {}", file.getAbsolutePath() );
                }
                else
                {
                    LOG.warn( "Failed to delete unused index file {}", file.getAbsolutePath() );
                }
            }
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    protected Index<?, String> convertAndInit( Index<?, String> index ) throws LdapException
    {
        JdbmIndex<?> jdbmIndex;

        if ( index instanceof JdbmRdnIndex )
        {
            jdbmIndex = ( JdbmRdnIndex ) index;
        }
        else if ( index instanceof JdbmDnIndex )
        {
            jdbmIndex = ( JdbmDnIndex ) index;
        }
        else if ( index instanceof JdbmIndex<?> )
        {
            jdbmIndex = ( JdbmIndex<?> ) index;
        }
        else
        {
            LOG.debug( "Supplied index {} is not a JdbmIndex.  "
                + "Will create new JdbmIndex using copied configuration parameters.", index );
            jdbmIndex = new JdbmIndex( index.getAttributeId(), true );
            jdbmIndex.setCacheSize( index.getCacheSize() );
            jdbmIndex.setNumDupLimit( JdbmIndex.DEFAULT_DUPLICATE_LIMIT );
        }

        try
        {
            jdbmIndex.init( recMan, schemaManager, schemaManager.lookupAttributeTypeRegistry( index.getAttributeId() ) );
        }
        catch ( IOException ioe )
        {
            throw new LdapOtherException( ioe.getMessage(), ioe );
        }

        return jdbmIndex;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    protected synchronized void doDestroy( PartitionTxn partitionTxn ) throws LdapException
    {
        MultiException errors = new MultiException( I18n.err( I18n.ERR_577 ) );

        if ( !initialized )
        {
            return;
        }

        try
        {
            super.doDestroy( partitionTxn );
        }
        catch ( Exception e )
        {
            errors.addThrowable( e );
        }

        // This is specific to the JDBM store : close the record manager
        try
        {
            recMan.close();
            LOG.debug( "Closed record manager for {} partition.", suffixDn );
        }
        catch ( IOException t )
        {
            LOG.error( I18n.err( I18n.ERR_127 ), t );
            errors.addThrowable( t );
        }
        finally
        {
            if ( entryCache != null )
            {
                entryCache.invalidateAll();
            }
        }

        if ( errors.size() > 0 )
        {
            throw new LdapOtherException( errors.getMessage(), errors );
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    protected final Index createSystemIndex( String oid, URI path, boolean withReverse ) throws LdapException
    {
        LOG.debug( "Supplied index {} is not a JdbmIndex.  "
            + "Will create new JdbmIndex using copied configuration parameters." );
        JdbmIndex<?> jdbmIndex;

        if ( oid.equals( ApacheSchemaConstants.APACHE_RDN_AT_OID ) )
        {
            jdbmIndex = new JdbmRdnIndex();
            jdbmIndex.setAttributeId( ApacheSchemaConstants.APACHE_RDN_AT_OID );
            jdbmIndex.setNumDupLimit( JdbmIndex.DEFAULT_DUPLICATE_LIMIT );
        }
        else if ( oid.equals( ApacheSchemaConstants.APACHE_ALIAS_AT_OID ) )
        {
            jdbmIndex = new JdbmDnIndex( ApacheSchemaConstants.APACHE_ALIAS_AT_OID );
            jdbmIndex.setAttributeId( ApacheSchemaConstants.APACHE_ALIAS_AT_OID );
            jdbmIndex.setNumDupLimit( JdbmIndex.DEFAULT_DUPLICATE_LIMIT );
        }
        else
        {
            jdbmIndex = new JdbmIndex( oid, withReverse );
            jdbmIndex.setNumDupLimit( JdbmIndex.DEFAULT_DUPLICATE_LIMIT );
        }

        jdbmIndex.setWkDirPath( path );

        return jdbmIndex;
    }


    @Override
    public void updateCache( OperationContext opCtx )
    {
        if ( entryCache == null )
        {
            return;
        }

        try
        {
            if ( opCtx instanceof ModifyOperationContext )
            {
                // replace the entry
                ModifyOperationContext modCtx = ( ModifyOperationContext ) opCtx;
                Entry entry = modCtx.getAlteredEntry();
                String id = entry.get( SchemaConstants.ENTRY_UUID_AT ).getString();

                if ( entry instanceof ClonedServerEntry )
                {
                    entry = ( ( ClonedServerEntry ) entry ).getOriginalEntry();
                }

                entryCache.put( id, entry );
            }
            else if ( ( opCtx instanceof MoveOperationContext )
                || ( opCtx instanceof MoveAndRenameOperationContext )
                || ( opCtx instanceof RenameOperationContext ) )
            {
                // clear the cache it is not worth updating all the children
                entryCache.invalidateAll();
            }
            else if ( opCtx instanceof DeleteOperationContext )
            {
                // delete the entry
                DeleteOperationContext delCtx = ( DeleteOperationContext ) opCtx;
                entryCache.invalidate( delCtx.getEntry().get( SchemaConstants.ENTRY_UUID_AT ).getString() );
            }
        }
        catch ( LdapException e )
        {
            LOG.warn( "Failed to update entry cache", e );
        }
    }


    @Override
    public Entry lookupCache( String id )
    {
        return ( entryCache != null ) ? entryCache.getIfPresent( id ) : null;
    }


    @Override
    public void addToCache( String id, Entry entry )
    {
        if ( entryCache == null )
        {
            return;
        }

        Entry addedEntry = entry;
        
        if ( entry instanceof ClonedServerEntry )
        {
            addedEntry = ( ( ClonedServerEntry ) entry ).getOriginalEntry();
        }

        entryCache.put( id, addedEntry );
    }


    @Override
    public PartitionReadTxn beginReadTransaction()
    {
        return new PartitionReadTxn();
    }


    @Override
    public PartitionWriteTxn beginWriteTransaction()
    {
        return new JdbmPartitionWriteTxn( recMan, isSyncOnWrite() );
    }
}
