/*
 * 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 legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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 legal-notices/CDDLv1_0.txt.
 * 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 2015 ForgeRock AS.
 */
package org.opends.server.backends.pluggable;

import static org.opends.messages.ToolMessages.*;
import static org.opends.server.util.StaticUtils.*;

import static com.forgerock.opendj.cli.ArgumentConstants.*;
import static com.forgerock.opendj.cli.Utils.*;

import java.io.OutputStream;
import java.io.PrintStream;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.SizeUnit;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.util.Option;
import org.forgerock.util.Options;
import org.opends.server.admin.std.server.BackendCfg;
import org.opends.server.admin.std.server.PluggableBackendCfg;
import org.opends.server.api.Backend;
import org.opends.server.backends.pluggable.spi.Cursor;
import org.opends.server.backends.pluggable.spi.ReadOperation;
import org.opends.server.backends.pluggable.spi.ReadableTransaction;
import org.opends.server.backends.pluggable.spi.StorageRuntimeException;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.core.CoreConfigManager;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.DirectoryServer.DirectoryServerVersionHandler;
import org.opends.server.core.LockFileManager;
import org.opends.server.extensions.ConfigFileHandler;
import org.opends.server.loggers.JDKLogging;
import org.opends.server.tools.BackendToolUtils;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;
import org.opends.server.types.NullOutputStream;
import org.opends.server.util.BuildVersion;
import org.opends.server.util.StaticUtils;

import com.forgerock.opendj.cli.Argument;
import com.forgerock.opendj.cli.ArgumentException;
import com.forgerock.opendj.cli.BooleanArgument;
import com.forgerock.opendj.cli.CommonArguments;
import com.forgerock.opendj.cli.IntegerArgument;
import com.forgerock.opendj.cli.StringArgument;
import com.forgerock.opendj.cli.SubCommand;
import com.forgerock.opendj.cli.SubCommandArgumentParser;
import com.forgerock.opendj.cli.TableBuilder;
import com.forgerock.opendj.cli.TextTablePrinter;

/**
 * This program provides a utility that may be used to debug a Pluggable Backend.
 * This tool provides the ability to:
 * <ul>
 * <li>list root containers</li>
 * <li>list entry containers</li>
 * <li>list Trees in a Backend or Storage</li>
 * <li>gather information about Backend indexes</li>
 * <li>dump the contents of a Tree either at the Backend or the Storage layer.</li>
 * </ul>
 * This will be a process that is intended to run outside of Directory Server and not
 * internally within the server process (e.g., via the tasks interface); it still
 * requires configuration information and access to Directory Server instance data.
 */
public class BackendStat
{
  /**
   * Collects all necessary interaction interfaces with either a Backend using TreeNames
   * or a storage using Trees.
   */
  interface TreeKeyValue
  {
    /**
     * Returns a key given a string representation of it.
     *
     * @param data a string representation of the key.
     *             Prefixing with "0x" will interpret the rest of the string as an hex dump
     *             of the intended value.
     * @return a key given a string representation of it
     */
    ByteString getTreeKey(String data);

    /**
     * Returns a printable string for the given key.
     *
     * @param key a key from the Tree
     * @return a printable string for the given key
     */
    String keyDecoder(ByteString key);

    /**
     * Returns a printable string for the given value.
     *
     * @param value a value from the tree
     * @return a printable string for the given value
     */
    String valueDecoder(ByteString value);

    /**
     * Returns the TreeName for this storage Tree.
     *
     * @return the TreeName for this storage Tree
     */
    TreeName getTreeName();
  }

  /** Stays at the storage level when cursoring Trees. */
  static class StorageTreeKeyValue implements TreeKeyValue
  {
    private TreeName treeName;

    public StorageTreeKeyValue(TreeName treeName)
    {
      this.treeName = treeName;
    }

    @Override
    public TreeName getTreeName()
    {
      return treeName;
    }

    @Override
    public ByteString getTreeKey(String data)
    {
      return ByteString.valueOfUtf8(data);
    }

    @Override
    public String keyDecoder(ByteString key)
    {
      throw new UnsupportedOperationException(ERR_BACKEND_TOOL_DECODER_NOT_AVAILABLE.get().toString());
    }

    @Override
    public String valueDecoder(ByteString value)
    {
      throw new UnsupportedOperationException(ERR_BACKEND_TOOL_DECODER_NOT_AVAILABLE.get().toString());
    }
  }

  /** Delegate key semantics to the backend. */
  static class BackendTreeKeyValue implements TreeKeyValue
  {
    private TreeName name;
    private Tree tree;

    public BackendTreeKeyValue(Tree tree)
    {
      this.tree = tree;
      this.name = tree.getName();
    }

    @Override
    public ByteString getTreeKey(String data)
    {
      if (data.length() == 0)
      {
        return ByteString.empty();
      }
      return tree.generateKey(data);
    }

    @Override
    public String keyDecoder(ByteString key)
    {
      return tree.keyToString(key);
    }

    @Override
    public String valueDecoder(ByteString value)
    {
      return tree.valueToString(value);
    }

    @Override
    public TreeName getTreeName()
    {
      return name;
    }
  }

  /** Statistics collector. */
  class TreeStats
  {
    final long count;
    final long totalKeySize;
    final long totalDataSize;

    TreeStats(long count, long tks, long tds)
    {
      this.count = count;
      this.totalKeySize = tks;
      this.totalDataSize = tds;
    }
  }

  private static final Option<Boolean> DUMP_DECODE_VALUE = Option.withDefault(true);
  private static final Option<Boolean> DUMP_STATS_ONLY = Option.withDefault(false);
  private static final Option<Boolean> DUMP_SINGLE_LINE = Option.withDefault(false);
  private static final Option<Argument> DUMP_MIN_KEY_VALUE = Option.of(Argument.class, null);
  private static final Option<Argument> DUMP_MAX_KEY_VALUE = Option.of(Argument.class, null);
  private static final Option<Boolean> DUMP_MIN_KEY_VALUE_IS_HEX = Option.withDefault(false);
  private static final Option<Boolean> DUMP_MAX_KEY_VALUE_IS_HEX = Option.withDefault(false);
  private static final Option<Integer> DUMP_MIN_DATA_SIZE = Option.of(Integer.class, 0);
  private static final Option<Integer> DUMP_MAX_DATA_SIZE = Option.of(Integer.class, Integer.MAX_VALUE);
  private static final Option<Integer> DUMP_INDENT = Option.of(Integer.class, 4);

  // Sub-command names.
  private static final String LIST_BACKENDS = "list-backends";
  private static final String LIST_BASE_DNS = "list-base-dns";
  private static final String LIST_INDEXES = "list-indexes";
  private static final String SHOW_INDEX_STATUS = "show-index-status";
  private static final String DUMP_INDEX = "dump-index";
  private static final String LIST_RAW_DBS = "list-raw-dbs";
  private static final String DUMP_RAW_DB = "dump-raw-db";

  private static final String BACKENDID_NAME = "backendid";
  private static final String BACKENDID = "backendID";
  private static final String BASEDN_NAME = "basedn";
  private static final String BASEDN = "baseDN";
  private static final String USESIUNITS_NAME = "usesiunits";
  private static final String USESIUNITS = "useSIUnits";
  private static final String MAXDATASIZE_NAME = "maxdatasize";
  private static final String MAXDATASIZE = "maxDataSize";
  private static final String MAXKEYVALUE_NAME = "maxkeyvalue";
  private static final String MAXKEYVALUE = "maxKeyValue";
  private static final String MAXHEXKEYVALUE_NAME = "maxhexkeyvalue";
  private static final String MAXHEXKEYVALUE = "maxHexKeyValue";
  private static final String MINDATASIZE_NAME = "mindatasize";
  private static final String MINDATASIZE = "minDataSize";
  private static final String MINKEYVALUE_NAME = "minkeyvalue";
  private static final String MINKEYVALUE = "minKeyValue";
  private static final String MINHEXKEYVALUE_NAME = "minhexkeyvalue";
  private static final String MINHEXKEYVALUE = "minHexKeyValue";
  private static final String SKIPDECODE_NAME = "skipdecode";
  private static final String SKIPDECODE = "skipDecode";
  private static final String STATSONLY_NAME = "statsonly";
  private static final String STATSONLY = "statsOnly";
  private static final String INDEXNAME_NAME = "indexname";
  private static final String INDEXNAME = "indexName";
  private static final String DBNAME_NAME = "dbname";
  private static final String DBNAME = "dbName";
  private static final String SINGLELINE_NAME = "singleline";
  private static final String SINGLELINE = "singleLine";

  private static final String HEXDUMP_LINE_FORMAT = "%s%s %s%n";

  /** The error stream which this application should use. */
  private final PrintStream err;
  /** The output stream which this application should use. */
  private final PrintStream out;

  /** The command-line argument parser. */
  private final SubCommandArgumentParser parser;
  /** The argument which should be used to request usage information. */
  private BooleanArgument showUsageArgument;
  /** The argument which should be used to specify the config class. */
  private StringArgument configClass;
  /** The argument which should be used to specify the config file. */
  private StringArgument configFile;

  /** Flag indicating whether or not the sub-commands have already been initialized. */
  private boolean subCommandsInitialized;
  /** Flag indicating whether or not the global arguments have already been initialized. */
  private boolean globalArgumentsInitialized;

  private DirectoryServer directoryServer;

  /**
   * Provides the command-line arguments to the main application for
   * processing.
   *
   * @param args The set of command-line arguments provided to this
   *             program.
   */
  public static void main(String[] args)
  {
    int exitCode = main(args, System.out, System.err);
    if (exitCode != 0)
    {
      System.exit(filterExitCode(exitCode));
    }
  }

  /**
   * Provides the command-line arguments to the main application for
   * processing and returns the exit code as an integer.
   *
   * @param args      The set of command-line arguments provided to this
   *                  program.
   * @param outStream The output stream for standard output.
   * @param errStream The output stream for standard error.
   * @return Zero to indicate that the program completed successfully,
   * or non-zero to indicate that an error occurred.
   */
  public static int main(String[] args, OutputStream outStream, OutputStream errStream)
  {
    BackendStat app = new BackendStat(outStream, errStream);
    return app.run(args);
  }

  /**
   * Creates a new dsconfig application instance.
   *
   * @param out The application output stream.
   * @param err The application error stream.
   */
  public BackendStat(OutputStream out, OutputStream err)
  {
    this.out = NullOutputStream.wrapOrNullStream(out);
    this.err = NullOutputStream.wrapOrNullStream(err);
    JDKLogging.disableLogging();

    LocalizableMessage toolDescription = INFO_DESCRIPTION_BACKEND_TOOL.get();
    this.parser = new SubCommandArgumentParser(getClass().getName(), toolDescription, false);
    this.parser.setShortToolDescription(REF_SHORT_DESC_BACKEND_TOOL.get());
    this.parser.setVersionHandler(new DirectoryServerVersionHandler());
  }

  /**
   * Registers the global arguments with the argument parser.
   *
   * @throws ArgumentException If a global argument could not be registered.
   */
  private void initializeGlobalArguments() throws ArgumentException
  {
    if (!globalArgumentsInitialized)
    {
      configClass =
          new StringArgument("configclass", OPTION_SHORT_CONFIG_CLASS, OPTION_LONG_CONFIG_CLASS, true, false, true,
              INFO_CONFIGCLASS_PLACEHOLDER.get(), ConfigFileHandler.class.getName(), null,
              INFO_DESCRIPTION_CONFIG_CLASS.get());
      configClass.setHidden(true);

      configFile =
          new StringArgument("configfile", 'f', "configFile", true, false, true, INFO_CONFIGFILE_PLACEHOLDER.get(),
              null, null, INFO_DESCRIPTION_CONFIG_FILE.get());
      configFile.setHidden(true);

      showUsageArgument = CommonArguments.getShowUsage();

      // Register the global arguments.
      parser.addGlobalArgument(showUsageArgument);
      parser.setUsageArgument(showUsageArgument, out);
      parser.addGlobalArgument(configClass);
      parser.addGlobalArgument(configFile);

      globalArgumentsInitialized = true;
    }
  }

  /**
   * Registers the sub-commands with the argument parser.
   *
   * @throws ArgumentException If a sub-command could not be created.
   */
  private void initializeSubCommands() throws ArgumentException
  {
    if (!subCommandsInitialized)
    {
      // list-backends
      new SubCommand(parser, LIST_BACKENDS,
                           INFO_DESCRIPTION_BACKEND_TOOL_SUBCMD_LIST_BACKENDS.get());

      // list-base-dns
      SubCommand sub = new SubCommand(parser, LIST_BASE_DNS,
                                      INFO_DESCRIPTION_BACKEND_DEBUG_SUBCMD_LIST_ENTRY_CONTAINERS.get());
      addBackendArgument(sub);

      // list-indexes
      sub = new SubCommand(parser, LIST_INDEXES, INFO_DESCRIPTION_BACKEND_TOOL_SUBCMD_LIST_INDEXES.get());
      addBackendBaseDNArguments(sub, false, false, true);

      // show-index-status
      sub = new SubCommand(parser, SHOW_INDEX_STATUS, INFO_DESCRIPTION_BACKEND_DEBUG_SUBCMD_LIST_INDEX_STATUS.get());
      sub.setDocDescriptionSupplement(SUPPLEMENT_DESCRIPTION_BACKEND_TOOL_SUBCMD_LIST_INDEX_STATUS.get());
      addBackendBaseDNArguments(sub, true, true, true);

      // dump-index
      sub = new SubCommand(parser, DUMP_INDEX, INFO_DESCRIPTION_BACKEND_TOOL_SUBCMD_DUMP_INDEX.get());
      addBackendBaseDNArguments(sub, true, false, true);
      sub.addArgument(new StringArgument(INDEXNAME_NAME, 'i', INDEXNAME, true, false, true,
                                         INFO_INDEX_NAME_PLACEHOLDER.get(), null, null,
                                         INFO_DESCRIPTION_BACKEND_DEBUG_INDEX_NAME.get()));
      addDumpSubCommandArguments(sub);
      BooleanArgument skipDecode =
          new BooleanArgument(SKIPDECODE_NAME, 'p', SKIPDECODE, INFO_DESCRIPTION_BACKEND_DEBUG_SKIP_DECODE.get());
      sub.addArgument(skipDecode);

      // list-raw-dbs
      sub = new SubCommand(parser, LIST_RAW_DBS, INFO_DESCRIPTION_BACKEND_TOOL_SUBCMD_LIST_RAW_DBS.get());
      addBackendArgument(sub);
      BooleanArgument useSIUnits =
          new BooleanArgument(USESIUNITS_NAME, 'u', USESIUNITS, INFO_DESCRIPTION_BACKEND_TOOL_USE_SI_UNITS.get());
      sub.addArgument(useSIUnits);

      // dump-raw-db
      sub = new SubCommand(parser, DUMP_RAW_DB, INFO_DESCRIPTION_BACKEND_TOOL_SUBCMD_DUMP_RAW_DB.get());
      addBackendArgument(sub);
      sub.addArgument(new StringArgument(DBNAME_NAME, 'd', DBNAME, true, false, true,
                                         INFO_DATABASE_NAME_PLACEHOLDER.get(), null, null,
                                         INFO_DESCRIPTION_BACKEND_DEBUG_RAW_DB_NAME.get()));
      addDumpSubCommandArguments(sub);
      BooleanArgument singleLine =
          new BooleanArgument(SINGLELINE_NAME, 'l', SINGLELINE, INFO_DESCRIPTION_BACKEND_TOOL_SUBCMD_SINGLE_LINE.get());
      sub.addArgument(singleLine);

      subCommandsInitialized = true;
    }
  }

  private void addBackendArgument(SubCommand sub) throws ArgumentException
  {
    sub.addArgument(
        new StringArgument(BACKENDID_NAME, 'n', BACKENDID, true, false, true, INFO_BACKENDNAME_PLACEHOLDER.get(), null,
            null, INFO_DESCRIPTION_BACKEND_DEBUG_BACKEND_ID.get()));
  }

  private void addBackendBaseDNArguments(SubCommand sub, boolean isRequired, boolean isMultiValued, boolean needsValue)
      throws ArgumentException
  {
    addBackendArgument(sub);
    sub.addArgument(new StringArgument(BASEDN_NAME, 'b', BASEDN, isRequired, isMultiValued, needsValue,
        INFO_BASEDN_PLACEHOLDER.get(), null, null, INFO_DESCRIPTION_BACKEND_DEBUG_BASE_DN.get()));
  }

  private void addDumpSubCommandArguments(SubCommand sub) throws ArgumentException
  {
    sub.addArgument(new BooleanArgument(STATSONLY_NAME, 'q', STATSONLY,
        INFO_DESCRIPTION_BACKEND_DEBUG_STATS_ONLY.get()));
    sub.addArgument(newMaxKeyValueArg());
    sub.addArgument(newMinKeyValueArg());
    sub.addArgument(new StringArgument(MAXHEXKEYVALUE_NAME, 'X', MAXHEXKEYVALUE, false, false, true,
        INFO_MAX_KEY_VALUE_PLACEHOLDER.get(), null, null, INFO_DESCRIPTION_BACKEND_DEBUG_MAX_KEY_VALUE.get()));
    sub.addArgument(new StringArgument(MINHEXKEYVALUE_NAME, 'x', MINHEXKEYVALUE, false, false, true,
        INFO_MIN_KEY_VALUE_PLACEHOLDER.get(), null, null, INFO_DESCRIPTION_BACKEND_DEBUG_MIN_KEY_VALUE.get()));
    sub.addArgument(new IntegerArgument(MAXDATASIZE_NAME, 'S', MAXDATASIZE, false, false, true,
        INFO_MAX_DATA_SIZE_PLACEHOLDER.get(), -1, null, INFO_DESCRIPTION_BACKEND_DEBUG_MAX_DATA_SIZE.get()));
    sub.addArgument(new IntegerArgument(MINDATASIZE_NAME, 's', MINDATASIZE, false, false, true,
        INFO_MIN_DATA_SIZE_PLACEHOLDER.get(), -1, null, INFO_DESCRIPTION_BACKEND_DEBUG_MIN_DATA_SIZE.get()));
  }

  private StringArgument newMinKeyValueArg() throws ArgumentException
  {
    return new StringArgument(MINKEYVALUE_NAME, 'k', MINKEYVALUE, false, false, true,
        INFO_MIN_KEY_VALUE_PLACEHOLDER.get(), null, null, INFO_DESCRIPTION_BACKEND_DEBUG_MIN_KEY_VALUE.get());
  }

  private StringArgument newMaxKeyValueArg() throws ArgumentException
  {
    return new StringArgument(MAXKEYVALUE_NAME, 'K', MAXKEYVALUE, false, false, true,
        INFO_MAX_KEY_VALUE_PLACEHOLDER.get(), null, null, INFO_DESCRIPTION_BACKEND_DEBUG_MAX_KEY_VALUE.get());
  }

  /**
   * Parses the provided command-line arguments and makes the
   * appropriate changes to the Directory Server configuration.
   *
   * @param args The command-line arguments provided to this program.
   * @return The exit code from the configuration processing. A
   * nonzero value indicates that there was some kind of
   * problem during the configuration processing.
   */
  private int run(String[] args)
  {
    // Register global arguments and sub-commands.
    try
    {
      initializeGlobalArguments();
      initializeSubCommands();
    }
    catch (ArgumentException e)
    {
      printWrappedText(err, ERR_CANNOT_INITIALIZE_ARGS.get(e.getMessage()));
      return 1;
    }

    try
    {
      parser.parseArguments(args);
    }
    catch (ArgumentException ae)
    {
      parser.displayMessageAndUsageReference(err, ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
      return 1;
    }

    if (parser.usageOrVersionDisplayed())
    {
      return 0;
    }

    if (parser.getSubCommand() == null)
    {
      parser.displayMessageAndUsageReference(err, ERR_BACKEND_DEBUG_MISSING_SUBCOMMAND.get());
      return 1;
    }

    try
    {
      BuildVersion.checkVersionMismatch();
    }
    catch (InitializationException e)
    {
      printWrappedText(err, e.getMessageObject());
      return 1;
    }

    // Perform the initial bootstrap of the Directory Server and process the configuration.
    directoryServer = DirectoryServer.getInstance();
    try
    {
      DirectoryServer.bootstrapClient();
      DirectoryServer.initializeJMX();
    }
    catch (Exception e)
    {
      printWrappedText(err, ERR_SERVER_BOOTSTRAP_ERROR.get(getStartUpExceptionMessage(e)));
      return 1;
    }

    try
    {
      directoryServer.initializeConfiguration(configClass.getValue(), configFile.getValue());
    }
    catch (Exception e)
    {
      printWrappedText(err, ERR_CANNOT_LOAD_CONFIG.get(getStartUpExceptionMessage(e)));
      return 1;
    }

    try
    {
      directoryServer.initializeSchema();
    }
    catch (Exception e)
    {
      printWrappedText(err, ERR_CANNOT_LOAD_SCHEMA.get(getStartUpExceptionMessage(e)));
      return 1;
    }

    try
    {
      CoreConfigManager coreConfigManager = new CoreConfigManager(directoryServer.getServerContext());
      coreConfigManager.initializeCoreConfig();
    }
    catch (Exception e)
    {
      printWrappedText(err, ERR_CANNOT_INITIALIZE_CORE_CONFIG.get(getStartUpExceptionMessage(e)));
      return 1;
    }

    try
    {
      directoryServer.initializeCryptoManager();
    }
    catch (Exception e)
    {
      printWrappedText(err, ERR_CANNOT_INITIALIZE_CRYPTO_MANAGER.get(getStartUpExceptionMessage(e)));
      return 1;
    }

    SubCommand subCommand = parser.getSubCommand();
    if (LIST_BACKENDS.equals(subCommand.getName()))
    {
      return listRootContainers();
    }
    BackendImpl backend = getBackendById(subCommand.getArgument(BACKENDID_NAME));
    if (backend == null)
    {
      return 1;
    }
    RootContainer rootContainer = getAndLockRootContainer(backend);
    if (rootContainer == null)
    {
      return 1;
    }
    try
    {
      switch (subCommand.getName())
      {
      case LIST_BASE_DNS:
        return listBaseDNs(rootContainer);
      case LIST_RAW_DBS:
        return listRawDBs(rootContainer, subCommand.getArgument(USESIUNITS_NAME));
      case LIST_INDEXES:
        return listIndexes(rootContainer, backend, subCommand.getArgument(BASEDN_NAME));
      case DUMP_RAW_DB:
        return dumpTree(rootContainer, backend, subCommand, false);
      case DUMP_INDEX:
        return dumpTree(rootContainer, backend, subCommand, true);
      case SHOW_INDEX_STATUS:
        return showIndexStatus(rootContainer, backend, subCommand.getArgument(BASEDN_NAME));
      default:
        return 1;
      }
    }
    catch (Exception e)
    {
      printWrappedText(err, ERR_BACKEND_TOOL_EXECUTING_COMMAND.get(subCommand.getName(),
          StaticUtils.stackTraceToString(e)));
      return 1;
    }
    finally
    {
      close(rootContainer);
      releaseExclusiveLock(backend);
    }
  }

  private String getStartUpExceptionMessage(Exception e)
  {
    if (e instanceof ConfigException || e instanceof InitializationException)
    {
      return e.getMessage();
    }
    return getExceptionMessage(e).toString();
  }

  private int dumpTree(RootContainer rc, BackendImpl backend, SubCommand subCommand, boolean isBackendTree)
      throws ArgumentException, DirectoryException
  {
    Options options = Options.defaultOptions();
    if (!setDumpTreeOptionArguments(subCommand, options))
    {
      return 1;
    }
    if (isBackendTree)
    {
      return dumpBackendTree(rc, backend, subCommand.getArgument(BASEDN_NAME), subCommand.getArgument(INDEXNAME_NAME),
          options);
    }
    return dumpStorageTree(rc, backend, subCommand.getArgument(DBNAME_NAME), options);
  }

  private boolean setDumpTreeOptionArguments(SubCommand subCommand, Options options) throws ArgumentException
  {
    try
    {
      Argument arg = subCommand.getArgument(SINGLELINE_NAME);
      if (arg != null && arg.isPresent())
      {
        options.set(DUMP_SINGLE_LINE, true);
      }
      if (subCommand.getArgument(STATSONLY_NAME).isPresent())
      {
        options.set(DUMP_STATS_ONLY, true);
      }
      arg = subCommand.getArgument(SKIPDECODE_NAME);
      if (arg == null || arg.isPresent())
      {
        options.set(DUMP_DECODE_VALUE, false);
      }
      if (subCommand.getArgument(MINDATASIZE_NAME).isPresent())
      {
        options.set(DUMP_MIN_DATA_SIZE, subCommand.getArgument(MINDATASIZE_NAME).getIntValue());
      }
      if (subCommand.getArgument(MAXDATASIZE_NAME).isPresent())
      {
        options.set(DUMP_MAX_DATA_SIZE, subCommand.getArgument(MAXDATASIZE_NAME).getIntValue());
      }

      options.set(DUMP_MIN_KEY_VALUE, subCommand.getArgument(MINKEYVALUE_NAME));
      if (subCommand.getArgument(MINHEXKEYVALUE_NAME).isPresent())
      {
        if (subCommand.getArgument(MINKEYVALUE_NAME).isPresent())
        {
          printWrappedText(err, ERR_BACKEND_TOOL_ONLY_ONE_MIN_KEY.get());
          return false;
        }
        options.set(DUMP_MIN_KEY_VALUE_IS_HEX, true);
        options.set(DUMP_MIN_KEY_VALUE, subCommand.getArgument(MINHEXKEYVALUE_NAME));
      }

      options.set(DUMP_MAX_KEY_VALUE, subCommand.getArgument(MAXKEYVALUE_NAME));
      if (subCommand.getArgument(MAXHEXKEYVALUE_NAME).isPresent())
      {
        if (subCommand.getArgument(MAXKEYVALUE_NAME).isPresent())
        {
          printWrappedText(err, ERR_BACKEND_TOOL_ONLY_ONE_MAX_KEY.get());
          return false;
        }
        options.set(DUMP_MAX_KEY_VALUE_IS_HEX, true);
        options.set(DUMP_MAX_KEY_VALUE, subCommand.getArgument(MAXHEXKEYVALUE_NAME));
      }
      return true;
    }
    catch (ArgumentException ae)
    {
      printWrappedText(err, ERR_BACKEND_TOOL_PROCESSING_ARGUMENT.get(StaticUtils.stackTraceToString(ae)));
      throw ae;
    }
  }

  private int listRootContainers()
  {
    TableBuilder builder = new TableBuilder();

    builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_BACKEND_ID.get());
    builder.appendHeading(INFO_LABEL_BACKEND_TOOL_STORAGE.get());

    final Map<PluggableBackendCfg, BackendImpl> pluggableBackends = getPluggableBackends();
    for (Map.Entry<PluggableBackendCfg, BackendImpl> backend : pluggableBackends.entrySet())
    {
      builder.startRow();
      builder.appendCell(backend.getValue().getBackendID());
      builder.appendCell(backend.getKey().getJavaClass());
    }

    builder.print(new TextTablePrinter(out));
    out.format(INFO_LABEL_BACKEND_TOOL_TOTAL.get(pluggableBackends.size()).toString());

    return 0;
  }

  private int listBaseDNs(RootContainer rc)
  {
    try
    {
      TableBuilder builder = new TableBuilder();
      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_BASE_DN.get());
      Collection<EntryContainer> entryContainers = rc.getEntryContainers();
      for (EntryContainer ec : entryContainers)
      {
        builder.startRow();
        builder.appendCell(ec.getBaseDN());
      }

      builder.print(new TextTablePrinter(out));
      out.format(INFO_LABEL_BACKEND_TOOL_TOTAL.get(entryContainers.size()).toString());

      return 0;
    }
    catch (StorageRuntimeException de)
    {
      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_LISTING_BASE_DNS.get(stackTraceToSingleLineString(de)));
      return 1;
    }
  }

  private int listRawDBs(RootContainer rc, Argument useSIUnits)
  {
    try
    {
      TableBuilder builder = new TableBuilder();

      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_RAW_DB_NAME.get());
      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_TOTAL_KEYS.get());
      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_KEYS_SIZE.get());
      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_VALUES_SIZE.get());
      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_TOTAL_SIZES.get());

      SortedSet<TreeName> treeNames = new TreeSet<>(rc.getStorage().listTrees());
      for (TreeName tree: treeNames)
      {
        builder.startRow();
        builder.appendCell(tree);
        appendStorageTreeStats(builder, rc, new StorageTreeKeyValue(tree), useSIUnits.isPresent());
      }

      builder.print(new TextTablePrinter(out));
      out.format(INFO_LABEL_BACKEND_TOOL_TOTAL.get(treeNames.size()).toString());

      return 0;
    }
    catch (StorageRuntimeException de)
    {
      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_LISTING_TREES.get(stackTraceToSingleLineString(de)));
      return 1;
    }
  }

  private void appendStorageTreeStats(TableBuilder builder, RootContainer rc, TreeKeyValue targetTree,
      boolean useSIUnit)
  {
    Options options = Options.defaultOptions();
    options.set(DUMP_STATS_ONLY, true);
    try
    {
      options.set(DUMP_MIN_KEY_VALUE, newMinKeyValueArg());
      options.set(DUMP_MAX_KEY_VALUE, newMaxKeyValueArg());
      TreeStats treeStats = cursorTreeToDump(rc, targetTree, options);
      builder.appendCell(treeStats.count);
      builder.appendCell(appendKeyValueSize(treeStats.totalKeySize, useSIUnit));
      builder.appendCell(appendKeyValueSize(treeStats.totalDataSize, useSIUnit));
      builder.appendCell(appendKeyValueSize(treeStats.totalKeySize + treeStats.totalDataSize, useSIUnit));
    }
    catch (Exception e)
    {
      appendStatsNoData(builder, 3);
    }
  }

  private String appendKeyValueSize(long size, boolean useSIUnit)
  {
    if (useSIUnit && size > SizeUnit.KILO_BYTES.getSize())
    {
      NumberFormat format = NumberFormat.getNumberInstance();
      format.setMaximumFractionDigits(2);
      SizeUnit unit = SizeUnit.getBestFitUnit(size);
      return format.format(unit.fromBytes(size)) + " " + unit;
    }
    else
    {
      return String.valueOf(size);
    }
  }

  private int listIndexes(RootContainer rc, BackendImpl backend, Argument baseDNArg) throws DirectoryException
  {
    DN base = null;
    if (baseDNArg.isPresent())
    {
      base = getBaseDNFromArg(baseDNArg);
    }

    try
    {
      TableBuilder builder = new TableBuilder();
      int count = 0;

      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_INDEX_NAME.get());
      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_RAW_DB_NAME.get());
      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_INDEX_TYPE.get());
      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_RECORD_COUNT.get());

      if (base != null)
      {
        EntryContainer ec = rc.getEntryContainer(base);
        if (ec == null)
        {
          return printEntryContainerError(backend, base);
        }
        count = appendTreeRows(builder, ec);
      }
      else
      {
        for (EntryContainer ec : rc.getEntryContainers())
        {
          builder.startRow();
          builder.appendCell("Base DN: " + ec.getBaseDN());
          count += appendTreeRows(builder, ec);
        }
      }

      builder.print(new TextTablePrinter(out));
      out.format(INFO_LABEL_BACKEND_TOOL_TOTAL.get(count).toString());

      return 0;
    }
    catch (StorageRuntimeException de)
    {
      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_LISTING_TREES.get(stackTraceToSingleLineString(de)));
      return 1;
    }
  }

  private int printEntryContainerError(BackendImpl backend, DN base)
  {
    printWrappedText(err, ERR_BACKEND_DEBUG_NO_ENTRY_CONTAINERS_FOR_BASE_DN.get(base, backend.getBackendID()));
    return 1;
  }

  private DN getBaseDNFromArg(Argument baseDNArg) throws DirectoryException
  {
    try
    {
      return DN.valueOf(baseDNArg.getValue());
    }
    catch (DirectoryException de)
    {
      printWrappedText(err, ERR_BACKEND_DEBUG_DECODE_BASE_DN.get(baseDNArg.getValue(), getExceptionMessage(de)));
      throw de;
    }
  }

  private RootContainer getAndLockRootContainer(BackendImpl backend)
  {
    try
    {
      String lockFile = LockFileManager.getBackendLockFileName(backend);
      StringBuilder failureReason = new StringBuilder();
      if (!LockFileManager.acquireExclusiveLock(lockFile, failureReason))
      {
        printWrappedText(err, ERR_BACKEND_DEBUG_CANNOT_LOCK_BACKEND.get(backend.getBackendID(), failureReason));
        return null;
      }
    }
    catch (Exception e)
    {
      printWrappedText(err, ERR_BACKEND_DEBUG_CANNOT_LOCK_BACKEND.get(backend.getBackendID(), StaticUtils
          .getExceptionMessage(e)));
      return null;
    }

    try
    {
      return backend.getReadOnlyRootContainer();
    }
    catch (Exception e)
    {
      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_INITIALIZING_BACKEND.get(backend.getBackendID(),
          stackTraceToSingleLineString(e)));
      return null;
    }
  }

  private int appendTreeRows(TableBuilder builder, EntryContainer ec)
  {
    int count = 0;
    for (final Tree tree : ec.listTrees())
    {
      builder.startRow();
      builder.appendCell(tree.getName().getIndexId());
      builder.appendCell(tree.getName());
      builder.appendCell(tree.getClass().getSimpleName());
      builder.appendCell(getTreeRecordCount(ec, tree));
      count++;
    }
    return count;
  }

  private long getTreeRecordCount(EntryContainer ec, final Tree tree)
  {
    try
    {
      return ec.getRootContainer().getStorage().read(new ReadOperation<Long>()
      {
        @Override
        public Long run(ReadableTransaction txn) throws Exception
        {
          return tree.getRecordCount(txn);
        }
      });
    }
    catch (Exception e)
    {
      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_READING_TREE.get(stackTraceToSingleLineString(e)));
      return -1;
    }
  }

  private void close(RootContainer rc)
  {
    try
    {
      rc.close();
    }
    catch (StorageRuntimeException ignored)
    {
      // Ignore.
    }
  }

  private void releaseExclusiveLock(BackendImpl backend)
  {
    try
    {
      String lockFile = LockFileManager.getBackendLockFileName(backend);
      StringBuilder failureReason = new StringBuilder();
      if (!LockFileManager.releaseLock(lockFile, failureReason))
      {
        printWrappedText(err, WARN_BACKEND_DEBUG_CANNOT_UNLOCK_BACKEND.get(backend.getBackendID(), failureReason));
      }
    }
    catch (Exception e)
    {
      printWrappedText(err, WARN_BACKEND_DEBUG_CANNOT_UNLOCK_BACKEND.get(backend.getBackendID(),
          StaticUtils.getExceptionMessage(e)));
    }
  }

  private BackendImpl getBackendById(Argument backendIdArg)
  {
    final String backendID = backendIdArg.getValue();
    final Map<PluggableBackendCfg, BackendImpl> pluggableBackends = getPluggableBackends();

    for (Map.Entry<PluggableBackendCfg, BackendImpl> backend : pluggableBackends.entrySet())
    {
      final BackendImpl b = backend.getValue();
      if (b.getBackendID().equalsIgnoreCase(backendID))
      {
        try
        {
          b.configureBackend(backend.getKey(), directoryServer.getServerContext());
          return b;
        }
        catch (ConfigException ce)
        {
          printWrappedText(err, ERR_BACKEND_TOOL_CANNOT_CONFIGURE_BACKEND.get(backendID, ce));
          return null;
        }
      }
    }

    printWrappedText(err, ERR_BACKEND_DEBUG_NO_BACKENDS_FOR_ID.get(backendID));
    return null;
  }

  private int showIndexStatus(RootContainer rc, BackendImpl backend, Argument baseDNArg) throws DirectoryException
  {
    DN base = getBaseDNFromArg(baseDNArg);

    try
    {
      // Create a table of their properties.
      TableBuilder builder = new TableBuilder();
      int count = 0;

      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_INDEX_NAME.get());
      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_RAW_DB_NAME.get());
      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_INDEX_STATUS.get());
      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_RECORD_COUNT.get());
      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_INDEX_UNDEFINED_RECORD_COUNT.get());
      builder.appendHeading(LocalizableMessage.raw("95%"));
      builder.appendHeading(LocalizableMessage.raw("90%"));
      builder.appendHeading(LocalizableMessage.raw("85%"));

      EntryContainer ec = rc.getEntryContainer(base);
      if (ec == null)
      {
        return printEntryContainerError(backend, base);
      }

      Map<Index, StringBuilder> undefinedKeys = new HashMap<>();
      for (AttributeIndex attrIndex : ec.getAttributeIndexes())
      {
        for (AttributeIndex.MatchingRuleIndex index : attrIndex.getNameToIndexes().values())
        {
          builder.startRow();
          builder.appendCell(index.getName().getIndexId());
          builder.appendCell(index.getName());
          builder.appendCell(index.isTrusted());
          if (index.isTrusted())
          {
            appendIndexStats(builder, ec, index, undefinedKeys);
          }
          else
          {
            appendStatsNoData(builder, 5);
          }
          count++;
        }
      }

      for (VLVIndex vlvIndex : ec.getVLVIndexes())
      {
        builder.startRow();
        builder.appendCell(vlvIndex.getName().getIndexId());
        builder.appendCell(vlvIndex.getName());
        builder.appendCell(vlvIndex.isTrusted());
        builder.appendCell(getTreeRecordCount(ec, vlvIndex));
        appendStatsNoData(builder, 4);
        count++;
      }

      builder.print(new TextTablePrinter(out));
      out.format(INFO_LABEL_BACKEND_TOOL_TOTAL.get(count).toString());
      for (Map.Entry<Index, StringBuilder> e : undefinedKeys.entrySet())
      {
        out.format(INFO_LABEL_BACKEND_TOOL_INDEX.get(e.getKey().getName()).toString());
        out.format(INFO_LABEL_BACKEND_TOOL_OVER_INDEX_LIMIT_KEYS.get(e.getValue()).toString());
      }
      return 0;
    }
    catch (StorageRuntimeException de)
    {
      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_READING_TREE.get(stackTraceToSingleLineString(de)));
      return 1;
    }
  }

  private void appendStatsNoData(TableBuilder builder, int columns)
  {
    while (columns > 0)
    {
      builder.appendCell("-");
      columns--;
    }
  }

  private void appendIndexStats(final TableBuilder builder, EntryContainer ec, final Index index,
      final Map<Index, StringBuilder> undefinedKeys)
  {
    final long entryLimit = index.getIndexEntryLimit();

    try
    {
      ec.getRootContainer().getStorage().read(new ReadOperation<Void>()
      {
        @Override
        public Void run(ReadableTransaction txn) throws Exception
        {
          long eighty = 0;
          long ninety = 0;
          long ninetyFive = 0;
          long undefined = 0;
          long count = 0;
          BackendTreeKeyValue keyDecoder = new BackendTreeKeyValue(index);
          try (Cursor<ByteString, EntryIDSet> cursor = index.openCursor(txn))
          {
            while (cursor.next())
            {
              count++;
              EntryIDSet entryIDSet;
              try
              {
                entryIDSet = cursor.getValue();
              }
              catch (Exception e)
              {
                continue;
              }

              if (entryIDSet.isDefined())
              {
                if (entryIDSet.size() >= entryLimit * 0.8)
                {
                  if (entryIDSet.size() >= entryLimit * 0.95)
                  {
                    ninetyFive++;
                  }
                  else if (entryIDSet.size() >= entryLimit * 0.9)
                  {
                    ninety++;
                  }
                  else
                  {
                    eighty++;
                  }
                }
              }
              else
              {
                undefined++;
                StringBuilder keyList = undefinedKeys.get(index);
                if (keyList == null)
                {
                  keyList = new StringBuilder();
                  undefinedKeys.put(index, keyList);
                }
                else
                {
                  keyList.append(" ");
                }
                keyList.append("[").append(keyDecoder.keyDecoder(cursor.getKey())).append("]");
              }
            }
          }
          builder.appendCell(count);
          builder.appendCell(undefined);
          builder.appendCell(ninetyFive);
          builder.appendCell(ninety);
          builder.appendCell(eighty);
          return null;
        }
      });
    }
    catch (Exception e)
    {
      appendStatsNoData(builder, 5);
      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_READING_TREE.get(index.getName()));
    }
  }

  private int dumpStorageTree(RootContainer rc, BackendImpl backend, Argument treeNameArg, Options options)
  {
    TreeName targetTree = getStorageTreeName(treeNameArg, rc);
    if (targetTree == null)
    {
      printWrappedText(err,
          ERR_BACKEND_TOOL_NO_TREE_FOR_NAME_IN_STORAGE.get(treeNameArg.getValue(), backend.getBackendID()));
      return 1;
    }

    try
    {
      dumpActualTree(rc, new StorageTreeKeyValue(targetTree), options);
      return 0;
    }
    catch (Exception e)
    {
      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_READING_TREE.get(stackTraceToSingleLineString(e)));
      return 1;
    }
  }

  private TreeName getStorageTreeName(Argument treeNameArg, RootContainer rc)
  {
    for (TreeName tree : rc.getStorage().listTrees())
    {
      if (treeNameArg.getValue().equals(tree.toString()))
      {
        return tree;
      }
    }
    return null;
  }

  private int dumpBackendTree(RootContainer rc, BackendImpl backend, Argument baseDNArg, Argument treeNameArg,
      Options options) throws DirectoryException
  {
    DN base = getBaseDNFromArg(baseDNArg);

    EntryContainer ec = rc.getEntryContainer(base);
    if (ec == null)
    {
      return printEntryContainerError(backend, base);
    }

    Tree targetTree = getBackendTree(treeNameArg, ec);
    if (targetTree == null)
    {
      printWrappedText(err,
          ERR_BACKEND_TOOL_NO_TREE_FOR_NAME.get(treeNameArg.getValue(), base, backend.getBackendID()));
      return 1;
    }

    try
    {
      dumpActualTree(rc, new BackendTreeKeyValue(targetTree), options);
      return 0;
    }
    catch (Exception e)
    {
      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_READING_TREE.get(stackTraceToSingleLineString(e)));
      return 1;
    }
  }

  private Tree getBackendTree(Argument treeNameArg, EntryContainer ec)
  {
    for (Tree tree : ec.listTrees())
    {
      if (treeNameArg.getValue().contains(tree.getName().getIndexId())
          || treeNameArg.getValue().equals(tree.getName().toString()))
      {
        return tree;
      }
    }
    return null;
  }

  private void dumpActualTree(RootContainer rc, final TreeKeyValue target, final Options options) throws Exception
  {
    TreeStats treeStats =  cursorTreeToDump(rc, target, options);
    out.format(INFO_LABEL_BACKEND_TOOL_TOTAL_RECORDS.get(treeStats.count).toString());
    if (treeStats.count > 0)
    {
      out.format(INFO_LABEL_BACKEND_TOOL_TOTAL_KEY_SIZE_AND_AVG.get(
          treeStats.totalKeySize, treeStats.totalKeySize / treeStats.count).toString());
      out.format(INFO_LABEL_BACKEND_TOOL_TOTAL_DATA_SIZE_AND_AVG.get(
          treeStats.totalDataSize, treeStats.totalDataSize / treeStats.count).toString());
    }
  }

  private TreeStats cursorTreeToDump(RootContainer rc, final TreeKeyValue target, final Options options)
      throws Exception
  {
    return rc.getStorage().read(new ReadOperation<TreeStats>()
      {
        @Override
        public TreeStats run(ReadableTransaction txn) throws Exception
        {
          long count = 0;
          long totalKeySize = 0;
          long totalDataSize = 0;
          try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(target.getTreeName()))
          {
            ByteString key;
            ByteString maxKey = null;
            ByteString value;

            if (options.get(DUMP_MIN_KEY_VALUE).isPresent())
            {
              key = getMinOrMaxKey(options, DUMP_MIN_KEY_VALUE, DUMP_MIN_KEY_VALUE_IS_HEX);
              if (!cursor.positionToKeyOrNext(key))
              {
                return new TreeStats(0, 0, 0);
              }
            }
            else
            {
              if (!cursor.next())
              {
                return new TreeStats(0, 0, 0);
              }
            }

            if (options.get(DUMP_MAX_KEY_VALUE).isPresent())
            {
              maxKey = getMinOrMaxKey(options, DUMP_MAX_KEY_VALUE, DUMP_MAX_KEY_VALUE_IS_HEX);
            }

            do
            {
              key = cursor.getKey();
              if (maxKey != null && key.compareTo(maxKey) > 0)
              {
                break;
              }
              value = cursor.getValue();
              long valueLen = value.length();
              if (options.get(DUMP_MIN_DATA_SIZE) <= valueLen && valueLen <= options.get(DUMP_MAX_DATA_SIZE))
              {
                count++;
                int keyLen = key.length();
                totalKeySize += keyLen;
                totalDataSize += valueLen;
                if (!options.get(DUMP_STATS_ONLY))
                {
                  if (options.get(DUMP_DECODE_VALUE))
                  {
                    String k = target.keyDecoder(key);
                    String v = target.valueDecoder(value);
                    out.format(INFO_LABEL_BACKEND_TOOL_KEY_FORMAT.get(keyLen) + " %s%n"
                        + INFO_LABEL_BACKEND_TOOL_VALUE_FORMAT.get(valueLen) + " %s%n", k, v);
                  }
                  else
                  {
                    hexDumpRecord(key, value, out, options);
                  }
                }
              }
            }
            while (cursor.next());
          }
          catch (Exception e)
          {
            out.format(ERR_BACKEND_TOOL_CURSOR_AT_KEY_NUMBER.get(count, e.getCause()).toString());
            e.printStackTrace(out);
            out.format("%n");
            throw e;
          }
          return new TreeStats(count, totalKeySize, totalDataSize);
        }

      private ByteString getMinOrMaxKey(Options options, Option<Argument> keyOpt, Option<Boolean> isHexKey)
      {
        ByteString key;
        if (options.get(isHexKey))
        {
          key = ByteString.valueOfHex(options.get(keyOpt).getValue());
        }
        else
        {
          key = target.getTreeKey(options.get(keyOpt).getValue());
        }
        return key;
      }
    });
  }

  final void hexDumpRecord(ByteString key, ByteString value, PrintStream out, Options options)
  {
    if (options.get(DUMP_SINGLE_LINE))
    {
      out.format(INFO_LABEL_BACKEND_TOOL_KEY_FORMAT.get(key.length()) + " ");
      toHexDumpSingleLine(out, key);
      out.format(INFO_LABEL_BACKEND_TOOL_VALUE_FORMAT.get(value.length()) + " ");
      toHexDumpSingleLine(out, value);
    }
    else
    {
      out.format(INFO_LABEL_BACKEND_TOOL_KEY_FORMAT.get(key.length()) + "%n");
      toHexDumpWithAsciiCompact(key, options.get(DUMP_INDENT), out);
      out.format(INFO_LABEL_BACKEND_TOOL_VALUE_FORMAT.get(value.length()) + "%n");
      toHexDumpWithAsciiCompact(value, options.get(DUMP_INDENT), out);
    }
  }

  final void toHexDumpSingleLine(PrintStream out, ByteString data)
  {
    for (int i = 0; i < data.length(); i++)
    {
      out.format("%s", StaticUtils.byteToHex(data.byteAt(i)));
    }
    out.format("%n");
  }

  final void toHexDumpWithAsciiCompact(ByteString data, int indent, PrintStream out)
  {
    StringBuilder hexDump = new StringBuilder();
    StringBuilder indentBuilder = new StringBuilder();
    StringBuilder asciiDump = new StringBuilder();
    for (int i = 0; i < indent; i++)
    {
      indentBuilder.append(' ');
    }
    int pos = 0;
    while (pos < data.length())
    {
      byte val = data.byteAt(pos);
      hexDump.append(StaticUtils.byteToHex(val));
      hexDump.append(' ');
      asciiDump.append(val >= ' ' ? (char)val : ".");
      pos++;
      if (pos % 16 == 0)
      {
        out.format(HEXDUMP_LINE_FORMAT, indentBuilder.toString(), hexDump.toString(), asciiDump.toString());
        hexDump.setLength(0);
        asciiDump.setLength(0);
      }
    }
    while (pos % 16 != 0)
    {
      hexDump.append("   ");
      pos++;
    }
    out.format(HEXDUMP_LINE_FORMAT, indentBuilder.toString(), hexDump.toString(), asciiDump.toString());
  }

  private static Map<PluggableBackendCfg, BackendImpl> getPluggableBackends()
  {
    ArrayList<Backend> backendList = new ArrayList<>();
    ArrayList<BackendCfg> entryList = new ArrayList<>();
    ArrayList<List<DN>> dnList = new ArrayList<>();
    BackendToolUtils.getBackends(backendList, entryList, dnList);

    final Map<PluggableBackendCfg, BackendImpl> pluggableBackends = new LinkedHashMap<>();
    for (int i = 0; i < backendList.size(); i++)
    {
      Backend<?> backend = backendList.get(i);
      if (backend instanceof BackendImpl)
      {
        pluggableBackends.put((PluggableBackendCfg) entryList.get(i), (BackendImpl) backend);
      }
    }
    return pluggableBackends;
  }
}
