001/*
002 * Copyright 2020-2026 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020-2026 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2020-2026 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.unboundidds.tools;
037
038
039
040import java.io.File;
041import java.io.IOException;
042import java.io.OutputStream;
043import java.io.PrintWriter;
044import java.lang.reflect.Method;
045import java.text.SimpleDateFormat;
046import java.util.ArrayList;
047import java.util.Arrays;
048import java.util.Collections;
049import java.util.Date;
050import java.util.LinkedHashMap;
051import java.util.List;
052import java.util.concurrent.TimeUnit;
053import java.util.concurrent.atomic.AtomicReference;
054import java.util.logging.Level;
055
056import com.unboundid.asn1.ASN1OctetString;
057import com.unboundid.ldap.sdk.Control;
058import com.unboundid.ldap.sdk.LDAPConnectionPool;
059import com.unboundid.ldap.sdk.LDAPException;
060import com.unboundid.ldap.sdk.ResultCode;
061import com.unboundid.ldap.sdk.Version;
062import com.unboundid.ldap.sdk.unboundidds.controls.NoOpRequestControl;
063import com.unboundid.ldap.sdk.unboundidds.extensions.
064            CollectSupportDataExtendedRequest;
065import com.unboundid.ldap.sdk.unboundidds.extensions.
066            CollectSupportDataExtendedRequestProperties;
067import com.unboundid.ldap.sdk.unboundidds.extensions.
068            CollectSupportDataExtendedResult;
069import com.unboundid.ldap.sdk.unboundidds.extensions.
070            DurationCollectSupportDataLogCaptureWindow;
071import com.unboundid.ldap.sdk.unboundidds.extensions.
072            HeadAndTailSizeCollectSupportDataLogCaptureWindow;
073import com.unboundid.ldap.sdk.unboundidds.extensions.
074            StartAdministrativeSessionExtendedRequest;
075import com.unboundid.ldap.sdk.unboundidds.extensions.
076            StartAdministrativeSessionPostConnectProcessor;
077import com.unboundid.ldap.sdk.unboundidds.extensions.
078            TimeWindowCollectSupportDataLogCaptureWindow;
079import com.unboundid.ldap.sdk.unboundidds.tasks.CollectSupportDataSecurityLevel;
080import com.unboundid.util.Base64;
081import com.unboundid.util.Debug;
082import com.unboundid.util.LDAPCommandLineTool;
083import com.unboundid.util.NotNull;
084import com.unboundid.util.Nullable;
085import com.unboundid.util.ObjectPair;
086import com.unboundid.util.PasswordReader;
087import com.unboundid.util.StaticUtils;
088import com.unboundid.util.ThreadLocalSecureRandom;
089import com.unboundid.util.ThreadSafety;
090import com.unboundid.util.ThreadSafetyLevel;
091import com.unboundid.util.args.Argument;
092import com.unboundid.util.args.ArgumentException;
093import com.unboundid.util.args.ArgumentParser;
094import com.unboundid.util.args.BooleanArgument;
095import com.unboundid.util.args.DurationArgument;
096import com.unboundid.util.args.FileArgument;
097import com.unboundid.util.args.IntegerArgument;
098import com.unboundid.util.args.StringArgument;
099import com.unboundid.util.args.TimestampArgument;
100
101import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*;
102
103
104
105/**
106 * This class provides a command-line tool that may be used to invoke the
107 * collect-support-data utility in the Ping Identity Directory Server and
108 * related server products.
109 * <BR>
110 * <BLOCKQUOTE>
111 *   <B>NOTE:</B>  This class, and other classes within the
112 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
113 *   supported for use against Ping Identity, UnboundID, and
114 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
115 *   for proprietary functionality or for external specifications that are not
116 *   considered stable or mature enough to be guaranteed to work in an
117 *   interoperable way with other types of LDAP servers.
118 * </BLOCKQUOTE>
119 * <BR>
120 * Note that this is a client-side wrapper for the application.  While it may
121 * be used to invoke the tool against a remote server using the
122 * {@link CollectSupportDataExtendedRequest}, it does not include direct support
123 * for invoking the tool against a local instance.  That will be accomplished
124 * indirectly by invoking the server-side version of the tool via reflection.
125 */
126@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
127public final class CollectSupportData
128       extends LDAPCommandLineTool
129{
130  /**
131   * The column at which to wrap long lines.
132   */
133  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
134
135
136
137  /**
138   * The Ping Identity Directory Server's default access log timestamp format
139   * when configured to use millisecond precision.
140   */
141  @NotNull static final String SERVER_LOG_TIMESTAMP_FORMAT_WITH_MILLIS =
142       "'['dd/MMM/yyyy:HH:mm:ss.SSS Z']'";
143
144
145
146  /**
147   * The Ping Identity Directory Server's default access log timestamp format
148   * when configured to use second precision.
149   */
150  @NotNull static final String SERVER_LOG_TIMESTAMP_FORMAT_WITHOUT_MILLIS =
151       "'['dd/MMM/yyyy:HH:mm:ss Z']'";
152
153
154
155  /**
156   * The fully-qualified name of the class in the server codebase that will be
157   * invoked to perform collect-support-data processing when the
158   * --useRemoteServer argument is not provided.
159   */
160  @NotNull private static final String SERVER_CSD_TOOL_CLASS =
161       "com.unboundid.directory.server.tools.CollectSupportData";
162
163
164
165  // The completion message for this tool, if available.
166  @NotNull private final AtomicReference<String> toolCompletionMessage;
167
168  // The command-line arguments supported by this tool.
169  @Nullable private ArgumentParser parser;
170  @Nullable private BooleanArgument archiveExtensionSourceArg;
171  @Nullable private BooleanArgument collectExpensiveDataArg;
172  @Nullable private BooleanArgument collectReplicationStateDumpArg;
173  @Nullable private BooleanArgument dryRunArg;
174  @Nullable private BooleanArgument encryptArg;
175  @Nullable private BooleanArgument generatePassphraseArg;
176  @Nullable private BooleanArgument includeBinaryFilesArg;
177  @Nullable private BooleanArgument noJFRArg;
178  @Nullable private BooleanArgument noLDAPArg;
179  @Nullable private BooleanArgument noPromptArg;
180  @Nullable private BooleanArgument scriptFriendlyArg;
181  @Nullable private BooleanArgument sequentialArg;
182  @Nullable private BooleanArgument useAdministrativeSessionArg;
183  @Nullable private BooleanArgument useRemoteServerArg;
184  @Nullable private DurationArgument logDurationArg;
185  @Nullable private FileArgument decryptArg;
186  @Nullable private FileArgument outputPathArg;
187  @Nullable private FileArgument passphraseFileArg;
188  @Nullable private IntegerArgument jstackCountArg;
189  @Nullable private IntegerArgument logFileHeadCollectionSizeKBArg;
190  @Nullable private IntegerArgument logFileTailCollectionSizeKBArg;
191  @Nullable private IntegerArgument reportCountArg;
192  @Nullable private IntegerArgument reportIntervalSecondsArg;
193  @Nullable private IntegerArgument pidArg;
194  @Nullable private IntegerArgument proxyToServerPortArg;
195  @Nullable private StringArgument commentArg;
196  @Nullable private StringArgument proxyToServerAddressArg;
197  @Nullable private StringArgument securityLevelArg;
198  @Nullable private StringArgument logTimeRangeArg;
199
200
201
202  /**
203   * Invokes this tool with the provided set of command-line arguments.  The
204   * JVM's default standard output and standard error streams will be used.
205   *
206   * @param  args  The set of command-line arguments provided to this program.
207   *               It must not be {@code null} but may be empty.
208   */
209  public static void main(@NotNull final String... args)
210  {
211    final ResultCode resultCode = main(System.out, System.err, args);
212    if ((resultCode != ResultCode.SUCCESS) &&
213         (resultCode != ResultCode.NO_OPERATION))
214    {
215      System.exit(resultCode.intValue());
216    }
217  }
218
219
220
221  /**
222   * Invokes this tool with the provided set of command-line arguments, using
223   * the given output and error streams.
224   *
225   * @param  out   The output stream to use for standard output.  It may be
226   *               {@code null} if standard output should be suppressed.
227   * @param  err   The output stream to use for standard error.  It may be
228   *               {@code null} if standard error should be suppressed.
229   * @param  args  The set of command-line arguments provided to this program.
230   *               It must not be {@code null} but may be empty.
231   *
232   * @return  A result code indicating the final status of the processing that
233   *          was performed.  A result code of {@link ResultCode#SUCCESS}
234   *          indicates that all processing was successful.  A result code of
235   *          {@link ResultCode#NO_OPERATION} indicates that it is likely that
236   *          processing would have been successful if the --dryRun argument
237   *          had not been provided.  Any other result code indicates that the
238   *          processing did not complete successfully.
239   */
240  @NotNull()
241  public static ResultCode main(@Nullable final OutputStream out,
242                                @Nullable final OutputStream err,
243                                @NotNull final String... args)
244  {
245    final CollectSupportData tool = new CollectSupportData(out, err);
246    return tool.runTool(args);
247  }
248
249
250
251  /**
252   * Creates a new instance of this tool that will use the provided streams for
253   * standard output and standard error.
254   *
255   * @param  out  The output stream to use for standard output.  It may be
256   *              {@code null} if standard output should be suppressed.
257   * @param  err  The output stream to use for standard error.  It may be
258   *              {@code null} if standard error should be suppressed.
259   */
260  public CollectSupportData(@Nullable final OutputStream out,
261                            @Nullable final OutputStream err)
262  {
263    super(out, err);
264
265    toolCompletionMessage = new AtomicReference<>();
266
267    parser = null;
268    archiveExtensionSourceArg = null;
269    collectExpensiveDataArg = null;
270    collectReplicationStateDumpArg = null;
271    dryRunArg = null;
272    encryptArg = null;
273    generatePassphraseArg = null;
274    includeBinaryFilesArg = null;
275    noJFRArg = null;
276    noLDAPArg = null;
277    noPromptArg = null;
278    scriptFriendlyArg = null;
279    sequentialArg = null;
280    useRemoteServerArg = null;
281    logDurationArg = null;
282    logFileHeadCollectionSizeKBArg = null;
283    logFileTailCollectionSizeKBArg = null;
284    jstackCountArg = null;
285    outputPathArg = null;
286    reportCountArg = null;
287    decryptArg = null;
288    passphraseFileArg = null;
289    reportIntervalSecondsArg = null;
290    pidArg = null;
291    proxyToServerPortArg = null;
292    commentArg = null;
293    logTimeRangeArg = null;
294    proxyToServerAddressArg = null;
295    securityLevelArg = null;
296  }
297
298
299
300  /**
301   * {@inheritDoc}
302   */
303  @Override()
304  @NotNull()
305  public String getToolName()
306  {
307    return "collect-support-data";
308  }
309
310
311
312  /**
313   * {@inheritDoc}
314   */
315  @Override()
316  @NotNull()
317  public String getToolDescription()
318  {
319    return INFO_CSD_TOOL_DESCRIPTION_1.get();
320  }
321
322
323
324  /**
325   * {@inheritDoc}
326   */
327  @Override()
328  @NotNull()
329  public List<String> getAdditionalDescriptionParagraphs()
330  {
331    return Collections.singletonList(INFO_CSD_TOOL_DESCRIPTION_2.get());
332  }
333
334
335
336  /**
337   * {@inheritDoc}
338   */
339  @Override()
340  @NotNull()
341  public String getToolVersion()
342  {
343    return Version.NUMERIC_VERSION_STRING;
344  }
345
346
347
348  /**
349   * {@inheritDoc}
350   */
351  @Override()
352  public boolean supportsInteractiveMode()
353  {
354    return true;
355  }
356
357
358
359  /**
360   * {@inheritDoc}
361   */
362  @Override()
363  public boolean defaultsToInteractiveMode()
364  {
365    return false;
366  }
367
368
369
370  /**
371   * {@inheritDoc}
372   */
373  @Override()
374  public boolean supportsPropertiesFile()
375  {
376    return true;
377  }
378
379
380
381  /**
382   * {@inheritDoc}
383   */
384  @Override()
385  protected boolean supportsDebugLogging()
386  {
387    return true;
388  }
389
390
391
392  /**
393   * {@inheritDoc}
394   */
395  @Override()
396  protected boolean defaultToPromptForBindPassword()
397  {
398    if ((noPromptArg != null) && noPromptArg.isPresent())
399    {
400      return false;
401    }
402
403    return true;
404  }
405
406
407
408  /**
409   * {@inheritDoc}
410   */
411  @Override()
412  protected boolean includeAlternateLongIdentifiers()
413  {
414    return true;
415  }
416
417
418
419  /**
420   * {@inheritDoc}
421   */
422  @Override()
423  protected boolean supportsSSLDebugging()
424  {
425    return true;
426  }
427
428
429
430  /**
431   * {@inheritDoc}
432   */
433  @Override()
434  protected boolean logToolInvocationByDefault()
435  {
436    return true;
437  }
438
439
440
441  /**
442   * {@inheritDoc}
443   */
444  @Override()
445  @Nullable()
446  protected String getToolCompletionMessage()
447  {
448    return toolCompletionMessage.get();
449  }
450
451
452
453  /**
454   * {@inheritDoc}
455   */
456  @Override()
457  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
458         throws ArgumentException
459  {
460    this.parser = parser;
461
462    // Output-related arguments.
463    outputPathArg = new FileArgument(null, "outputPath", false, 1, null,
464         INFO_CSD_ARG_DESC_OUTPUT_PATH.get(), false, true, false, false);
465    outputPathArg.addLongIdentifier("output-path", true);
466    outputPathArg.addLongIdentifier("outputFile", true);
467    outputPathArg.addLongIdentifier("output-file", true);
468    outputPathArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
469    parser.addArgument(outputPathArg);
470
471    encryptArg = new BooleanArgument(null, "encrypt", 1,
472         INFO_CSD_ARG_DESC_ENCRYPT.get());
473    encryptArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
474    parser.addArgument(encryptArg);
475
476    passphraseFileArg = new FileArgument(null, "passphraseFile", false, 1,
477         null, INFO_CSD_ARG_DESC_PASSPHRASE_FILE.get(), false, true, true,
478         false);
479    passphraseFileArg.addLongIdentifier("passphrase-file", true);
480    passphraseFileArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
481    parser.addArgument(passphraseFileArg);
482
483    generatePassphraseArg = new BooleanArgument(null, "generatePassphrase", 1,
484         INFO_CSD_ARG_DESC_GENERATE_PASSPHRASE.get());
485    generatePassphraseArg.addLongIdentifier("generate-passphrase", true);
486    generatePassphraseArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
487    parser.addArgument(generatePassphraseArg);
488
489    decryptArg = new FileArgument(null, "decrypt", false, 1, null,
490         INFO_CSD_ARG_DESC_DECRYPT.get(), false, true, true, false);
491    decryptArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
492    parser.addArgument(decryptArg);
493
494
495    // Collection-related arguments.
496    collectExpensiveDataArg = new BooleanArgument(null, "collectExpensiveData",
497         1, INFO_CSD_ARG_DESC_COLLECT_EXPENSIVE_DATA.get());
498    collectExpensiveDataArg.addLongIdentifier("collect-expensive-data", true);
499    collectExpensiveDataArg.addLongIdentifier("includeExpensiveData", true);
500    collectExpensiveDataArg.addLongIdentifier("include-expensive-data", true);
501    collectExpensiveDataArg.setArgumentGroupName(
502         INFO_CSD_ARG_GROUP_COLLECTION.get());
503    parser.addArgument(collectExpensiveDataArg);
504
505    collectReplicationStateDumpArg = new BooleanArgument(null,
506         "collectReplicationStateDump", 1,
507         INFO_CSD_ARG_DESC_COLLECT_REPL_STATE.get());
508    collectReplicationStateDumpArg.addLongIdentifier(
509         "collect-replication-state-dump", true);
510    collectReplicationStateDumpArg.addLongIdentifier(
511         "collectReplicationState", true);
512    collectReplicationStateDumpArg.addLongIdentifier(
513         "collect-replication-state", true);
514    collectReplicationStateDumpArg.addLongIdentifier(
515         "includeReplicationStateDump", true);
516    collectReplicationStateDumpArg.addLongIdentifier(
517         "include-replication-state-dump", true);
518    collectReplicationStateDumpArg.addLongIdentifier(
519         "includeReplicationState", true);
520    collectReplicationStateDumpArg.addLongIdentifier(
521         "include-replication-state", true);
522    collectReplicationStateDumpArg.setArgumentGroupName(
523         INFO_CSD_ARG_GROUP_COLLECTION.get());
524    parser.addArgument(collectReplicationStateDumpArg);
525
526    includeBinaryFilesArg = new BooleanArgument(null, "includeBinaryFiles", 1,
527         INFO_CSD_ARG_DESC_INCLUDE_BINARY_FILES.get());
528    includeBinaryFilesArg.addLongIdentifier("include-binary-files", true);
529    includeBinaryFilesArg.setArgumentGroupName(
530         INFO_CSD_ARG_GROUP_COLLECTION.get());
531    parser.addArgument(includeBinaryFilesArg);
532
533    archiveExtensionSourceArg = new BooleanArgument(null,
534         "archiveExtensionSource", 1,
535         INFO_CSD_ARG_DESC_ARCHIVE_EXTENSION_SOURCE.get());
536    archiveExtensionSourceArg.addLongIdentifier("archive-extension-source",
537         true);
538    archiveExtensionSourceArg.addLongIdentifier("includeExtensionSource", true);
539    archiveExtensionSourceArg.addLongIdentifier("include-extension-source",
540         true);
541    archiveExtensionSourceArg.setArgumentGroupName(
542         INFO_CSD_ARG_GROUP_COLLECTION.get());
543    parser.addArgument(archiveExtensionSourceArg);
544
545    sequentialArg = new BooleanArgument(null, "sequential", 1,
546         INFO_CSD_ARG_DESC_SEQUENTIAL.get());
547    sequentialArg.addLongIdentifier("sequentialMode", true);
548    sequentialArg.addLongIdentifier("sequential-mode", true);
549    sequentialArg.addLongIdentifier("useSequentialMode", true);
550    sequentialArg.addLongIdentifier("use-sequential-mode", true);
551    sequentialArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
552    parser.addArgument(sequentialArg);
553
554    securityLevelArg = new StringArgument(null, "securityLevel", false, 1,
555         INFO_CSD_ARG_PLACEHOLDER_SECURITY_LEVEL.get(),
556         INFO_CSD_ARG_DESC_SECURITY_LEVEL.get(),
557         StaticUtils.setOf(
558              CollectSupportDataSecurityLevel.NONE.getName(),
559              CollectSupportDataSecurityLevel.OBSCURE_SECRETS.getName(),
560              CollectSupportDataSecurityLevel.MAXIMUM.getName()));
561    securityLevelArg.addLongIdentifier("security-level", true);
562    securityLevelArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
563    parser.addArgument(securityLevelArg);
564
565    jstackCountArg = new IntegerArgument(null, "jstackCount", false, 1,
566         INFO_CSD_ARG_PLACEHOLDER_COUNT.get(),
567         INFO_CSD_ARG_DESC_JSTACK_COUNT.get(), 0, Integer.MAX_VALUE);
568    jstackCountArg.addLongIdentifier("jstack-count", true);
569    jstackCountArg.addLongIdentifier("maxJstacks", true);
570    jstackCountArg.addLongIdentifier("max-jstacks", true);
571    jstackCountArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
572    parser.addArgument(jstackCountArg);
573
574    noJFRArg = new BooleanArgument(null, "noJFR", 1,
575         INFO_CSD_ARG_DESC_NO_JFR.get());
576    noJFRArg.addLongIdentifier("noJFRData", true);
577    noJFRArg.addLongIdentifier("excludeJFR", true);
578    noJFRArg.addLongIdentifier("excludeJFRData", true);
579    noJFRArg.addLongIdentifier("no-jfr", true);
580    noJFRArg.addLongIdentifier("no-jfr-data", true);
581    noJFRArg.addLongIdentifier("exclude-jfr", true);
582    noJFRArg.addLongIdentifier("exclude-jfr-data", true);
583    noJFRArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
584    parser.addArgument(noJFRArg);
585
586    reportCountArg = new IntegerArgument(null, "reportCount", false, 1,
587         INFO_CSD_ARG_PLACEHOLDER_COUNT.get(),
588         INFO_CSD_ARG_DESC_REPORT_COUNT.get(), 0, Integer.MAX_VALUE);
589    reportCountArg.addLongIdentifier("report-count", true);
590    reportCountArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
591    parser.addArgument(reportCountArg);
592
593    reportIntervalSecondsArg = new IntegerArgument(null,
594         "reportIntervalSeconds", false, 1,
595         INFO_CSD_ARG_PLACEHOLDER_SECONDS.get(),
596         INFO_CSD_ARG_DESC_REPORT_INTERVAL_SECONDS.get(), 1,
597         Integer.MAX_VALUE);
598    reportIntervalSecondsArg.addLongIdentifier("report-interval-seconds", true);
599    reportIntervalSecondsArg.addLongIdentifier("reportInterval", true);
600    reportIntervalSecondsArg.addLongIdentifier("report-interval", true);
601    reportIntervalSecondsArg.setArgumentGroupName(
602         INFO_CSD_ARG_GROUP_COLLECTION.get());
603    parser.addArgument(reportIntervalSecondsArg);
604
605    logTimeRangeArg = new StringArgument(null, "logTimeRange", false, 1,
606         INFO_CSD_ARG_PLACEHOLDER_TIME_RANGE.get(),
607         INFO_CSD_ARG_DESC_TIME_RANGE.get());
608    logTimeRangeArg.addLongIdentifier("log-time-range", true);
609    logTimeRangeArg.addLongIdentifier("timeRange", true);
610    logTimeRangeArg.addLongIdentifier("time-range", true);
611    logTimeRangeArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
612    parser.addArgument(logTimeRangeArg);
613
614    logDurationArg = new DurationArgument(null, "logDuration", false, null,
615         INFO_CSD_ARG_DESC_DURATION.get());
616    logDurationArg.addLongIdentifier("log-duration", true);
617    logDurationArg.addLongIdentifier("duration", true);
618    logDurationArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
619    parser.addArgument(logDurationArg);
620
621    logFileHeadCollectionSizeKBArg = new IntegerArgument(null,
622         "logFileHeadCollectionSizeKB", false, 1,
623         INFO_CSD_ARG_PLACEHOLDER_SIZE_KB.get(),
624         INFO_CSD_ARG_DESC_LOG_HEAD_SIZE_KB.get(), 0, Integer.MAX_VALUE);
625    logFileHeadCollectionSizeKBArg.addLongIdentifier(
626         "log-file-head-collection-size-kb", true);
627    logFileHeadCollectionSizeKBArg.addLongIdentifier("logFileHeadSizeKB", true);
628    logFileHeadCollectionSizeKBArg.addLongIdentifier("log-file-head-size-kb",
629         true);
630    logFileHeadCollectionSizeKBArg.addLongIdentifier("logHeadCollectionSizeKB",
631         true);
632    logFileHeadCollectionSizeKBArg.addLongIdentifier(
633         "log-head-collection-size-kb", true);
634    logFileHeadCollectionSizeKBArg.addLongIdentifier("logHeadSizeKB", true);
635    logFileHeadCollectionSizeKBArg.addLongIdentifier("log-head-size-kb", true);
636    logFileHeadCollectionSizeKBArg.setArgumentGroupName(
637         INFO_CSD_ARG_GROUP_COLLECTION.get());
638    parser.addArgument(logFileHeadCollectionSizeKBArg);
639
640    logFileTailCollectionSizeKBArg = new IntegerArgument(null,
641         "logFileTailCollectionSizeKB", false, 1,
642         INFO_CSD_ARG_PLACEHOLDER_SIZE_KB.get(),
643         INFO_CSD_ARG_DESC_LOG_TAIL_SIZE_KB.get(), 0, Integer.MAX_VALUE);
644    logFileTailCollectionSizeKBArg.addLongIdentifier(
645         "log-file-tail-collection-size-kb", true);
646    logFileTailCollectionSizeKBArg.addLongIdentifier("logFileTailSizeKB", true);
647    logFileTailCollectionSizeKBArg.addLongIdentifier("log-file-tail-size-kb",
648         true);
649    logFileTailCollectionSizeKBArg.addLongIdentifier("logTailCollectionSizeKB",
650         true);
651    logFileTailCollectionSizeKBArg.addLongIdentifier(
652         "log-tail-collection-size-kb", true);
653    logFileTailCollectionSizeKBArg.addLongIdentifier("logTailSizeKB", true);
654    logFileTailCollectionSizeKBArg.addLongIdentifier("log-tail-size-kb", true);
655    logFileTailCollectionSizeKBArg.setArgumentGroupName(
656         INFO_CSD_ARG_GROUP_COLLECTION.get());
657    parser.addArgument(logFileTailCollectionSizeKBArg);
658
659    pidArg = new IntegerArgument(null, "pid", false, 0,
660         INFO_CSD_ARG_PLACEHOLDER_PID.get(), INFO_CSD_ARG_DESC_PID.get());
661    pidArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
662    parser.addArgument(pidArg);
663
664    commentArg = new StringArgument(null, "comment", false, 1, null,
665         INFO_CSD_ARG_DESC_COMMENT.get());
666    commentArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
667    parser.addArgument(commentArg);
668
669
670    // Communication-related arguments.
671    useRemoteServerArg = new BooleanArgument(null, "useRemoteServer", 1,
672         INFO_CSD_ARG_DEC_USE_REMOTE_SERVER.get());
673    useRemoteServerArg.addLongIdentifier("use-remote-server", true);
674    useRemoteServerArg.setArgumentGroupName(
675         INFO_CSD_ARG_GROUP_COMMUNICATION.get());
676    parser.addArgument(useRemoteServerArg);
677
678    useAdministrativeSessionArg = new BooleanArgument(null,
679         "useAdministrativeSession", 1,
680         INFO_CSD_ARG_DESC_USE_ADMIN_SESSION.get());
681    useAdministrativeSessionArg.addLongIdentifier("use-administrative-session",
682         true);
683    useAdministrativeSessionArg.addLongIdentifier("useAdminSession", true);
684    useAdministrativeSessionArg.addLongIdentifier("use-admin-session",
685         true);
686    useAdministrativeSessionArg.addLongIdentifier("administrativeSession",
687         true);
688    useAdministrativeSessionArg.addLongIdentifier("administrative-session",
689         true);
690    useAdministrativeSessionArg.addLongIdentifier("adminSession", true);
691    useAdministrativeSessionArg.addLongIdentifier("admin-session", true);
692    useAdministrativeSessionArg.setArgumentGroupName(
693         INFO_CSD_ARG_GROUP_COMMUNICATION.get());
694    parser.addArgument(useAdministrativeSessionArg);
695
696    proxyToServerAddressArg = new StringArgument(null, "proxyToServerAddress",
697         false, 1, INFO_CSD_ARG_PLACEHOLDER_ADDRESS.get(),
698         INFO_CSD_ARG_DESC_PROXY_TO_ADDRESS.get());
699    proxyToServerAddressArg.addLongIdentifier("proxy-to-server-address", true);
700    proxyToServerAddressArg.addLongIdentifier("proxyToAddress", true);
701    proxyToServerAddressArg.addLongIdentifier("proxy-to-address", true);
702    proxyToServerAddressArg.setArgumentGroupName(
703         INFO_CSD_ARG_GROUP_COMMUNICATION.get());
704    parser.addArgument(proxyToServerAddressArg);
705
706    proxyToServerPortArg = new IntegerArgument(null, "proxyToServerPort", false,
707         1, INFO_CSD_ARG_PLACEHOLDER_PORT.get(),
708         INFO_CSD_ARG_DESC_PROXY_TO_PORT.get(), 1, 65535);
709    proxyToServerPortArg.addLongIdentifier("proxy-to-server-port", true);
710    proxyToServerPortArg.addLongIdentifier("proxyToPort", true);
711    proxyToServerPortArg.addLongIdentifier("proxy-to-port", true);
712    proxyToServerPortArg.setArgumentGroupName(
713         INFO_CSD_ARG_GROUP_COMMUNICATION.get());
714    parser.addArgument(proxyToServerPortArg);
715
716    noLDAPArg = new BooleanArgument(null, "noLDAP", 1,
717         INFO_CSD_ARG_DESC_NO_LDAP.get());
718    noLDAPArg.addLongIdentifier("no-ldap", true);
719    noLDAPArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COMMUNICATION.get());
720    parser.addArgument(noLDAPArg);
721
722
723    // Other arguments.
724    noPromptArg = new BooleanArgument('n', "noPrompt",  1,
725         INFO_CSD_ARG_DESC_NO_PROMPT.get());
726    noPromptArg.addLongIdentifier("no-prompt", true);
727    parser.addArgument(noPromptArg);
728
729    dryRunArg = new BooleanArgument(null, "dryRun", 1,
730         INFO_CSD_ARG_DESC_DRY_RUN.get());
731    dryRunArg.addLongIdentifier("dry-run", true);
732    dryRunArg.addLongIdentifier("noOperation", true);
733    dryRunArg.addLongIdentifier("no-operation", true);
734    dryRunArg.addLongIdentifier("noOp", true);
735    dryRunArg.addLongIdentifier("no-op", true);
736    parser.addArgument(dryRunArg);
737
738    scriptFriendlyArg = new BooleanArgument(null, "scriptFriendly", 1,
739         INFO_CSD_ARG_DESC_SCRIPT_FRIENDLY.get());
740    scriptFriendlyArg.addLongIdentifier("script-friendly", true);
741    scriptFriendlyArg.setHidden(true);
742    parser.addArgument(scriptFriendlyArg);
743
744
745    // If the --useRemoteServer argument is provided, then none of the --pid,
746    // --decrypt, --noLDAP, or --scriptFriendly arguments may be given.
747    parser.addExclusiveArgumentSet(useRemoteServerArg, pidArg);
748    parser.addExclusiveArgumentSet(useRemoteServerArg, decryptArg);
749    parser.addExclusiveArgumentSet(useRemoteServerArg, noLDAPArg);
750    parser.addExclusiveArgumentSet(useRemoteServerArg, scriptFriendlyArg);
751
752    // The --useAdministrativeSession argument can only be provided if the
753    // --useRemoteServer argument is also given.
754    parser.addDependentArgumentSet(useAdministrativeSessionArg,
755         useRemoteServerArg);
756
757    // If the --proxyToServerAddress or --proxyToServerPort argument is given,
758    // then the other must be provided as well.
759    parser.addMutuallyDependentArgumentSet(proxyToServerAddressArg,
760         proxyToServerPortArg);
761
762    // The --proxyToServerAddress and --proxyToServerPort arguments can only
763    // be used if the --useRemoteServer argument is given.
764    parser.addDependentArgumentSet(proxyToServerAddressArg, useRemoteServerArg);
765    parser.addDependentArgumentSet(proxyToServerPortArg, useRemoteServerArg);
766
767    // The --logTimeRange and --logDuration arguments cannot be used together.
768    parser.addExclusiveArgumentSet(logTimeRangeArg, logDurationArg);
769
770    // Neither the --logFileHeadCollectionSizeKB argument nor the
771    // --logFileTailCollectionSizeKB argument can be used in conjunction with
772    // either the --logTimeRange or --logDuration argument.
773    parser.addExclusiveArgumentSet(logFileHeadCollectionSizeKBArg,
774         logTimeRangeArg, logDurationArg);
775    parser.addExclusiveArgumentSet(logFileTailCollectionSizeKBArg,
776         logTimeRangeArg, logDurationArg);
777
778    // The --generatePassphrase argument can only be used if both the
779    // --encrypt and --passphraseFile arguments are provided.
780    parser.addDependentArgumentSet(generatePassphraseArg, encryptArg);
781    parser.addDependentArgumentSet(generatePassphraseArg, passphraseFileArg);
782
783    // The --encrypt and --decrypt arguments cannot be used together.
784    parser.addExclusiveArgumentSet(encryptArg, decryptArg);
785
786
787    // There are several arguments that the LDAP SDK's LDAP command-line tool
788    // framework offers that the server-side version of the framework does not
789    // provide.  Those arguments can only be used in conjunction with the
790    // --useRemoteServer argument.
791    for (final String argumentIdentifier :
792         Arrays.asList("promptForBindPassword", "promptForKeyStorePassword",
793              "promptForTrustStorePassword", "enableSSLDebugging",
794              "useSASLExternal"))
795    {
796      final Argument arg = parser.getNamedArgument(argumentIdentifier);
797      parser.addDependentArgumentSet(arg, useRemoteServerArg);
798    }
799  }
800
801
802
803  /**
804   * {@inheritDoc}
805   */
806  @Override()
807  public void doExtendedNonLDAPArgumentValidation()
808         throws ArgumentException
809  {
810    // If the --logTimeRange argument was provided, then make sure we can
811    // parse each of the values and that the end time is greater than or equal
812    // to the start time.
813    if (logTimeRangeArg.isPresent())
814    {
815      try
816      {
817        parseTimeRange(logTimeRangeArg.getValue(),
818             useRemoteServerArg.isPresent());
819      }
820      catch (final LDAPException e)
821      {
822        Debug.debugException(e);
823        toolCompletionMessage.set(e.getMessage());
824        throw new ArgumentException(e.getMessage(), e);
825      }
826    }
827
828
829    // If the --passphraseFile argument was provided without the
830    // --generatePassphrase argument, then make sure the file exists.
831    if (passphraseFileArg.isPresent() && (! generatePassphraseArg.isPresent()))
832    {
833      final File passphraseFile = passphraseFileArg.getValue();
834      if (! passphraseFile.exists())
835      {
836        final String message =ERR_CSD_PASSPHRASE_FILE_MISSING.get(
837             passphraseFile.getAbsolutePath());
838        toolCompletionMessage.set(message);
839        throw new ArgumentException(message);
840      }
841    }
842
843
844    // If either the --encrypt or --decrypt argument is provided in conjunction
845    // with the --noPrompt argument, then the --passphraseFile argument must
846    // also have been provided.
847    if (noPromptArg.isPresent() &&
848         (encryptArg.isPresent() || decryptArg.isPresent()) &&
849         (! passphraseFileArg.isPresent()))
850    {
851      final String message = ERR_CSD_NO_PASSPHRASE_WITH_NO_PROMPT.get();
852      toolCompletionMessage.set(message);
853      throw new ArgumentException(message);
854    }
855  }
856
857
858
859  /**
860   * Parses the provided string as a time range.  If both start and end time
861   * values are provided, then they must be separated by a comma; otherwise,
862   * there must only be a start time value.  Each timestamp must be in either
863   * the generalized time format or the Ping Identity Directory Server's default
864   * access log format (with or without millisecond precision).
865   *
866   * @param  timeRangeStr  The string to be parsed as a time range.  It must not
867   *                       be {@code null}.
868   * @param  strict        Indicates whether to require strict compliance with
869   *                       the timestamp format.  This should be {@code true}
870   *                       when the useRemoteServer argument was provided, and
871   *                       {@code false} otherwise.
872   *
873   * @return  An object pair in which the first value is the start time for
874   *          the range and the second value is the end time for the range.  The
875   *          first element will always be non-{@code null}, but the second
876   *          element may be {@code null} if the time range did not specify an
877   *          end time.  The entire return value may be {@code null} if the
878   *          time range string could not be parsed and {@code strict} is
879   *          {@code false}.
880   *
881   * @throws  LDAPException  If a problem is encountered while parsing the
882   *                         provided string as a time range, or if the start
883   *                         time is greater than the end time.
884   */
885  @Nullable()
886  static ObjectPair<Date,Date> parseTimeRange(
887              @NotNull final String timeRangeStr,
888              final boolean strict)
889         throws LDAPException
890  {
891    final Date startTime;
892    final Date endTime;
893
894    try
895    {
896      // See if there is a comma to separate the before and after times.  If so,
897      // then parse each value separately.  Otherwise, the value will be just
898      // the start time and the current time will be used as the end time.
899      final int commaPos = timeRangeStr.indexOf(',');
900      if (commaPos > 0)
901      {
902        startTime = parseTimestamp(timeRangeStr.substring(0, commaPos).trim());
903        endTime = parseTimestamp(timeRangeStr.substring(commaPos+1).trim());
904      }
905      else
906      {
907        startTime = parseTimestamp(timeRangeStr);
908        endTime = null;
909      }
910    }
911    catch (final LDAPException e)
912    {
913      Debug.debugException(e);
914
915      // NOTE:  The server-side version of the collect-support-data tool has a
916      // not-so-documented feature in which you can provide rotated file names
917      // as an alternative to an actual time range.  We can't handle that
918      // when operating against a remote server, so we'll require strict
919      // timestamp compliance when --useRemoteServer is provided, but we'll just
920      // return null and let the argument value be passed through to the
921      // server-side code otherwise.
922      if (strict)
923      {
924        throw e;
925      }
926      else
927      {
928        return null;
929      }
930    }
931
932    if ((endTime != null) && (startTime.getTime() > endTime.getTime()))
933    {
934      throw new LDAPException(ResultCode.PARAM_ERROR,
935           ERR_CSD_TIME_RANGE_START_GREATER_THAN_END.get());
936    }
937
938    return new ObjectPair<>(startTime, endTime);
939  }
940
941
942
943  /**
944   * Parses the provided string as a timestamp value in either the generalized
945   * time format or the Ping Identity Directory Server's default access log
946   * format (with or without millisecond precision).
947   *
948   * @param  timestampStr  The timestamp to be parsed.  It must not be
949   *                       {@code null}.
950   *
951   * @return  The {@code Date} created by parsing the timestamp.
952   *
953   * @throws  LDAPException  If the provided string cannot be parsed as a
954   *                         valid timestamp.
955   */
956  @NotNull()
957  static Date parseTimestamp(@NotNull final String timestampStr)
958         throws LDAPException
959  {
960    // First, try using the timestamp argument to parse the timestamp.
961    try
962    {
963      return TimestampArgument.parseTimestamp(timestampStr);
964    }
965    catch (final Exception e)
966    {
967      Debug.debugException(Level.FINEST, e);
968    }
969
970
971    // Next, try the server's default access log format with millisecond
972    // precision.
973    try
974    {
975      final SimpleDateFormat timestampFormatter =
976           new SimpleDateFormat(SERVER_LOG_TIMESTAMP_FORMAT_WITH_MILLIS);
977      timestampFormatter.setLenient(false);
978      return timestampFormatter.parse(timestampStr);
979    }
980    catch (final Exception e)
981    {
982      Debug.debugException(Level.FINEST, e);
983    }
984
985
986    // Next, try the server's default access log format with second precision.
987    try
988    {
989      final SimpleDateFormat timestampFormatter =
990           new SimpleDateFormat(SERVER_LOG_TIMESTAMP_FORMAT_WITHOUT_MILLIS);
991      timestampFormatter.setLenient(false);
992      return timestampFormatter.parse(timestampStr);
993    }
994    catch (final Exception e)
995    {
996      Debug.debugException(Level.FINEST, e);
997    }
998
999
1000    // If we've gotten here, then we could not parse the timestamp.
1001    throw new LDAPException(ResultCode.PARAM_ERROR,
1002         ERR_CSD_TIME_RANGE_CANNOT_PARSE_TIMESTAMP.get(timestampStr));
1003  }
1004
1005
1006
1007  /**
1008   * {@inheritDoc}
1009   */
1010  @Override()
1011  @NotNull()
1012  public ResultCode doToolProcessing()
1013  {
1014    // If the --useRemoteServer argument was provided, then use the extended
1015    // operation to perform the processing.  Otherwise, use reflection to invoke
1016    // the server's version of the collect-support-data tool.
1017    if (useRemoteServerArg.isPresent())
1018    {
1019      return doExtendedOperationProcessing();
1020    }
1021    else
1022    {
1023      return doLocalProcessing();
1024    }
1025  }
1026
1027
1028
1029  /**
1030   * Performs the collect-support-data processing against a remote server using
1031   * the extended operation.
1032   *
1033   * @return  A result code that indicates the result of the processing.
1034   */
1035  @NotNull()
1036  private ResultCode doExtendedOperationProcessing()
1037  {
1038    // Create a connection pool that will be used to communicate with the
1039    // server.  Use an administrative session if appropriate.
1040    final StartAdministrativeSessionPostConnectProcessor p;
1041    if (useAdministrativeSessionArg.isPresent())
1042    {
1043      p = new StartAdministrativeSessionPostConnectProcessor(
1044           new StartAdministrativeSessionExtendedRequest(getToolName(),
1045                true));
1046    }
1047    else
1048    {
1049      p = null;
1050    }
1051
1052    final LDAPConnectionPool pool;
1053    try
1054    {
1055      pool = getConnectionPool(1, 1, 0, p, null, true,
1056           new ReportBindResultLDAPConnectionPoolHealthCheck(this, true,
1057                false));
1058    }
1059    catch (final LDAPException e)
1060    {
1061      Debug.debugException(e);
1062      wrapErr(0, WRAP_COLUMN, e.getMessage());
1063      toolCompletionMessage.set(e.getMessage());
1064      return e.getResultCode();
1065    }
1066
1067
1068    try
1069    {
1070      // Create the properties to use for the extended request.
1071      final CollectSupportDataExtendedRequestProperties properties =
1072           new CollectSupportDataExtendedRequestProperties();
1073
1074      final File outputPath = outputPathArg.getValue();
1075      if (outputPath != null)
1076      {
1077        if (! (outputPath.exists() && outputPath.isDirectory()))
1078        {
1079          properties.setArchiveFileName(outputPath.getName());
1080        }
1081      }
1082
1083      try
1084      {
1085        properties.setEncryptionPassphrase(
1086             getEncryptionPassphraseForExtOpProcessing());
1087      }
1088      catch (final LDAPException e)
1089      {
1090        Debug.debugException(e);
1091        wrapErr(0, WRAP_COLUMN, e.getMessage());
1092        toolCompletionMessage.set(e.getMessage());
1093        return e.getResultCode();
1094      }
1095
1096
1097      properties.setIncludeExpensiveData(collectExpensiveDataArg.isPresent());
1098      properties.setIncludeReplicationStateDump(
1099           collectReplicationStateDumpArg.isPresent());
1100      properties.setIncludeBinaryFiles(includeBinaryFilesArg.isPresent());
1101      properties.setIncludeExtensionSource(
1102           archiveExtensionSourceArg.isPresent());
1103      properties.setUseSequentialMode(sequentialArg.isPresent());
1104      properties.setNoJFR(noJFRArg.isPresent());
1105
1106      if (securityLevelArg.isPresent())
1107      {
1108        properties.setSecurityLevel(CollectSupportDataSecurityLevel.forName(
1109             securityLevelArg.getValue()));
1110      }
1111
1112      if (jstackCountArg.isPresent())
1113      {
1114        properties.setJStackCount(jstackCountArg.getValue());
1115      }
1116
1117      if (reportCountArg.isPresent())
1118      {
1119        properties.setReportCount(reportCountArg.getValue());
1120      }
1121
1122      if (reportIntervalSecondsArg.isPresent())
1123      {
1124        properties.setReportIntervalSeconds(
1125             reportIntervalSecondsArg.getValue());
1126      }
1127
1128      if (logTimeRangeArg.isPresent())
1129      {
1130        try
1131        {
1132          final ObjectPair<Date,Date> timeRange =
1133               parseTimeRange(logTimeRangeArg.getValue(), true);
1134          properties.setLogCaptureWindow(
1135               new TimeWindowCollectSupportDataLogCaptureWindow(
1136                    timeRange.getFirst(), timeRange.getSecond()));
1137        }
1138        catch (final LDAPException e)
1139        {
1140          // This should never happen because we should have pre-validated the
1141          // value.  But handle it just in case.
1142          Debug.debugException(e);
1143          wrapErr(0, WRAP_COLUMN, e.getMessage());
1144          toolCompletionMessage.set(e.getMessage());
1145          return e.getResultCode();
1146        }
1147      }
1148      else if (logDurationArg.isPresent())
1149      {
1150        properties.setLogCaptureWindow(
1151             new DurationCollectSupportDataLogCaptureWindow(
1152                  logDurationArg.getValue(TimeUnit.MILLISECONDS)));
1153      }
1154      else if (logFileHeadCollectionSizeKBArg.isPresent() ||
1155           logFileTailCollectionSizeKBArg.isPresent())
1156      {
1157        properties.setLogCaptureWindow(
1158             new HeadAndTailSizeCollectSupportDataLogCaptureWindow(
1159                  logFileHeadCollectionSizeKBArg.getValue(),
1160                  logFileTailCollectionSizeKBArg.getValue()));
1161      }
1162
1163      if (commentArg.isPresent())
1164      {
1165        properties.setComment(commentArg.getValue());
1166      }
1167
1168      if (proxyToServerAddressArg.isPresent())
1169      {
1170        properties.setProxyToServer(proxyToServerAddressArg.getValue(),
1171             proxyToServerPortArg.getValue());
1172      }
1173
1174
1175      // Create the intermediate response listener that will be used to handle
1176      // output and archive fragment messages.
1177      ResultCode resultCode = null;
1178      try (CollectSupportDataIRListener listener =
1179           new CollectSupportDataIRListener(this, outputPathArg.getValue()))
1180      {
1181        // Construct the extended request to send to the server.
1182        final Control[] controls;
1183        if (dryRunArg.isPresent())
1184        {
1185          controls = new Control[]
1186          {
1187            new NoOpRequestControl()
1188          };
1189        }
1190        else
1191        {
1192          controls = StaticUtils.NO_CONTROLS;
1193        }
1194
1195        final CollectSupportDataExtendedRequest request =
1196             new CollectSupportDataExtendedRequest(properties, listener,
1197                  controls);
1198        request.setResponseTimeoutMillis(0L);
1199
1200
1201        // Send the request and read the result.
1202        final CollectSupportDataExtendedResult result;
1203        try
1204        {
1205          result = (CollectSupportDataExtendedResult)
1206               pool.processExtendedOperation(request);
1207        }
1208        catch (final LDAPException e)
1209        {
1210          Debug.debugException(e);
1211          final String message = ERR_CSD_ERROR_SENDING_REQUEST.get(
1212               StaticUtils.getExceptionMessage(e));
1213          wrapErr(0, WRAP_COLUMN, message);
1214          toolCompletionMessage.set(message);
1215          return e.getResultCode();
1216        }
1217
1218
1219        resultCode = result.getResultCode();
1220        final String diagnosticMessage = result.getDiagnosticMessage();
1221        if (diagnosticMessage != null)
1222        {
1223          if ((resultCode == ResultCode.SUCCESS) ||
1224               (resultCode == ResultCode.NO_OPERATION))
1225          {
1226            wrapOut(0, WRAP_COLUMN, diagnosticMessage);
1227          }
1228          else
1229          {
1230            wrapErr(0, WRAP_COLUMN, diagnosticMessage);
1231          }
1232
1233          toolCompletionMessage.set(diagnosticMessage);
1234        }
1235        else
1236        {
1237          toolCompletionMessage.set(INFO_CSD_COMPLETED_WITH_RESULT_CODE.get(
1238               String.valueOf(resultCode)));
1239        }
1240      }
1241      catch (final IOException e)
1242      {
1243        Debug.debugException(e);
1244
1245        if (resultCode == ResultCode.SUCCESS)
1246        {
1247          resultCode = ResultCode.LOCAL_ERROR;
1248          toolCompletionMessage.set(e.getMessage());
1249        }
1250      }
1251
1252      return resultCode;
1253    }
1254    finally
1255    {
1256      pool.close();
1257    }
1258  }
1259
1260
1261
1262  /**
1263   * Retrieves the passphrase to use to generate the key for encrypting the
1264   * support data archive.  This method should only be used when the tool
1265   * processing will be performed using an extended operation.
1266   *
1267   * @return  The passphrase to use to generate the key for encrypting the
1268   *          support data archive.
1269   *
1270   * @throws  LDAPException  If a problem is encountered while attempting to
1271   *                         obtain the passphrase.
1272   */
1273  @Nullable()
1274  private ASN1OctetString getEncryptionPassphraseForExtOpProcessing()
1275          throws LDAPException
1276  {
1277    if (! encryptArg.isPresent())
1278    {
1279      return null;
1280    }
1281
1282    if (passphraseFileArg.isPresent())
1283    {
1284      final File passphraseFile = passphraseFileArg.getValue();
1285      if (generatePassphraseArg.isPresent())
1286      {
1287        // Generate a passphrase as a base64url-encoded representation of some
1288        // randomly generated data.
1289        final byte[] randomBytes = new byte[64];
1290        ThreadLocalSecureRandom.get().nextBytes(randomBytes);
1291        final String passphrase = Base64.urlEncode(randomBytes, false);
1292
1293        try (PrintWriter writer = new PrintWriter(passphraseFile))
1294        {
1295          writer.println(passphrase);
1296        }
1297        catch (final Exception e)
1298        {
1299          Debug.debugException(e);
1300          throw new LDAPException(ResultCode.LOCAL_ERROR,
1301               ERR_CSD_CANNOT_WRITE_GENERATED_PASSPHRASE.get(
1302                    passphraseFile.getAbsolutePath(),
1303                    StaticUtils.getExceptionMessage(e)),
1304               e);
1305        }
1306
1307        return new ASN1OctetString(passphrase);
1308      }
1309      else
1310      {
1311        try
1312        {
1313          final char[] passphrase =
1314               getPasswordFileReader().readPassword(passphraseFile);
1315          return new ASN1OctetString(new String(passphrase));
1316        }
1317        catch (final Exception e)
1318        {
1319          Debug.debugException(e);
1320
1321          ResultCode resultCode = ResultCode.LOCAL_ERROR;
1322          if (e instanceof LDAPException)
1323          {
1324            resultCode = ((LDAPException) e).getResultCode();
1325          }
1326
1327          throw new LDAPException(resultCode,
1328               ERR_CSD_CANNOT_READ_PASSPHRASE.get(
1329                    passphraseFile.getAbsolutePath(),
1330                    StaticUtils.getExceptionMessage(e)),
1331               e);
1332        }
1333      }
1334    }
1335
1336
1337    // Prompt for the encryption passphrase.
1338    while (true)
1339    {
1340      try
1341      {
1342        getOut().print(INFO_CSD_PASSPHRASE_INITIAL_PROMPT.get());
1343        final byte[] passphraseBytes = PasswordReader.readPassword();
1344
1345        getOut().print(INFO_CSD_PASSPHRASE_CONFIRM_PROMPT.get());
1346        final byte[] confirmBytes = PasswordReader.readPassword();
1347        if (Arrays.equals(passphraseBytes, confirmBytes))
1348        {
1349          return new ASN1OctetString(passphraseBytes);
1350        }
1351        else
1352        {
1353          wrapErr(0, WRAP_COLUMN, ERR_CSD_PASSPHRASE_MISMATCH.get());
1354          err();
1355        }
1356      }
1357      catch (final Exception e)
1358      {
1359        throw new LDAPException(ResultCode.LOCAL_ERROR,
1360             ERR_CSD_PASSPHRASE_PROMPT_READ_ERROR.get(
1361                  StaticUtils.getExceptionMessage(e)),
1362             e);
1363      }
1364    }
1365  }
1366
1367
1368
1369  /**
1370   * Performs the collect-support-data tool using reflection to invoke the
1371   * server-side version of the tool.
1372 *
1373   * @return  A result code that indicates the result of the processing.
1374     */
1375  @NotNull()
1376  private ResultCode doLocalProcessing()
1377  {
1378    // Construct the argument list to use when invoking the server-side code.
1379    // Although this tool supports all of the arguments that the server-side
1380    // tool provides, the server-side tool does not support all of the arguments
1381    // that this version offers, nor does it support all of the variants (e.g.,
1382    // alternate names) for the arguments that do overlap.
1383    final List<String> argList = new ArrayList<>(20);
1384
1385    if (outputPathArg.isPresent())
1386    {
1387      argList.add("--outputPath");
1388      argList.add(outputPathArg.getValue().getAbsolutePath());
1389    }
1390
1391    if (noLDAPArg.isPresent())
1392    {
1393      argList.add("--noLdap");
1394    }
1395
1396    if (pidArg.isPresent())
1397    {
1398      for (final Integer pid : pidArg.getValues())
1399      {
1400        argList.add("--pid");
1401        argList.add(pid.toString());
1402      }
1403    }
1404
1405    if (sequentialArg.isPresent())
1406    {
1407      argList.add("--sequential");
1408    }
1409
1410    if (reportCountArg.isPresent())
1411    {
1412      argList.add("--reportCount");
1413      argList.add(reportCountArg.getValue().toString());
1414    }
1415
1416    if (reportIntervalSecondsArg.isPresent())
1417    {
1418      argList.add("--reportInterval");
1419      argList.add(reportIntervalSecondsArg.getValue().toString());
1420    }
1421
1422    if (jstackCountArg.isPresent())
1423    {
1424      argList.add("--maxJstacks");
1425      argList.add(jstackCountArg.getValue().toString());
1426    }
1427
1428    if (noJFRArg.isPresent())
1429    {
1430      argList.add("--noJFR");
1431    }
1432
1433    if (collectExpensiveDataArg.isPresent())
1434    {
1435      argList.add("--collectExpensiveData");
1436    }
1437
1438    if (collectReplicationStateDumpArg.isPresent())
1439    {
1440      argList.add("--collectReplicationStateDump");
1441    }
1442
1443    if (commentArg.isPresent())
1444    {
1445      argList.add("--comment");
1446      argList.add(commentArg.getValue());
1447    }
1448
1449    if (includeBinaryFilesArg.isPresent())
1450    {
1451      argList.add("--includeBinaryFiles");
1452    }
1453
1454    if (securityLevelArg.isPresent())
1455    {
1456      argList.add("--securityLevel");
1457      argList.add(securityLevelArg.getValue());
1458    }
1459
1460    if (encryptArg.isPresent())
1461    {
1462      argList.add("--encrypt");
1463    }
1464
1465    if (passphraseFileArg.isPresent())
1466    {
1467      argList.add("--passphraseFile");
1468      argList.add(passphraseFileArg.getValue().getAbsolutePath());
1469    }
1470
1471    if (generatePassphraseArg.isPresent())
1472    {
1473      argList.add("--generatePassphrase");
1474    }
1475
1476    if (decryptArg.isPresent())
1477    {
1478      argList.add("--decrypt");
1479      argList.add(decryptArg.getValue().getAbsolutePath());
1480    }
1481
1482    if (logTimeRangeArg.isPresent())
1483    {
1484      final ObjectPair<Date,Date> timeRange;
1485      try
1486      {
1487        timeRange = parseTimeRange(logTimeRangeArg.getValue(), false);
1488      }
1489      catch (final LDAPException e)
1490      {
1491        // This should never happen because we should have pre-validated the
1492        // value.  But handle it just in case.
1493        Debug.debugException(e);
1494        wrapErr(0, WRAP_COLUMN, e.getMessage());
1495        return e.getResultCode();
1496      }
1497
1498      if (timeRange == null)
1499      {
1500        // We'll assume that this means the time range was specified using
1501        // rotated log filenames, which we can't handle in the LDAP SDK code so
1502        // we'll just pass the argument value through to the server code.
1503        argList.add("--timeRange");
1504        argList.add(logTimeRangeArg.getValue());
1505      }
1506      else
1507      {
1508        final Date startTime = timeRange.getFirst();
1509        Date endTime = timeRange.getSecond();
1510        if (endTime == null)
1511        {
1512          endTime = new Date(Math.max(System.currentTimeMillis(),
1513               startTime.getTime()));
1514        }
1515
1516        final SimpleDateFormat timestampFormatter =
1517             new SimpleDateFormat(SERVER_LOG_TIMESTAMP_FORMAT_WITH_MILLIS);
1518        argList.add("--timeRange");
1519        argList.add(timestampFormatter.format(startTime) + ',' +
1520             timestampFormatter.format(endTime));
1521      }
1522    }
1523
1524    if (logDurationArg.isPresent())
1525    {
1526      argList.add("--duration");
1527      argList.add(DurationArgument.nanosToDuration(
1528           logDurationArg.getValue(TimeUnit.NANOSECONDS)));
1529    }
1530
1531    if (logFileHeadCollectionSizeKBArg.isPresent())
1532    {
1533      argList.add("--logFileHeadCollectionSizeKB");
1534      argList.add(String.valueOf(logFileHeadCollectionSizeKBArg.getValue()));
1535    }
1536
1537    if (logFileTailCollectionSizeKBArg.isPresent())
1538    {
1539      argList.add("--logFileTailCollectionSizeKB");
1540      argList.add(String.valueOf(logFileTailCollectionSizeKBArg.getValue()));
1541    }
1542
1543    if (archiveExtensionSourceArg.isPresent())
1544    {
1545      argList.add("--archiveExtensionSource");
1546    }
1547
1548    if (noPromptArg.isPresent())
1549    {
1550      argList.add("--no-prompt");
1551    }
1552
1553    if (scriptFriendlyArg.isPresent())
1554    {
1555      argList.add("--script-friendly");
1556    }
1557
1558
1559    // We also need to include values for arguments provided by the LDAP
1560    // command-line tool framework.
1561    for (final String argumentIdentifier :
1562         Arrays.asList("hostname", "port", "bindDN", "bindPassword",
1563              "bindPasswordFile", "useSSL", "useStartTLS", "trustAll",
1564              "keyStorePath", "keyStorePassword", "keyStorePasswordFile",
1565              "keyStoreFormat", "trustStorePath", "trustStorePassword",
1566              "trustStorePasswordFile", "trustStoreFormat", "certNickname",
1567              "saslOption", "propertiesFilePath", "noPropertiesFile"))
1568    {
1569      final Argument arg = parser.getNamedArgument(argumentIdentifier);
1570      if (arg.getNumOccurrences() > 0)
1571      {
1572        for (final String value : arg.getValueStringRepresentations(false))
1573        {
1574          argList.add("--" + argumentIdentifier);
1575          if (arg.takesValue())
1576          {
1577            argList.add(value);
1578          }
1579        }
1580      }
1581    }
1582
1583
1584    // If the --dryRun argument was provided, then return without actually
1585    // invoking the tool.
1586    if (dryRunArg.isPresent())
1587    {
1588      final String message = INFO_CSD_LOCAL_MODE_DRY_RUN.get();
1589      wrapOut(0, WRAP_COLUMN, message);
1590      toolCompletionMessage.set(message);
1591      return ResultCode.NO_OPERATION;
1592    }
1593
1594
1595    // Make sure that we have access to the method in the server codebase that
1596    // we need to invoke local collect-support-data processing.
1597    final Method doMainMethod;
1598    try
1599    {
1600      final Class<?> csdToolClass = Class.forName(SERVER_CSD_TOOL_CLASS);
1601      doMainMethod = csdToolClass.getMethod("doMain", Boolean.TYPE,
1602           OutputStream.class, OutputStream.class, String[].class);
1603    }
1604    catch (final Throwable t)
1605    {
1606      Debug.debugException(t);
1607      final String message = ERR_CSD_SERVER_CODE_NOT_AVAILABLE.get();
1608      wrapErr(0, WRAP_COLUMN, message);
1609      toolCompletionMessage.set(message);
1610      return ResultCode.NOT_SUPPORTED;
1611    }
1612
1613
1614    // Invoke the doMain method via reflection
1615    final String[] argArray = new String[argList.size()];
1616    argList.toArray(argArray);
1617
1618    try
1619    {
1620      final Object doMainMethodReturnValue = doMainMethod.invoke(null,
1621           true, getOut(), getErr(), argArray);
1622      final int exitCode = ((Integer) doMainMethodReturnValue).intValue();
1623      return ResultCode.valueOf(exitCode);
1624    }
1625    catch (final Throwable t)
1626    {
1627      Debug.debugException(t);
1628      final String message =
1629           ERR_CSD_INVOKE_ERROR.get(StaticUtils.getExceptionMessage(t));
1630      wrapErr(0, WRAP_COLUMN, message);
1631      toolCompletionMessage.set(message);
1632      return ResultCode.LOCAL_ERROR;
1633    }
1634  }
1635
1636
1637
1638  /**
1639   * {@inheritDoc}
1640   */
1641  @Override()
1642  @NotNull()
1643  public LinkedHashMap<String[],String> getExampleUsages()
1644  {
1645    final LinkedHashMap<String[],String> examples =
1646         new LinkedHashMap<>(StaticUtils.computeMapCapacity(3));
1647
1648    examples.put(
1649         new String[]
1650         {
1651           "--bindDN", "uid=admin,dc=example,dc=com",
1652           "--bindPasswordFile", "admin-pw.txt"
1653         },
1654         INFO_CSD_EXAMPLE_1.get());
1655
1656    examples.put(
1657         new String[]
1658         {
1659           "--useRemoteServer",
1660           "--hostname", "ds.example.com",
1661           "--port", "636",
1662           "--useSSL",
1663           "--trustStorePath", "config/truststore",
1664           "--bindDN", "uid=admin,dc=example,dc=com",
1665           "--bindPasswordFile", "admin-pw.txt",
1666           "--collectExpensiveData",
1667           "--collectReplicationStateDump",
1668           "--securityLevel", "maximum",
1669           "--logDuration", "10 minutes",
1670           "--encrypt",
1671           "--passphraseFile", "encryption-passphrase.txt",
1672           "--generatePassphrase",
1673           "--outputPath", "csd.zip"
1674         },
1675         INFO_CSD_EXAMPLE_2.get());
1676
1677    examples.put(
1678         new String[]
1679         {
1680           "--decrypt", "support-data-ds-inst1-" +
1681                StaticUtils.encodeGeneralizedTime(new Date()) +
1682              "-zip-encrypted",
1683           "--passphraseFile", "encryption-passphrase.txt"
1684         },
1685         INFO_CSD_EXAMPLE_3.get());
1686
1687    return examples;
1688  }
1689}