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