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