mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

abobrov
05.14.2007 36a0eff3016b9a79eaab3e1aed8b1d17b7f799c2
[Issue 1117] Create an entry cache backed by DB+tmpfs
3 files added
6 files modified
3259 ■■■■■ changed files
opends/resource/schema/02-config.ldif 18 ●●●●● patch | view | raw | blame | history
opends/src/admin/defn/org/opends/server/admin/std/FileSystemEntryCacheConfiguration.xml 274 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/backends/jeb/BackendImpl.java 73 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/config/ConfigConstants.java 112 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/DirectoryServer.java 53 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/FileSystemEntryCache.java 2363 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/FileSystemEntryCacheIndex.java 73 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/messages/ExtensionsMessages.java 280 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/messages/JebMessages.java 13 ●●●● patch | view | raw | blame | history
opends/resource/schema/02-config.ldif
@@ -1245,6 +1245,18 @@
  NAME 'ds-cfg-writer-append'
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE
  X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.362
  NAME 'ds-cfg-max-memory-size' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  SINGLE-VALUE X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.363
  NAME 'ds-cfg-cache-type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
  SINGLE-VALUE X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.364
  NAME 'ds-cfg-cache-directory' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
  SINGLE-VALUE X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.365
  NAME 'ds-cfg-persistent-cache' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
  SINGLE-VALUE X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.375
  NAME 'ds-cfg-allow-retrieving-membership' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
  SINGLE-VALUE X-ORIGIN 'OpenDS Directory Server' )
@@ -1807,4 +1819,10 @@
  NAME 'ds-cfg-member-virtual-attribute' SUP ds-cfg-virtual-attribute
  STRUCTURAL MUST ds-cfg-allow-retrieving-membership
  X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.110 NAME 'ds-cfg-file-system-entry-cache'
  SUP ds-cfg-entry-cache STRUCTURAL MAY ( ds-cfg-max-entries $
  ds-cfg-max-memory-size $ ds-cfg-lock-timeout $ ds-cfg-exclude-filter $
  ds-cfg-include-filter $ ds-cfg-cache-directory $ ds-cfg-cache-type $
  ds-cfg-persistent-cache $ ds-cfg-database-cache-percent $
  ds-cfg-database-cache-size ) X-ORIGIN 'OpenDS Directory Server' )
opends/src/admin/defn/org/opends/server/admin/std/FileSystemEntryCacheConfiguration.xml
New file
@@ -0,0 +1,274 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
 ! CDDL HEADER START
 !
 ! The contents of this file are subject to the terms of the
 ! Common Development and Distribution License, Version 1.0 only
 ! (the "License").  You may not use this file except in compliance
 ! with the License.
 !
 ! You can obtain a copy of the license at
 ! trunk/opends/resource/legal-notices/OpenDS.LICENSE
 ! or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 ! See the License for the specific language governing permissions
 ! and limitations under the License.
 !
 ! When distributing Covered Code, include this CDDL HEADER in each
 ! file and include the License file at
 ! trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 ! add the following below this CDDL HEADER, with the fields enclosed
 ! by brackets "[]" replaced with your own identifying information:
 !      Portions Copyright [yyyy] [name of copyright owner]
 !
 ! CDDL HEADER END
 !
 !
 !      Portions Copyright 2007 Sun Microsystems, Inc.
 ! -->
<adm:managed-object
  name="file-system-entry-cache"
  plural-name="file-system-entry-caches"
  package="org.opends.server.admin.std"
  extends="entry-cache"
  xmlns:adm="http://www.opends.org/admin"
  xmlns:ldap="http://www.opends.org/admin-ldap"
  >
  <adm:synopsis>
    The <adm:user-friendly-name/> is an entry cache implementation which uses
    a JE database to keep track of the entries.
  </adm:synopsis>
  <adm:description>
    For the best performance the JE database should reside in a memory based
    file system, although any file system will do for this cache to function.
    Entries are maintained either by FIFO (default) or LRU (configurable)
    based list implementation.
    Cache sizing is based on the size or percentage of free space availble in
    the file system, such that if enough memory is free, then adding an entry
    to the cache will not require purging, but if more than a specified
    percentage of the file system available space is already consumed, then
    one or more entries will need to be removed in order to make room for a
    new entry.  It is also possible to configure a maximum number of entries
    for the cache. If this is specified, then the number of entries will not
    be allowed to exceed this value, but it may not be possible to hold this
    many entries if the available memory fills up first.
    Other configurable parameters for this cache include the maximum length of
    time to block while waiting to acquire a lock, and a set of filters that
    may be used to define criteria for determining which entries are stored in
    the cache. If a set of filters are provided then an entry must match at
    least one of them in order to be stored in the cache.
    JE environment cache size can also be configured either as percentage of
    the free memory available in the JVM, or as an absolute size in bytes.
    This cache has a persistence property which, if enabled, allows for the
    contents of the cache to persist across server or cache restarts.
  </adm:description>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:oid>1.3.6.1.4.1.26027.1.2.110</ldap:oid>
      <ldap:name>ds-cfg-file-system-entry-cache</ldap:name>
      <ldap:superior>ds-cfg-entry-cache</ldap:superior>
    </ldap:object-class>
  </adm:profile>
  <adm:property name="lock-timeout">
    <adm:synopsis>
      The length of time in milliseconds to wait while
      attempting to acquire a read or write lock.
    </adm:synopsis>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>2000.0ms</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:duration base-unit="ms" allow-unlimited="true"/>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:oid>1.3.6.1.4.1.26027.1.1.58</ldap:oid>
        <ldap:name>ds-cfg-lock-timeout</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="max-memory-size">
    <adm:synopsis>
      The maximum size of the entry cache in bytes.
    </adm:synopsis>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>0b</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:size/>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:oid>1.3.6.1.4.1.26027.1.1.362</ldap:oid>
        <ldap:name>ds-cfg-max-memory-size</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="max-entries">
    <adm:synopsis>
      The maximum number of entries allowed in the cache.
    </adm:synopsis>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>2147483647</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:integer lower-limit="0"/>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:oid>1.3.6.1.4.1.26027.1.1.65</ldap:oid>
        <ldap:name>ds-cfg-max-entries</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="cache-type">
    <adm:synopsis>
      Specifies the policy which should be used for purging entries from the
      cache. FIFO by default and LRU as configurable.
    </adm:synopsis>
    <adm:requires-admin-action>
      <adm:component-restart/>
    </adm:requires-admin-action>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>fifo</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:enumeration>
        <adm:value name="fifo">
          <adm:synopsis>
            FIFO based entry cache.
          </adm:synopsis>
        </adm:value>
        <adm:value name="lru">
        <adm:synopsis>
            LRU based entry cache.
        </adm:synopsis>
        </adm:value>
      </adm:enumeration>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:oid>1.3.6.1.4.1.26027.1.1.363</ldap:oid>
        <ldap:name>ds-cfg-cache-type</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="cache-directory">
    <adm:synopsis>
      Specifies the directory in which the JE environment should store the
      cache.
    </adm:synopsis>
    <adm:requires-admin-action>
      <adm:component-restart/>
    </adm:requires-admin-action>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>/tmp/OpenDS.FSCache</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:string/>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:oid>1.3.6.1.4.1.26027.1.1.364</ldap:oid>
        <ldap:name>ds-cfg-cache-directory</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="persistent-cache">
    <adm:synopsis>
      Specifies whether the cache should persist across restarts.
    </adm:synopsis>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>false</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:boolean/>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:oid>1.3.6.1.4.1.26027.1.1.365</ldap:oid>
        <ldap:name>ds-cfg-persistent-cache</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="database-cache-percent">
    <adm:synopsis>
      The maximum memory usage for the internal JE cache as a percentage
      of the total JVM memory.
    </adm:synopsis>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>0</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:integer lower-limit="0" upper-limit="100"/>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:oid>1.3.6.1.4.1.26027.1.1.25</ldap:oid>
        <ldap:name>ds-cfg-database-cache-percent</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="database-cache-size">
    <adm:synopsis>
      The maximum JVM memory usage in bytes for the internal JE cache.
    </adm:synopsis>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>0b</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:size/>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:oid>1.3.6.1.4.1.26027.1.1.27</ldap:oid>
        <ldap:name>ds-cfg-database-cache-size</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property-reference name="include-filter"/>
  <adm:property-reference name="exclude-filter"/>
  <adm:property-override name="entry-cache-class">
    <adm:default-behavior>
      <adm:defined>
        <adm:value>
          org.opends.server.extensions.FileSystemEntryCache
        </adm:value>
      </adm:defined>
    </adm:default-behavior>
  </adm:property-override>
</adm:managed-object>
opends/src/server/org/opends/server/backends/jeb/BackendImpl.java
@@ -32,6 +32,14 @@
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.util.Arrays;
import java.util.Collections;
import java.util.zip.Adler32;
import java.util.zip.CheckedInputStream;
import com.sleepycat.je.DatabaseException;
import org.opends.server.api.Backend;
@@ -186,6 +194,60 @@
  }
  /**
  * This method will attempt to checksum the current JE db environment by
  * computing the Adler-32 checksum on the latest JE log file available.
  *
  * @return  The checksum of JE db environment or zero if checksum failed.
  */
  private long checksumDbEnv() {
    List<File> jdbFiles =
     Arrays.asList(config.getBackendDirectory().listFiles(new FilenameFilter() {
      public boolean accept(File dir, String name) {
        return name.endsWith(".jdb");
      }
    }));
    if ( !jdbFiles.isEmpty() ) {
      Collections.sort(jdbFiles, Collections.reverseOrder());
      FileInputStream fis = null;
      try {
        fis = new FileInputStream(jdbFiles.get(0).toString());
        CheckedInputStream cis = new CheckedInputStream(fis, new Adler32());
        byte[] tempBuf = new byte[8192];
        while (cis.read(tempBuf) >= 0) {
        }
        return cis.getChecksum().getValue();
      } catch (Exception e) {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }
      } finally {
        if (fis != null) {
          try {
            fis.close();
          } catch (Exception e) {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }
          }
        }
      }
    }
    // Log a warning that we have failed to checksum this db environment.
    int msgID = MSGID_JEB_BACKEND_CHECKSUM_FAIL;
    String message = getMessage(msgID, cfg.getBackendId());
    logError(ErrorLogCategory.BACKEND, ErrorLogSeverity.SEVERE_WARNING,
        message, msgID);
    return 0;
  }
  /**
   * This method constructs a container name from a base DN. Only alphanumeric
   * characters are preserved, all other characters are replaced with an
   * underscore.
@@ -239,9 +301,12 @@
  public void initializeBackend()
       throws ConfigException, InitializationException
  {
    // Checksum this db environment and register its offline state id/checksum.
    DirectoryServer.registerOfflineBackendStateID(this.getBackendID(),
      checksumDbEnv());
    // Open the database environment
    try
    {
    try {
      rootContainer = new RootContainer(config, this);
      rootContainer.open();
    }
@@ -391,6 +456,10 @@
               message, msgID);
    }
    // Checksum this db environment and register its offline state id/checksum.
    DirectoryServer.registerOfflineBackendStateID(this.getBackendID(),
      checksumDbEnv());
    // Make sure the thread counts are zero for next initialization.
    threadTotalCount.set(0);
    threadWriteCount.set(0);
opends/src/server/org/opends/server/config/ConfigConstants.java
@@ -766,6 +766,118 @@
  public static final int DEFAULT_FIFOCACHE_MAX_MEMORY_PCT = 90;
  /**
   * The name of the configuration attribute that contains a set of search
   * filters to use to determine which entries should be excluded from the
   * cache.
   */
  public static final String ATTR_FSCACHE_EXCLUDE_FILTER =
       NAME_PREFIX_CFG + "exclude-filter";
  /**
   * The name of the configuration attribute that contains a set of search
   * filters to use to determine which entries should be included in the cache.
   */
  public static final String ATTR_FSCACHE_INCLUDE_FILTER =
       NAME_PREFIX_CFG + "include-filter";
  /**
   * The name of the configuration attribute that indicates the maximum length
   * of time in milliseconds to spend trying to acquire a lock for an entry in
   * the cache.
   */
  public static final String ATTR_FSCACHE_LOCK_TIMEOUT =
       NAME_PREFIX_CFG + "lock-timeout";
  /**
   * The default value for the entry cache lockout timeout that will be used if
   * no other value is specified.
   */
  public static final long DEFAULT_FSCACHE_LOCK_TIMEOUT = 2000L;
  /**
   * The name of the configuration attribute that indicates the maximum number
   * of entries that the FIFO entry cache will be allowed to hold.
   */
  public static final String ATTR_FSCACHE_MAX_ENTRIES =
       NAME_PREFIX_CFG + "max-entries";
  /**
   * The default value for the entry cache max entries that will be used if no
   * other value is specified.
   */
  public static final long DEFAULT_FSCACHE_MAX_ENTRIES = Long.MAX_VALUE;
  /**
   * The name of the configuration attribute that indicates the maximum
   * memory size of the FS entry cache.
   */
  public static final String ATTR_FSCACHE_MAX_MEMORY_SIZE =
       NAME_PREFIX_CFG + "max-memory-size";
  /**
   * The name of the configuration attribute that specifies the entry cache JE
   * environment home.
   */
  public static final String ATTR_FSCACHE_HOME =
      NAME_PREFIX_CFG + "cache-directory";
  /**
   * The default value for the entry cache JE environment home that will be used
   * if no other value is specified.
   */
  public static final String DEFAULT_FSCACHE_HOME = "/tmp/OpenDS.FSCache";
  /**
   * The name of the configuration attribute that indicates the maximum
   * available space in bytes in the file system that JE cache will be
   * allowed to consume.
   */
  public static final String ATTR_FSCACHE_JE_CACHE_SIZE =
       NAME_PREFIX_CFG + "database-cache-size";
  /**
   * The default value for the JE cache size in bytes that will be used
   * if no other value is specified.
   */
  public static final long DEFAULT_FSCACHE_JE_CACHE_SIZE = 0;
  /**
   * The name of the configuration attribute that indicates the maximum
   * available memory percent that JE cache can consume.
   */
  public static final String ATTR_FSCACHE_JE_CACHE_PCT =
       NAME_PREFIX_CFG + "database-cache-percent";
  /**
   * The default value for the JE cache size percent that will be used
   * if no other value is specified.
   */
  public static final int DEFAULT_FSCACHE_JE_CACHE_PCT = 0;
  /**
   * The name of the configuration attribute that indicates whether
   * file system entry cache is configured as persistent or not.
   */
  public static final String ATTR_FSCACHE_IS_PERSISTENT =
       NAME_PREFIX_CFG + "persistent-cache";
  /**
   * The default value to indicate whether the cache is persistent or not.
   */
  public static final boolean DEFAULT_FSCACHE_IS_PERSISTENT = false;
  /**
   * The default value to indicate which cache type to use.
   */
  public static final String DEFAULT_FSCACHE_TYPE = "FIFO";
  /**
   * The name of the configuration attribute that indicates which
   * cache type will be used.
   */
  public static final String ATTR_FSCACHE_TYPE =
       NAME_PREFIX_CFG + "cache-type";
  /**
   * The name of the configuration attribute that specifies the fully-qualified
opends/src/server/org/opends/server/core/DirectoryServer.java
@@ -540,6 +540,11 @@
  // The set of backends registered with the server.
  private TreeMap<String,Backend> backends;
  // The mapping between backends and their unique indentifiers for their
  // offline state, representing either checksum or other unique value to
  // be used for detecting any offline modifications to a given backend.
  private ConcurrentHashMap<String,Long> offlineBackendsStateIDs;
  // The set of supported controls registered with the Directory Server.
  private TreeSet<String> supportedControls;
@@ -662,6 +667,8 @@
    directoryServer.monitorProviders =
         new ConcurrentHashMap<String,MonitorProvider>();
    directoryServer.backends = new TreeMap<String,Backend>();
    directoryServer.offlineBackendsStateIDs =
         new ConcurrentHashMap<String,Long>();
    directoryServer.backendInitializationListeners =
         new CopyOnWriteArraySet<BackendInitializationListener>();
    directoryServer.baseDNs = new TreeMap<DN,Backend>();
@@ -1060,11 +1067,6 @@
      initializeAlertHandlers();
      // Initialize the entry cache.
      entryCacheConfigManager = new EntryCacheConfigManager();
      entryCacheConfigManager.initializeEntryCache();
      // Initialize the key manager provider.
      keyManagerProviderConfigManager = new KeyManagerProviderConfigManager();
      keyManagerProviderConfigManager.initializeKeyManagerProviders();
@@ -1099,6 +1101,13 @@
      // Initialize all the backends and their associated suffixes.
      initializeBackends();
      // Initialize the entry cache.
      entryCacheConfigManager = new EntryCacheConfigManager();
      entryCacheConfigManager.initializeEntryCache();
      // Reset the map as we can no longer guarantee offline state.
      directoryServer.offlineBackendsStateIDs.clear();
      // Register the supported controls and supported features.
      initializeSupportedControls();
      initializeSupportedFeatures();
@@ -6018,6 +6027,37 @@
  /**
   * This method returns a map that contains a unique offline state id,
   * such as checksum, for every server backend that has registered one.
   *
   * @return  <CODE>Map</CODE> backend to checksum map for offline state.
   */
  public static Map<String,Long> getOfflineBackendsStateIDs() {
    return Collections.unmodifiableMap(directoryServer.offlineBackendsStateIDs);
  }
  /**
   * This method allows any server backend to register its unique offline
   * state, such as checksum, in a global map other server components can
   * access to determine if any changes were made to given backend while
   * offline.
   *
   * @param  backend  As returned by <CODE>getBackendID()</CODE> method.
   *
   * @param  id       Unique offline state identifier such as checksum.
   */
  public static void registerOfflineBackendStateID(String backend, long id) {
    // Zero means failed checksum so just skip it.
    if (id != 0) {
      directoryServer.offlineBackendsStateIDs.put(backend, id);
    }
  }
  /**
   * Retrieves the entire set of base DNs registered with the Directory Server,
   * mapped from the base DN to the backend responsible for that base DN.  The
   * same backend may be present multiple times, mapped from different base DNs.
@@ -8111,6 +8151,9 @@
      }
    }
    // Finalize the entry cache.
    DirectoryServer.getEntryCache().finalizeEntryCache();
    // Release the exclusive lock for the Directory Server process.
    String lockFile = LockFileManager.getServerLockFileName();
    try
opends/src/server/org/opends/server/extensions/FileSystemEntryCache.java
New file
@@ -0,0 +1,2363 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.extensions;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.SortedMap;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;
import java.io.UnsupportedEncodingException;
import java.io.File;
import com.sleepycat.bind.EntryBinding;
import com.sleepycat.bind.serial.SerialBinding;
import com.sleepycat.bind.serial.StoredClassCatalog;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.EnvironmentMutableConfig;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseNotFoundException;
import com.sleepycat.je.DbInternal;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.cleaner.UtilizationProfile;
import com.sleepycat.je.dbi.EnvironmentImpl;
import org.opends.server.api.Backend;
import org.opends.server.api.EntryCache;
import org.opends.server.admin.std.server.FileSystemEntryCacheCfg;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.protocols.asn1.ASN1Element;
import org.opends.server.protocols.asn1.ASN1OctetString;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeType;
import org.opends.server.types.AttributeValue;
import org.opends.server.types.ConfigChangeResult;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.ErrorLogCategory;
import org.opends.server.types.ErrorLogSeverity;
import org.opends.server.types.InitializationException;
import org.opends.server.types.LockType;
import org.opends.server.types.ResultCode;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.FilePermission;
import org.opends.server.types.LockManager;
import org.opends.server.types.ObjectClass;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.loggers.debug.DebugTracer;
import static org.opends.server.loggers.debug.DebugLogger.*;
import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.messages.ExtensionsMessages.*;
import static org.opends.server.messages.MessageHandler.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;
/**
 * This class defines a Directory Server entry cache that uses JE database to
 * keep track of the entries. Intended use is when JE database resides in the
 * memory based file system which has obvious performance benefits, although
 * any file system will do for this cache to function. Entries are maintained
 * either by FIFO (default) or LRU (configurable) based list implementation.
 * <BR><BR>
 * Cache sizing is based on the size or percentage of free space availble in
 * the file system, such that if enough memory is free, then adding an entry
 * to the cache will not require purging, but if more than a specified
 * percentage of the file system available space is already consumed, then
 * one or more entries will need to be removed in order to make room for a
 * new entry.  It is also possible to configure a maximum number of entries
 * for the cache. If this is specified, then the number of entries will not
 * be allowed to exceed this value, but it may not be possible to hold this
 * many entries if the available memory fills up first.
 * <BR><BR>
 * Other configurable parameters for this cache include the maximum length of
 * time to block while waiting to acquire a lock, and a set of filters that may
 * be used to define criteria for determining which entries are stored in the
 * cache.  If a filter list is provided, then only entries matching at least
 * one of the given filters will be stored in the cache.
 * <BR><BR>
 * JE environment cache size can also be configured either as percentage of
 * the free memory available in the JVM or as explicit size in bytes.
 * <BR><BR>
 * This cache has a persistence property which, if enabled, allows for the
 * contents of the cache to stay persistent across server or cache restarts.
 */
public class FileSystemEntryCache
        extends EntryCache <FileSystemEntryCacheCfg>
        implements ConfigurationChangeListener <FileSystemEntryCacheCfg> {
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();
  /**
   * The set of time units that will be used for expressing the task retention
   * time.
   */
  private static final LinkedHashMap<String, Double> timeUnits =
          new LinkedHashMap<String, Double>();
  /**
    * The set of units and their multipliers for configuration attributes
    * representing a number of bytes.
    */
  private static HashMap<String, Double> memoryUnits =
      new HashMap<String, Double>();
  // Permissions for cache db environment.
  private static final FilePermission CACHE_HOME_PERMISSIONS =
      new FilePermission(0700);
  // The DN of the configuration entry for this entry cache.
  private DN configEntryDN;
  // The set of filters that define the entries that should be excluded from the
  // cache.
  private Set<SearchFilter> excludeFilters;
  // The set of filters that define the entries that should be included in the
  // cache.
  private Set<SearchFilter> includeFilters;
  // The maximum amount of space in bytes that can be consumed in the filesystem
  // before we need to start purging entries.
  private long maxAllowedMemory;
  // The maximum number of entries that may be held in the cache.
  // Atomic for additional safely and in case we decide to push
  // some locks further down later. Does not inhere in additional
  // overhead, via blocking on synchronization primitive, on most
  // modern platforms being implemented via cpu instruction set.
  private AtomicLong maxEntries;
  // The maximum percentage of memory dedicated to JE cache.
  private int jeCachePercent;
  // The maximum amount of memory in bytes dedicated to JE cache.
  private long jeCacheSize;
  // The entry cache home folder to host db environment.
  private String cacheHome;
  // The type of this cache.
  // It can be either FIFO (default) or LRU (configurable).
  private String cacheType;
  // This regulates whether we persist the cache across restarts or not.
  private boolean persistentCache;
  // The lock used to provide threadsafe access when changing the contents
  // of the cache maps.
  private ReentrantReadWriteLock cacheLock;
  private Lock cacheReadLock;
  private Lock cacheWriteLock;
  // The maximum length of time to try to obtain a lock before giving up.
  private long lockTimeout;
  // The mapping between DNs and IDs. This is the main index map for this
  // cache, keyed to the underlying JE database where entries are stored.
  private Map<DN,Long> dnMap;
  // The mapping between entry backends/IDs and DNs to identify all
  // entries that belong to given backend since entry ID is only
  // per backend unique.
  private Map<Backend,Map<Long,DN>> backendMap;
  // Access order for this cache. FIFO by default.
  boolean accessOrder = false;
  // JE environment and database related fields for this cache.
  private Environment entryCacheEnv;
  private EnvironmentConfig entryCacheEnvConfig;
  private EnvironmentMutableConfig entryCacheEnvMutableConfig;
  private DatabaseConfig entryCacheDBConfig;
  // The main entry cache database.
  private Database entryCacheDB;
  // Class database, catalog and binding for serialization.
  private Database entryCacheClassDB;
  private StoredClassCatalog classCatalog;
  private EntryBinding entryCacheDataBinding;
  // JE naming constants.
  private static final String ENTRYCACHEDBNAME = "EntryCacheDB";
  private static final String INDEXCLASSDBNAME = "IndexClassDB";
  private static final String INDEXKEY = "EntryCacheIndex";
  // JE config constants.
  // TODO: All hardcoded for now but we need to use a common
  // ds-cfg-je-property like multi-valued attribute for this, see Issue 1481.
  private static final Long JEBYTESINTERVAL = 10485760L;
  private static final Long JELOGFILEMAX = 10485760L;
  private static final Integer JEMINFILEUTILIZATION = 50;
  private static final Integer JEMINUTILIZATION = 90;
  private static final Integer JEMAXBATCHFILES = 1;
  private static final Integer JEMINAGE = 1;
  // The number of milliseconds between persistent state save/restore
  // progress reports.
  private long progressInterval = 5000;
  // Persistent state save/restore progress report counters.
  private long persistentEntriesSaved    = 0;
  private long persistentEntriesRestored = 0;
  /**
   * Creates a new instance of this entry cache.
   */
  public FileSystemEntryCache() {
    super();
    // All initialization should be performed in the initializeEntryCache.
  }
  /**
   * {@inheritDoc}
   */
  public void initializeEntryCache(FileSystemEntryCacheCfg configuration)
          throws ConfigException, InitializationException {
    configuration.addFileSystemChangeListener (this);
    configEntryDN = configuration.dn();
    // Read and apply configuration.
    boolean applyChanges = true;
    EntryCacheCommon.ConfigErrorHandler errorHandler =
      EntryCacheCommon.getConfigErrorHandler (
          EntryCacheCommon.ConfigPhase.PHASE_INIT, null, null
          );
    processEntryCacheConfig(configuration, applyChanges, errorHandler);
    // Set the cache type.
    if (cacheType.equalsIgnoreCase("LRU")) {
      accessOrder = true;
    } else {
      // Admin framework should only allow for either FIFO or LRU but
      // we set the type to default here explicitly if it is not LRU.
      cacheType = DEFAULT_FSCACHE_TYPE;
      accessOrder = false;
    }
    // Initialize the cache maps and locks.
    backendMap = new LinkedHashMap<Backend,Map<Long,DN>>();
    dnMap = new LinkedHashMapRotator<DN,Long>((int) 16, (float) 0.75,
        (boolean) accessOrder);
    cacheLock = new ReentrantReadWriteLock();
    if (accessOrder) {
      // In access-ordered linked hash maps, merely querying the map
      // with get() is a structural modification.
      cacheReadLock = cacheLock.writeLock();
    } else {
      cacheReadLock = cacheLock.readLock();
    }
    cacheWriteLock = cacheLock.writeLock();
    // Setup the cache home.
    try {
      checkAndSetupCacheHome(cacheHome);
    } catch (Exception e) {
      if (debugEnabled()) {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      // Log an error message.
      logError(ErrorLogCategory.CONFIGURATION,
          ErrorLogSeverity.SEVERE_ERROR,
          MSGID_FSCACHE_INVALID_HOME,
          String.valueOf(configEntryDN), stackTraceToSingleLineString(e),
          cacheHome, DEFAULT_FSCACHE_HOME);
      // User specified home is no good, reset to default.
      cacheHome = DEFAULT_FSCACHE_HOME;
      // Try again.
      try {
        checkAndSetupCacheHome(cacheHome);
      } catch (Exception e2) {
        // Not having any home for the cache db environment at this point is a
        // fatal error as we are unable to continue any further without it.
        if (debugEnabled()) {
          TRACER.debugCaught(DebugLogLevel.ERROR, e2);
        }
        int msgID = MSGID_FSCACHE_HOMELESS;
        String message = getMessage(msgID, stackTraceToSingleLineString(e2));
        throw new InitializationException(msgID, message, e2);
      }
    }
    // Open JE environment and cache database.
    try {
      entryCacheEnvConfig = new EnvironmentConfig();
      // All these environment properties are cranked up to their extreme
      // values, either max or min, to get the smallest space consumption,
      // which turns into memory consumption for memory based filesystems,
      // possible. This will negate the performance somewhat but preserves
      // the memory to a much greater extent.
      //
      // TODO: All these options should be configurable, see Issue 1481.
      //
      entryCacheEnvConfig.setConfigParam("je.log.fileMax",
          JELOGFILEMAX.toString());
      entryCacheEnvConfig.setConfigParam("je.cleaner.minUtilization",
          JEMINUTILIZATION.toString());
      entryCacheEnvConfig.setConfigParam("je.cleaner.maxBatchFiles",
          JEMAXBATCHFILES.toString());
      entryCacheEnvConfig.setConfigParam("je.cleaner.minAge",
          JEMINAGE.toString());
      entryCacheEnvConfig.setConfigParam("je.cleaner.minFileUtilization",
          JEMINFILEUTILIZATION.toString());
      entryCacheEnvConfig.setConfigParam("je.checkpointer.bytesInterval",
          JEBYTESINTERVAL.toString());
      entryCacheEnvConfig.setAllowCreate(true);
      entryCacheEnv = new Environment(new File(cacheHome), entryCacheEnvConfig);
      // Set JE cache percent and size where the size value will prevail if set.
      entryCacheEnvMutableConfig = new EnvironmentMutableConfig();
      if (jeCachePercent != 0) {
        try {
          entryCacheEnvMutableConfig.setCachePercent(jeCachePercent);
        } catch (IllegalArgumentException e) {
          if (debugEnabled()) {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }
          // Its safe to ignore and continue here, JE will use its default
          // value for this however we have to let the user know about it
          // so just log an error message.
          logError(ErrorLogCategory.CONFIGURATION,
              ErrorLogSeverity.SEVERE_ERROR,
              MSGID_FSCACHE_CANNOT_SET_JE_MEMORY_PCT,
              String.valueOf(configEntryDN), stackTraceToSingleLineString(e));
        }
      }
      if (jeCacheSize != 0) {
        try {
          entryCacheEnvMutableConfig.setCacheSize(jeCacheSize);
        } catch (IllegalArgumentException e) {
          if (debugEnabled()) {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }
          // Its safe to ignore and continue here, JE will use its default
          // value for this however we have to let the user know about it
          // so just log an error message.
          logError(ErrorLogCategory.CONFIGURATION,
              ErrorLogSeverity.SEVERE_ERROR,
              MSGID_FSCACHE_CANNOT_SET_JE_MEMORY_SIZE,
              String.valueOf(configEntryDN), stackTraceToSingleLineString(e));
        }
      }
      entryCacheEnv.setMutableConfig(entryCacheEnvMutableConfig);
      entryCacheDBConfig = new DatabaseConfig();
      entryCacheDBConfig.setAllowCreate(true);
      // Remove old cache databases if this cache is not persistent.
      if ( !persistentCache ) {
        try {
          entryCacheEnv.removeDatabase(null, INDEXCLASSDBNAME);
        } catch (DatabaseNotFoundException e) {}
        try {
          entryCacheEnv.removeDatabase(null, ENTRYCACHEDBNAME);
        } catch (DatabaseNotFoundException e) {}
      }
      entryCacheDB = entryCacheEnv.openDatabase(null,
              ENTRYCACHEDBNAME, entryCacheDBConfig);
      entryCacheClassDB =
        entryCacheEnv.openDatabase(null, INDEXCLASSDBNAME, entryCacheDBConfig);
      // Instantiate the class catalog
      classCatalog = new StoredClassCatalog(entryCacheClassDB);
      entryCacheDataBinding =
          new SerialBinding(classCatalog, FileSystemEntryCacheIndex.class);
      // Restoration is static and not subject to the current configuration
      // constraints so that the persistent state is truly preserved and
      // restored to the exact same state where we left off when the cache
      // has been made persistent. The only exception to this is the backend
      // offline state matching where entries that belong to backend which
      // we cannot match offline state for are discarded from the cache.
      if ( persistentCache ) {
        // Retrieve cache index.
        try {
          FileSystemEntryCacheIndex entryCacheIndex;
          DatabaseEntry indexData = new DatabaseEntry();
          DatabaseEntry indexKey = new DatabaseEntry(
              INDEXKEY.getBytes("UTF-8"));
          if (OperationStatus.SUCCESS ==
              entryCacheDB.get(null, indexKey, indexData, LockMode.DEFAULT)) {
            entryCacheIndex =
                (FileSystemEntryCacheIndex)
                entryCacheDataBinding.entryToObject(indexData);
          } else {
            throw new CacheIndexNotFoundException();
          }
          // Check cache index state.
          if ((entryCacheIndex.dnMap.isEmpty()) ||
              (entryCacheIndex.backendMap.isEmpty()) ||
              (entryCacheIndex.offlineState.isEmpty())) {
            throw new CacheIndexImpairedException();
          } else {
            // Restore entry cache maps from this index.
            // Push maxEntries and make it unlimited til restoration complete.
            AtomicLong currentMaxEntries = maxEntries;
            maxEntries.set(DEFAULT_FSCACHE_MAX_ENTRIES);
            // Convert cache index maps to entry cache maps.
            Set<String> backendSet = entryCacheIndex.backendMap.keySet();
            Iterator<String> backendIterator = backendSet.iterator();
            // Start a timer for the progress report.
            final long persistentEntriesTotal = entryCacheIndex.dnMap.size();
            Timer timer = new Timer();
            TimerTask progressTask = new TimerTask() {
              // Persistent state restore progress report.
              public void run() {
                if ((persistentEntriesRestored > 0) &&
                    (persistentEntriesRestored < persistentEntriesTotal)) {
                  int msgID = MSGID_FSCACHE_RESTORE_PROGRESS_REPORT;
                  String message = getMessage(msgID, persistentEntriesRestored,
                      persistentEntriesTotal);
                  logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.NOTICE,
                      message, msgID);
                }
              }
            };
            timer.scheduleAtFixedRate(progressTask, progressInterval,
                                      progressInterval);
            try {
              while (backendIterator.hasNext()) {
                String backend = backendIterator.next();
                Map<Long,String> entriesMap =
                    entryCacheIndex.backendMap.get(backend);
                Set<Long> entriesSet = entriesMap.keySet();
                Iterator<Long> entriesIterator = entriesSet.iterator();
                LinkedHashMap<Long,DN> entryMap = new LinkedHashMap<Long,DN>();
                while (entriesIterator.hasNext()) {
                  Long entryID = entriesIterator.next();
                  String entryStringDN = entriesMap.get(entryID);
                  DN entryDN = DN.decode(entryStringDN);
                  dnMap.put(entryDN, entryID);
                  entryMap.put(entryID, entryDN);
                  persistentEntriesRestored++;
                }
                backendMap.put(DirectoryServer.getBackend(backend), entryMap);
              }
            } finally {
              // Stop persistent state restore progress report timer.
              timer.cancel();
              // Final persistent state restore progress report.
              int msgID = MSGID_FSCACHE_RESTORE_PROGRESS_REPORT;
              String message = getMessage(msgID, persistentEntriesRestored,
                      persistentEntriesTotal);
              logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.NOTICE,
                      message, msgID);
            }
            // Compare last known offline states to offline states on startup.
            Map<String,Long> currentBackendsState =
                DirectoryServer.getOfflineBackendsStateIDs();
            Set<String> offlineBackendSet =
                entryCacheIndex.offlineState.keySet();
            Iterator<String> offlineBackendIterator =
                offlineBackendSet.iterator();
            while (offlineBackendIterator.hasNext()) {
              String backend = offlineBackendIterator.next();
              Long offlineId = entryCacheIndex.offlineState.get(backend);
              Long currentId = currentBackendsState.get(backend);
              if ( !(offlineId.equals(currentId)) ) {
                // Remove cache entries specific to this backend.
                clearBackend(DirectoryServer.getBackend(backend));
                // Log an error message.
                logError(ErrorLogCategory.EXTENSIONS,
                    ErrorLogSeverity.SEVERE_WARNING,
                    MSGID_FSCACHE_OFFLINE_STATE_FAIL,
                    backend);
              }
            }
            // Pop max entries limit.
            maxEntries = currentMaxEntries;
          }
        } catch (CacheIndexNotFoundException e) {
          if (debugEnabled()) {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }
          // Log an error message.
          logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.NOTICE,
              MSGID_FSCACHE_INDEX_NOT_FOUND);
          // Clear the entry cache.
          clear();
        } catch (CacheIndexImpairedException e) {
          if (debugEnabled()) {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }
          // Log an error message.
          logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.SEVERE_ERROR,
              MSGID_FSCACHE_INDEX_IMPAIRED);
          // Clear the entry cache.
          clear();
        } catch (Exception e) {
          if (debugEnabled()) {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }
          // Log an error message.
          logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.SEVERE_ERROR,
              MSGID_FSCACHE_CANNOT_LOAD_PERSISTENT_DATA,
              stackTraceToSingleLineString(e));
          // Clear the entry cache.
          clear();
        }
      }
    } catch (Exception e) {
      // If we got here it means we have failed to have a proper backend
      // for this entry cache and there is absolutely no point going any
      // farther from here.
      if (debugEnabled()) {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      int msgID = MSGID_FSCACHE_CANNOT_INITIALIZE;
      String message = getMessage(msgID, stackTraceToSingleLineString(e));
      throw new InitializationException(msgID, message, e);
    }
  }
  /**
   * {@inheritDoc}
   */
  public void finalizeEntryCache() {
    cacheWriteLock.lock();
    // Store index/maps in case of persistent cache. Since the cache database
    // already exist at this point all we have to do is to serialize cache
    // index maps @see FileSystemEntryCacheIndex and put them under indexkey
    // allowing for the index to be restored and cache contents reused upon
    // the next initialization.
    if (persistentCache) {
      FileSystemEntryCacheIndex entryCacheIndex =
          new FileSystemEntryCacheIndex();
      // There must be at least one backend at this stage.
      entryCacheIndex.offlineState =
          DirectoryServer.getOfflineBackendsStateIDs();
      // Convert entry cache maps to serializable maps for the cache index.
      Set<Backend> backendSet = backendMap.keySet();
      Iterator<Backend> backendIterator = backendSet.iterator();
      // Start a timer for the progress report.
      final long persistentEntriesTotal = dnMap.size();
      Timer timer = new Timer();
      TimerTask progressTask = new TimerTask() {
        // Persistent state save progress report.
        public void run() {
          if ((persistentEntriesSaved > 0) &&
              (persistentEntriesSaved < persistentEntriesTotal)) {
            int msgID = MSGID_FSCACHE_SAVE_PROGRESS_REPORT;
            String message = getMessage(msgID, persistentEntriesSaved,
                persistentEntriesTotal);
            logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.NOTICE,
                message, msgID);
          }
        }
      };
      timer.scheduleAtFixedRate(progressTask, progressInterval,
          progressInterval);
      try {
        while (backendIterator.hasNext()) {
          Backend backend = backendIterator.next();
          Map<Long,DN> entriesMap = backendMap.get(backend);
          Map<Long,String> entryMap = new LinkedHashMap<Long,String>();
          for (Long entryID : entriesMap.keySet()) {
            DN entryDN = entriesMap.get(entryID);
            entryCacheIndex.dnMap.put(entryDN.toNormalizedString(), entryID);
            entryMap.put(entryID, entryDN.toNormalizedString());
            persistentEntriesSaved++;
          }
          entryCacheIndex.backendMap.put(backend.getBackendID(), entryMap);
        }
      } finally {
        // Stop persistent state save progress report timer.
        timer.cancel();
        // Final persistent state save progress report.
        int msgID = MSGID_FSCACHE_SAVE_PROGRESS_REPORT;
        String message = getMessage(msgID, persistentEntriesSaved,
            persistentEntriesTotal);
        logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.NOTICE,
            message, msgID);
      }
      // Store the index.
      try {
        DatabaseEntry indexData = new DatabaseEntry();
        entryCacheDataBinding.objectToEntry(entryCacheIndex, indexData);
        DatabaseEntry indexKey = new DatabaseEntry(INDEXKEY.getBytes("UTF-8"));
        if (OperationStatus.SUCCESS !=
            entryCacheDB.put(null, indexKey, indexData)) {
          throw new Exception();
        }
      } catch (Exception e) {
        if (debugEnabled()) {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }
        // Log an error message.
        logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.SEVERE_ERROR,
            MSGID_FSCACHE_CANNOT_STORE_PERSISTENT_DATA,
            stackTraceToSingleLineString(e));
      }
    }
    // Close JE databases and environment and clear all the maps.
    try {
      backendMap.clear();
      dnMap.clear();
      if (entryCacheDB != null) {
        entryCacheDB.close();
      }
      if (entryCacheClassDB != null) {
        entryCacheClassDB.close();
      }
      if (entryCacheEnv != null) {
        // Remove cache and index dbs if this cache is not persistent.
        if ( !persistentCache ) {
          try {
            entryCacheEnv.removeDatabase(null, INDEXCLASSDBNAME);
          } catch (DatabaseNotFoundException e) {}
          try {
            entryCacheEnv.removeDatabase(null, ENTRYCACHEDBNAME);
          } catch (DatabaseNotFoundException e) {}
        }
        entryCacheEnv.cleanLog();
        entryCacheEnv.close();
      }
    } catch (Exception e) {
      if (debugEnabled()) {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      // That is ok, JE verification and repair on startup should take care of
      // this so if there are any unrecoverable errors during next startup
      // and we are unable to handle and cleanup them we will log errors then.
    } finally {
      cacheWriteLock.unlock();
    }
  }
  /**
   * {@inheritDoc}
   */
  public boolean containsEntry(DN entryDN)
  {
    // Indicate whether the DN map contains the specified DN.
    boolean containsEntry = false;
    cacheReadLock.lock();
    try {
      containsEntry = dnMap.containsKey(entryDN);
    } finally {
      cacheReadLock.unlock();
    }
    return containsEntry;
  }
  /**
   * {@inheritDoc}
   */
  public Entry getEntry(DN entryDN) {
    // Get the entry from the DN map if it is present.  If not, then return
    // null.
    Entry entry = null;
    cacheReadLock.lock();
    try {
      if (dnMap.containsKey(entryDN)) {
        entry = getEntryFromDB(entryDN);
      }
    } finally {
      cacheReadLock.unlock();
    }
    return entry;
  }
  /**
   * {@inheritDoc}
   */
  public long getEntryID(DN entryDN) {
    long entryID = -1;
    cacheReadLock.lock();
    try {
      Long eid = dnMap.get(entryDN);
      if (eid != null) {
        entryID = eid.longValue();
      }
    } finally {
      cacheReadLock.unlock();
    }
    return entryID;
  }
  /**
   * {@inheritDoc}
   */
  public Entry getEntry(DN entryDN, LockType lockType, List<Lock> lockList) {
    Entry entry = getEntry(entryDN);
    if (entry == null)
    {
      return null;
    }
    // Obtain a lock for the entry as appropriate.  If an error occurs, then
    // make sure no lock is held and return null.  Otherwise, return the entry.
    switch (lockType)
    {
      case READ:
        // Try to obtain a read lock for the entry.
        Lock readLock = LockManager.lockRead(entryDN, lockTimeout);
        if (readLock == null)
        {
          // We couldn't get the lock, so we have to return null.
          return null;
        }
        else
        {
          try
          {
            lockList.add(readLock);
            return entry;
          }
          catch (Exception e)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }
            // The attempt to add the lock to the list failed, so we need to
            // release the lock and return null.
            try
            {
              LockManager.unlock(entryDN, readLock);
            }
            catch (Exception e2)
            {
              if (debugEnabled())
              {
                TRACER.debugCaught(DebugLogLevel.ERROR, e2);
              }
            }
            return null;
          }
        }
      case WRITE:
        // Try to obtain a write lock for the entry.
        Lock writeLock = LockManager.lockWrite(entryDN, lockTimeout);
        if (writeLock == null)
        {
          // We couldn't get the lock, so we have to return null.
          return null;
        }
        else
        {
          try
          {
            lockList.add(writeLock);
            return entry;
          }
          catch (Exception e)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }
            // The attempt to add the lock to the list failed, so we need to
            // release the lock and return null.
            try
            {
              LockManager.unlock(entryDN, writeLock);
            }
            catch (Exception e2)
            {
              if (debugEnabled())
              {
                TRACER.debugCaught(DebugLogLevel.ERROR, e2);
              }
            }
            return null;
          }
        }
      case NONE:
        // We don't need to obtain a lock, so just return the entry.
        return entry;
      default:
        // This is an unknown type of lock, so we'll return null.
        return null;
    }
  }
  /**
   * Retrieves the requested entry if it is present in the cache.
   *
   * @param  backend   The backend associated with the entry to retrieve.
   * @param  entryID   The entry ID within the provided backend for the
   *                   specified entry.
   *
   * @return  The requested entry if it is present in the cache, or
   *          <CODE>null</CODE> if it is not present.
   */
  public Entry getEntry(Backend backend, long entryID) {
    Entry entry = null;
    cacheReadLock.lock();
    try {
      // Get the map for the provided backend.  If it isn't present, then
      // return null.
      Map map = backendMap.get(backend);
      if ( !(map == null) ) {
        // Get the entry from the map by its ID.  If it isn't present, then
        // return null.
        DN dn = (DN) map.get(entryID);
        if ( !(dn == null) ) {
          if (dnMap.containsKey(dn)) {
            entry = getEntryFromDB(dn);
          }
        }
      }
    } finally {
      cacheReadLock.unlock();
    }
    return entry;
  }
  /**
   * {@inheritDoc}
   */
  public Entry getEntry(Backend backend, long entryID, LockType lockType,
          List<Lock> lockList) {
    Entry entry = getEntry(backend, entryID);
    if (entry == null)
    {
      return null;
    }
    // Obtain a lock for the entry as appropriate.  If an error occurs, then
    // make sure no lock is held and return null.  Otherwise, return the entry.
    switch (lockType)
    {
      case READ:
        // Try to obtain a read lock for the entry.
        Lock readLock = LockManager.lockRead(entry.getDN(), lockTimeout);
        if (readLock == null)
        {
          // We couldn't get the lock, so we have to return null.
          return null;
        }
        else
        {
          try
          {
            lockList.add(readLock);
            return entry;
          }
          catch (Exception e)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }
            // The attempt to add the lock to the list failed, so we need to
            // release the lock and return null.
            try
            {
              LockManager.unlock(entry.getDN(), readLock);
            }
            catch (Exception e2)
            {
              if (debugEnabled())
              {
                TRACER.debugCaught(DebugLogLevel.ERROR, e2);
              }
            }
            return null;
          }
        }
      case WRITE:
        // Try to obtain a write lock for the entry.
        Lock writeLock = LockManager.lockWrite(entry.getDN(), lockTimeout);
        if (writeLock == null)
        {
          // We couldn't get the lock, so we have to return null.
          return null;
        }
        else
        {
          try
          {
            lockList.add(writeLock);
            return entry;
          }
          catch (Exception e)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }
            // The attempt to add the lock to the list failed, so we need to
            // release the lock and return null.
            try
            {
              LockManager.unlock(entry.getDN(), writeLock);
            }
            catch (Exception e2)
            {
              if (debugEnabled())
              {
                TRACER.debugCaught(DebugLogLevel.ERROR, e2);
              }
            }
            return null;
          }
        }
      case NONE:
        // We don't need to obtain a lock, so just return the entry.
        return entry;
      default:
        // This is an unknown type of lock, so we'll return null.
        return null;
    }
  }
  /**
   * {@inheritDoc}
   */
  public void putEntry(Entry entry, Backend backend, long entryID) {
    // If there is a set of exclude filters, then make sure that the provided
    // entry doesn't match any of them.
    if (! excludeFilters.isEmpty()) {
      for (SearchFilter f : excludeFilters) {
        try {
          if (f.matchesEntry(entry)) {
            return;
          }
        } catch (Exception e) {
          if (debugEnabled()) {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }
          // This shouldn't happen, but if it does then we can't be sure whether
          // the entry should be excluded, so we will by default.
          return;
        }
      }
    }
    // If there is a set of include filters, then make sure that the provided
    // entry matches at least one of them.
    if (! includeFilters.isEmpty()) {
      boolean matchFound = false;
      for (SearchFilter f : includeFilters) {
        try {
          if (f.matchesEntry(entry)) {
            matchFound = true;
            break;
          }
        } catch (Exception e) {
          if (debugEnabled()) {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }
          // This shouldn't happen, but if it does, then just ignore it.
        }
      }
      if (! matchFound) {
        return;
      }
    }
    // Obtain a lock on the cache.  If this fails, then don't do anything.
    try {
      if (!cacheWriteLock.tryLock(lockTimeout, TimeUnit.MILLISECONDS)) {
        return;
      }
      putEntryToDB(entry, backend, entryID);
    } catch (Exception e) {
      if (debugEnabled()) {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      return;
    } finally {
      cacheWriteLock.unlock();
    }
  }
  /**
   * {@inheritDoc}
   */
  public boolean putEntryIfAbsent(Entry entry, Backend backend, long entryID)
  {
    // If there is a set of exclude filters, then make sure that the provided
    // entry doesn't match any of them.
    if (! excludeFilters.isEmpty()) {
      for (SearchFilter f : excludeFilters) {
        try {
          if (f.matchesEntry(entry)) {
            return true;
          }
        } catch (Exception e) {
          if (debugEnabled()) {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }
          // This shouldn't happen, but if it does then we can't be sure whether
          // the entry should be excluded, so we will by default.
          return false;
        }
      }
    }
    // If there is a set of include filters, then make sure that the provided
    // entry matches at least one of them.
    if (! includeFilters.isEmpty()) {
      boolean matchFound = false;
      for (SearchFilter f : includeFilters) {
        try {
          if (f.matchesEntry(entry)) {
            matchFound = true;
            break;
          }
        } catch (Exception e) {
          if (debugEnabled()) {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }
          // This shouldn't happen, but if it does, then just ignore it.
        }
      }
      if (! matchFound) {
        return true;
      }
    }
    try {
      // Obtain a lock on the cache.  If this fails, then don't do anything.
      if (! cacheWriteLock.tryLock(lockTimeout, TimeUnit.MILLISECONDS)) {
        // We can't rule out the possibility of a conflict, so return false.
        return false;
      }
      // See if the entry already exists in the cache.  If it does, then we will
      // fail and not actually store the entry.
      if (dnMap.containsKey(entry.getDN())) {
        return false;
      }
      return putEntryToDB(entry, backend, entryID);
    } catch (Exception e) {
      if (debugEnabled()) {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      // We can't rule out the possibility of a conflict, so return false.
      return false;
    } finally {
      cacheWriteLock.unlock();
    }
  }
  /**
   * {@inheritDoc}
   */
  public void removeEntry(DN entryDN) {
    cacheWriteLock.lock();
    try {
      Long entryID = dnMap.get(entryDN);
      if (entryID == null) {
        return;
      }
      for (Map<Long,DN> map : backendMap.values()) {
        if ((map.get(entryID) != null) &&
            (map.get(entryID).equals(entryDN))) {
          map.remove(entryID);
        }
      }
      dnMap.remove(entryDN);
      entryCacheDB.delete(null,
        new DatabaseEntry(entryDN.toNormalizedString().getBytes("UTF-8")));
    } catch (Exception e) {
      if (debugEnabled()) {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
    } finally {
      cacheWriteLock.unlock();
    }
  }
  /**
   * {@inheritDoc}
   */
  public void clear() {
    cacheWriteLock.lock();
    dnMap.clear();
    backendMap.clear();
    try {
      if ((entryCacheDB != null) && (entryCacheEnv != null) &&
          (entryCacheClassDB != null) && (entryCacheDBConfig != null)) {
        entryCacheDBConfig = entryCacheDB.getConfig();
        entryCacheDB.close();
        entryCacheClassDB.close();
        entryCacheEnv.truncateDatabase(null, ENTRYCACHEDBNAME, false);
        entryCacheEnv.truncateDatabase(null, INDEXCLASSDBNAME, false);
        entryCacheEnv.cleanLog();
        entryCacheDB = entryCacheEnv.openDatabase(null,
            ENTRYCACHEDBNAME, entryCacheDBConfig);
        entryCacheClassDB = entryCacheEnv.openDatabase(null,
            INDEXCLASSDBNAME, entryCacheDBConfig);
        // Instantiate the class catalog
        classCatalog = new StoredClassCatalog(entryCacheClassDB);
        entryCacheDataBinding =
            new SerialBinding(classCatalog, FileSystemEntryCacheIndex.class);
      }
    } catch (Exception e) {
      if (debugEnabled()) {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
    } finally {
      cacheWriteLock.unlock();
    }
  }
  /**
   * {@inheritDoc}
   */
  public void clearBackend(Backend backend) {
    cacheWriteLock.lock();
    Map<Long,DN> backendEntriesMap = backendMap.get(backend);
    try {
      int entriesExamined = 0;
      Set<Long> entriesSet = backendEntriesMap.keySet();
      Iterator<Long> backendEntriesIterator = entriesSet.iterator();
      while (backendEntriesIterator.hasNext()) {
        long entryID = backendEntriesIterator.next();
        DN entryDN = backendEntriesMap.get(entryID);
        entryCacheDB.delete(null,
            new DatabaseEntry(entryDN.toNormalizedString().getBytes("UTF-8")));
        dnMap.remove(entryDN);
        // This can take a while, so we'll periodically release and re-acquire
        // the lock in case anyone else is waiting on it so this doesn't become
        // a stop-the-world event as far as the cache is concerned.
        entriesExamined++;
        if ((entriesExamined % 1000) == 0) {
          cacheWriteLock.unlock();
          Thread.currentThread().yield();
          cacheWriteLock.lock();
        }
      }
    } catch (Exception e) {
      if (debugEnabled()) {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
    } finally {
      backendMap.remove(backend);
      cacheWriteLock.unlock();
    }
  }
  /**
   * {@inheritDoc}
   */
  public void clearSubtree(DN baseDN) {
    // Determine which backend should be used for the provided base DN.  If
    // there is none, then we don't need to do anything.
    Backend backend = DirectoryServer.getBackend(baseDN);
    if (backend == null)
    {
      return;
    }
    // Acquire a lock on the cache.  We should not return until the cache has
    // been cleared, so we will block until we can obtain the lock.
    cacheWriteLock.lock();
    // At this point, it is absolutely critical that we always release the lock
    // before leaving this method, so do so in a finally block.
    try
    {
      clearSubtree(baseDN, backend);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      // This shouldn't happen, but there's not much that we can do if it does.
    }
    finally
    {
      cacheWriteLock.unlock();
    }
  }
  /**
   * Clears all entries at or below the specified base DN that are associated
   * with the given backend.  The caller must already hold the cache lock.
   *
   * @param  baseDN   The base DN below which all entries should be flushed.
   * @param  backend  The backend for which to remove the appropriate entries.
   */
  private void clearSubtree(DN baseDN, Backend backend) {
    // See if there are any entries for the provided backend in the cache.  If
    // not, then return.
    Map<Long,DN> map = backendMap.get(backend);
    if (map == null)
    {
      // No entries were in the cache for this backend, so we can return without
      // doing anything.
      return;
    }
    // Since the provided base DN could hold a subset of the information in the
    // specified backend, we will have to do this by iterating through all the
    // entries for that backend.  Since this could take a while, we'll
    // periodically release and re-acquire the lock in case anyone else is
    // waiting on it so this doesn't become a stop-the-world event as far as the
    // cache is concerned.
    int entriesExamined = 0;
    Iterator<DN> iterator = map.values().iterator();
    while (iterator.hasNext())
    {
      DN entryDN = iterator.next();
      if (entryDN.isDescendantOf(baseDN))
      {
        iterator.remove();
        dnMap.remove(entryDN);
        try {
          entryCacheDB.delete(null,
              new DatabaseEntry(
              entryDN.toNormalizedString().getBytes("UTF-8")));
        } catch (Exception e) {
          if (debugEnabled()) {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }
        }
      }
      entriesExamined++;
      if ((entriesExamined % 1000) == 0)
      {
        cacheWriteLock.unlock();
        Thread.currentThread().yield();
        cacheWriteLock.lock();
      }
    }
    // See if the backend has any subordinate backends.  If so, then process
    // them recursively.
    for (Backend subBackend : backend.getSubordinateBackends())
    {
      boolean isAppropriate = false;
      for (DN subBase : subBackend.getBaseDNs())
      {
        if (subBase.isDescendantOf(baseDN))
        {
          isAppropriate = true;
          break;
        }
      }
      if (isAppropriate)
      {
        clearSubtree(baseDN, subBackend);
      }
    }
  }
  /**
   * {@inheritDoc}
   */
  public void handleLowMemory() {
    // This is about all we can do.
    if (entryCacheEnv != null) {
      try {
        // Free some JVM memory.
        entryCacheEnv.evictMemory();
        // Free some main memory/space.
        entryCacheEnv.cleanLog();
      } catch (Exception e) {
        if (debugEnabled()) {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }
      }
    }
  }
  /**
   * {@inheritDoc}
   */
  public boolean isConfigurationChangeAcceptable(
      FileSystemEntryCacheCfg configuration,
      List<String>      unacceptableReasons
      )
  {
    // Make sure that we can process the defined character sets.  If so, then
    // we'll accept the new configuration.
    boolean applyChanges = false;
    EntryCacheCommon.ConfigErrorHandler errorHandler =
      EntryCacheCommon.getConfigErrorHandler (
          EntryCacheCommon.ConfigPhase.PHASE_ACCEPTABLE,
          unacceptableReasons,
          null
        );
    processEntryCacheConfig (configuration, applyChanges, errorHandler);
    return errorHandler.getIsAcceptable();
  }
  /**
   * {@inheritDoc}
   */
  public ConfigChangeResult applyConfigurationChange(
      FileSystemEntryCacheCfg configuration
      )
  {
    // Make sure that we can process the defined character sets.  If so, then
    // activate the new configuration.
    boolean applyChanges = false;
    ArrayList<String> errorMessages = new ArrayList<String>();
    EntryCacheCommon.ConfigErrorHandler errorHandler =
      EntryCacheCommon.getConfigErrorHandler (
          EntryCacheCommon.ConfigPhase.PHASE_APPLY, null, errorMessages
          );
    processEntryCacheConfig (configuration, applyChanges, errorHandler);
    boolean adminActionRequired = false;
    ConfigChangeResult changeResult = new ConfigChangeResult(
        errorHandler.getResultCode(),
        adminActionRequired,
        errorHandler.getErrorMessages()
        );
    return changeResult;
  }
  /**
   * Makes a best-effort attempt to apply the configuration contained in the
   * provided entry.  Information about the result of this processing should be
   * added to the provided message list.  Information should always be added to
   * this list if a configuration change could not be applied.  If detailed
   * results are requested, then information about the changes applied
   * successfully (and optionally about parameters that were not changed) should
   * also be included.
   *
   * @param  configuration    The entry containing the new configuration to
   *                          apply for this component.
   * @param  detailedResults  Indicates whether detailed information about the
   *                          processing should be added to the list.
   *
   * @return  Information about the result of the configuration update.
   */
  public ConfigChangeResult applyNewConfiguration(
      FileSystemEntryCacheCfg configuration,
      boolean           detailedResults
      )
  {
    // Store the current value to detect changes.
    long                  prevLockTimeout      = lockTimeout;
    long                  prevMaxEntries       = maxEntries.longValue();
    Set<SearchFilter>     prevIncludeFilters   = includeFilters;
    Set<SearchFilter>     prevExcludeFilters   = excludeFilters;
    long                  prevMaxAllowedMemory = maxAllowedMemory;
    int                   prevJECachePercent   = jeCachePercent;
    long                  prevJECacheSize      = jeCacheSize;
    boolean               prevPersistentCache  = persistentCache;
    // Activate the new configuration.
    ConfigChangeResult changeResult = applyConfigurationChange(configuration);
    // Add detailed messages if needed.
    ResultCode resultCode = changeResult.getResultCode();
    boolean configIsAcceptable = (resultCode == ResultCode.SUCCESS);
    if (detailedResults && configIsAcceptable)
    {
      if (maxEntries.longValue() != prevMaxEntries)
      {
        changeResult.addMessage(
            getMessage (MSGID_FSCACHE_UPDATED_MAX_ENTRIES, maxEntries));
      }
      if (lockTimeout != prevLockTimeout)
      {
        changeResult.addMessage(
            getMessage (MSGID_FSCACHE_UPDATED_LOCK_TIMEOUT, lockTimeout));
      }
      if (!includeFilters.equals(prevIncludeFilters))
      {
        changeResult.addMessage(
            getMessage (MSGID_FSCACHE_UPDATED_INCLUDE_FILTERS));
      }
      if (!excludeFilters.equals(prevExcludeFilters))
      {
        changeResult.addMessage(
            getMessage (MSGID_FSCACHE_UPDATED_EXCLUDE_FILTERS));
      }
      if (maxAllowedMemory != prevMaxAllowedMemory)
      {
        changeResult.addMessage(
            getMessage (MSGID_FSCACHE_UPDATED_MAX_MEMORY_SIZE,
            maxAllowedMemory));
      }
      if (jeCachePercent != prevJECachePercent)
      {
        changeResult.addMessage(
            getMessage (MSGID_FSCACHE_UPDATED_JE_MEMORY_PCT, jeCachePercent));
      }
      if (jeCacheSize != prevJECacheSize)
      {
        changeResult.addMessage(
            getMessage (MSGID_FSCACHE_UPDATED_JE_MEMORY_SIZE, jeCacheSize));
      }
      if (persistentCache != prevPersistentCache)
      {
        changeResult.addMessage(
            getMessage (MSGID_FSCACHE_UPDATED_IS_PERSISTENT, persistentCache));
      }
    }
    return changeResult;
  }
  /**
   * Parses the provided configuration and configure the entry cache.
   *
   * @param configuration  The new configuration containing the changes.
   * @param applyChanges   If true then take into account the new configuration.
   * @param errorHandler   An handler used to report errors.
   *
   * @return  The mapping between strings of character set values and the
   *          minimum number of characters required from those sets.
   */
  public boolean processEntryCacheConfig(
      FileSystemEntryCacheCfg             configuration,
      boolean                             applyChanges,
      EntryCacheCommon.ConfigErrorHandler errorHandler
      )
  {
    // Local variables to read configuration.
    DN                    newConfigEntryDN;
    long                  newLockTimeout;
    long                  newMaxEntries;
    long                  newMaxAllowedMemory;
    HashSet<SearchFilter> newIncludeFilters = null;
    HashSet<SearchFilter> newExcludeFilters = null;
    int                   newJECachePercent;
    long                  newJECacheSize;
    boolean               newPersistentCache;
    String                newCacheType = DEFAULT_FSCACHE_TYPE;
    String                newCacheHome = DEFAULT_FSCACHE_HOME;
    // Read configuration.
    newConfigEntryDN = configuration.dn();
    newLockTimeout   = configuration.getLockTimeout();
    newMaxEntries    = configuration.getMaxEntries();
    // Maximum memory/space this cache can utilize.
    newMaxAllowedMemory = configuration.getMaxMemorySize();
    // Determine JE cache percent.
    newJECachePercent = configuration.getDatabaseCachePercent();
    // Determine JE cache size.
    newJECacheSize = configuration.getDatabaseCacheSize();
    // Check if this cache is persistent.
    newPersistentCache = configuration.isPersistentCache();
    switch (errorHandler.getConfigPhase())
    {
    case PHASE_INIT:
      // Determine the cache type.
      newCacheType = configuration.getCacheType().toString();
      // Determine the cache home.
      newCacheHome = configuration.getCacheDirectory();
      newIncludeFilters = EntryCacheCommon.getFilters(
          configuration.getIncludeFilter(),
          MSGID_FIFOCACHE_INVALID_INCLUDE_FILTER,
          MSGID_FIFOCACHE_CANNOT_DECODE_ANY_INCLUDE_FILTERS,
          errorHandler,
          configEntryDN
          );
      newExcludeFilters = EntryCacheCommon.getFilters (
          configuration.getExcludeFilter(),
          MSGID_FIFOCACHE_CANNOT_DECODE_EXCLUDE_FILTER,
          MSGID_FIFOCACHE_CANNOT_DECODE_ANY_EXCLUDE_FILTERS,
          errorHandler,
          configEntryDN
          );
      break;
    case PHASE_ACCEPTABLE:  // acceptable and apply are using the same
    case PHASE_APPLY:       // error ID codes
      newIncludeFilters = EntryCacheCommon.getFilters (
          configuration.getIncludeFilter(),
          MSGID_FIFOCACHE_INVALID_INCLUDE_FILTER,
          0,
          errorHandler,
          configEntryDN
          );
      newExcludeFilters = EntryCacheCommon.getFilters (
          configuration.getExcludeFilter(),
          MSGID_FIFOCACHE_INVALID_EXCLUDE_FILTER,
          0,
          errorHandler,
          configEntryDN
          );
      break;
    }
    if (applyChanges && errorHandler.getIsAcceptable())
    {
      switch (errorHandler.getConfigPhase()) {
      case PHASE_INIT:
        cacheType      = newCacheType;
        cacheHome      = newCacheHome;
        jeCachePercent = newJECachePercent;
        jeCacheSize    = newJECacheSize;
        break;
      case PHASE_APPLY:
        jeCachePercent = newJECachePercent;
        try {
            EnvironmentConfig envConfig = entryCacheEnv.getConfig();
            envConfig.setCachePercent(jeCachePercent);
            entryCacheEnv.setMutableConfig(envConfig);
            entryCacheEnv.evictMemory();
        } catch (Exception e) {
            if (debugEnabled()) {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }
            errorHandler.reportError(
              ErrorLogCategory.CONFIGURATION,
              ErrorLogSeverity.SEVERE_WARNING,
              MSGID_FSCACHE_CANNOT_SET_JE_MEMORY_PCT,
              String.valueOf(configEntryDN),
              stackTraceToSingleLineString(e),
              null,
              false,
              ResultCode.OPERATIONS_ERROR
              );
        }
        jeCacheSize = newJECacheSize;
        try {
            EnvironmentConfig envConfig = entryCacheEnv.getConfig();
            envConfig.setCacheSize(jeCacheSize);
            entryCacheEnv.setMutableConfig(envConfig);
            entryCacheEnv.evictMemory();
        } catch (Exception e) {
            if (debugEnabled()) {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }
            errorHandler.reportError(
              ErrorLogCategory.CONFIGURATION,
              ErrorLogSeverity.SEVERE_WARNING,
              MSGID_FSCACHE_CANNOT_SET_JE_MEMORY_SIZE,
              String.valueOf(configEntryDN),
              stackTraceToSingleLineString(e),
              null,
              false,
              ResultCode.OPERATIONS_ERROR
              );
        }
        break;
      }
      configEntryDN    = newConfigEntryDN;
      lockTimeout      = newLockTimeout;
      maxEntries       = new AtomicLong(newMaxEntries);
      maxAllowedMemory = newMaxAllowedMemory;
      includeFilters   = newIncludeFilters;
      excludeFilters   = newExcludeFilters;
      persistentCache  = newPersistentCache;
    }
    return errorHandler.getIsAcceptable();
  }
  /**
   * Retrieves and decodes the entry with the specified DN from JE backend db.
   *
   * @param  entryDN   The DN of the entry to retrieve.
   *
   * @return  The requested entry if it is present in the cache, or
   *          <CODE>null</CODE> if it is not present.
   */
  private Entry getEntryFromDB(DN entryDN)
  {
    DatabaseEntry cacheEntryKey = new DatabaseEntry();
    DatabaseEntry primaryData = new DatabaseEntry();
    try {
      // Get the primary key and data.
      cacheEntryKey.setData(entryDN.toNormalizedString().getBytes("UTF-8"));
      if (entryCacheDB.get(null, cacheEntryKey,
              primaryData,
              LockMode.DEFAULT) == OperationStatus.SUCCESS) {
        return decodeEntry(entryDN, primaryData.getData());
      } else {
        throw new Exception();
      }
    } catch (Exception e) {
      if (debugEnabled()) {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      // Log an error message.
      logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.SEVERE_ERROR,
              MSGID_FSCACHE_CANNOT_RETRIEVE_ENTRY,
              stackTraceToSingleLineString(e));
    }
    return null;
  }
  /**
   * Encodes and stores the entry in the JE backend db.
   *
   * @param  entry    The entry to store in the cache.
   * @param  backend  The backend with which the entry is associated.
   * @param  entryID  The entry ID within the provided backend that uniquely
   *                  identifies the specified entry.
   *
   * @return  <CODE>false</CODE> if some problem prevented the method from
   *          completing successfully, or <CODE>true</CODE> if the entry
   *          was either stored or the cache determined that this entry
   *          should never be cached for some reason.
   */
  private boolean putEntryToDB(Entry entry, Backend backend, long entryID) {
    try {
      // See if the current fs space usage is within acceptable constraints. If
      // so, then add the entry to the cache (or replace it if it is already
      // present).  If not, then remove an existing entry and don't add the new
      // entry.
      long usedMemory = 0;
      // Zero means unlimited here.
      if (maxAllowedMemory != 0) {
        // TODO: This should be done using JE public API
        // EnvironmentStats.getTotalLogSize() when JE 3.2.28 is available.
        EnvironmentImpl envImpl =
              DbInternal.envGetEnvironmentImpl(entryCacheEnv);
        UtilizationProfile utilProfile = envImpl.getUtilizationProfile();
        SortedMap map = utilProfile.getFileSummaryMap(false);
        // This calculation is not exactly precise as the last JE logfile
        // will always be less than JELOGFILEMAX however in the interest
        // of performance and the fact that JE environment is always a
        // moving target we will allow for that margin of error here.
        usedMemory = map.size() * JELOGFILEMAX.longValue();
        // TODO: Check and log a warning if usedMemory hits default or
        // configurable watermark, see Issue 1735.
        if (usedMemory > maxAllowedMemory) {
          long savedMaxEntries = maxEntries.longValue();
          // Cap maxEntries artificially but dont let it go negative under
          // any circumstances.
          maxEntries.set((dnMap.isEmpty() ? 0 : dnMap.size() - 1));
          // Add the entry to the map to trigger remove of the eldest entry.
          // @see LinkedHashMapRotator.removeEldestEntry() for more details.
          dnMap.put(entry.getDN(), entryID);
          // Restore the map and maxEntries.
          dnMap.remove(entry.getDN());
          maxEntries.set(savedMaxEntries);
          // We'll always return true in this case, even tho we didn't actually
          // add the entry due to memory constraints.
          return true;
        }
      }
      // Create key.
      DatabaseEntry cacheEntryKey = new DatabaseEntry();
      cacheEntryKey.setData(
          entry.getDN().toNormalizedString().getBytes("UTF-8"));
      // Create data and put this cache entry into the database.
      if (entryCacheDB.put(null, cacheEntryKey,
          new DatabaseEntry(encodeEntry(entry))) == OperationStatus.SUCCESS) {
        // Add the entry to the cache maps.
        dnMap.put(entry.getDN(), entryID);
        Map<Long,DN> map = backendMap.get(backend);
        if (map == null) {
          map = new LinkedHashMap<Long,DN>();
          map.put(entryID, entry.getDN());
          backendMap.put(backend, map);
        } else {
          map.put(entryID, entry.getDN());
        }
      }
      // We'll always return true in this case, even if we didn't actually add
      // the entry due to memory constraints.
      return true;
    } catch (Exception e) {
      if (debugEnabled()) {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      // Log an error message.
      logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.SEVERE_ERROR,
              MSGID_FSCACHE_CANNOT_STORE_ENTRY,
              stackTraceToSingleLineString(e));
      return false;
    }
  }
  /**
   * TODO: Custom encoding used here due to performance and space
   * considerations. The caller should use Entry.encode() method,
   * see Issue 1675.
   */
  private static byte[] encodeEntry(Entry entry)
  {
    // Encode cache entry.
    int totalBytes = 0;
    // The object classes will be encoded as one-to-five byte length
    // followed by a zero-delimited UTF-8 byte representation of the
    // names (e.g., top\0person\0organizationalPerson\0inetOrgPerson).
    int i=0;
    int totalOCBytes = entry.getObjectClasses().size() - 1;
    byte[][] ocBytes = new byte[entry.getObjectClasses().size()][];
    for (String ocName : entry.getObjectClasses().values()) {
      ocBytes[i] = getBytes(ocName);
      totalOCBytes += ocBytes[i++].length;
    }
    byte[] ocLength = ASN1Element.encodeLength(totalOCBytes);
    totalBytes += totalOCBytes + ocLength.length;
    // The user attributes will be encoded as a one-to-five byte
    // number of attributes followed by a sequence of:
    // - A UTF-8 byte representation of the attribute name.
    // - A zero delimiter
    // - A one-to-five byte number of values for the attribute
    // - A sequence of:
    //   - A one-to-five byte length for the value
    //   - A UTF-8 byte representation for the value
    i=0;
    int numUserAttributes = 0;
    int totalUserAttrBytes = 0;
    LinkedList<byte[]> userAttrBytes = new LinkedList<byte[]>();
    for (List<Attribute> attrList :
      entry.getUserAttributes().values()) {
      for (Attribute a : attrList) {
        if (a.isVirtual() || (! a.hasValue())) {
          continue;
        }
        numUserAttributes++;
        byte[] nameBytes = getBytes(a.getNameWithOptions());
        int numValues = 0;
        int totalValueBytes = 0;
        LinkedList<byte[]> valueBytes = new LinkedList<byte[]>();
        for (AttributeValue v : a.getValues()) {
          numValues++;
          byte[] vBytes = v.getValueBytes();
          byte[] vLength = ASN1Element.encodeLength(vBytes.length);
          valueBytes.add(vLength);
          valueBytes.add(vBytes);
          totalValueBytes += vLength.length + vBytes.length;
        }
        byte[] numValuesBytes = ASN1Element.encodeLength(numValues);
        byte[] attrBytes = new byte[nameBytes.length +
            numValuesBytes.length +
            totalValueBytes + 1];
        System.arraycopy(nameBytes, 0, attrBytes, 0,
            nameBytes.length);
        int pos = nameBytes.length+1;
        System.arraycopy(numValuesBytes, 0, attrBytes, pos,
            numValuesBytes.length);
        pos += numValuesBytes.length;
        for (byte[] b : valueBytes) {
          System.arraycopy(b, 0, attrBytes, pos, b.length);
          pos += b.length;
        }
        userAttrBytes.add(attrBytes);
        totalUserAttrBytes += attrBytes.length;
      }
    }
    byte[] userAttrCount =
        ASN1OctetString.encodeLength(numUserAttributes);
    totalBytes += totalUserAttrBytes + userAttrCount.length;
    // The operational attributes will be encoded in the same way as
    // the user attributes.
    i=0;
    int numOperationalAttributes = 0;
    int totalOperationalAttrBytes = 0;
    LinkedList<byte[]> operationalAttrBytes =
        new LinkedList<byte[]>();
    for (List<Attribute> attrList :
      entry.getOperationalAttributes().values()) {
      for (Attribute a : attrList) {
        if (a.isVirtual() || (! a.hasValue())) {
          continue;
        }
        numOperationalAttributes++;
        byte[] nameBytes = getBytes(a.getNameWithOptions());
        int numValues = 0;
        int totalValueBytes = 0;
        LinkedList<byte[]> valueBytes = new LinkedList<byte[]>();
        for (AttributeValue v : a.getValues()) {
          numValues++;
          byte[] vBytes = v.getValueBytes();
          byte[] vLength = ASN1Element.encodeLength(vBytes.length);
          valueBytes.add(vLength);
          valueBytes.add(vBytes);
          totalValueBytes += vLength.length + vBytes.length;
        }
        byte[] numValuesBytes = ASN1Element.encodeLength(numValues);
        byte[] attrBytes = new byte[nameBytes.length +
            numValuesBytes.length +
            totalValueBytes + 1];
        System.arraycopy(nameBytes, 0, attrBytes, 0,
            nameBytes.length);
        int pos = nameBytes.length+1;
        System.arraycopy(numValuesBytes, 0, attrBytes, pos,
            numValuesBytes.length);
        pos += numValuesBytes.length;
        for (byte[] b : valueBytes) {
          System.arraycopy(b, 0, attrBytes, pos, b.length);
          pos += b.length;
        }
        operationalAttrBytes.add(attrBytes);
        totalOperationalAttrBytes += attrBytes.length;
      }
    }
    byte[] operationalAttrCount =
        ASN1OctetString.encodeLength(numOperationalAttributes);
    totalBytes += totalOperationalAttrBytes +
        operationalAttrCount.length;
    // Now we've got all the data that we need.  Create a big byte
    // array to hold it all and pack it in.
    byte[] entryBytes = new byte[totalBytes];
    int pos = 0;
    // Add the object classes length and values.
    System.arraycopy(ocLength, 0, entryBytes, pos, ocLength.length);
    pos += ocLength.length;
    for (byte[] b : ocBytes) {
      System.arraycopy(b, 0, entryBytes, pos, b.length);
      pos += b.length + 1;
    }
    // We need to back up one because there's no zero-teriminator
    // after the last object class name.
    pos--;
    // Next, add the user attribute count and the user attribute
    // data.
    System.arraycopy(userAttrCount, 0, entryBytes, pos,
        userAttrCount.length);
    pos += userAttrCount.length;
    for (byte[] b : userAttrBytes) {
      System.arraycopy(b, 0, entryBytes, pos, b.length);
      pos += b.length;
    }
    // Finally, add the operational attribute count and the
    // operational attribute data.
    System.arraycopy(operationalAttrCount, 0, entryBytes, pos,
        operationalAttrCount.length);
    pos += operationalAttrCount.length;
    for (byte[] b : operationalAttrBytes) {
      System.arraycopy(b, 0, entryBytes, pos, b.length);
      pos += b.length;
    }
    return entryBytes;
  }
  /**
   * TODO: Custom decoding used here due to performance and space
   * considerations. The caller should use Entry.decode() method,
   * see Issue 1675.
   */
  private static Entry decodeEntry(DN entryDN, byte[] entryBytes)
      throws UnsupportedEncodingException
  {
    // Decode cache entry.
    int pos = 0;
    // The length of the object classes.  It may be a single
    // byte or multiple bytes.
    int ocLength = entryBytes[pos] & 0x7F;
    if (entryBytes[pos++] != ocLength) {
      int numLengthBytes = ocLength;
      ocLength = 0;
      for (int i=0; i < numLengthBytes; i++, pos++) {
        ocLength = (ocLength << 8) | (entryBytes[pos] & 0xFF);
      }
    }
    // Next is the encoded set of object classes.  It will be a
    // single string with the object class names separated by zeros.
    LinkedHashMap<ObjectClass,String> objectClasses =
        new LinkedHashMap<ObjectClass,String>();
    int startPos = pos;
    for (int i=0; i < ocLength; i++,pos++) {
      if (entryBytes[pos] == 0x00) {
        String name = new String(entryBytes, startPos, pos-startPos,
            "UTF-8");
        String lowerName = toLowerCase(name);
        ObjectClass oc =
            DirectoryServer.getObjectClass(lowerName, true);
        objectClasses.put(oc, name);
        startPos = pos+1;
      }
    }
    String name = new String(entryBytes, startPos, pos-startPos,
        "UTF-8");
    String lowerName = toLowerCase(name);
    ObjectClass oc =
        DirectoryServer.getObjectClass(lowerName, true);
    objectClasses.put(oc, name);
    // Next is the total number of user attributes.  It may be a
    // single byte or multiple bytes.
    int numUserAttrs = entryBytes[pos] & 0x7F;
    if (entryBytes[pos++] != numUserAttrs) {
      int numLengthBytes = numUserAttrs;
      numUserAttrs = 0;
      for (int i=0; i < numLengthBytes; i++, pos++) {
        numUserAttrs = (numUserAttrs << 8) |
            (entryBytes[pos] & 0xFF);
      }
    }
    // Now, we should iterate through the user attributes and decode
    // each one.
    LinkedHashMap<AttributeType,List<Attribute>> userAttributes =
        new LinkedHashMap<AttributeType,List<Attribute>>();
    for (int i=0; i < numUserAttrs; i++) {
      // First, we have the zero-terminated attribute name.
      startPos = pos;
      while (entryBytes[pos] != 0x00) {
        pos++;
      }
      name = new String(entryBytes, startPos, pos-startPos,
          "UTF-8");
      LinkedHashSet<String> options;
      int semicolonPos = name.indexOf(';');
      if (semicolonPos > 0) {
        String baseName = name.substring(0, semicolonPos);
        lowerName = toLowerCase(baseName);
        options   = new LinkedHashSet<String>();
        int nextPos = name.indexOf(';', semicolonPos+1);
        while (nextPos > 0) {
          String option = name.substring(semicolonPos+1, nextPos);
          if (option.length() > 0) {
            options.add(option);
          }
          semicolonPos = nextPos;
          nextPos = name.indexOf(';', semicolonPos+1);
        }
        String option = name.substring(semicolonPos+1);
        if (option.length() > 0) {
          options.add(option);
        }
        name = baseName;
      } else {
        lowerName = toLowerCase(name);
        options   = new LinkedHashSet<String>(0);
      }
      AttributeType attributeType =
          DirectoryServer.getAttributeType(lowerName, true);
      // Next, we have the number of values.
      int numValues = entryBytes[++pos] & 0x7F;
      if (entryBytes[pos++] != numValues) {
        int numLengthBytes = numValues;
        numValues = 0;
        for (int j=0; j < numLengthBytes; j++, pos++) {
          numValues = (numValues << 8) | (entryBytes[pos] & 0xFF);
        }
      }
      // Next, we have the sequence of length-value pairs.
      LinkedHashSet<AttributeValue> values =
          new LinkedHashSet<AttributeValue>(numValues);
      for (int j=0; j < numValues; j++) {
        int valueLength = entryBytes[pos] & 0x7F;
        if (entryBytes[pos++] != valueLength) {
          int numLengthBytes = valueLength;
          valueLength = 0;
          for (int k=0; k < numLengthBytes; k++, pos++) {
            valueLength = (valueLength << 8) |
                (entryBytes[pos] & 0xFF);
          }
        }
        byte[] valueBytes = new byte[valueLength];
        System.arraycopy(entryBytes, pos, valueBytes, 0,
            valueLength);
        values.add(new AttributeValue(attributeType,
            new ASN1OctetString(valueBytes)));
        pos += valueLength;
      }
      // Create the attribute and add it to the set of user
      // attributes.
      Attribute a = new Attribute(attributeType, name, options,
          values);
      List<Attribute> attrList = userAttributes.get(attributeType);
      if (attrList == null) {
        attrList = new ArrayList<Attribute>(1);
        attrList.add(a);
        userAttributes.put(attributeType, attrList);
      } else {
        attrList.add(a);
      }
    }
    // Next is the total number of operational attributes.  It may
    // be a single byte or multiple bytes.
    int numOperationalAttrs = entryBytes[pos] & 0x7F;
    if (entryBytes[pos++] != numOperationalAttrs) {
      int numLengthBytes = numOperationalAttrs;
      numOperationalAttrs = 0;
      for (int i=0; i < numLengthBytes; i++, pos++) {
        numOperationalAttrs =
            (numOperationalAttrs << 8) | (entryBytes[pos] & 0xFF);
      }
    }
    // Now, we should iterate through the operational attributes and
    // decode each one.
    LinkedHashMap<AttributeType,List<Attribute>>
        operationalAttributes =
        new LinkedHashMap<AttributeType,List<Attribute>>();
    for (int i=0; i < numOperationalAttrs; i++) {
      // First, we have the zero-terminated attribute name.
      startPos = pos;
      while (entryBytes[pos] != 0x00) {
        pos++;
      }
      name = new String(entryBytes, startPos, pos-startPos,
          "UTF-8");
      LinkedHashSet<String> options;
      int semicolonPos = name.indexOf(';');
      if (semicolonPos > 0) {
        String baseName = name.substring(0, semicolonPos);
        lowerName = toLowerCase(baseName);
        options   = new LinkedHashSet<String>();
        int nextPos = name.indexOf(';', semicolonPos+1);
        while (nextPos > 0) {
          String option = name.substring(semicolonPos+1, nextPos);
          if (option.length() > 0) {
            options.add(option);
          }
          semicolonPos = nextPos;
          nextPos = name.indexOf(';', semicolonPos+1);
        }
        String option = name.substring(semicolonPos+1);
        if (option.length() > 0) {
          options.add(option);
        }
        name = baseName;
      } else {
        lowerName = toLowerCase(name);
        options   = new LinkedHashSet<String>(0);
      }
      AttributeType attributeType =
          DirectoryServer.getAttributeType(lowerName, true);
      // Next, we have the number of values.
      int numValues = entryBytes[++pos] & 0x7F;
      if (entryBytes[pos++] != numValues) {
        int numLengthBytes = numValues;
        numValues = 0;
        for (int j=0; j < numLengthBytes; j++, pos++) {
          numValues = (numValues << 8) | (entryBytes[pos] & 0xFF);
        }
      }
      // Next, we have the sequence of length-value pairs.
      LinkedHashSet<AttributeValue> values =
          new LinkedHashSet<AttributeValue>(numValues);
      for (int j=0; j < numValues; j++) {
        int valueLength = entryBytes[pos] & 0x7F;
        if (entryBytes[pos++] != valueLength) {
          int numLengthBytes = valueLength;
          valueLength = 0;
          for (int k=0; k < numLengthBytes; k++, pos++) {
            valueLength = (valueLength << 8) |
                (entryBytes[pos] & 0xFF);
          }
        }
        byte[] valueBytes = new byte[valueLength];
        System.arraycopy(entryBytes, pos, valueBytes, 0,
            valueLength);
        values.add(new AttributeValue(attributeType,
            new ASN1OctetString(valueBytes)));
        pos += valueLength;
      }
      // Create the attribute and add it to the set of operational
      // attributes.
      Attribute a = new Attribute(attributeType, name, options,
          values);
      List<Attribute> attrList =
          operationalAttributes.get(attributeType);
      if (attrList == null) {
        attrList = new ArrayList<Attribute>(1);
        attrList.add(a);
        operationalAttributes.put(attributeType, attrList);
      } else {
        attrList.add(a);
      }
    }
    // We've got everything that we need, so create and return the
    // entry.
    return new
        Entry(entryDN, objectClasses, userAttributes, operationalAttributes);
  }
 /**
  * Checks if the cache home exist and if not tries to recursively create it.
  * If either is successful adjusts cache home access permissions accordingly
  * to allow only process owner or the superuser to access JE environment.
  *
  * @param  cacheHome  String representation of complete file system path.
  *
  * @throws Exception  If failed to establish cache home.
  */
  private void checkAndSetupCacheHome(String cacheHome) throws Exception {
    boolean cacheHasHome = false;
    File cacheHomeDir = new File(cacheHome);
    if (cacheHomeDir.exists() &&
        cacheHomeDir.canRead() &&
        cacheHomeDir.canWrite()) {
      cacheHasHome = true;
    } else {
      try {
        cacheHasHome = cacheHomeDir.mkdirs();
      } catch (SecurityException e) {
        cacheHasHome = false;
      }
    }
    if ( cacheHasHome ) {
      // TODO: Investigate if its feasible to employ SetFileAttributes()
      // FILE_ATTRIBUTE_TEMPORARY attribute on Windows via native code.
      if(FilePermission.canSetPermissions()) {
        try {
          if(!FilePermission.setPermissions(cacheHomeDir,
              CACHE_HOME_PERMISSIONS)) {
            throw new Exception();
          }
        } catch(Exception e) {
          // Log an warning that the permissions were not set.
          int msgID = MSGID_FSCACHE_SET_PERMISSIONS_FAILED;
          String message = getMessage(msgID, cacheHome);
          logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.SEVERE_WARNING,
              message, msgID);
        }
      }
    } else {
      throw new Exception();
    }
  }
 /**
  * This inner class exist solely to override <CODE>removeEldestEntry()</CODE>
  * method of the LinkedHashMap.
  *
  * @see  java.util.LinkedHashMap<K,V>
  */
  private class LinkedHashMapRotator<K,V> extends LinkedHashMap<K,V> {
    static final long serialVersionUID = 5271482121415968435L;
    public LinkedHashMapRotator(int initialCapacity,
                                float loadFactor,
                                boolean accessOrder) {
      super(initialCapacity, loadFactor, accessOrder);
    }
    // This method will get called each time we add a new key/value
    // pair to the map. The eldest entry will be selected by the
    // underlying LinkedHashMap implementation based on the access
    // order configured and will follow either FIFO implementation
    // by default or LRU implementation if configured so explicitly.
    @Override protected boolean removeEldestEntry(Map.Entry eldest) {
      // Check if we hit the limit on max entries and if so remove
      // the eldest entry otherwise do nothing.
      if (size() > maxEntries.longValue()) {
        DatabaseEntry cacheEntryKey = new DatabaseEntry();
        cacheWriteLock.lock();
        try {
          // Remove the the eldest entry from supporting maps.
          cacheEntryKey.setData(
              ((DN) eldest.getKey()).toNormalizedString().getBytes("UTF-8"));
          long entryID = (long) ((Long) eldest.getValue()).longValue();
          Set<Backend> backendSet = backendMap.keySet();
          Iterator<Backend> backendIterator = backendSet.iterator();
          while (backendIterator.hasNext()) {
            Map<Long,DN> map = backendMap.get(backendIterator.next());
            map.remove(entryID);
          }
          // Remove the the eldest entry from the database.
          entryCacheDB.delete(null, cacheEntryKey);
        } catch (Exception e) {
          if (debugEnabled()) {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }
        } finally {
          cacheWriteLock.unlock();
        }
        return true;
      } else {
        return false;
      }
    }
  }
  /**
   * This exception should be thrown if an error occurs while
   * trying to locate and load persistent cache index from
   * the existing entry cache database.
   */
  private class CacheIndexNotFoundException extends Exception {
    static final long serialVersionUID = 6444756053577853869L;
    public CacheIndexNotFoundException() {}
    public CacheIndexNotFoundException(String message) {
      super(message);
    }
  }
  /**
   * This exception should be thrown if persistent cache index
   * found in the existing entry cache database is determined
   * to be empty, inconsistent or damaged.
   */
  private class CacheIndexImpairedException extends Exception {
    static final long serialVersionUID = -369455697709478407L;
    public CacheIndexImpairedException() {}
    public CacheIndexImpairedException(String message) {
      super(message);
    }
  }
}
opends/src/server/org/opends/server/extensions/FileSystemEntryCacheIndex.java
New file
@@ -0,0 +1,73 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.extensions;
import java.io.Serializable;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.concurrent.ConcurrentHashMap;
/**
 * This class represents serializable entry cache index structures
 * and supporting data types used for the entry cache persistence.
 * Structurally it should be an inner class of FileSystemEntryCache
 * however due to serialization constraints it has been separated.
 */
class FileSystemEntryCacheIndex implements Serializable {
    static final long serialVersionUID = 4537634108673038611L;
    /**
     * Backend to Checksum/id map for offline state.
     */
    public Map<String,Long> offlineState;
    /**
     * The mapping between entry backends/IDs and DNs.
     */
    public Map<String,Map<Long,String>> backendMap;
    /**
     * The mapping between DNs and IDs.
     */
    public Map<String,Long> dnMap;
    /**
     * Default constructor.
     */
    public FileSystemEntryCacheIndex() {
      offlineState =
          //               <Backend,Long>
          new ConcurrentHashMap<String,Long>();
      backendMap =
          //               <Backend,Map<Long,DN>>
          new LinkedHashMap<String,Map<Long,String>>();
      dnMap      =
          //               <DN,Long>
          new LinkedHashMap<String,Long>();
    }
  }
opends/src/server/org/opends/server/messages/ExtensionsMessages.java
@@ -5013,6 +5013,202 @@
  /**
   * The message ID for the message that will be used if maximum number of cache
   * entries has been updated.  This takes a single argument, which is the new
   * maximum number of entries.
   */
  public static final int MSGID_FSCACHE_UPDATED_MAX_ENTRIES =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 477;
  /**
   * The message ID for the message that will be used if the cache lock timeout
   * has been updated.  This takes a single argument, which is the new lock
   * timeout.
   */
  public static final int MSGID_FSCACHE_UPDATED_LOCK_TIMEOUT =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 478;
  /**
   * The message ID for the message that will be used if the cache include
   * filter set has been updated.  This does not take any arguments.
   */
  public static final int MSGID_FSCACHE_UPDATED_INCLUDE_FILTERS =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 479;
  /**
   * The message ID for the message that will be used if the cache exclude
   * filter set has been updated.  This does not take any arguments.
   */
  public static final int MSGID_FSCACHE_UPDATED_EXCLUDE_FILTERS =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 480;
  /**
   * The message ID for the message that will be used if entry cache memory
   * size has been updated.  This takes one argument, which is the new size
   * of memory that may be used, in bytes.
   */
  public static final int MSGID_FSCACHE_UPDATED_MAX_MEMORY_SIZE =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 481;
  /**
   * The message ID for the message that will be used if entry cache type
   * has been changed.  This takes one argument, which is the new entry
   * cache type.
   */
  public static final int MSGID_FSCACHE_UPDATED_TYPE =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 482;
  /**
   * The message ID for the message that will be used if entry cache JE
   * cache memory percent has been changed.  This takes one argument,
   * which is the new percentage of JVM memory that can be utilized.
   */
  public static final int MSGID_FSCACHE_UPDATED_JE_MEMORY_PCT =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 483;
  /**
   * The message ID for the message that will be used if entry cache JE
   * cache memory size has been changed.  This takes one argument,
   * which is the new maximum size of JVM memory that can be utilized.
   */
  public static final int MSGID_FSCACHE_UPDATED_JE_MEMORY_SIZE =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 484;
  /**
   * The message ID for the message that will be used if entry cache
   * persistence state has been changed.  This takes one argument,
   * which is the new persistence state.
   */
  public static final int MSGID_FSCACHE_UPDATED_IS_PERSISTENT =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 485;
  /**
   * The message ID for the message that will be used if an error occurs while
   * trying to load persistent cache for the FS-based entry cache.
   * This takes one argument which is a string representation of the exception
   * that was caught.
   */
  public static final int MSGID_FSCACHE_CANNOT_LOAD_PERSISTENT_DATA =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 486;
  /**
   * The message ID for the message that will be used if an error occurs while
   * trying to store persistence cache for the FS-based entry cache.
   * This takes one argument which is a string representation of the exception
   * that was caught.
   */
  public static final int MSGID_FSCACHE_CANNOT_STORE_PERSISTENT_DATA =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 487;
  /**
   * The message ID for the message that will be used if a fatal error occurs
   * while trying to initialize the FS-based entry cache.  This takes one
   * argument which is a string representation of the exception that was caught.
   */
  public static final int MSGID_FSCACHE_CANNOT_INITIALIZE =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_FATAL_ERROR | 488;
  /**
   * The message ID for the message that will be used if we are unable to put
   * a new entry to the cache. This takes one argument, which is a string
   * representation of the exception that was caught.
   */
  public static final int MSGID_FSCACHE_CANNOT_STORE_ENTRY =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 489;
  /**
   * The message ID for the message that will be used if we are unable to get
   * an existing entry from the cache. This takes one argument, which is a
   * string representation of the exception that was caught.
   */
  public static final int MSGID_FSCACHE_CANNOT_RETRIEVE_ENTRY =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 490;
  /**
   * The message ID for the message that will be used if we are unable to set
   * the JE cache size as percentage. This takes one argument, which is a
   * string representation of the exception that was caught.
   */
  public static final int MSGID_FSCACHE_CANNOT_SET_JE_MEMORY_PCT =
      CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 491;
  /**
   * The message ID for the message that will be used if we are unable to set
   * the JE cache size in bytes. This takes one argument, which is a string
   * representation of the exception that was caught.
   */
  public static final int MSGID_FSCACHE_CANNOT_SET_JE_MEMORY_SIZE =
      CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 492;
  /**
   * The message ID for the message that will be used if entry cache home
   * either cannot be created or exist but already in use by another instance
   * This takes four arguments, which are the DN of the configuration entry,
   * the cache-home attribute value of the configuration entry, default cache
   * home value and a string representation of the exception that was caught.
   */
  public static final int MSGID_FSCACHE_INVALID_HOME =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 493;
  /**
   * The message ID for the message that will be used if a fatal error occurs
   * while trying to setup the cache db environment home.  This takes one
   * argument which is a string representation of the exception that was caught.
   */
  public static final int MSGID_FSCACHE_HOMELESS =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_FATAL_ERROR | 494;
  /**
   * The message ID of an error indicating that the file permissions for the
   * file system entry cache database directory were not set.
   */
  public static final int MSGID_FSCACHE_SET_PERMISSIONS_FAILED =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_WARNING | 495;
  /**
   * The message ID for the message that will be used if recorded offline state
   * does not match the current offline state for given backend. This takes one
   * argument which is the backend ID.
   */
  public static final int MSGID_FSCACHE_OFFLINE_STATE_FAIL =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_WARNING | 496;
  /**
   * The message ID for the message that will be used to report the cache
   * persistent state restoration progress. This takes two arguments which
   * are the restored entries count and the total number of entries to be
   * restored.
   */
  public static final int MSGID_FSCACHE_RESTORE_PROGRESS_REPORT =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 497;
  /**
   * The message ID for the message that will be used to report the cache
   * persistent state preservation progress. This takes two arguments which
   * are the preserved entries count and the total number of entries to be
   * preserved.
   */
  public static final int MSGID_FSCACHE_SAVE_PROGRESS_REPORT =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 498;
  /**
   * The message ID for the message that will be used if an error occurs while
   * trying to load persistent cache index of the FS-based persistent entry
   * cache. Takes no arguments.
   */
  public static final int MSGID_FSCACHE_INDEX_NOT_FOUND =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 499;
  /**
   * The message ID for the message that will be used if persistent cache index
   * of the FS-based persistent entry cache is empty or damaged in any way we
   * can detect. Takes no arguments.
   */
  public static final int MSGID_FSCACHE_INDEX_IMPAIRED =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 500;
  /**
   * Associates a set of generic messages with the message IDs defined in this
   * class.
   */
@@ -7205,6 +7401,90 @@
                    "which is itself a virtual static group.  One " +
                    "virtual static group is not allowed to reference " +
                    "another as its target group");
    registerMessage(MSGID_FSCACHE_UPDATED_MAX_MEMORY_SIZE,
                    "The amount of space that may be used for the entry " +
                    "cache has been updated to %d bytes. If the previous " +
                    "amount has been reduced, it may take some time for " +
                    "entries to be purged so that the current cache space" +
                    "consumption can reflect this new setting.");
    registerMessage(MSGID_FSCACHE_UPDATED_MAX_ENTRIES,
                    "The number of entries that may be held in the entry " +
                    "cache has been updated to %d.  If this value has been " +
                    "reduced, it may take some time for entries to be purged " +
                    "so that the cache can reflect this new setting.");
    registerMessage(MSGID_FSCACHE_UPDATED_LOCK_TIMEOUT,
                    "The lock timeout that will be used to determine the " +
                    "length of time that the cache should block while " +
                    "attempting to acquire a lock for an entry has been " +
                    "set to %d milliseconds.");
    registerMessage(MSGID_FSCACHE_UPDATED_INCLUDE_FILTERS,
                    "The set of search filters that will control which " +
                    "entries may be included in the cache has been updated.");
    registerMessage(MSGID_FSCACHE_UPDATED_EXCLUDE_FILTERS,
                    "The set of search filters that will control which " +
                    "entries should be be excluded from the cache has been " +
                    "updated.");
    registerMessage(MSGID_FSCACHE_UPDATED_TYPE,
                    "The entry cache type has been changed to %s.");
    registerMessage(MSGID_FSCACHE_UPDATED_JE_MEMORY_PCT,
                    "The amount of memory that may be used for the entry " +
                    "cache Berkeley DB JE internal cache has been updated " +
                    "to %d percent of the total memory available to the JVM.");
    registerMessage(MSGID_FSCACHE_UPDATED_JE_MEMORY_SIZE,
                    "The amount of JVM memory that may be used for the entry " +
                    "cache Berkeley DB JE internal cache has been updated " +
                    "to %d bytes.");
    registerMessage(MSGID_FSCACHE_UPDATED_IS_PERSISTENT,
                    "The persistence state for the entry cache has been " +
                    "changed to %s");
    registerMessage(MSGID_FSCACHE_CANNOT_LOAD_PERSISTENT_DATA,
                    "An error occurred while trying to load persistent cache." +
                    " Persistent cache will be flushed now.");
    registerMessage(MSGID_FSCACHE_CANNOT_STORE_PERSISTENT_DATA,
                    "An error occurred while trying to store persistent cache" +
                    ". Persistent cache will be flushed now.");
    registerMessage(MSGID_FSCACHE_CANNOT_INITIALIZE,
                    "A fatal error occurred while trying to initialize file " +
                    "system entry cache. This cache will be disabled now.");
    registerMessage(MSGID_FSCACHE_HOMELESS,
                    "A fatal error occurred while trying to setup file " +
                    "system entry cache home. No suitable path can be found " +
                    "to host the cache home. This cache will be disabled now.");
    registerMessage(MSGID_FSCACHE_SET_PERMISSIONS_FAILED,
                    "Unable to set file permissions for the file system " +
                    "entry cache backend database directory %s.");
    registerMessage(MSGID_FSCACHE_CANNOT_STORE_ENTRY,
                    "Unable to store new cache entry in the file system " +
                    "entry cache.");
    registerMessage(MSGID_FSCACHE_CANNOT_RETRIEVE_ENTRY,
                    "Unable to retrieve an existing cache entry from the " +
                    "file system entry cache.");
    registerMessage(MSGID_FSCACHE_CANNOT_SET_JE_MEMORY_PCT,
                    "Internal error occured while trying to set the entry " +
                    "cache backend internal cache size as percentage. The " +
                    "previous or default value will be used instead.");
    registerMessage(MSGID_FSCACHE_CANNOT_SET_JE_MEMORY_SIZE,
                    "Internal error occured while trying to set the entry " +
                    "cache backend internal cache size in bytes. The " +
                    "previous or default value will be used instead.");
    registerMessage(MSGID_FSCACHE_OFFLINE_STATE_FAIL,
                    "%s backend current offline state does not match " +
                    "persistent cache last recorded offline state. All " +
                    "cached data for this backend is now discarded.");
    registerMessage(MSGID_FSCACHE_RESTORE_PROGRESS_REPORT,
                    "Restored %d cache entries of %d total persistent cache " +
                    "entries found.");
    registerMessage(MSGID_FSCACHE_SAVE_PROGRESS_REPORT,
                    "Made persistent %d cache entries of %d total cache " +
                    "entries found.");
    registerMessage(MSGID_FSCACHE_INDEX_NOT_FOUND,
                    "No previous persistent cache state can be found. " +
                    "Starting with an empty cache.");
    registerMessage(MSGID_FSCACHE_INDEX_IMPAIRED,
                    "The persistent cache index is inconsistent or damaged. " +
                    "Persistent cache will be flushed now.");
  }
}
opends/src/server/org/opends/server/messages/JebMessages.java
@@ -1158,6 +1158,14 @@
      CATEGORY_MASK_JEB | SEVERITY_MASK_SEVERE_WARNING | 146;
  /**
   * The message ID to use when we have failed to checksum db environment.
   * This takes one argument which is the backend ID.
   */
  public static final int MSGID_JEB_BACKEND_CHECKSUM_FAIL =
      CATEGORY_MASK_JEB | SEVERITY_MASK_SEVERE_WARNING | 147;
  /**
   * Associates a set of generic messages with the message IDs defined in this
   * class.
   */
@@ -1466,8 +1474,9 @@
    registerMessage(MSGID_JEB_REBUILD_BACKEND_ONLINE,
                    "Rebuilding system index(es) must be done with the " +
                    "backend containing the base DN disabled");
    registerMessage(MSGID_JEB_BACKEND_CHECKSUM_FAIL,
                    "Failed to checksum the database environment for " +
                    "backend %s");
    registerMessage(MSGID_ENTRYIDSORTER_CANNOT_EXAMINE_ENTRY,
                    "Unable to examine the entry with ID %s for sorting " +
                    "purposes:  %s");