001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Portions Copyright 2015-2016 ForgeRock AS.
015 */
016package org.opends.server.backends.pluggable;
017
018import static org.opends.messages.ToolMessages.*;
019import static org.opends.server.util.StaticUtils.*;
020
021import static com.forgerock.opendj.cli.ArgumentConstants.*;
022import static com.forgerock.opendj.cli.Utils.*;
023import static com.forgerock.opendj.cli.CommonArguments.*;
024
025import java.io.OutputStream;
026import java.io.PrintStream;
027import java.text.NumberFormat;
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.HashMap;
031import java.util.LinkedHashMap;
032import java.util.List;
033import java.util.Map;
034import java.util.SortedSet;
035import java.util.TreeSet;
036
037import org.forgerock.i18n.LocalizableMessage;
038import org.forgerock.i18n.LocalizedIllegalArgumentException;
039import org.forgerock.opendj.config.SizeUnit;
040import org.forgerock.opendj.config.server.ConfigException;
041import org.forgerock.opendj.ldap.ByteString;
042import org.forgerock.util.Option;
043import org.forgerock.util.Options;
044import org.opends.server.admin.std.server.BackendCfg;
045import org.opends.server.admin.std.server.PluggableBackendCfg;
046import org.opends.server.api.Backend;
047import org.opends.server.backends.pluggable.spi.Cursor;
048import org.opends.server.backends.pluggable.spi.ReadOperation;
049import org.opends.server.backends.pluggable.spi.ReadableTransaction;
050import org.opends.server.backends.pluggable.spi.StorageRuntimeException;
051import org.opends.server.backends.pluggable.spi.TreeName;
052import org.opends.server.core.CoreConfigManager;
053import org.opends.server.core.DirectoryServer;
054import org.opends.server.core.DirectoryServer.DirectoryServerVersionHandler;
055import org.opends.server.core.LockFileManager;
056import org.opends.server.extensions.ConfigFileHandler;
057import org.opends.server.loggers.JDKLogging;
058import org.opends.server.tools.BackendToolUtils;
059import org.forgerock.opendj.ldap.DN;
060import org.opends.server.types.DirectoryException;
061import org.opends.server.types.InitializationException;
062import org.opends.server.types.NullOutputStream;
063import org.opends.server.util.BuildVersion;
064import org.opends.server.util.StaticUtils;
065
066import com.forgerock.opendj.cli.Argument;
067import com.forgerock.opendj.cli.ArgumentException;
068import com.forgerock.opendj.cli.BooleanArgument;
069import com.forgerock.opendj.cli.IntegerArgument;
070import com.forgerock.opendj.cli.StringArgument;
071import com.forgerock.opendj.cli.SubCommand;
072import com.forgerock.opendj.cli.SubCommandArgumentParser;
073import com.forgerock.opendj.cli.TableBuilder;
074import com.forgerock.opendj.cli.TextTablePrinter;
075
076/**
077 * This program provides a utility that may be used to debug a Pluggable Backend.
078 * This tool provides the ability to:
079 * <ul>
080 * <li>list root containers</li>
081 * <li>list entry containers</li>
082 * <li>list Trees in a Backend or Storage</li>
083 * <li>gather information about Backend indexes</li>
084 * <li>dump the contents of a Tree either at the Backend or the Storage layer.</li>
085 * </ul>
086 * This will be a process that is intended to run outside of Directory Server and not
087 * internally within the server process (e.g., via the tasks interface); it still
088 * requires configuration information and access to Directory Server instance data.
089 */
090public class BackendStat
091{
092  /**
093   * Collects all necessary interaction interfaces with either a Backend using TreeNames
094   * or a storage using Trees.
095   */
096  private interface TreeKeyValue
097  {
098    /**
099     * Returns a key given a string representation of it.
100     *
101     * @param data a string representation of the key.
102     *             Prefixing with "0x" will interpret the rest of the string as an hex dump
103     *             of the intended value.
104     * @return a key given a string representation of it
105     */
106    ByteString getTreeKey(String data);
107
108    /**
109     * Returns a printable string for the given key.
110     *
111     * @param key a key from the Tree
112     * @return a printable string for the given key
113     */
114    String keyDecoder(ByteString key);
115
116    /**
117     * Returns a printable string for the given value.
118     *
119     * @param value a value from the tree
120     * @return a printable string for the given value
121     */
122    String valueDecoder(ByteString value);
123
124    /**
125     * Returns the TreeName for this storage Tree.
126     *
127     * @return the TreeName for this storage Tree
128     */
129    TreeName getTreeName();
130  }
131
132  /** Stays at the storage level when cursoring Trees. */
133  private static class StorageTreeKeyValue implements TreeKeyValue
134  {
135    private final TreeName treeName;
136
137    private StorageTreeKeyValue(TreeName treeName)
138    {
139      this.treeName = treeName;
140    }
141
142    @Override
143    public TreeName getTreeName()
144    {
145      return treeName;
146    }
147
148    @Override
149    public ByteString getTreeKey(String data)
150    {
151      return ByteString.valueOfUtf8(data);
152    }
153
154    @Override
155    public String keyDecoder(ByteString key)
156    {
157      throw new UnsupportedOperationException(ERR_BACKEND_TOOL_DECODER_NOT_AVAILABLE.get().toString());
158    }
159
160    @Override
161    public String valueDecoder(ByteString value)
162    {
163      throw new UnsupportedOperationException(ERR_BACKEND_TOOL_DECODER_NOT_AVAILABLE.get().toString());
164    }
165  }
166
167  /** Delegate key semantics to the backend. */
168  private static class BackendTreeKeyValue implements TreeKeyValue
169  {
170    private final TreeName name;
171    private final Tree tree;
172
173    private BackendTreeKeyValue(Tree tree)
174    {
175      this.tree = tree;
176      this.name = tree.getName();
177    }
178
179    @Override
180    public ByteString getTreeKey(String data)
181    {
182      if (data.length() == 0)
183      {
184        return ByteString.empty();
185      }
186      return tree.generateKey(data);
187    }
188
189    @Override
190    public String keyDecoder(ByteString key)
191    {
192      return tree.keyToString(key);
193    }
194
195    @Override
196    public String valueDecoder(ByteString value)
197    {
198      return tree.valueToString(value);
199    }
200
201    @Override
202    public TreeName getTreeName()
203    {
204      return name;
205    }
206  }
207
208  /** Statistics collector. */
209  private class TreeStats
210  {
211    private final long count;
212    private final long totalKeySize;
213    private final long totalDataSize;
214
215    private TreeStats(long count, long tks, long tds)
216    {
217      this.count = count;
218      this.totalKeySize = tks;
219      this.totalDataSize = tds;
220    }
221  }
222
223  private static final Option<Boolean> DUMP_DECODE_VALUE = Option.withDefault(true);
224  private static final Option<Boolean> DUMP_STATS_ONLY = Option.withDefault(false);
225  private static final Option<Boolean> DUMP_SINGLE_LINE = Option.withDefault(false);
226  private static final Option<Argument> DUMP_MIN_KEY_VALUE = Option.of(Argument.class, null);
227  private static final Option<Argument> DUMP_MAX_KEY_VALUE = Option.of(Argument.class, null);
228  private static final Option<Boolean> DUMP_MIN_KEY_VALUE_IS_HEX = Option.withDefault(false);
229  private static final Option<Boolean> DUMP_MAX_KEY_VALUE_IS_HEX = Option.withDefault(false);
230  private static final Option<Integer> DUMP_MIN_DATA_SIZE = Option.of(Integer.class, 0);
231  private static final Option<Integer> DUMP_MAX_DATA_SIZE = Option.of(Integer.class, Integer.MAX_VALUE);
232  private static final Option<Integer> DUMP_INDENT = Option.of(Integer.class, 4);
233
234  // Sub-command names.
235  private static final String LIST_BACKENDS = "list-backends";
236  private static final String LIST_BASE_DNS = "list-base-dns";
237  private static final String LIST_INDEXES = "list-indexes";
238  private static final String SHOW_INDEX_STATUS = "show-index-status";
239  private static final String DUMP_INDEX = "dump-index";
240  private static final String LIST_RAW_DBS = "list-raw-dbs";
241  private static final String DUMP_RAW_DB = "dump-raw-db";
242
243  private static final String BACKENDID_NAME = "backendid";
244  private static final String BACKENDID = "backendID";
245  private static final String BASEDN_NAME = "basedn";
246  private static final String BASEDN = "baseDN";
247  private static final String USESIUNITS_NAME = "usesiunits";
248  private static final String USESIUNITS = "useSIUnits";
249  private static final String MAXDATASIZE_NAME = "maxdatasize";
250  private static final String MAXDATASIZE = "maxDataSize";
251  private static final String MAXKEYVALUE_NAME = "maxkeyvalue";
252  private static final String MAXKEYVALUE = "maxKeyValue";
253  private static final String MAXHEXKEYVALUE_NAME = "maxhexkeyvalue";
254  private static final String MAXHEXKEYVALUE = "maxHexKeyValue";
255  private static final String MINDATASIZE_NAME = "mindatasize";
256  private static final String MINDATASIZE = "minDataSize";
257  private static final String MINKEYVALUE_NAME = "minkeyvalue";
258  private static final String MINKEYVALUE = "minKeyValue";
259  private static final String MINHEXKEYVALUE_NAME = "minhexkeyvalue";
260  private static final String MINHEXKEYVALUE = "minHexKeyValue";
261  private static final String SKIPDECODE_NAME = "skipdecode";
262  private static final String SKIPDECODE = "skipDecode";
263  private static final String STATSONLY_NAME = "statsonly";
264  private static final String STATSONLY = "statsOnly";
265  private static final String INDEXNAME_NAME = "indexname";
266  private static final String INDEXNAME = "indexName";
267  private static final String DBNAME_NAME = "dbname";
268  private static final String DBNAME = "dbName";
269  private static final String SINGLELINE_NAME = "singleline";
270  private static final String SINGLELINE = "singleLine";
271
272  private static final String HEXDUMP_LINE_FORMAT = "%s%s %s%n";
273
274  /** The error stream which this application should use. */
275  private final PrintStream err;
276  /** The output stream which this application should use. */
277  private final PrintStream out;
278
279  /** The command-line argument parser. */
280  private final SubCommandArgumentParser parser;
281  /** The argument which should be used to request usage information. */
282  private BooleanArgument showUsageArgument;
283  /** The argument which should be used to specify the config class. */
284  private StringArgument configClass;
285  /** The argument which should be used to specify the config file. */
286  private StringArgument configFile;
287
288  /** Flag indicating whether or not the sub-commands have already been initialized. */
289  private boolean subCommandsInitialized;
290  /** Flag indicating whether or not the global arguments have already been initialized. */
291  private boolean globalArgumentsInitialized;
292
293  private DirectoryServer directoryServer;
294
295  /**
296   * Provides the command-line arguments to the main application for
297   * processing.
298   *
299   * @param args The set of command-line arguments provided to this
300   *             program.
301   */
302  public static void main(String[] args)
303  {
304    int exitCode = main(args, System.out, System.err);
305    if (exitCode != 0)
306    {
307      System.exit(filterExitCode(exitCode));
308    }
309  }
310
311  /**
312   * Provides the command-line arguments to the main application for
313   * processing and returns the exit code as an integer.
314   *
315   * @param args      The set of command-line arguments provided to this
316   *                  program.
317   * @param outStream The output stream for standard output.
318   * @param errStream The output stream for standard error.
319   * @return Zero to indicate that the program completed successfully,
320   * or non-zero to indicate that an error occurred.
321   */
322  public static int main(String[] args, OutputStream outStream, OutputStream errStream)
323  {
324    BackendStat app = new BackendStat(outStream, errStream);
325    return app.run(args);
326  }
327
328  /**
329   * Creates a new dsconfig application instance.
330   *
331   * @param out The application output stream.
332   * @param err The application error stream.
333   */
334  public BackendStat(OutputStream out, OutputStream err)
335  {
336    this.out = NullOutputStream.wrapOrNullStream(out);
337    this.err = NullOutputStream.wrapOrNullStream(err);
338    JDKLogging.disableLogging();
339
340    LocalizableMessage toolDescription = INFO_DESCRIPTION_BACKEND_TOOL.get();
341    this.parser = new SubCommandArgumentParser(getClass().getName(), toolDescription, false);
342    this.parser.setShortToolDescription(REF_SHORT_DESC_BACKEND_TOOL.get());
343    this.parser.setVersionHandler(new DirectoryServerVersionHandler());
344  }
345
346  /**
347   * Registers the global arguments with the argument parser.
348   *
349   * @throws ArgumentException If a global argument could not be registered.
350   */
351  private void initializeGlobalArguments() throws ArgumentException
352  {
353    if (!globalArgumentsInitialized)
354    {
355      configClass =
356              StringArgument.builder(OPTION_LONG_CONFIG_CLASS)
357                      .shortIdentifier(OPTION_SHORT_CONFIG_CLASS)
358                      .description(INFO_DESCRIPTION_CONFIG_CLASS.get())
359                      .hidden()
360                      .required()
361                      .defaultValue(ConfigFileHandler.class.getName())
362                      .valuePlaceholder(INFO_CONFIGCLASS_PLACEHOLDER.get())
363                      .buildArgument();
364      configFile =
365              StringArgument.builder("configFile")
366                      .shortIdentifier('f')
367                      .description(INFO_DESCRIPTION_CONFIG_FILE.get())
368                      .hidden()
369                      .required()
370                      .valuePlaceholder(INFO_CONFIGFILE_PLACEHOLDER.get())
371                      .buildArgument();
372
373      showUsageArgument = showUsageArgument();
374
375      // Register the global arguments.
376      parser.addGlobalArgument(showUsageArgument);
377      parser.setUsageArgument(showUsageArgument, out);
378      parser.addGlobalArgument(configClass);
379      parser.addGlobalArgument(configFile);
380
381      globalArgumentsInitialized = true;
382    }
383  }
384
385  /**
386   * Registers the sub-commands with the argument parser.
387   *
388   * @throws ArgumentException If a sub-command could not be created.
389   */
390  private void initializeSubCommands() throws ArgumentException
391  {
392    if (!subCommandsInitialized)
393    {
394      // list-backends
395      new SubCommand(parser, LIST_BACKENDS,
396                           INFO_DESCRIPTION_BACKEND_TOOL_SUBCMD_LIST_BACKENDS.get());
397
398      // list-base-dns
399      addBackendArgument(new SubCommand(
400              parser, LIST_BASE_DNS, INFO_DESCRIPTION_BACKEND_DEBUG_SUBCMD_LIST_ENTRY_CONTAINERS.get()));
401
402      // list-indexes
403      final SubCommand listIndexes = new SubCommand(
404              parser, LIST_INDEXES, INFO_DESCRIPTION_BACKEND_TOOL_SUBCMD_LIST_INDEXES.get());
405      addBackendBaseDNArguments(listIndexes, false, false);
406
407      // show-index-status
408      final SubCommand showIndexStatus = new SubCommand(
409              parser, SHOW_INDEX_STATUS, INFO_DESCRIPTION_BACKEND_DEBUG_SUBCMD_LIST_INDEX_STATUS.get());
410      showIndexStatus.setDocDescriptionSupplement(SUPPLEMENT_DESCRIPTION_BACKEND_TOOL_SUBCMD_LIST_INDEX_STATUS.get());
411      addBackendBaseDNArguments(showIndexStatus, true, true);
412
413      // dump-index
414      final SubCommand dumpIndex = new SubCommand(
415              parser, DUMP_INDEX, INFO_DESCRIPTION_BACKEND_TOOL_SUBCMD_DUMP_INDEX.get());
416      addBackendBaseDNArguments(dumpIndex, true, false);
417      dumpIndex.addArgument(StringArgument.builder(INDEXNAME)
418              .shortIdentifier('i')
419              .description(INFO_DESCRIPTION_BACKEND_DEBUG_INDEX_NAME.get())
420              .required()
421              .valuePlaceholder(INFO_INDEX_NAME_PLACEHOLDER.get())
422              .buildArgument());
423      addDumpSubCommandArguments(dumpIndex);
424      dumpIndex.addArgument(BooleanArgument.builder(SKIPDECODE)
425              .shortIdentifier('p')
426              .description(INFO_DESCRIPTION_BACKEND_DEBUG_SKIP_DECODE.get())
427              .buildArgument());
428
429      // list-raw-dbs
430      final SubCommand listRawDBs = new SubCommand(
431              parser, LIST_RAW_DBS, INFO_DESCRIPTION_BACKEND_TOOL_SUBCMD_LIST_RAW_DBS.get());
432      addBackendArgument(listRawDBs);
433      listRawDBs.addArgument(BooleanArgument.builder(USESIUNITS)
434              .shortIdentifier('u')
435              .description(INFO_DESCRIPTION_BACKEND_TOOL_USE_SI_UNITS.get())
436              .buildArgument());
437
438      // dump-raw-db
439      final SubCommand dumbRawDB = new SubCommand(
440              parser, DUMP_RAW_DB, INFO_DESCRIPTION_BACKEND_TOOL_SUBCMD_DUMP_RAW_DB.get());
441      addBackendArgument(dumbRawDB);
442      dumbRawDB.addArgument(StringArgument.builder(DBNAME)
443              .shortIdentifier('d')
444              .description(INFO_DESCRIPTION_BACKEND_DEBUG_RAW_DB_NAME.get())
445              .required()
446              .valuePlaceholder(INFO_DATABASE_NAME_PLACEHOLDER.get())
447              .buildArgument());
448      addDumpSubCommandArguments(dumbRawDB);
449      dumbRawDB.addArgument(BooleanArgument.builder(SINGLELINE)
450              .shortIdentifier('l')
451              .description(INFO_DESCRIPTION_BACKEND_TOOL_SUBCMD_SINGLE_LINE.get())
452              .buildArgument());
453
454      subCommandsInitialized = true;
455    }
456  }
457
458  private void addBackendArgument(SubCommand sub) throws ArgumentException
459  {
460    sub.addArgument(
461            StringArgument.builder(BACKENDID)
462                    .shortIdentifier('n')
463                    .description(INFO_DESCRIPTION_BACKEND_DEBUG_BACKEND_ID.get())
464                    .required()
465                    .valuePlaceholder(INFO_BACKENDNAME_PLACEHOLDER.get())
466                    .buildArgument());
467  }
468
469  private void addBackendBaseDNArguments(SubCommand sub, boolean isRequired, boolean isMultiValued)
470      throws ArgumentException
471  {
472    addBackendArgument(sub);
473    final StringArgument.Builder builder = StringArgument.builder(BASEDN)
474            .shortIdentifier('b')
475            .description(INFO_DESCRIPTION_BACKEND_DEBUG_BASE_DN.get())
476            .valuePlaceholder(INFO_BASEDN_PLACEHOLDER.get());
477    if (isMultiValued)
478    {
479      builder.multiValued();
480    }
481    if (isRequired) {
482      builder.required();
483    }
484    sub.addArgument(builder.buildArgument());
485  }
486
487  private void addDumpSubCommandArguments(SubCommand sub) throws ArgumentException
488  {
489    sub.addArgument(BooleanArgument.builder(STATSONLY)
490            .shortIdentifier('q')
491            .description(INFO_DESCRIPTION_BACKEND_DEBUG_STATS_ONLY.get())
492            .buildArgument());
493
494    sub.addArgument(newMaxKeyValueArg());
495    sub.addArgument(newMinKeyValueArg());
496    sub.addArgument(StringArgument.builder(MAXHEXKEYVALUE)
497            .shortIdentifier('X')
498            .description(INFO_DESCRIPTION_BACKEND_DEBUG_MAX_KEY_VALUE.get())
499            .valuePlaceholder(INFO_MAX_KEY_VALUE_PLACEHOLDER.get())
500            .buildArgument());
501
502    sub.addArgument(StringArgument.builder(MINHEXKEYVALUE)
503            .shortIdentifier('x')
504            .description(INFO_DESCRIPTION_BACKEND_DEBUG_MIN_KEY_VALUE.get())
505            .valuePlaceholder(INFO_MIN_KEY_VALUE_PLACEHOLDER.get())
506            .buildArgument());
507
508    sub.addArgument(IntegerArgument.builder(MAXDATASIZE)
509            .shortIdentifier('S')
510            .description(INFO_DESCRIPTION_BACKEND_DEBUG_MAX_DATA_SIZE.get())
511            .defaultValue(-1)
512            .valuePlaceholder(INFO_MAX_DATA_SIZE_PLACEHOLDER.get())
513            .buildArgument());
514
515    sub.addArgument(IntegerArgument.builder(MINDATASIZE)
516            .shortIdentifier('s')
517            .description(INFO_DESCRIPTION_BACKEND_DEBUG_MIN_DATA_SIZE.get())
518            .defaultValue(-1)
519            .valuePlaceholder(INFO_MIN_DATA_SIZE_PLACEHOLDER.get())
520            .buildArgument());
521  }
522
523  private StringArgument newMinKeyValueArg() throws ArgumentException
524  {
525    return StringArgument.builder(MINKEYVALUE)
526            .shortIdentifier('k')
527            .description(INFO_DESCRIPTION_BACKEND_DEBUG_MIN_KEY_VALUE.get())
528            .valuePlaceholder(INFO_MIN_KEY_VALUE_PLACEHOLDER.get())
529            .buildArgument();
530  }
531
532  private StringArgument newMaxKeyValueArg() throws ArgumentException
533  {
534    return StringArgument.builder(MAXKEYVALUE)
535            .shortIdentifier('K')
536            .description(INFO_DESCRIPTION_BACKEND_DEBUG_MAX_KEY_VALUE.get())
537            .valuePlaceholder(INFO_MAX_KEY_VALUE_PLACEHOLDER.get())
538            .buildArgument();
539  }
540
541  /**
542   * Parses the provided command-line arguments and makes the
543   * appropriate changes to the Directory Server configuration.
544   *
545   * @param args The command-line arguments provided to this program.
546   * @return The exit code from the configuration processing. A
547   * nonzero value indicates that there was some kind of
548   * problem during the configuration processing.
549   */
550  private int run(String[] args)
551  {
552    // Register global arguments and sub-commands.
553    try
554    {
555      initializeGlobalArguments();
556      initializeSubCommands();
557    }
558    catch (ArgumentException e)
559    {
560      printWrappedText(err, ERR_CANNOT_INITIALIZE_ARGS.get(e.getMessage()));
561      return 1;
562    }
563
564    try
565    {
566      parser.parseArguments(args);
567    }
568    catch (ArgumentException ae)
569    {
570      parser.displayMessageAndUsageReference(err, ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
571      return 1;
572    }
573
574    if (parser.usageOrVersionDisplayed())
575    {
576      return 0;
577    }
578
579    if (parser.getSubCommand() == null)
580    {
581      parser.displayMessageAndUsageReference(err, ERR_BACKEND_DEBUG_MISSING_SUBCOMMAND.get());
582      return 1;
583    }
584
585    try
586    {
587      BuildVersion.checkVersionMismatch();
588    }
589    catch (InitializationException e)
590    {
591      printWrappedText(err, e.getMessageObject());
592      return 1;
593    }
594
595    // Perform the initial bootstrap of the Directory Server and process the configuration.
596    directoryServer = DirectoryServer.getInstance();
597    try
598    {
599      DirectoryServer.bootstrapClient();
600      DirectoryServer.initializeJMX();
601    }
602    catch (Exception e)
603    {
604      printWrappedText(err, ERR_SERVER_BOOTSTRAP_ERROR.get(getStartUpExceptionMessage(e)));
605      return 1;
606    }
607
608    try
609    {
610      directoryServer.initializeConfiguration(configClass.getValue(), configFile.getValue());
611    }
612    catch (Exception e)
613    {
614      printWrappedText(err, ERR_CANNOT_LOAD_CONFIG.get(getStartUpExceptionMessage(e)));
615      return 1;
616    }
617
618    try
619    {
620      directoryServer.initializeSchema();
621    }
622    catch (Exception e)
623    {
624      printWrappedText(err, ERR_CANNOT_LOAD_SCHEMA.get(getStartUpExceptionMessage(e)));
625      return 1;
626    }
627
628    try
629    {
630      CoreConfigManager coreConfigManager = new CoreConfigManager(directoryServer.getServerContext());
631      coreConfigManager.initializeCoreConfig();
632    }
633    catch (Exception e)
634    {
635      printWrappedText(err, ERR_CANNOT_INITIALIZE_CORE_CONFIG.get(getStartUpExceptionMessage(e)));
636      return 1;
637    }
638
639    try
640    {
641      directoryServer.initializeCryptoManager();
642    }
643    catch (Exception e)
644    {
645      printWrappedText(err, ERR_CANNOT_INITIALIZE_CRYPTO_MANAGER.get(getStartUpExceptionMessage(e)));
646      return 1;
647    }
648
649    SubCommand subCommand = parser.getSubCommand();
650    if (LIST_BACKENDS.equals(subCommand.getName()))
651    {
652      return listRootContainers();
653    }
654    BackendImpl backend = getBackendById(subCommand.getArgument(BACKENDID_NAME));
655    if (backend == null)
656    {
657      return 1;
658    }
659    RootContainer rootContainer = getAndLockRootContainer(backend);
660    if (rootContainer == null)
661    {
662      return 1;
663    }
664    try
665    {
666      switch (subCommand.getName())
667      {
668      case LIST_BASE_DNS:
669        return listBaseDNs(rootContainer);
670      case LIST_RAW_DBS:
671        return listRawDBs(rootContainer, subCommand.getArgument(USESIUNITS_NAME));
672      case LIST_INDEXES:
673        return listIndexes(rootContainer, backend, subCommand.getArgument(BASEDN_NAME));
674      case DUMP_RAW_DB:
675        return dumpTree(rootContainer, backend, subCommand, false);
676      case DUMP_INDEX:
677        return dumpTree(rootContainer, backend, subCommand, true);
678      case SHOW_INDEX_STATUS:
679        return showIndexStatus(rootContainer, backend, subCommand.getArgument(BASEDN_NAME));
680      default:
681        return 1;
682      }
683    }
684    catch (Exception e)
685    {
686      printWrappedText(err, ERR_BACKEND_TOOL_EXECUTING_COMMAND.get(subCommand.getName(),
687          StaticUtils.stackTraceToString(e)));
688      return 1;
689    }
690    finally
691    {
692      close(rootContainer);
693      releaseExclusiveLock(backend);
694    }
695  }
696
697  private String getStartUpExceptionMessage(Exception e)
698  {
699    if (e instanceof ConfigException || e instanceof InitializationException)
700    {
701      return e.getMessage();
702    }
703    return getExceptionMessage(e).toString();
704  }
705
706  private int dumpTree(RootContainer rc, BackendImpl backend, SubCommand subCommand, boolean isBackendTree)
707      throws ArgumentException, DirectoryException
708  {
709    Options options = Options.defaultOptions();
710    if (!setDumpTreeOptionArguments(subCommand, options))
711    {
712      return 1;
713    }
714    if (isBackendTree)
715    {
716      return dumpBackendTree(rc, backend, subCommand.getArgument(BASEDN_NAME), subCommand.getArgument(INDEXNAME_NAME),
717          options);
718    }
719    return dumpStorageTree(rc, backend, subCommand.getArgument(DBNAME_NAME), options);
720  }
721
722  private boolean setDumpTreeOptionArguments(SubCommand subCommand, Options options) throws ArgumentException
723  {
724    try
725    {
726      Argument arg = subCommand.getArgument(SINGLELINE_NAME);
727      if (arg != null && arg.isPresent())
728      {
729        options.set(DUMP_SINGLE_LINE, true);
730      }
731      if (subCommand.getArgument(STATSONLY_NAME).isPresent())
732      {
733        options.set(DUMP_STATS_ONLY, true);
734      }
735      arg = subCommand.getArgument(SKIPDECODE_NAME);
736      if (arg == null || arg.isPresent())
737      {
738        options.set(DUMP_DECODE_VALUE, false);
739      }
740      if (subCommand.getArgument(MINDATASIZE_NAME).isPresent())
741      {
742        options.set(DUMP_MIN_DATA_SIZE, subCommand.getArgument(MINDATASIZE_NAME).getIntValue());
743      }
744      if (subCommand.getArgument(MAXDATASIZE_NAME).isPresent())
745      {
746        options.set(DUMP_MAX_DATA_SIZE, subCommand.getArgument(MAXDATASIZE_NAME).getIntValue());
747      }
748
749      options.set(DUMP_MIN_KEY_VALUE, subCommand.getArgument(MINKEYVALUE_NAME));
750      if (subCommand.getArgument(MINHEXKEYVALUE_NAME).isPresent())
751      {
752        if (subCommand.getArgument(MINKEYVALUE_NAME).isPresent())
753        {
754          printWrappedText(err, ERR_BACKEND_TOOL_ONLY_ONE_MIN_KEY.get());
755          return false;
756        }
757        options.set(DUMP_MIN_KEY_VALUE_IS_HEX, true);
758        options.set(DUMP_MIN_KEY_VALUE, subCommand.getArgument(MINHEXKEYVALUE_NAME));
759      }
760
761      options.set(DUMP_MAX_KEY_VALUE, subCommand.getArgument(MAXKEYVALUE_NAME));
762      if (subCommand.getArgument(MAXHEXKEYVALUE_NAME).isPresent())
763      {
764        if (subCommand.getArgument(MAXKEYVALUE_NAME).isPresent())
765        {
766          printWrappedText(err, ERR_BACKEND_TOOL_ONLY_ONE_MAX_KEY.get());
767          return false;
768        }
769        options.set(DUMP_MAX_KEY_VALUE_IS_HEX, true);
770        options.set(DUMP_MAX_KEY_VALUE, subCommand.getArgument(MAXHEXKEYVALUE_NAME));
771      }
772      return true;
773    }
774    catch (ArgumentException ae)
775    {
776      printWrappedText(err, ERR_BACKEND_TOOL_PROCESSING_ARGUMENT.get(StaticUtils.stackTraceToString(ae)));
777      throw ae;
778    }
779  }
780
781  private int listRootContainers()
782  {
783    TableBuilder builder = new TableBuilder();
784
785    builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_BACKEND_ID.get());
786    builder.appendHeading(INFO_LABEL_BACKEND_TOOL_STORAGE.get());
787
788    final Map<PluggableBackendCfg, BackendImpl> pluggableBackends = getPluggableBackends();
789    for (Map.Entry<PluggableBackendCfg, BackendImpl> backend : pluggableBackends.entrySet())
790    {
791      builder.startRow();
792      builder.appendCell(backend.getValue().getBackendID());
793      builder.appendCell(backend.getKey().getJavaClass());
794    }
795
796    builder.print(new TextTablePrinter(out));
797    out.format(INFO_LABEL_BACKEND_TOOL_TOTAL.get(pluggableBackends.size()).toString());
798
799    return 0;
800  }
801
802  private int listBaseDNs(RootContainer rc)
803  {
804    try
805    {
806      TableBuilder builder = new TableBuilder();
807      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_BASE_DN.get());
808      Collection<EntryContainer> entryContainers = rc.getEntryContainers();
809      for (EntryContainer ec : entryContainers)
810      {
811        builder.startRow();
812        builder.appendCell(ec.getBaseDN());
813      }
814
815      builder.print(new TextTablePrinter(out));
816      out.format(INFO_LABEL_BACKEND_TOOL_TOTAL.get(entryContainers.size()).toString());
817
818      return 0;
819    }
820    catch (StorageRuntimeException de)
821    {
822      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_LISTING_BASE_DNS.get(stackTraceToSingleLineString(de)));
823      return 1;
824    }
825  }
826
827  private int listRawDBs(RootContainer rc, Argument useSIUnits)
828  {
829    try
830    {
831      TableBuilder builder = new TableBuilder();
832
833      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_RAW_DB_NAME.get());
834      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_TOTAL_KEYS.get());
835      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_KEYS_SIZE.get());
836      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_VALUES_SIZE.get());
837      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_TOTAL_SIZES.get());
838
839      SortedSet<TreeName> treeNames = new TreeSet<>(rc.getStorage().listTrees());
840      for (TreeName tree: treeNames)
841      {
842        builder.startRow();
843        builder.appendCell(tree);
844        appendStorageTreeStats(builder, rc, new StorageTreeKeyValue(tree), useSIUnits.isPresent());
845      }
846
847      builder.print(new TextTablePrinter(out));
848      out.format(INFO_LABEL_BACKEND_TOOL_TOTAL.get(treeNames.size()).toString());
849
850      return 0;
851    }
852    catch (StorageRuntimeException de)
853    {
854      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_LISTING_TREES.get(stackTraceToSingleLineString(de)));
855      return 1;
856    }
857  }
858
859  private void appendStorageTreeStats(TableBuilder builder, RootContainer rc, TreeKeyValue targetTree,
860      boolean useSIUnit)
861  {
862    Options options = Options.defaultOptions();
863    options.set(DUMP_STATS_ONLY, true);
864    try
865    {
866      options.set(DUMP_MIN_KEY_VALUE, newMinKeyValueArg());
867      options.set(DUMP_MAX_KEY_VALUE, newMaxKeyValueArg());
868      TreeStats treeStats = cursorTreeToDump(rc, targetTree, options);
869      builder.appendCell(treeStats.count);
870      builder.appendCell(appendKeyValueSize(treeStats.totalKeySize, useSIUnit));
871      builder.appendCell(appendKeyValueSize(treeStats.totalDataSize, useSIUnit));
872      builder.appendCell(appendKeyValueSize(treeStats.totalKeySize + treeStats.totalDataSize, useSIUnit));
873    }
874    catch (Exception e)
875    {
876      appendStatsNoData(builder, 3);
877    }
878  }
879
880  private String appendKeyValueSize(long size, boolean useSIUnit)
881  {
882    if (useSIUnit && size > SizeUnit.KILO_BYTES.getSize())
883    {
884      NumberFormat format = NumberFormat.getNumberInstance();
885      format.setMaximumFractionDigits(2);
886      SizeUnit unit = SizeUnit.getBestFitUnit(size);
887      return format.format(unit.fromBytes(size)) + " " + unit;
888    }
889    else
890    {
891      return String.valueOf(size);
892    }
893  }
894
895  private int listIndexes(RootContainer rc, BackendImpl backend, Argument baseDNArg) throws DirectoryException
896  {
897    DN base = null;
898    if (baseDNArg.isPresent())
899    {
900      base = getBaseDNFromArg(baseDNArg);
901    }
902
903    try
904    {
905      TableBuilder builder = new TableBuilder();
906      int count = 0;
907
908      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_INDEX_NAME.get());
909      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_RAW_DB_NAME.get());
910      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_INDEX_TYPE.get());
911      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_RECORD_COUNT.get());
912
913      if (base != null)
914      {
915        EntryContainer ec = rc.getEntryContainer(base);
916        if (ec == null)
917        {
918          return printEntryContainerError(backend, base);
919        }
920        count = appendTreeRows(builder, ec);
921      }
922      else
923      {
924        for (EntryContainer ec : rc.getEntryContainers())
925        {
926          builder.startRow();
927          builder.appendCell("Base DN: " + ec.getBaseDN());
928          count += appendTreeRows(builder, ec);
929        }
930      }
931
932      builder.print(new TextTablePrinter(out));
933      out.format(INFO_LABEL_BACKEND_TOOL_TOTAL.get(count).toString());
934
935      return 0;
936    }
937    catch (StorageRuntimeException de)
938    {
939      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_LISTING_TREES.get(stackTraceToSingleLineString(de)));
940      return 1;
941    }
942  }
943
944  private int printEntryContainerError(BackendImpl backend, DN base)
945  {
946    printWrappedText(err, ERR_BACKEND_DEBUG_NO_ENTRY_CONTAINERS_FOR_BASE_DN.get(base, backend.getBackendID()));
947    return 1;
948  }
949
950  private DN getBaseDNFromArg(Argument baseDNArg) throws DirectoryException
951  {
952    try
953    {
954      return DN.valueOf(baseDNArg.getValue());
955    }
956    catch (LocalizedIllegalArgumentException e)
957    {
958      printWrappedText(err, ERR_BACKEND_DEBUG_DECODE_BASE_DN.get(baseDNArg.getValue(), getExceptionMessage(e)));
959      throw e;
960    }
961  }
962
963  private RootContainer getAndLockRootContainer(BackendImpl backend)
964  {
965    try
966    {
967      String lockFile = LockFileManager.getBackendLockFileName(backend);
968      StringBuilder failureReason = new StringBuilder();
969      if (!LockFileManager.acquireExclusiveLock(lockFile, failureReason))
970      {
971        printWrappedText(err, ERR_BACKEND_DEBUG_CANNOT_LOCK_BACKEND.get(backend.getBackendID(), failureReason));
972        return null;
973      }
974    }
975    catch (Exception e)
976    {
977      printWrappedText(err, ERR_BACKEND_DEBUG_CANNOT_LOCK_BACKEND.get(backend.getBackendID(), StaticUtils
978          .getExceptionMessage(e)));
979      return null;
980    }
981
982    try
983    {
984      return backend.getReadOnlyRootContainer();
985    }
986    catch (Exception e)
987    {
988      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_INITIALIZING_BACKEND.get(backend.getBackendID(),
989          stackTraceToSingleLineString(e)));
990      return null;
991    }
992  }
993
994  private int appendTreeRows(TableBuilder builder, EntryContainer ec)
995  {
996    int count = 0;
997    for (final Tree tree : ec.listTrees())
998    {
999      builder.startRow();
1000      builder.appendCell(tree.getName().getIndexId());
1001      builder.appendCell(tree.getName());
1002      builder.appendCell(tree.getClass().getSimpleName());
1003      builder.appendCell(getTreeRecordCount(ec, tree));
1004      count++;
1005    }
1006    return count;
1007  }
1008
1009  private long getTreeRecordCount(EntryContainer ec, final Tree tree)
1010  {
1011    try
1012    {
1013      return ec.getRootContainer().getStorage().read(new ReadOperation<Long>()
1014      {
1015        @Override
1016        public Long run(ReadableTransaction txn) throws Exception
1017        {
1018          return tree.getRecordCount(txn);
1019        }
1020      });
1021    }
1022    catch (Exception e)
1023    {
1024      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_READING_TREE.get(stackTraceToSingleLineString(e)));
1025      return -1;
1026    }
1027  }
1028
1029  private void close(RootContainer rc)
1030  {
1031    try
1032    {
1033      rc.close();
1034    }
1035    catch (StorageRuntimeException ignored)
1036    {
1037      // Ignore.
1038    }
1039  }
1040
1041  private void releaseExclusiveLock(BackendImpl backend)
1042  {
1043    try
1044    {
1045      String lockFile = LockFileManager.getBackendLockFileName(backend);
1046      StringBuilder failureReason = new StringBuilder();
1047      if (!LockFileManager.releaseLock(lockFile, failureReason))
1048      {
1049        printWrappedText(err, WARN_BACKEND_DEBUG_CANNOT_UNLOCK_BACKEND.get(backend.getBackendID(), failureReason));
1050      }
1051    }
1052    catch (Exception e)
1053    {
1054      printWrappedText(err, WARN_BACKEND_DEBUG_CANNOT_UNLOCK_BACKEND.get(backend.getBackendID(),
1055          StaticUtils.getExceptionMessage(e)));
1056    }
1057  }
1058
1059  private BackendImpl getBackendById(Argument backendIdArg)
1060  {
1061    final String backendID = backendIdArg.getValue();
1062    final Map<PluggableBackendCfg, BackendImpl> pluggableBackends = getPluggableBackends();
1063
1064    for (Map.Entry<PluggableBackendCfg, BackendImpl> backend : pluggableBackends.entrySet())
1065    {
1066      final BackendImpl b = backend.getValue();
1067      if (b.getBackendID().equalsIgnoreCase(backendID))
1068      {
1069        try
1070        {
1071          b.configureBackend(backend.getKey(), directoryServer.getServerContext());
1072          return b;
1073        }
1074        catch (ConfigException ce)
1075        {
1076          printWrappedText(err, ERR_BACKEND_TOOL_CANNOT_CONFIGURE_BACKEND.get(backendID, ce));
1077          return null;
1078        }
1079      }
1080    }
1081
1082    printWrappedText(err, ERR_BACKEND_DEBUG_NO_BACKENDS_FOR_ID.get(backendID));
1083    return null;
1084  }
1085
1086  private int showIndexStatus(RootContainer rc, BackendImpl backend, Argument baseDNArg) throws DirectoryException
1087  {
1088    DN base = getBaseDNFromArg(baseDNArg);
1089
1090    try
1091    {
1092      // Create a table of their properties.
1093      TableBuilder builder = new TableBuilder();
1094      int count = 0;
1095
1096      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_INDEX_NAME.get());
1097      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_RAW_DB_NAME.get());
1098      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_INDEX_STATUS.get());
1099      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_RECORD_COUNT.get());
1100      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_INDEX_UNDEFINED_RECORD_COUNT.get());
1101      builder.appendHeading(LocalizableMessage.raw("95%"));
1102      builder.appendHeading(LocalizableMessage.raw("90%"));
1103      builder.appendHeading(LocalizableMessage.raw("85%"));
1104
1105      EntryContainer ec = rc.getEntryContainer(base);
1106      if (ec == null)
1107      {
1108        return printEntryContainerError(backend, base);
1109      }
1110
1111      Map<Index, StringBuilder> undefinedKeys = new HashMap<>();
1112      for (AttributeIndex attrIndex : ec.getAttributeIndexes())
1113      {
1114        for (AttributeIndex.MatchingRuleIndex index : attrIndex.getNameToIndexes().values())
1115        {
1116          builder.startRow();
1117          builder.appendCell(index.getName().getIndexId());
1118          builder.appendCell(index.getName());
1119          builder.appendCell(index.isTrusted());
1120          if (index.isTrusted())
1121          {
1122            appendIndexStats(builder, ec, index, undefinedKeys);
1123          }
1124          else
1125          {
1126            appendStatsNoData(builder, 5);
1127          }
1128          count++;
1129        }
1130      }
1131
1132      for (VLVIndex vlvIndex : ec.getVLVIndexes())
1133      {
1134        builder.startRow();
1135        builder.appendCell(vlvIndex.getName().getIndexId());
1136        builder.appendCell(vlvIndex.getName());
1137        builder.appendCell(vlvIndex.isTrusted());
1138        builder.appendCell(getTreeRecordCount(ec, vlvIndex));
1139        appendStatsNoData(builder, 4);
1140        count++;
1141      }
1142
1143      builder.print(new TextTablePrinter(out));
1144      out.format(INFO_LABEL_BACKEND_TOOL_TOTAL.get(count).toString());
1145      for (Map.Entry<Index, StringBuilder> e : undefinedKeys.entrySet())
1146      {
1147        out.format(INFO_LABEL_BACKEND_TOOL_INDEX.get(e.getKey().getName()).toString());
1148        out.format(INFO_LABEL_BACKEND_TOOL_OVER_INDEX_LIMIT_KEYS.get(e.getValue()).toString());
1149      }
1150      return 0;
1151    }
1152    catch (StorageRuntimeException de)
1153    {
1154      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_READING_TREE.get(stackTraceToSingleLineString(de)));
1155      return 1;
1156    }
1157  }
1158
1159  private void appendStatsNoData(TableBuilder builder, int columns)
1160  {
1161    while (columns > 0)
1162    {
1163      builder.appendCell("-");
1164      columns--;
1165    }
1166  }
1167
1168  private void appendIndexStats(final TableBuilder builder, EntryContainer ec, final Index index,
1169      final Map<Index, StringBuilder> undefinedKeys)
1170  {
1171    final long entryLimit = index.getIndexEntryLimit();
1172
1173    try
1174    {
1175      ec.getRootContainer().getStorage().read(new ReadOperation<Void>()
1176      {
1177        @Override
1178        public Void run(ReadableTransaction txn) throws Exception
1179        {
1180          long eighty = 0;
1181          long ninety = 0;
1182          long ninetyFive = 0;
1183          long undefined = 0;
1184          long count = 0;
1185          BackendTreeKeyValue keyDecoder = new BackendTreeKeyValue(index);
1186          try (Cursor<ByteString, EntryIDSet> cursor = index.openCursor(txn))
1187          {
1188            while (cursor.next())
1189            {
1190              count++;
1191              EntryIDSet entryIDSet;
1192              try
1193              {
1194                entryIDSet = cursor.getValue();
1195              }
1196              catch (Exception e)
1197              {
1198                continue;
1199              }
1200
1201              if (entryIDSet.isDefined())
1202              {
1203                if (entryIDSet.size() >= entryLimit * 0.8)
1204                {
1205                  if (entryIDSet.size() >= entryLimit * 0.95)
1206                  {
1207                    ninetyFive++;
1208                  }
1209                  else if (entryIDSet.size() >= entryLimit * 0.9)
1210                  {
1211                    ninety++;
1212                  }
1213                  else
1214                  {
1215                    eighty++;
1216                  }
1217                }
1218              }
1219              else
1220              {
1221                undefined++;
1222                StringBuilder keyList = undefinedKeys.get(index);
1223                if (keyList == null)
1224                {
1225                  keyList = new StringBuilder();
1226                  undefinedKeys.put(index, keyList);
1227                }
1228                else
1229                {
1230                  keyList.append(" ");
1231                }
1232                keyList.append("[").append(keyDecoder.keyDecoder(cursor.getKey())).append("]");
1233              }
1234            }
1235          }
1236          builder.appendCell(count);
1237          builder.appendCell(undefined);
1238          builder.appendCell(ninetyFive);
1239          builder.appendCell(ninety);
1240          builder.appendCell(eighty);
1241          return null;
1242        }
1243      });
1244    }
1245    catch (Exception e)
1246    {
1247      appendStatsNoData(builder, 5);
1248      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_READING_TREE.get(index.getName()));
1249    }
1250  }
1251
1252  private int dumpStorageTree(RootContainer rc, BackendImpl backend, Argument treeNameArg, Options options)
1253  {
1254    TreeName targetTree = getStorageTreeName(treeNameArg, rc);
1255    if (targetTree == null)
1256    {
1257      printWrappedText(err,
1258          ERR_BACKEND_TOOL_NO_TREE_FOR_NAME_IN_STORAGE.get(treeNameArg.getValue(), backend.getBackendID()));
1259      return 1;
1260    }
1261
1262    try
1263    {
1264      dumpActualTree(rc, new StorageTreeKeyValue(targetTree), options);
1265      return 0;
1266    }
1267    catch (Exception e)
1268    {
1269      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_READING_TREE.get(stackTraceToSingleLineString(e)));
1270      return 1;
1271    }
1272  }
1273
1274  private TreeName getStorageTreeName(Argument treeNameArg, RootContainer rc)
1275  {
1276    for (TreeName tree : rc.getStorage().listTrees())
1277    {
1278      if (treeNameArg.getValue().equals(tree.toString()))
1279      {
1280        return tree;
1281      }
1282    }
1283    return null;
1284  }
1285
1286  private int dumpBackendTree(RootContainer rc, BackendImpl backend, Argument baseDNArg, Argument treeNameArg,
1287      Options options) throws DirectoryException
1288  {
1289    DN base = getBaseDNFromArg(baseDNArg);
1290
1291    EntryContainer ec = rc.getEntryContainer(base);
1292    if (ec == null)
1293    {
1294      return printEntryContainerError(backend, base);
1295    }
1296
1297    Tree targetTree = getBackendTree(treeNameArg, ec);
1298    if (targetTree == null)
1299    {
1300      printWrappedText(err,
1301          ERR_BACKEND_TOOL_NO_TREE_FOR_NAME.get(treeNameArg.getValue(), base, backend.getBackendID()));
1302      return 1;
1303    }
1304
1305    try
1306    {
1307      dumpActualTree(rc, new BackendTreeKeyValue(targetTree), options);
1308      return 0;
1309    }
1310    catch (Exception e)
1311    {
1312      printWrappedText(err, ERR_BACKEND_TOOL_ERROR_READING_TREE.get(stackTraceToSingleLineString(e)));
1313      return 1;
1314    }
1315  }
1316
1317  private Tree getBackendTree(Argument treeNameArg, EntryContainer ec)
1318  {
1319    for (Tree tree : ec.listTrees())
1320    {
1321      if (treeNameArg.getValue().contains(tree.getName().getIndexId())
1322          || treeNameArg.getValue().equals(tree.getName().toString()))
1323      {
1324        return tree;
1325      }
1326    }
1327    return null;
1328  }
1329
1330  private void dumpActualTree(RootContainer rc, final TreeKeyValue target, final Options options) throws Exception
1331  {
1332    TreeStats treeStats =  cursorTreeToDump(rc, target, options);
1333    out.format(INFO_LABEL_BACKEND_TOOL_TOTAL_RECORDS.get(treeStats.count).toString());
1334    if (treeStats.count > 0)
1335    {
1336      out.format(INFO_LABEL_BACKEND_TOOL_TOTAL_KEY_SIZE_AND_AVG.get(
1337          treeStats.totalKeySize, treeStats.totalKeySize / treeStats.count).toString());
1338      out.format(INFO_LABEL_BACKEND_TOOL_TOTAL_DATA_SIZE_AND_AVG.get(
1339          treeStats.totalDataSize, treeStats.totalDataSize / treeStats.count).toString());
1340    }
1341  }
1342
1343  private TreeStats cursorTreeToDump(RootContainer rc, final TreeKeyValue target, final Options options)
1344      throws Exception
1345  {
1346    return rc.getStorage().read(new ReadOperation<TreeStats>()
1347      {
1348        @Override
1349        public TreeStats run(ReadableTransaction txn) throws Exception
1350        {
1351          long count = 0;
1352          long totalKeySize = 0;
1353          long totalDataSize = 0;
1354          try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(target.getTreeName()))
1355          {
1356            ByteString key;
1357            ByteString maxKey = null;
1358            ByteString value;
1359
1360            if (options.get(DUMP_MIN_KEY_VALUE).isPresent())
1361            {
1362              key = getMinOrMaxKey(options, DUMP_MIN_KEY_VALUE, DUMP_MIN_KEY_VALUE_IS_HEX);
1363              if (!cursor.positionToKeyOrNext(key))
1364              {
1365                return new TreeStats(0, 0, 0);
1366              }
1367            }
1368            else
1369            {
1370              if (!cursor.next())
1371              {
1372                return new TreeStats(0, 0, 0);
1373              }
1374            }
1375
1376            if (options.get(DUMP_MAX_KEY_VALUE).isPresent())
1377            {
1378              maxKey = getMinOrMaxKey(options, DUMP_MAX_KEY_VALUE, DUMP_MAX_KEY_VALUE_IS_HEX);
1379            }
1380
1381            do
1382            {
1383              key = cursor.getKey();
1384              if (maxKey != null && key.compareTo(maxKey) > 0)
1385              {
1386                break;
1387              }
1388              value = cursor.getValue();
1389              long valueLen = value.length();
1390              if (options.get(DUMP_MIN_DATA_SIZE) <= valueLen && valueLen <= options.get(DUMP_MAX_DATA_SIZE))
1391              {
1392                count++;
1393                int keyLen = key.length();
1394                totalKeySize += keyLen;
1395                totalDataSize += valueLen;
1396                if (!options.get(DUMP_STATS_ONLY))
1397                {
1398                  if (options.get(DUMP_DECODE_VALUE))
1399                  {
1400                    String k = target.keyDecoder(key);
1401                    String v = target.valueDecoder(value);
1402                    out.format(INFO_LABEL_BACKEND_TOOL_KEY_FORMAT.get(keyLen) + " %s%n"
1403                        + INFO_LABEL_BACKEND_TOOL_VALUE_FORMAT.get(valueLen) + " %s%n", k, v);
1404                  }
1405                  else
1406                  {
1407                    hexDumpRecord(key, value, out, options);
1408                  }
1409                }
1410              }
1411            }
1412            while (cursor.next());
1413          }
1414          catch (Exception e)
1415          {
1416            out.format(ERR_BACKEND_TOOL_CURSOR_AT_KEY_NUMBER.get(count, e.getCause()).toString());
1417            e.printStackTrace(out);
1418            out.format("%n");
1419            throw e;
1420          }
1421          return new TreeStats(count, totalKeySize, totalDataSize);
1422        }
1423
1424      private ByteString getMinOrMaxKey(Options options, Option<Argument> keyOpt, Option<Boolean> isHexKey)
1425      {
1426        ByteString key;
1427        if (options.get(isHexKey))
1428        {
1429          key = ByteString.valueOfHex(options.get(keyOpt).getValue());
1430        }
1431        else
1432        {
1433          key = target.getTreeKey(options.get(keyOpt).getValue());
1434        }
1435        return key;
1436      }
1437    });
1438  }
1439
1440  final void hexDumpRecord(ByteString key, ByteString value, PrintStream out, Options options)
1441  {
1442    if (options.get(DUMP_SINGLE_LINE))
1443    {
1444      out.format(INFO_LABEL_BACKEND_TOOL_KEY_FORMAT.get(key.length()) + " ");
1445      toHexDumpSingleLine(out, key);
1446      out.format(INFO_LABEL_BACKEND_TOOL_VALUE_FORMAT.get(value.length()) + " ");
1447      toHexDumpSingleLine(out, value);
1448    }
1449    else
1450    {
1451      out.format(INFO_LABEL_BACKEND_TOOL_KEY_FORMAT.get(key.length()) + "%n");
1452      toHexDumpWithAsciiCompact(key, options.get(DUMP_INDENT), out);
1453      out.format(INFO_LABEL_BACKEND_TOOL_VALUE_FORMAT.get(value.length()) + "%n");
1454      toHexDumpWithAsciiCompact(value, options.get(DUMP_INDENT), out);
1455    }
1456  }
1457
1458  final void toHexDumpSingleLine(PrintStream out, ByteString data)
1459  {
1460    for (int i = 0; i < data.length(); i++)
1461    {
1462      out.format("%s", StaticUtils.byteToHex(data.byteAt(i)));
1463    }
1464    out.format("%n");
1465  }
1466
1467  final void toHexDumpWithAsciiCompact(ByteString data, int indent, PrintStream out)
1468  {
1469    StringBuilder hexDump = new StringBuilder();
1470    StringBuilder indentBuilder = new StringBuilder();
1471    StringBuilder asciiDump = new StringBuilder();
1472    for (int i = 0; i < indent; i++)
1473    {
1474      indentBuilder.append(' ');
1475    }
1476    int pos = 0;
1477    while (pos < data.length())
1478    {
1479      byte val = data.byteAt(pos);
1480      hexDump.append(StaticUtils.byteToHex(val));
1481      hexDump.append(' ');
1482      asciiDump.append(val >= ' ' ? (char)val : ".");
1483      pos++;
1484      if (pos % 16 == 0)
1485      {
1486        out.format(HEXDUMP_LINE_FORMAT, indentBuilder.toString(), hexDump.toString(), asciiDump.toString());
1487        hexDump.setLength(0);
1488        asciiDump.setLength(0);
1489      }
1490    }
1491    while (pos % 16 != 0)
1492    {
1493      hexDump.append("   ");
1494      pos++;
1495    }
1496    out.format(HEXDUMP_LINE_FORMAT, indentBuilder.toString(), hexDump.toString(), asciiDump.toString());
1497  }
1498
1499  private static Map<PluggableBackendCfg, BackendImpl> getPluggableBackends()
1500  {
1501    ArrayList<Backend> backendList = new ArrayList<>();
1502    ArrayList<BackendCfg> entryList = new ArrayList<>();
1503    ArrayList<List<DN>> dnList = new ArrayList<>();
1504    BackendToolUtils.getBackends(backendList, entryList, dnList);
1505
1506    final Map<PluggableBackendCfg, BackendImpl> pluggableBackends = new LinkedHashMap<>();
1507    for (int i = 0; i < backendList.size(); i++)
1508    {
1509      Backend<?> backend = backendList.get(i);
1510      if (backend instanceof BackendImpl)
1511      {
1512        pluggableBackends.put((PluggableBackendCfg) entryList.get(i), (BackendImpl) backend);
1513      }
1514    }
1515    return pluggableBackends;
1516  }
1517}