All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.unboundid.util.CommandLineToolInteractiveModeProcessor Maven / Gradle / Ivy

/*
 * Copyright 2015-2021 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright 2015-2021 Ping Identity Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * Copyright (C) 2015-2021 Ping Identity Corporation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPLv2 only)
 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see .
 */
package com.unboundid.util;



import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;
import javax.net.ssl.KeyManager;
import javax.net.ssl.TrustManager;

import com.unboundid.ldap.sdk.BindRequest;
import com.unboundid.ldap.sdk.Control;
import com.unboundid.ldap.sdk.CRAMMD5BindRequest;
import com.unboundid.ldap.sdk.DIGESTMD5BindRequest;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.EXTERNALBindRequest;
import com.unboundid.ldap.sdk.ExtendedResult;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.InternalSDKHelper;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.PLAINBindRequest;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldap.sdk.SimpleBindRequest;
import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
import com.unboundid.ldap.sdk.unboundidds.LDAPConnectionHandlerConfiguration;
import com.unboundid.util.args.Argument;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentHelper;
import com.unboundid.util.args.ArgumentListArgument;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.BooleanArgument;
import com.unboundid.util.args.BooleanValueArgument;
import com.unboundid.util.args.ControlArgument;
import com.unboundid.util.args.DNArgument;
import com.unboundid.util.args.DurationArgument;
import com.unboundid.util.args.FileArgument;
import com.unboundid.util.args.FilterArgument;
import com.unboundid.util.args.IntegerArgument;
import com.unboundid.util.args.ScopeArgument;
import com.unboundid.util.args.StringArgument;
import com.unboundid.util.args.SubCommand;
import com.unboundid.util.args.TimestampArgument;
import com.unboundid.util.ssl.KeyStoreKeyManager;
import com.unboundid.util.ssl.SSLUtil;
import com.unboundid.util.ssl.TrustAllTrustManager;
import com.unboundid.util.ssl.TrustStoreTrustManager;

import static com.unboundid.util.UtilityMessages.*;



/**
 * This class performs the appropriate processing to obtain values to use for
 * command-line arguments when a tool is invoked in interactive mode.
 */
@InternalUseOnly()
@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
final class CommandLineToolInteractiveModeProcessor
{
  /**
   * Indicates whether this processor is being run in a unit test environment.
   * If so, then we will read passwords in a way that is compatible with the
   * unit test framework.
   */
  private static volatile boolean IN_UNIT_TEST = false;



  // The argument parser for the command-line tool.
  @NotNull private final ArgumentParser parser;

  // The reader that may be used to read from standard input.
  @NotNull private final BufferedReader systemInReader;

  // The associated command-line tool.
  @NotNull private final CommandLineTool tool;

  // The maximum column length to use when wrapping long lines.
  private final int wrapColumn;



  /**
   * Creates a new instance of this command-line tool interactive mode
   * processor with the provided information.
   *
   * @param  tool    The command-line tool for which to perform interactive mode
   *                 processing.
   * @param  parser  The argument parser for the provided command-line tool.
   */
  CommandLineToolInteractiveModeProcessor(@NotNull final CommandLineTool tool,
                                          @NotNull final ArgumentParser parser)
  {
    this.tool   = tool;
    this.parser = parser;

    systemInReader = new BufferedReader(new InputStreamReader(System.in));
    wrapColumn = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;

    ArgumentHelper.reset(parser.getNamedArgument("interactive"));
  }



  /**
   * Performs the appropriate interactive mode processing for the tool.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  void doInteractiveModeProcessing()
       throws LDAPException
  {
    tool.wrapStandardOut(0, 0, wrapColumn, true,
         INFO_INTERACTIVE_LAUNCHING.get(tool.getToolName()));

    final List allArgs = new ArrayList<>(10);

    final List subCommands = parser.getSubCommands();
    if (! subCommands.isEmpty())
    {
      final SubCommand subcommand = promptForSubCommand();
      ArgumentHelper.setSelectedSubCommand(parser, subcommand);
      allArgs.add(subcommand.getPrimaryName());
    }

    final List ldapArgs = new ArrayList<>(10);
    if (tool instanceof LDAPCommandLineTool)
    {
      promptForLDAPArguments(ldapArgs, true);
    }
    else if (tool instanceof MultiServerLDAPCommandLineTool)
    {
      promptForMultiServerLDAPArguments(ldapArgs, true);
    }

    allArgs.addAll(ldapArgs);

    final List toolArgs = displayInteractiveMenu(ldapArgs);
    allArgs.addAll(toolArgs);

    tool.out();
    if (allArgs.isEmpty())
    {
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_RUNNING_WITH_NO_ARGS.get(tool.getToolName()));
    }
    else
    {
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_RUNNING_WITH_ARGS.get());
      printArgs(allArgs);
    }
    tool.out();
  }



  /**
   * Prints the provided arguments in a user-friendly way to standard output.
   *
   * @param  args  The arguments to be printed.
   */
  private void printArgs(@NotNull final List args)
  {
    final StringBuilder buffer = new StringBuilder();
    buffer.append("     ");
    buffer.append(tool.getToolName());
    if (! args.isEmpty())
    {
      buffer.append(' ');
      buffer.append(StaticUtils.getCommandLineContinuationString());
    }
    tool.out(buffer);

    for (int i=0; i < args.size(); i++)
    {
      buffer.setLength(0);
      buffer.append("          ");

      final String arg = args.get(i);
      buffer.append(ExampleCommandLineArgument.getCleanArgument(
           arg).getLocalForm());

      if (arg.startsWith("-") && ((i+1) < args.size()))
      {
        final String nextArg = args.get(i+1);
        if (! nextArg.startsWith("-"))
        {
          buffer.append(' ');
          buffer.append(ExampleCommandLineArgument.getCleanArgument(
               nextArg).getLocalForm());
          i++;
        }
      }

      if (i < (args.size() - 1))
      {
        buffer.append(' ');
        buffer.append(StaticUtils.getCommandLineContinuationString());
      }

      tool.out(buffer);
    }
  }



  /**
   * Interactively prompts for the subcommand that should be used when running
   * the tool.
   *
   * @return  The selected subcommand.
   *
   * @throws  LDAPException  If a problem is encountered while determining which
   *                         subcommand to use.
   */
  @NotNull()
  private SubCommand promptForSubCommand()
          throws LDAPException
  {
    // Get all of the subcommands sorted by name.
    final List subCommands = parser.getSubCommands();
    final TreeMap subCommandsByName = new TreeMap<>();
    for (final SubCommand sc : subCommands)
    {
      subCommandsByName.put(sc.getPrimaryName(), sc);
    }


    // Create an array of the subcommand names we want to use.
    int index = 0;
    final String[] subCommandNames = new String[subCommandsByName.size()];
    for (final SubCommand sc : subCommandsByName.values())
    {
      subCommandNames[index++] = sc.getPrimaryName();
    }


    // Prompt the user to determine which subcommand to use, and associate that
    // with the target subcommand.
    final int selectedSubCommandNumber = getNumberedMenuChoice(
         INFO_INTERACTIVE_SUBCOMMAND_PROMPT.get(), false, null,
         subCommandNames);
    return parser.getSubCommand(subCommandNames[selectedSubCommandNumber]);
  }



  /**
   * Interactively prompts for the arguments used to connect and optionally
   * authenticate to the directory server.
   *
   * @param  argList  The list to which the string representations of all LDAP
   *                  arguments should be added.
   * @param  test     Indicates whether to attempt to use the arguments to
   *                  establish an LDAP connection.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForLDAPArguments(@NotNull final List argList,
                                      final boolean test)
          throws LDAPException
  {
    final LDAPCommandLineTool ldapTool = (LDAPCommandLineTool) tool;
    argList.clear();


    // Get the address of the directory server.
    final String defaultHostname;
    final StringArgument hostnameArgument =
         parser.getStringArgument("hostname");
    if (hostnameArgument.isPresent())
    {
      defaultHostname = hostnameArgument.getValue();
    }
    else
    {
      defaultHostname = "localhost";
    }

    ArgumentHelper.reset(hostnameArgument);

    final String hostname = promptForString(
         INFO_INTERACTIVE_LDAP_PROMPT_HOST.get(), defaultHostname, true);
    ArgumentHelper.addValueSuppressException(hostnameArgument, hostname);

    argList.add("--hostname");
    argList.add(hostname);


    // If this tool is running with access to Directory Server data, and if the
    // selected hostname is "localhost" or a loopback address, then try to load
    // information about the server's connection handlers.
    List serverListenerConfigs = null;
    final File dsInstanceRoot = InternalSDKHelper.getPingIdentityServerRoot();
    if (dsInstanceRoot != null)
    {
      final File configFile = StaticUtils.constructPath(dsInstanceRoot,
           "config", "config.ldif");
      if (configFile.exists() && configFile.isFile())
      {
        try
        {
          serverListenerConfigs = LDAPConnectionHandlerConfiguration.
               readConfiguration(configFile, true);
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
        }
      }
    }


    // Determine the type of connection security to use.
    final BooleanArgument useSSLArgument = parser.getBooleanArgument("useSSL");
    final BooleanArgument useStartTLSArgument =
         parser.getBooleanArgument("useStartTLS");

    final String defaultSecurityChoice;
    if (useSSLArgument.isPresent())
    {
      defaultSecurityChoice = "1";
    }
    else if (useStartTLSArgument.isPresent())
    {
      defaultSecurityChoice = "3";
    }
    else if ((serverListenerConfigs != null) &&
         (! serverListenerConfigs.isEmpty()))
    {
      final LDAPConnectionHandlerConfiguration cfg =
           serverListenerConfigs.get(0);
      if (cfg.usesSSL())
      {
        defaultSecurityChoice = "1";
      }
      else if (cfg.supportsStartTLS())
      {
        defaultSecurityChoice = "3";
      }
      else
      {
        defaultSecurityChoice = "5";
      }
    }
    else
    {
      defaultSecurityChoice = "1";
    }

    ArgumentHelper.reset(useSSLArgument);
    ArgumentHelper.reset(useStartTLSArgument);

    final boolean useSSL;
    final boolean useStartTLS;
    final boolean defaultTrust;
    final int securityType = getNumberedMenuChoice(
         INFO_INTERACTIVE_LDAP_SECURITY_PROMPT.get(),
         false,
         defaultSecurityChoice,
         INFO_INTERACTIVE_LDAP_SECURITY_OPTION_SSL_DEFAULT.get(),
         INFO_INTERACTIVE_LDAP_SECURITY_OPTION_SSL_MANUAL.get(),
         INFO_INTERACTIVE_LDAP_SECURITY_OPTION_START_TLS_DEFAULT.get(),
         INFO_INTERACTIVE_LDAP_SECURITY_OPTION_START_TLS_MANUAL.get(),
         INFO_INTERACTIVE_LDAP_SECURITY_OPTION_NONE.get());
    switch (securityType)
    {
      case 0:
        useSSL = true;
        useStartTLS = false;
        defaultTrust = true;
        argList.add("--useSSL");
        ArgumentHelper.incrementOccurrencesSuppressException(useSSLArgument);
        break;
      case 1:
        useSSL = true;
        useStartTLS = false;
        defaultTrust = false;
        argList.add("--useSSL");
        ArgumentHelper.incrementOccurrencesSuppressException(useSSLArgument);
        break;
      case 2:
        useSSL = false;
        useStartTLS = true;
        defaultTrust = true;
        argList.add("--useStartTLS");
        ArgumentHelper.incrementOccurrencesSuppressException(
             useStartTLSArgument);
        break;
      case 3:
        useSSL = false;
        useStartTLS = true;
        defaultTrust = false;
        argList.add("--useStartTLS");
        ArgumentHelper.incrementOccurrencesSuppressException(
             useStartTLSArgument);
        break;
      case 4:
      default:
        useSSL = false;
        useStartTLS = false;
        defaultTrust = false;
        break;
    }


    // If we are to use security without default trust configuration, then
    // prompt for the appropriate settings.
    BindRequest bindRequest = null;
    boolean trustAll = false;
    byte[] keyStorePIN = null;
    byte[] trustStorePIN = null;
    File keyStorePath = null;
    File trustStorePath = null;
    String certificateNickname = null;
    String keyStoreFormat = null;
    String trustStoreFormat = null;
    final StringArgument keyStorePasswordArgument =
         parser.getStringArgument("keyStorePassword");
    final StringArgument trustStorePasswordArgument =
         parser.getStringArgument("trustStorePassword");
    final StringArgument saslOptionArgument =
         parser.getStringArgument("saslOption");
    if (! defaultTrust)
    {
      // If the user wants to connect securely, then get the appropriate set of
      // arguments pertaining to key and trust managers.
      ArgumentHelper.reset(keyStorePasswordArgument);
      ArgumentHelper.reset(trustStorePasswordArgument);
      ArgumentHelper.reset(parser.getNamedArgument("keyStorePasswordFile"));
      ArgumentHelper.reset(parser.getNamedArgument(
           "promptForKeyStorePassword"));
      ArgumentHelper.reset(parser.getNamedArgument("trustStorePasswordFile"));
      ArgumentHelper.reset(parser.getNamedArgument(
           "promptForTrustStorePassword"));

      if (useSSL || useStartTLS)
      {
        final StringArgument keyStorePathArgument =
             parser.getStringArgument("keyStorePath");
        final StringArgument keyStoreFormatArgument =
             parser.getStringArgument("keyStoreFormat");

        // Determine if a client certificate should be presented.
        final String defaultStoreTypeChoice;
        if (keyStoreFormatArgument.isPresent())
        {
          final String format = keyStoreFormatArgument.getValue();
          if (format.equalsIgnoreCase(CryptoHelper.KEY_STORE_TYPE_PKCS_12))
          {
            defaultStoreTypeChoice = "3";
          }
          else
          {
            defaultStoreTypeChoice = "2";
          }
        }
        else if (keyStorePathArgument.isPresent())
        {
          defaultStoreTypeChoice = "2";
        }
        else
        {
          defaultStoreTypeChoice = "1";
        }

        final String defaultKeyStorePath;
        if (keyStorePathArgument.isPresent())
        {
          defaultKeyStorePath = keyStorePathArgument.getValue();
        }
        else
        {
          defaultKeyStorePath = null;
        }

        final String defaultCertNickname;
        final StringArgument certNicknameArgument =
             parser.getStringArgument("certNickname");
        if (certNicknameArgument.isPresent())
        {
          defaultCertNickname = certNicknameArgument.getValue();
        }
        else
        {
          defaultCertNickname = null;
        }

        ArgumentHelper.reset(keyStorePathArgument);
        ArgumentHelper.reset(keyStoreFormatArgument);
        ArgumentHelper.reset(certNicknameArgument);

        final int keystoreType = getNumberedMenuChoice(
             INFO_INTERACTIVE_LDAP_CLIENT_CERT_PROMPT.get(),
             false,
             defaultStoreTypeChoice,
             INFO_INTERACTIVE_LDAP_CLIENT_CERT_OPTION_NO.get(),
             INFO_INTERACTIVE_LDAP_CLIENT_CERT_OPTION_JKS.get(),
             INFO_INTERACTIVE_LDAP_CLIENT_CERT_OPTION_PKCS12.get());
        ArgumentHelper.reset(keyStoreFormatArgument);

        switch (keystoreType)
        {
          case 1:
            keyStoreFormat = CryptoHelper.KEY_STORE_TYPE_JKS;
            break;
          case 2:
            keyStoreFormat = CryptoHelper.KEY_STORE_TYPE_PKCS_12;
            break;
          case 0:
          default:
            break;
        }

        if (keyStoreFormat != null)
        {
          ArgumentHelper.addValueSuppressException(keyStoreFormatArgument,
               keyStoreFormat);

          // Get the path to the keystore file.
          keyStorePath = promptForPath(
               INFO_INTERACTIVE_LDAP_KEYSTORE_PATH_PROMPT.get(),
               defaultKeyStorePath, true, true, true, true, false);
          argList.add("--keyStorePath");
          argList.add(keyStorePath.getAbsolutePath());
          ArgumentHelper.addValueSuppressException(keyStorePathArgument,
               keyStorePath.getAbsolutePath());

          // Get the PIN needed to access the keystore.
          keyStorePIN = promptForPassword(
               INFO_INTERACTIVE_LDAP_KEYSTORE_PIN_PROMPT.get(), null, false);
          if (keyStorePIN != null)
          {
            argList.add("--keyStorePassword");
            argList.add("***REDACTED***");
            ArgumentHelper.addValueSuppressException(keyStorePasswordArgument,
                 StaticUtils.toUTF8String(keyStorePIN));
          }

          argList.add("--keyStoreFormat");
          argList.add(keyStoreFormat);

          certificateNickname = promptForString(
               INFO_INTERACTIVE_LDAP_CERT_NICKNAME_PROMPT.get(),
               defaultCertNickname, false);
          if (certificateNickname != null)
          {
            argList.add("--certNickname");
            argList.add(certificateNickname);
            ArgumentHelper.addValueSuppressException(certNicknameArgument,
                 certificateNickname);
          }

          if (ldapTool.supportsAuthentication() && promptForYesNo(
               INFO_INTERACTIVE_LDAP_CERT_AUTH_PROMPT.get(), false, true))
          {
            bindRequest = new EXTERNALBindRequest();
            argList.add("--saslOption");
            argList.add("mech=EXTERNAL");

            ArgumentHelper.reset(saslOptionArgument);
            ArgumentHelper.addValueSuppressException(saslOptionArgument,
                 "mech=EXTERNAL");
          }
        }

        // Determine how to trust the server certificate.
        final BooleanArgument trustAllArgument =
             parser.getBooleanArgument("trustAll");
        final StringArgument trustStorePathArgument =
             parser.getStringArgument("trustStorePath");
        final StringArgument trustStoreFormatArgument =
             parser.getStringArgument("trustStoreFormat");

        final String defaultTrustTypeChoice;
        if (trustAllArgument.isPresent())
        {
          defaultTrustTypeChoice = "4";
        }
        else if (trustStoreFormatArgument.isPresent())
        {
          final String format = trustStoreFormatArgument.getValue();
          if (format.equalsIgnoreCase(CryptoHelper.KEY_STORE_TYPE_PKCS_12))
          {
            defaultTrustTypeChoice = "3";
          }
          else
          {
            defaultTrustTypeChoice = "2";
          }
        }
        else if (trustStorePathArgument.isPresent())
        {
          defaultTrustTypeChoice = "2";
        }
        else
        {
          defaultTrustTypeChoice = "1";
        }

        final String defaultTrustStorePath;
        if (trustStorePathArgument.isPresent())
        {
          defaultTrustStorePath = trustStorePathArgument.getValue();
        }
        else
        {
          defaultTrustStorePath = null;
        }
        ArgumentHelper.reset(trustAllArgument);
        ArgumentHelper.reset(trustStorePathArgument);
        ArgumentHelper.reset(trustStoreFormatArgument);

        final int trustType = getNumberedMenuChoice(
             INFO_INTERACTIVE_LDAP_TRUST_PROMPT.get(),
             false,
             defaultTrustTypeChoice,
             INFO_INTERACTIVE_LDAP_TRUST_OPTION_PROMPT.get(),
             INFO_INTERACTIVE_LDAP_TRUST_OPTION_JKS.get(),
             INFO_INTERACTIVE_LDAP_TRUST_OPTION_PKCS12.get(),
             INFO_INTERACTIVE_LDAP_TRUST_OPTION_BLIND.get());
        switch (trustType)
        {
          case 1:
            trustStoreFormat = CryptoHelper.KEY_STORE_TYPE_JKS;
            break;
          case 2:
            trustStoreFormat = CryptoHelper.KEY_STORE_TYPE_PKCS_12;
            break;
          case 3:
            trustAll = true;
            argList.add("--trustAll");
            ArgumentHelper.incrementOccurrencesSuppressException(
                 trustAllArgument);
            break;
          case 0:
          default:
            // We will interactively prompt the user about whether to trust the
            // certificate in the test section below.  However, to avoid
            // prompting the user twice, we will configure the tool behind the
            // scenes to trust all certificates.
            ArgumentHelper.incrementOccurrencesSuppressException(
                 trustAllArgument);
            break;
        }

        if (trustStoreFormat != null)
        {
          ArgumentHelper.addValueSuppressException(trustStoreFormatArgument,
               trustStoreFormat);

          // Get the path to the truststore file.
          trustStorePath = promptForPath(
               INFO_INTERACTIVE_LDAP_TRUSTSTORE_PATH_PROMPT.get(),
               defaultTrustStorePath, true, true, true, true, false);
          argList.add("--trustStorePath");
          argList.add(trustStorePath.getAbsolutePath());
          ArgumentHelper.addValueSuppressException(trustStorePathArgument,
               trustStorePath.getAbsolutePath());

          // Get the PIN needed to access the truststore.
          trustStorePIN = promptForPassword(
               INFO_INTERACTIVE_LDAP_TRUSTSTORE_PIN_PROMPT.get(), null, false);
          if (trustStorePIN != null)
          {
            argList.add("--trustStorePassword");
            argList.add("***REDACTED***");
            ArgumentHelper.addValueSuppressException(trustStorePasswordArgument,
                 StaticUtils.toUTF8String(trustStorePIN));
          }

          argList.add("--trustStoreFormat");
          argList.add(trustStoreFormat);
        }
      }
      else
      {
        ArgumentHelper.reset(parser.getNamedArgument("keyStorePath"));
        ArgumentHelper.reset(parser.getNamedArgument("keyStoreFormat"));
        ArgumentHelper.reset(parser.getNamedArgument("trustStorePath"));
        ArgumentHelper.reset(parser.getNamedArgument("trustStoreFormat"));
        ArgumentHelper.reset(parser.getNamedArgument("certNickname"));
      }
    }


    // Get the port of the directory server.
    int defaultPort;
    final IntegerArgument portArgument =
         parser.getIntegerArgument("port");
    if (portArgument.getNumOccurrences() > 0)
    {
      // Note -- We're using getNumOccurrences here because isPresent also
      // returns true if there is a default value, and that could be wrong in
      // this case because the default value doesn't know about SSL.
      defaultPort = portArgument.getValue();
    }
    else if (useSSL)
    {
      defaultPort = 636;
      if (serverListenerConfigs != null)
      {
        for (final LDAPConnectionHandlerConfiguration cfg :
             serverListenerConfigs)
        {
          if (cfg.usesSSL())
          {
            defaultPort = cfg.getPort();
            break;
          }
        }
      }
    }
    else if (useStartTLS)
    {
      defaultPort = 389;
      if (serverListenerConfigs != null)
      {
        for (final LDAPConnectionHandlerConfiguration cfg :
             serverListenerConfigs)
        {
          if (cfg.supportsStartTLS())
          {
            defaultPort = cfg.getPort();
            break;
          }
        }
      }
    }
    else
    {
      defaultPort = 389;
      if (serverListenerConfigs != null)
      {
        for (final LDAPConnectionHandlerConfiguration cfg :
             serverListenerConfigs)
        {
          if (! cfg.usesSSL())
          {
            defaultPort = cfg.getPort();
            break;
          }
        }
      }
    }
    ArgumentHelper.reset(portArgument);

    final int port = promptForInteger(INFO_INTERACTIVE_LDAP_PROMPT_PORT.get(),
         defaultPort, 1, 65_535, true);
    argList.add("--port");
    argList.add(String.valueOf(port));
    ArgumentHelper.addValueSuppressException(portArgument,
         String.valueOf(port));


    // Determine how to authenticate to the directory server.
    if (ldapTool.supportsAuthentication())
    {
      final DNArgument bindDNArgument =
           parser.getDNArgument("bindDN");
      final StringArgument bindPasswordArgument =
           parser.getStringArgument("bindPassword");

      ArgumentHelper.reset(bindPasswordArgument);
      ArgumentHelper.reset(parser.getNamedArgument("bindPasswordFile"));
      ArgumentHelper.reset(parser.getNamedArgument("promptForBindPassword"));

      if (bindRequest == null)
      {
        final String defaultAuthTypeChoice;
        final String defaultBindDN;
        if (saslOptionArgument.isPresent())
        {
          defaultAuthTypeChoice = "2";
          defaultBindDN = null;
        }
        else
        {
          defaultAuthTypeChoice = "1";
          if (bindDNArgument.isPresent())
          {
            defaultBindDN = bindDNArgument.getStringValue();
          }
          else
          {
            defaultBindDN = null;
          }
        }

        ArgumentHelper.reset(bindDNArgument);

        boolean useSimpleAuth = false;
        boolean useSASLAuth = false;
        final int authMethod = getNumberedMenuChoice(
             INFO_INTERACTIVE_LDAP_AUTH_PROMPT.get(),
             false,
             defaultAuthTypeChoice,
             INFO_INTERACTIVE_LDAP_AUTH_OPTION_SIMPLE.get(),
             INFO_INTERACTIVE_LDAP_AUTH_OPTION_SASL.get(),
             INFO_INTERACTIVE_LDAP_AUTH_OPTION_NONE.get());
        switch (authMethod)
        {
          case 0:
            useSimpleAuth = true;
            break;
          case 1:
            useSASLAuth = true;
            break;
          case 2:
          default:
            break;
        }

        if (useSimpleAuth)
        {
          ArgumentHelper.reset(saslOptionArgument);

          final DN bindDN = promptForDN(
               INFO_INTERACTIVE_LDAP_AUTH_BIND_DN_PROMPT.get(), defaultBindDN,
               true);
          if (bindDN.isNullDN())
          {
            bindRequest = new SimpleBindRequest();
            argList.add("--bindDN");
            argList.add("");
            argList.add("--bindPassword");
            argList.add("");

            ArgumentHelper.addValueSuppressException(bindDNArgument, "");
            ArgumentHelper.addValueSuppressException(bindPasswordArgument, "");
          }
          else
          {
            final byte[] bindPassword = promptForPassword(
                 INFO_INTERACTIVE_LDAP_AUTH_PW_PROMPT.get(), null, true);
            bindRequest = new SimpleBindRequest(bindDN, bindPassword);
            argList.add("--bindDN");
            argList.add(bindDN.toString());
            argList.add("--bindPassword");
            argList.add("***REDACTED***");

            ArgumentHelper.addValueSuppressException(bindDNArgument,
                 bindDN.toString());
            ArgumentHelper.addValueSuppressException(bindPasswordArgument,
                 StaticUtils.toUTF8String(bindPassword));
          }
        }
        else if (useSASLAuth)
        {
          String defaultMechChoice = null;
          String defaultAuthID = null;
          String defaultAuthzID = null;
          String defaultRealm = null;
          if (saslOptionArgument.isPresent())
          {
            for (final String saslOption : saslOptionArgument.getValues())
            {
              final String lowerOption = StaticUtils.toLowerCase(saslOption);
              if (lowerOption.equals("mech=cram-md5"))
              {
                defaultMechChoice = "1";
              }
              else if (lowerOption.equals("mech=digest-md5"))
              {
                defaultMechChoice = "2";
              }
              else if (lowerOption.equals("mech=plain"))
              {
                defaultMechChoice = "3";
              }
              else if (lowerOption.startsWith("authid="))
              {
                defaultAuthID = saslOption.substring(7);
              }
              else if (lowerOption.startsWith("authzid="))
              {
                defaultAuthzID = saslOption.substring(8);
              }
              else if (lowerOption.startsWith("realm="))
              {
                defaultRealm= saslOption.substring(6);
              }
            }
          }
          ArgumentHelper.reset(saslOptionArgument);

          final int mech = getNumberedMenuChoice(
               INFO_INTERACTIVE_LDAP_AUTH_SASL_PROMPT.get(),
               false,
               defaultMechChoice,
               INFO_INTERACTIVE_LDAP_SASL_OPTION_CRAM_MD5.get(),
               INFO_INTERACTIVE_LDAP_SASL_OPTION_DIGEST_MD5.get(),
               INFO_INTERACTIVE_LDAP_SASL_OPTION_PLAIN.get());
          switch (mech)
          {
            case 0:
              String authID = promptForString(
                   INFO_INTERACTIVE_LDAP_AUTH_AUTHID_PROMPT.get(),
                   defaultAuthID, true);
              byte[] pw = promptForPassword(
                   INFO_INTERACTIVE_LDAP_AUTH_PW_PROMPT.get(), null, true);
              bindRequest = new CRAMMD5BindRequest(authID, pw);

              argList.add("--saslOption");
              argList.add("mech=CRAM-MD5");
              argList.add("--saslOption");
              argList.add("authID=" + authID);
              argList.add("--bindPassword");
              argList.add("***REDACTED***");

              ArgumentHelper.addValueSuppressException(saslOptionArgument,
                   "mech=CRAM-MD5");
              ArgumentHelper.addValueSuppressException(saslOptionArgument,
                   "authID=" + authID);
              ArgumentHelper.addValueSuppressException(bindPasswordArgument,
                   StaticUtils.toUTF8String(pw));
              break;

            case 1:
              authID = promptForString(
                   INFO_INTERACTIVE_LDAP_AUTH_AUTHID_PROMPT.get(),
                   defaultAuthID, true);
              String authzID = promptForString(
                   INFO_INTERACTIVE_LDAP_AUTH_AUTHZID_PROMPT.get(),
                   defaultAuthzID, false);
              final String realm = promptForString(
                   INFO_INTERACTIVE_LDAP_AUTH_REALM_PROMPT.get(), defaultRealm,
                   false);
              pw = promptForPassword(INFO_INTERACTIVE_LDAP_AUTH_PW_PROMPT.get(),
                   null, true);
              bindRequest = new DIGESTMD5BindRequest(authID, authzID, pw,
                   realm);

              argList.add("--saslOption");
              argList.add("mech=DIGEST-MD5");
              argList.add("--saslOption");
              argList.add("authID=" + authID);
              ArgumentHelper.addValueSuppressException(saslOptionArgument,
                   "mech=DIGEST-MD5");
              ArgumentHelper.addValueSuppressException(saslOptionArgument,
                   "authID=" + authID);

              if (authzID != null)
              {
                argList.add("--saslOption");
                argList.add("authzID=" + authzID);
                ArgumentHelper.addValueSuppressException(saslOptionArgument,
                     "authzID=" + authzID);
              }

              if (realm != null)
              {
                argList.add("--saslOption");
                argList.add("realm=" + realm);
                ArgumentHelper.addValueSuppressException(saslOptionArgument,
                     "realm=" + realm);
              }

              argList.add("--bindPassword");
              argList.add("***REDACTED***");
              ArgumentHelper.addValueSuppressException(bindPasswordArgument,
                   StaticUtils.toUTF8String(pw));
              break;

            case 2:
              authID = promptForString(
                   INFO_INTERACTIVE_LDAP_AUTH_AUTHID_PROMPT.get(),
                   defaultAuthID, true);
              authzID = promptForString(
                   INFO_INTERACTIVE_LDAP_AUTH_AUTHZID_PROMPT.get(),
                   defaultAuthzID, false);
              pw = promptForPassword(INFO_INTERACTIVE_LDAP_AUTH_PW_PROMPT.get(),
                 null, true);
              bindRequest = new PLAINBindRequest(authID, authzID, pw);

              argList.add("--saslOption");
              argList.add("mech=PLAIN");
              argList.add("--saslOption");
              argList.add("authID=" + authID);
              ArgumentHelper.addValueSuppressException(saslOptionArgument,
                   "mech=PLAIN");
              ArgumentHelper.addValueSuppressException(saslOptionArgument,
                   "authID=" + authID);

              if (authzID != null)
              {
                argList.add("--saslOption");
                argList.add("authzID=" + authzID);
                ArgumentHelper.addValueSuppressException(saslOptionArgument,
                     "authzID=" + authzID);
              }

              argList.add("--bindPassword");
              argList.add("***REDACTED***");
              ArgumentHelper.addValueSuppressException(bindPasswordArgument,
                   StaticUtils.toUTF8String(pw));
              break;
          }
        }
      }
      else
      {
        ArgumentHelper.reset(bindDNArgument);
      }
    }

    if (test)
    {
      // Perform the necessary initialization for SSL/TLS communication.
      final SSLUtil sslUtil;
      if (useSSL || useStartTLS)
      {
        final KeyManager keyManager;
        if (keyStorePath == null)
        {
          keyManager = null;
        }
        else
        {
          final char[] pinChars;
          if (keyStorePIN == null)
          {
            pinChars = null;
          }
          else
          {
            final String pinString = StaticUtils.toUTF8String(keyStorePIN);
            pinChars = pinString.toCharArray();
          }

          try
          {
            keyManager = new KeyStoreKeyManager(keyStorePath, pinChars,
                 keyStoreFormat, certificateNickname, true);
          }
          catch (final Exception e)
          {
            Debug.debugException(e);
            tool.wrapErr(0, wrapColumn,
                 ERR_INTERACTIVE_LDAP_CANNOT_CREATE_KEY_MANAGER.get(
                      StaticUtils.getExceptionMessage(e)));
            if (promptForYesNo(
                 INFO_INTERACTIVE_LDAP_RETRY_PROMPT.get(), true, true))
            {
              promptForLDAPArguments(argList, test);
              return;
            }
            else
            {
              throw new LDAPException(ResultCode.LOCAL_ERROR, "", e);
            }
          }
        }

        final TrustManager trustManager;
        if (trustAll)
        {
          trustManager = new TrustAllTrustManager();
        }
        else if (trustStorePath == null)
        {
          trustManager = InternalSDKHelper.getPreferredPromptTrustManagerChain(
               null);
        }
        else
        {
          final char[] pinChars;
          if (trustStorePIN == null)
          {
            pinChars = null;
          }
          else
          {
            final String pinString = StaticUtils.toUTF8String(trustStorePIN);
            pinChars = pinString.toCharArray();
          }

          try
          {
            trustManager = new TrustStoreTrustManager(trustStorePath, pinChars,
                 trustStoreFormat, true);
          }
          catch (final Exception e)
          {
            Debug.debugException(e);
            tool.wrapErr(0, wrapColumn,
                 ERR_INTERACTIVE_LDAP_CANNOT_CREATE_TRUST_MANAGER.get(
                      StaticUtils.getExceptionMessage(e)));
            if (promptForYesNo(
                 INFO_INTERACTIVE_LDAP_RETRY_PROMPT.get(), true, true))
            {
              promptForLDAPArguments(argList, test);
              return;
            }
            else
            {
              throw new LDAPException(ResultCode.LOCAL_ERROR, "", e);
            }
          }
        }

        sslUtil = new SSLUtil(keyManager, trustManager);
      }
      else
      {
        sslUtil = null;
      }


      // Create and establish the connection.
      final LDAPConnection conn;
      if (useSSL)
      {
        try
        {
          conn = new LDAPConnection(sslUtil.createSSLSocketFactory());
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_LDAP_CANNOT_CREATE_SOCKET_FACTORY.get(
                    StaticUtils.getExceptionMessage(e)),
               e);
          if (promptForYesNo(
               INFO_INTERACTIVE_LDAP_RETRY_PROMPT.get(), true, true))
          {
            promptForLDAPArguments(argList, test);
            return;
          }
          else
          {
            throw new LDAPException(ResultCode.LOCAL_ERROR, "", e);
          }
        }
      }
      else
      {
        conn = new LDAPConnection();
      }

      try
      {
        try
        {
          conn.connect(hostname, port);
        }
        catch (final LDAPException le)
        {
          Debug.debugException(le);
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_LDAP_CANNOT_CONNECT.get(hostname, port,
                    le.getResultString()));
          if (promptForYesNo(
               INFO_INTERACTIVE_LDAP_RETRY_PROMPT.get(), true, true))
          {
            promptForLDAPArguments(argList, test);
            return;
          }
          else
          {
            throw new LDAPException(le.getResultCode(), "", le);
          }
        }


        // If we should use StartTLS to secure the connection, then do so now.
        if (useStartTLS)
        {
          try
          {
            final ExtendedResult startTLSResult = conn.processExtendedOperation(
                 new StartTLSExtendedRequest(sslUtil.createSSLContext()));
            if (startTLSResult.getResultCode() != ResultCode.SUCCESS)
            {
              throw new LDAPException(startTLSResult);
            }
          }
          catch (final Exception e)
          {
            Debug.debugException(e);

            final String msg;
            if (e instanceof LDAPException)
            {
              msg = ((LDAPException) e).getResultString();
            }
            else
            {
              msg = StaticUtils.getExceptionMessage(e);
            }

            tool.wrapErr(0, wrapColumn,
                 ERR_INTERACTIVE_LDAP_CANNOT_PERFORM_STARTTLS.get(msg));
            if (promptForYesNo(
                 INFO_INTERACTIVE_LDAP_RETRY_PROMPT.get(), true, true))
            {
              promptForLDAPArguments(argList, test);
              return;
            }
            else
            {
              throw new LDAPException(ResultCode.LOCAL_ERROR, "", e);
            }
          }
        }


        // Authenticate the connection if appropriate.
        if (bindRequest != null)
        {
          try
          {
            conn.bind(bindRequest);
          }
          catch (final LDAPException le)
          {
            Debug.debugException(le);
            tool.wrapErr(0, wrapColumn,
                 ERR_INTERACTIVE_LDAP_CANNOT_AUTHENTICATE.get(
                      le.getResultString()));
            if (promptForYesNo(
                 INFO_INTERACTIVE_LDAP_RETRY_PROMPT.get(), true, true))
            {
              promptForLDAPArguments(argList, test);
              return;
            }
            else
            {
              throw new LDAPException(le.getResultCode(), "", le);
            }
          }
        }
      }
      finally
      {
        conn.close();
      }
    }
  }



  /**
   * Interactively prompts for the arguments used to connect and optionally
   * authenticate to multiple directory servers.
   *
   * @param  argList  The list to which the string representations of all LDAP
   *                  arguments should be added.
   * @param  test     Indicates whether to attempt to use the arguments to
   *                  establish an LDAP connection.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForMultiServerLDAPArguments(
                    @NotNull final List argList, final boolean test)
          throws LDAPException
  {
    // FIXME -- Implement this.
    throw new LDAPException(ResultCode.LOCAL_ERROR,
         ERR_INTERACTIVE_MULTI_SERVER_LDAP_NOT_SUPPORTED.get());
  }



  /**
   * Displays a menu that allows the user to supply values for the command-line
   * arguments.  Note that this will not include arguments automatically added
   * by the {@link LDAPCommandLineTool} API.
   *
   * @param  ldapArgs  A list of the arguments used to connect and authenticate
   *                   to the LDAP server(s) in non-interactive mode.  The
   *                   contents of this list may be altered if the user opts to
   *                   change the LDAP connection settings.
   *
   * @return  The tool-specific arguments configured by the user.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  @NotNull()
  private List displayInteractiveMenu(
                            @NotNull final List ldapArgs)
          throws LDAPException
  {
    final ArrayList args =
         new ArrayList<>(parser.getNamedArguments());

    if (parser.getSelectedSubCommand() != null)
    {
      args.addAll(parser.getSelectedSubCommand().getArgumentParser().
           getNamedArguments());
    }

    final Set usageArguments =
         CommandLineTool.getUsageArgumentIdentifiers(tool);

    final Set ldapArguments;
    if (tool instanceof LDAPCommandLineTool)
    {
      ldapArguments = LDAPCommandLineTool.getLongLDAPArgumentIdentifiers(
           ((LDAPCommandLineTool) tool));
    }
    else
    {
      ldapArguments = Collections.emptySet();
    }

    int maxIdentifierLength = 0;
    final String trailingArgsIdentifier =
         INFO_INTERACTIVE_MENU_TRAILING_ARGS_IDENTIFIER.get();
    if (parser.allowsTrailingArguments())
    {
      maxIdentifierLength = trailingArgsIdentifier.length();
    }

    final Iterator argIterator = args.iterator();
    while (argIterator.hasNext())
    {
      final Argument a = argIterator.next();
      final String longID = a.getLongIdentifier();
      if (usageArguments.contains(longID) || ldapArguments.contains(longID))
      {
        argIterator.remove();
      }
      else
      {
        maxIdentifierLength = Math.max(maxIdentifierLength,
             a.getIdentifierString().length());
      }
    }

    if (args.isEmpty() && (! parser.allowsTrailingArguments()))
    {
      return Collections.emptyList();
    }
    else
    {
      // First, prompt for all required arguments that don't have a default
      // value.
      for (final Argument arg : args)
      {
        if (! arg.isRequired())
        {
          continue;
        }

        final List valueStrings =
             arg.getValueStringRepresentations(true);
        if (! valueStrings.isEmpty())
        {
          continue;
        }

        promptForArgument(arg);
      }


      // If the tool requires trailing arguments, then prompt for them.
      if (parser.requiresTrailingArguments())
      {
        promptForTrailingArguments();
      }


argsLoop:
      while (true)
      {
        final int maxNumberLength = String.valueOf(args.size()).length();
        final int subsequentIndent = maxNumberLength + maxIdentifierLength + 4;

        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_MENU_PROMPT.get());

        int optionNumber = 1;
        for (final Argument arg : args)
        {
          List valueStrings = arg.getValueStringRepresentations(true);
          if (arg.isSensitive())
          {
            final int size = valueStrings.size();
            switch (size)
            {
              case 0:
                // No need to do any thing.
                break;
              case 1:
                valueStrings = Collections.singletonList("***REDACTED***");
                break;
              default:
                valueStrings = new ArrayList<>(size);
                for (int i=0; i <= size; i++)
                {
                  valueStrings.add("***REDACTED" + i + "***");
                }
                break;
            }
          }

          switch (valueStrings.size())
          {
            case 0:
              tool.wrapStandardOut(0, subsequentIndent, wrapColumn, true,
                   rightAlign(String.valueOf(optionNumber), maxNumberLength),
                   ' ',
                   leftAlign(arg.getIdentifierString(), maxIdentifierLength),
                   " -");
              break;
            case 1:
              tool.wrapStandardOut(0, subsequentIndent, wrapColumn, true,
                   rightAlign(String.valueOf(optionNumber), maxNumberLength),
                   ' ',
                   leftAlign(arg.getIdentifierString(), maxIdentifierLength),
                   " - ", valueStrings.get(0));
              break;
            default:
              tool.wrapStandardOut(0, subsequentIndent, wrapColumn, true,
                   rightAlign(String.valueOf(optionNumber), maxNumberLength),
                   ' ',
                   leftAlign(arg.getIdentifierString(), maxIdentifierLength),
                   " - ", valueStrings.get(0));
              for (int i=1; i < valueStrings.size(); i++)
              {
                tool.wrapStandardOut(0, subsequentIndent, wrapColumn, true,
                     rightAlign("", maxNumberLength), ' ',
                     leftAlign("", maxIdentifierLength),
                     " - ", valueStrings.get(i));
              }
              break;
          }

          optionNumber++;
        }

        if (parser.allowsTrailingArguments())
        {
          final List trailingArgs = parser.getTrailingArguments();
          switch(trailingArgs.size())
          {
            case 0:
              tool.wrapStandardOut(0, subsequentIndent, wrapColumn, true,
                   rightAlign("t", maxNumberLength),
                   ' ',
                   leftAlign(trailingArgsIdentifier, maxIdentifierLength),
                   " -");
              break;
            case 1:
              tool.wrapStandardOut(0, subsequentIndent, wrapColumn, true,
                   rightAlign("t", maxNumberLength), ' ',
                   leftAlign(trailingArgsIdentifier, maxIdentifierLength),
                   " - ", trailingArgs.get(0));
              break;
            default:
              tool.wrapStandardOut(0, subsequentIndent, wrapColumn, true,
                   rightAlign("t", maxNumberLength), ' ',
                   leftAlign(trailingArgsIdentifier, maxIdentifierLength),
                   " - ", trailingArgs.get(0));
              for (int i=1; i < trailingArgs.size(); i++)
              {
                tool.wrapStandardOut(0, subsequentIndent, wrapColumn, true,
                     rightAlign("", maxNumberLength), ' ',
                     leftAlign("", maxIdentifierLength),
                     " - ", trailingArgs.get(i));
              }
              break;
          }
        }

        tool.out();

        if (tool instanceof LDAPCommandLineTool)
        {
          final LDAPCommandLineTool ldapTool = (LDAPCommandLineTool) tool;
          if (ldapTool.supportsAuthentication())
          {
            tool.wrapStandardOut((maxNumberLength - 1), subsequentIndent,
                 wrapColumn, true, "l - ",
                 INFO_INTERACTIVE_MENU_OPTION_REPROMPT_FOR_CONN_AUTH_ARGS.
                      get());
          }
          else
          {
            tool.wrapStandardOut((maxNumberLength - 1), subsequentIndent,
                 wrapColumn, true, "l - ",
                 INFO_INTERACTIVE_MENU_OPTION_REPROMPT_FOR_CONN_ARGS.get());
          }
        }
        else if (tool instanceof MultiServerLDAPCommandLineTool)
        {
            tool.wrapStandardOut((maxNumberLength - 1), subsequentIndent,
                 wrapColumn, true, "l - ",
                 INFO_INTERACTIVE_MENU_OPTION_REPROMPT_FOR_CONN_AUTH_ARGS.
                      get());
        }

        tool.wrapStandardOut((maxNumberLength - 1), subsequentIndent,
             wrapColumn, true, "d - ",
             INFO_INTERACTIVE_MENU_OPTION_DISPLAY_ARGS.get(tool.getToolName()));
        tool.wrapStandardOut((maxNumberLength - 1), subsequentIndent,
             wrapColumn, true, "r - ",
             INFO_INTERACTIVE_MENU_OPTION_RUN.get(tool.getToolName()));
        tool.wrapStandardOut((maxNumberLength - 1), subsequentIndent,
             wrapColumn, true, "q - ", INFO_INTERACTIVE_MENU_OPTION_QUIT.get());
        tool.out();
        tool.getOut().print(
             INFO_INTERACTIVE_MENU_ENTER_CHOICE_WITHOUT_DEFAULT.get() + ' ');

        final Argument selectedArg;
        try
        {
          while (true)
          {
            final String line = systemInReader.readLine().trim();
            if (line.equalsIgnoreCase("t") &&
                (tool.getMaxTrailingArguments() != 0))
            {
              promptForTrailingArguments();
              continue argsLoop;
            }
            else if (line.equalsIgnoreCase("l"))
            {
              if (tool instanceof LDAPCommandLineTool)
              {
                promptForLDAPArguments(ldapArgs, true);
              }
              else if (tool instanceof MultiServerLDAPCommandLineTool)
              {
                promptForMultiServerLDAPArguments(ldapArgs, true);
              }
              else
              {
                tool.wrapErr(0, wrapColumn,
                     ERR_INTERACTIVE_ARG_MENU_INVALID_CHOICE.get());
                tool.getOut().print(
                     INFO_INTERACTIVE_MENU_ENTER_CHOICE_WITHOUT_DEFAULT.get() +
                     ' ');
              }

              continue argsLoop;
            }
            else if (line.equalsIgnoreCase("d"))
            {
              try
              {
                validateRequiredExclusiveAndDependentArgumentSets();
                tool.doExtendedArgumentValidation();

                final ArrayList argStrings =
                     new ArrayList<>(2*args.size());

                final SubCommand subcommand = parser.getSelectedSubCommand();
                if (subcommand != null)
                {
                  argStrings.add(subcommand.getPrimaryName());
                }

                argStrings.addAll(ldapArgs);
                for (final Argument a : args)
                {
                  ArgumentHelper.addToCommandLine(a, argStrings);
                }
                argStrings.addAll(parser.getTrailingArguments());

                if (argStrings.isEmpty())
                {
                  tool.wrapStandardOut(0, 0, wrapColumn, true,
                       INFO_INTERACTIVE_MENU_NO_CURRENT_ARGS.get(
                            tool.getToolName()));
                }
                else
                {
                  tool.wrapStandardOut(0, 0, wrapColumn, true,
                       INFO_INTERACTIVE_MENU_CURRENT_ARGS_HEADER.get(
                            tool.getToolName()));
                  printArgs(argStrings);
                }
                tool.out();
                promptForString(
                     INFO_INTERACTIVE_MENU_PROMPT_PRESS_ENTER_TO_CONTINUE.get(),
                     null, false);
                continue argsLoop;
              }
              catch (final ArgumentException ae)
              {
                Debug.debugException(ae);
                tool.err();
                tool.wrapErr(0, wrapColumn,
                     ERR_INTERACTIVE_MENU_EXTENDED_VALIDATION_ERRORS.get(
                          ae.getMessage()));
                tool.err();
                tool.wrapErr(0, wrapColumn,
                     ERR_INTERACTIVE_MENU_CORRECT_VALIDATION_ERRORS.get());
                tool.err();
                promptForString(
                     INFO_INTERACTIVE_MENU_PROMPT_PRESS_ENTER_TO_CONTINUE.get(),
                     null, false);
                continue argsLoop;
              }
            }
            else if (line.equalsIgnoreCase("r"))
            {
              try
              {
                validateRequiredExclusiveAndDependentArgumentSets();
                tool.doExtendedArgumentValidation();
                break argsLoop;
              }
              catch (final ArgumentException ae)
              {
                Debug.debugException(ae);
                tool.err();
                tool.wrapErr(0, wrapColumn,
                     ERR_INTERACTIVE_MENU_EXTENDED_VALIDATION_ERRORS.get(
                          ae.getMessage()));
                tool.err();
                tool.wrapErr(0, wrapColumn,
                     ERR_INTERACTIVE_MENU_CORRECT_VALIDATION_ERRORS.get());
                tool.err();
                promptForString(
                     INFO_INTERACTIVE_MENU_PROMPT_PRESS_ENTER_TO_CONTINUE.get(),
                     null, false);
                continue argsLoop;
              }
            }
            else if (line.equalsIgnoreCase("q"))
            {
              throw new LDAPException(ResultCode.SUCCESS, "");
            }

            int selectedValue = -1;
            try
            {
              selectedValue = Integer.parseInt(line);
            }
            catch (final Exception e)
            {
              Debug.debugException(e);
            }

            if ((selectedValue < 1) || (selectedValue > args.size()))
            {
              tool.wrapErr(0, wrapColumn,
                   ERR_INTERACTIVE_ARG_MENU_INVALID_CHOICE.get());
              tool.getOut().print(
                   INFO_INTERACTIVE_MENU_ENTER_CHOICE_WITHOUT_DEFAULT.get() +
                        ' ');
            }
            else
            {
              selectedArg = args.get(selectedValue - 1);
              break;
            }
          }
        }
        catch (final LDAPException le)
        {
          Debug.debugException(le);
          throw le;
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
          throw new LDAPException(ResultCode.LOCAL_ERROR,
               ERR_INTERACTIVE_MENU_CANNOT_READ_CHOICE.get(
                    StaticUtils.getExceptionMessage(e)),
               e);
        }

        promptForArgument(selectedArg);
      }

      final ArrayList argStrings = new ArrayList<>(2*args.size());
      for (final Argument a : args)
      {
        ArgumentHelper.addToCommandLine(a, argStrings);
      }
      argStrings.addAll(parser.getTrailingArguments());

      return argStrings;
    }
  }



  /**
   * Prompts the user for the trailing argument value(s).
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForTrailingArguments()
          throws LDAPException
  {
    tool.out();

    ArgumentHelper.resetTrailingArguments(parser);

    if (parser.getMaxTrailingArguments() == 1)
    {
      if (parser.requiresTrailingArguments())
      {
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_TRAILING_DESC_SINGLE_REQUIRED.get(
                  tool.getToolName()));
      }
      else
      {
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_TRAILING_DESC_SINGLE_OPTIONAL.get(
                  tool.getToolName()));
      }
      tool.out("     ", tool.getTrailingArgumentsPlaceholder());

      tool.out();
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_TRAILING_PROMPT_SINGLE.get());

      while (true)
      {
        final String trailingArgValue = promptForString(
             INFO_INTERACTIVE_TRAILING_ARG_PROMPT.get(), null, false);
        if (trailingArgValue == null)
        {
          return;
        }

        try
        {
          ArgumentHelper.addTrailingArgument(parser, trailingArgValue);
          return;
        }
        catch (final ArgumentException ae)
        {
          Debug.debugException(ae);
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_TRAILING_VALUE_INVALID.get(ae.getMessage()));
        }
      }
    }
    else
    {
      if (parser.requiresTrailingArguments())
      {
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_TRAILING_DESC_MULTIPLE_REQUIRED.get(
             tool.getToolName(), parser.getMinTrailingArguments()));
      }
      else
      {
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_TRAILING_DESC_MULTIPLE_OPTIONAL.get(
             tool.getToolName()));
      }
      tool.out("     ", tool.getTrailingArgumentsPlaceholder());

      tool.out();
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_TRAILING_PROMPT_MULTIPLE.get());

      while (true)
      {
        final String trailingArgValue = promptForString(
             INFO_INTERACTIVE_TRAILING_ARG_PROMPT.get(), null, false);
        if (trailingArgValue == null)
        {
          return;
        }
        else
        {
          try
          {
            ArgumentHelper.addTrailingArgument(parser, trailingArgValue);
          }
          catch (final ArgumentException ae)
          {
            Debug.debugException(ae);
            tool.wrapErr(0, wrapColumn,
                 ERR_INTERACTIVE_TRAILING_VALUE_INVALID.get(ae.getMessage()));
          }
        }
      }
    }
  }



  /**
   * Prompts the user for the value(s) for the specified argument.
   *
   * @param  a  The argument for which to prompt.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForArgument(@NotNull final Argument a)
          throws LDAPException
  {
    tool.out();

    final int maxValues = a.getMaxOccurrences();
    if (maxValues == 1)
    {
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_ARG_PROMPT_SPECIFY_SINGLE_VALUE.get(
                a.getIdentifierString()));
    }
    else
    {
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_ARG_PROMPT_SPECIFY_MULTIPLE_VALUES.get(
                a.getIdentifierString()));
    }

    final String description = a.getDescription();
    if ((description != null) && (! description.isEmpty()))
    {
      tool.out();

      final String prompt = INFO_INTERACTIVE_ARG_PROMPT_DESCRIPTION.get();
      tool.wrapStandardOut(0, prompt.length(), wrapColumn, true, prompt,
           description);
    }

    final String constraints = a.getValueConstraints();
    if ((constraints != null) && (! constraints.isEmpty()))
    {
      tool.out();

      final String prompt = INFO_INTERACTIVE_ARG_PROMPT_CONSTRAINTS.get();
      tool.wrapStandardOut(0, prompt.length(), wrapColumn, true, prompt,
           constraints);

      if (a.isRequired())
      {
        if (maxValues == 1)
        {
          tool.wrapStandardOut(prompt.length(), prompt.length(), wrapColumn,
               true, INFO_INTERACTIVE_ARG_PROMPT_SINGLE_REQUIRED.get());
        }
        else
        {
          tool.wrapStandardOut(prompt.length(), prompt.length(), wrapColumn,
               true, INFO_INTERACTIVE_ARG_PROMPT_AT_LEAST_ONE_REQUIRED.get());
        }
      }
    }
    else if (a.isRequired())
    {
      tool.out();
      final String prompt = INFO_INTERACTIVE_ARG_PROMPT_CONSTRAINTS.get();

      if (maxValues == 1)
      {
        tool.wrapStandardOut(0, prompt.length(), wrapColumn, true, prompt,
             INFO_INTERACTIVE_ARG_PROMPT_SINGLE_REQUIRED.get());
      }
      else
      {
        tool.wrapStandardOut(0, prompt.length(), wrapColumn, true, prompt,
             INFO_INTERACTIVE_ARG_PROMPT_AT_LEAST_ONE_REQUIRED.get());
      }
    }

    if (a instanceof ArgumentListArgument)
    {
      promptForArgumentList((ArgumentListArgument) a);
    }
    else if (a instanceof BooleanArgument)
    {
      promptForBoolean((BooleanArgument) a);
    }
    else if (a instanceof BooleanValueArgument)
    {
      promptForBoolean((BooleanValueArgument) a);
    }
    else if (a instanceof ControlArgument)
    {
      promptForControl((ControlArgument) a);
    }
    else if (a instanceof DNArgument)
    {
      promptForDN((DNArgument) a);
    }
    else if (a instanceof DurationArgument)
    {
      promptForDuration((DurationArgument) a);
    }
    else if (a instanceof FileArgument)
    {
      promptForFile((FileArgument) a);
    }
    else if (a instanceof FilterArgument)
    {
      promptForFilter((FilterArgument) a);
    }
    else if (a instanceof IntegerArgument)
    {
      promptForInteger((IntegerArgument) a);
    }
    else if (a instanceof ScopeArgument)
    {
      promptForScope((ScopeArgument) a);
    }
    else if (a instanceof StringArgument)
    {
      promptForString((StringArgument) a);
    }
    else if (a instanceof TimestampArgument)
    {
      promptForTimestamp((TimestampArgument) a);
    }
    else
    {
      // This should never happen.
      throw new AssertionError("Unexpected argument type " +
           a.getClass().getName());
    }
  }



  /**
   * Prompts for one or more argument lists.
   *
   * @param  a  The argument list argument for which to prompt.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForArgumentList(@NotNull final ArgumentListArgument a)
          throws LDAPException
  {
    final List values = a.getValueStrings();
    ArgumentHelper.reset(a);

    if (a.getMaxOccurrences() == 1)
    {
      if (! values.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUE.get());
        tool.wrapStandardOut(5, 10, wrapColumn, true, values.get(0));
      }

      while (true)
      {
        final String newValue = promptForString(
             INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null, a.isRequired());
        try
        {
          if (newValue != null)
          {
            ArgumentHelper.addValue(a, newValue);
          }
          return;
        }
        catch (final ArgumentException ae)
        {
          Debug.debugException(ae);
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_ARG_PROMPT_INVALID_VALUE.get(ae.getMessage()));
        }
      }
    }
    else
    {
      if (! values.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUES.get());
        for (final String s : values)
        {
          tool.wrapStandardOut(5, 10, wrapColumn, true, s);
        }
      }

      tool.out();
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUES.get());

      boolean first = true;
      while (true)
      {
        final String s = promptForString(
             INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null,
             (first && a.isRequired()));
        if (s == null)
        {
          return;
        }

        try
        {
          ArgumentHelper.addValue(a, s);
          first = false;
        }
        catch (final ArgumentException ae)
        {
          Debug.debugException(ae);
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_ARG_PROMPT_INVALID_VALUE.get(ae.getMessage()));
        }
      }
    }
  }



  /**
   * Prompts for a Boolean value.
   *
   * @param  a  The Boolean argument for which to prompt.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForBoolean(@NotNull final BooleanArgument a)
          throws LDAPException
  {
    tool.out();
    tool.wrapStandardOut(0, 0, wrapColumn,true,
         INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUE.get());
    tool.wrapStandardOut(5, 10, wrapColumn, true, a.isPresent());

    ArgumentHelper.reset(a);

    if (promptForBoolean(
         INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null, true))
    {
      ArgumentHelper.incrementOccurrencesSuppressException(a);
    }
  }



  /**
   * Prompts for a Boolean value.
   *
   * @param  a  The Boolean argument for which to prompt.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForBoolean(@NotNull final BooleanValueArgument a)
          throws LDAPException
  {
    final Boolean value = a.getValue();
    ArgumentHelper.reset(a);

    final Boolean b = promptForBoolean(
         INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null, a.isRequired());
    if (b != null)
    {
      ArgumentHelper.addValueSuppressException(a, String.valueOf(b));
    }
  }



  /**
   * Prompts for one or more control values.
   *
   * @param  a  The control argument for which to prompt.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForControl(@NotNull final ControlArgument a)
          throws LDAPException
  {
    final List values = a.getValues();
    ArgumentHelper.reset(a);

    if (a.getMaxOccurrences() == 1)
    {
      if (! values.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUE.get());
        tool.wrapStandardOut(5, 10, wrapColumn, true, values.get(0));
      }

      while (true)
      {
        final String newValue = promptForString(
             INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null, false);
        try
        {
          if (newValue == null)
          {
            ArgumentHelper.addValue(a, "");
          }
          else
          {
            ArgumentHelper.addValue(a, newValue);
          }
          return;
        }
        catch (final ArgumentException ae)
        {
          Debug.debugException(ae);
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_ARG_PROMPT_INVALID_VALUE.get(ae.getMessage()));
        }
      }
    }
    else
    {
      if (! values.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUES.get());
        for (final Control c : values)
        {
          tool.wrapStandardOut(5, 10, wrapColumn, true, c);
        }
      }

      tool.out();
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUES.get());

      boolean first = true;
      while (true)
      {
        final String s = promptForString(
             INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null,
             (first && a.isRequired()));
        if (s == null)
        {
          return;
        }

        try
        {
          ArgumentHelper.addValue(a, s);
          first = false;
        }
        catch (final ArgumentException ae)
        {
          Debug.debugException(ae);
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_ARG_PROMPT_INVALID_VALUE.get(ae.getMessage()));
        }
      }
    }
  }



  /**
   * Prompts for one or more DN values.
   *
   * @param  a  The DN argument for which to prompt.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForDN(@NotNull final DNArgument a)
          throws LDAPException
  {
    final List values = a.getValues();
    ArgumentHelper.reset(a);

    if (a.getMaxOccurrences() == 1)
    {
      if (! values.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUE.get());
        tool.wrapStandardOut(5, 10, wrapColumn, true, values.get(0));
      }

      final DN dnValue = promptForDN(
           INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null, true);
      ArgumentHelper.addValueSuppressException(a, String.valueOf(dnValue));
    }
    else
    {
      if (! values.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUES.get());
        for (final DN dn : values)
        {
          tool.wrapStandardOut(5, 10, wrapColumn, true, dn);
        }
      }

      tool.out();
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUES.get());

      boolean first = true;
      while (true)
      {
        final boolean allowNullDN;
        if (first)
        {
          first = false;
          allowNullDN = ! a.isRequired();
        }
        else
        {
          allowNullDN = true;
        }

        final DN dnValue = promptForDN(
             INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null, allowNullDN);
        if (dnValue.isNullDN())
        {
          return;
        }
        else
        {
          ArgumentHelper.addValueSuppressException(a, String.valueOf(dnValue));
        }
      }
    }
  }



  /**
   * Prompts for one or more duration values.
   *
   * @param  a  The duration argument for which to prompt.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForDuration(@NotNull final DurationArgument a)
          throws LDAPException
  {
    final List values = a.getValueStringRepresentations(true);
    if (! values.isEmpty())
    {
      tool.out();
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUE.get());
      tool.wrapStandardOut(5, 10, wrapColumn, true, values.get(0));
    }

    while (true)
    {
      final String newValue = promptForString(
           INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null, a.isRequired());
      try
      {
        if (newValue != null)
        {
          ArgumentHelper.addValue(a, newValue);
        }
        return;
      }
      catch (final ArgumentException ae)
      {
        Debug.debugException(ae);
        tool.wrapErr(0, wrapColumn,
             ERR_INTERACTIVE_ARG_PROMPT_INVALID_VALUE.get(ae.getMessage()));
      }
    }
  }



  /**
   * Prompts for one or more path values.
   *
   * @param  a  The file argument for which to prompt.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForFile(@NotNull final FileArgument a)
          throws LDAPException
  {
    final List values = a.getValues();
    ArgumentHelper.reset(a);

    if (a.getMaxOccurrences() == 1)
    {
      if (! values.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUE.get());
        tool.wrapStandardOut(5, 10, wrapColumn, true, values.get(0));
      }

      final File fileValue = promptForPath(
           INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null, a.isRequired(),
           a.fileMustExist(), a.parentMustExist(), a.mustBeFile(),
           a.mustBeDirectory());
      if (fileValue != null)
      {
        ArgumentHelper.addValueSuppressException(a, fileValue.getPath());
      }
    }
    else
    {
      if (! values.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUES.get());
        for (final File f : values)
        {
          tool.wrapStandardOut(5, 10, wrapColumn, true, f.getPath());
        }
      }

      tool.out();
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUES.get());

      boolean first = true;
      while (true)
      {
        final boolean isRequired;
        if (first)
        {
          first = false;
          isRequired = a.isRequired();
        }
        else
        {
          isRequired = false;
        }

        final File fileValue = promptForPath(
             INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null, isRequired,
             a.fileMustExist(), a.parentMustExist(), a.mustBeFile(),
             a.mustBeDirectory());
        if (fileValue == null)
        {
          return;
        }
        else
        {
          ArgumentHelper.addValueSuppressException(a, fileValue.getPath());
        }
      }
    }
  }



  /**
   * Prompts for one or more filter values.
   *
   * @param  a  The filter argument for which to prompt.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForFilter(@NotNull final FilterArgument a)
          throws LDAPException
  {
    final List values = a.getValues();
    ArgumentHelper.reset(a);

    if (a.getMaxOccurrences() == 1)
    {
      if (! values.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUE.get());
        tool.wrapStandardOut(5, 10, wrapColumn, true, values.get(0));
      }

      final Filter filterValue = promptForFilter(
           INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null, a.isRequired());
      if (filterValue != null)
      {
        ArgumentHelper.addValueSuppressException(a, filterValue.toString());
      }
    }
    else
    {
      if (! values.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUES.get());
        for (final Filter f : values)
        {
          tool.wrapStandardOut(5, 10, wrapColumn, true, String.valueOf(f));
        }
      }

      tool.out();
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUES.get());

      boolean first = true;
      while (true)
      {
        final boolean isRequired;
        if (first)
        {
          first = false;
          isRequired = a.isRequired();
        }
        else
        {
          isRequired = false;
        }

        final Filter filterValue = promptForFilter(
             INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null, isRequired);
        if (filterValue == null)
        {
          return;
        }
        else
        {
          ArgumentHelper.addValueSuppressException(a,
               String.valueOf(filterValue));
        }
      }
    }
  }



  /**
   * Prompts for one or more integer values.
   *
   * @param  a  The integer argument for which to prompt.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForInteger(@NotNull final IntegerArgument a)
          throws LDAPException
  {
    final List values = a.getValues();
    ArgumentHelper.reset(a);

    if (a.getMaxOccurrences() == 1)
    {
      if (! values.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUE.get());
        tool.wrapStandardOut(5, 10, wrapColumn, true, values.get(0));
      }

      final Integer intValue = promptForInteger(
           INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null,
           a.getLowerBound(), a.getUpperBound(), a.isRequired());
      if (intValue != null)
      {
        ArgumentHelper.addValueSuppressException(a,
             String.valueOf(intValue));
      }
    }
    else
    {
      if (! values.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUES.get());
        for (final Integer i : values)
        {
          tool.wrapStandardOut(5, 10, wrapColumn, true, i);
        }
      }

      tool.out();
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUES.get());

      boolean first = true;
      while (true)
      {
        final boolean isRequired;
        if (first)
        {
          first = false;
          isRequired = a.isRequired();
        }
        else
        {
          isRequired = false;
        }

        final Integer intValue = promptForInteger(
             INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null,
             a.getLowerBound(), a.getUpperBound(), isRequired);
        if (intValue == null)
        {
          return;
        }
        else
        {
          ArgumentHelper.addValueSuppressException(a,
               String.valueOf(intValue));
        }
      }
    }
  }



  /**
   * Prompts for one or more scope values.
   *
   * @param  a  The scope argument for which to prompt.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForScope(@NotNull final ScopeArgument a)
          throws LDAPException
  {
    final SearchScope value = a.getValue();
    ArgumentHelper.reset(a);

    final String[] scopeValues =
    {
      "base",
      "one",
      "sub",
      "subordinates"
    };

    tool.out();
    if (value != null)
    {
      if (value.intValue() < scopeValues.length)
      {
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUE.get());
        tool.wrapStandardOut(5, 10, wrapColumn, true,
             scopeValues[value.intValue()]);
      }
    }

    final int newIntValue = getNumberedMenuChoice(
         INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(),
         (! a.isRequired()),
         null,
         scopeValues);
    if (newIntValue >= 0)
    {
      ArgumentHelper.addValueSuppressException(a, scopeValues[newIntValue]);
    }
  }



  /**
   * Prompts for one or more string values.
   *
   * @param  a  The string argument for which to prompt.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForString(@NotNull final StringArgument a)
          throws LDAPException
  {
    // If the argument has a relatively small set of allowed values, then
    // display a menu to allow the user to select one of those values.
    if ((a.getAllowedValues() != null) && (! a.getAllowedValues().isEmpty()) &&
        (a.getAllowedValues().size() <= 20))
    {
      promptForStringWithMenu(a);
      return;
    }

    final List values = a.getValues();
    ArgumentHelper.reset(a);

    if (a.getMaxOccurrences() == 1)
    {
      if (! values.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUE.get());
        tool.wrapStandardOut(5, 10, wrapColumn, true, values.get(0));
      }

      while (true)
      {
        final String newValue;
        if (a.isSensitive())
        {
          final byte[] newValueBytes = promptForPassword(
               INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(),
               INFO_INTERACTIVE_ARG_PROMPT_VALUE_CONFIRM.get(), a.isRequired());
          if (newValueBytes == null)
          {
            newValue = null;
          }
          else
          {
            newValue = StaticUtils.toUTF8String(newValueBytes);
          }
        }
        else
        {
          newValue = promptForString(
               INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null,
               a.isRequired());
        }

        try
        {
          if (newValue != null)
          {
            ArgumentHelper.addValue(a, newValue);
          }
          return;
        }
        catch (final ArgumentException ae)
        {
          Debug.debugException(ae);
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_ARG_PROMPT_INVALID_VALUE.get(ae.getMessage()));
        }
      }
    }
    else
    {
      if (! values.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUES.get());
        for (final String s : values)
        {
          tool.wrapStandardOut(5, 10, wrapColumn, true, s);
        }
      }

      tool.out();
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUES.get());

      boolean first = true;
      while (true)
      {
        final String s = promptForString(
             INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null,
             (first && a.isRequired()));
        if (s == null)
        {
          return;
        }

        try
        {
          ArgumentHelper.addValue(a, s);
          first = false;
        }
        catch (final ArgumentException ae)
        {
          Debug.debugException(ae);
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_ARG_PROMPT_INVALID_VALUE.get(ae.getMessage()));
        }
      }
    }
  }



  /**
   * Prompts for one or more string values using a menu to display the allowed
   * choices.
   *
   * @param  a  The string argument for which to prompt.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForStringWithMenu(@NotNull final StringArgument a)
          throws LDAPException
  {
    final List values = a.getValues();
    ArgumentHelper.reset(a);

    final String[] allowedValueArray = new String[a.getAllowedValues().size()];
    a.getAllowedValues().toArray(allowedValueArray);

    if (! values.isEmpty())
    {
      tool.out();
      if (values.size() == 1)
      {
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUE.get());
        tool.wrapStandardOut(5, 10, wrapColumn, true, values.get(0));
      }
      else
      {
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUES.get());
        for (final String s : values)
        {
          tool.wrapStandardOut(5, 10, wrapColumn, true, s);
        }
      }
    }

    final String message;
    if (a.getMaxOccurrences() > 1)
    {
      message = INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUES.get();
    }
    else
    {
      message = INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get();
    }

    final int firstChoice = getNumberedMenuChoice(message, (! a.isRequired()),
         null, allowedValueArray);
    if (firstChoice < 0)
    {
      return;
    }

    ArgumentHelper.addValueSuppressException(a, allowedValueArray[firstChoice]);

    if (a.getMaxOccurrences() > 1)
    {
      while (true)
      {
        final String stringValue = promptForString(
             INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null, false);
        if (stringValue == null)
        {
          return;
        }
        else if (stringValue.equalsIgnoreCase("q"))
        {
          throw new LDAPException(ResultCode.SUCCESS, "");
        }

        int selectedValue = -1;
        try
        {
          selectedValue = Integer.parseInt(stringValue);
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
        }

        if ((selectedValue < 1) || (selectedValue > allowedValueArray.length))
        {
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_MENU_INVALID_CHOICE.get());
        }
        else
        {
          ArgumentHelper.addValueSuppressException(a,
               allowedValueArray[selectedValue - 1]);
        }
      }
    }
  }



  /**
   * Prompts for one or more timestamp values.
   *
   * @param  a  The timestamp argument for which to prompt.
   *
   * @throws  LDAPException  If a problem is encountered while interacting with
   *                         the user, or if the user wants to quit.
   */
  private void promptForTimestamp(@NotNull final TimestampArgument a)
          throws LDAPException
  {
    final List stringValues = a.getValueStringRepresentations(true);
    ArgumentHelper.reset(a);

    if (a.getMaxOccurrences() == 1)
    {
      if (! stringValues.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUE.get());
        tool.wrapStandardOut(5, 10, wrapColumn, true, stringValues.get(0));
      }

      final ObjectPair p = promptForTimestamp(
           INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null, a.isRequired());
      if (p != null)
      {
        ArgumentHelper.addValueSuppressException(a, p.getSecond());
      }
    }
    else
    {
      if (! stringValues.isEmpty())
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, true,
             INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUES.get());
        for (final String s : stringValues)
        {
          tool.wrapStandardOut(5, 10, wrapColumn, true, String.valueOf(s));
        }
      }

      tool.out();
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUES.get());

      boolean first = true;
      while (true)
      {
        final boolean isRequired;
        if (first)
        {
          first = false;
          isRequired = a.isRequired();
        }
        else
        {
          isRequired = false;
        }

        final Filter filterValue = promptForFilter(
             INFO_INTERACTIVE_ARG_PROMPT_NEW_VALUE.get(), null, isRequired);
        if (filterValue == null)
        {
          return;
        }
        else
        {
          ArgumentHelper.addValueSuppressException(a,
               String.valueOf(filterValue));
        }
      }
    }
  }



  /**
   * Displays a menu of numbered options, and waits for the user to select one
   * of the options.
   *
   * @param  prompt               The string to display above the list of
   *                              numbered options.
   * @param  allowUndefined       Indicates whether to allow the value to remain
   *                              undefined.
   * @param  defaultOptionString  The number that corresponds to the default
   *                              option that will be selected if the user
   *                              presses ENTER without typing anything.  This
   *                              should be the display string value, so if it
   *                              is a number then it should be one-based rather
   *                              than zero-based.  It may be {@code null} if no
   *                              default string should be provided.
   * @param  options              The set of text to display next to the
   *                              numbered options.
   *
   * @return  The index of the option that was selected, or -1 if an undefined
   *          value is allowed and that option was selected.  Note that although
   *          the displayed menu will start numbering with one, the value
   *          returned will start numbering at zero so as to correspond with the
   *          elements in the provided options array.
   *
   * @throws  LDAPException  If an error occurs while determining which option
   *                         the user has chosen, or if the user has chosen to
   *                         quit rather than select a numbered option.
   */
  private int getNumberedMenuChoice(@NotNull final String prompt,
                                    final boolean allowUndefined,
                                    @Nullable final String defaultOptionString,
                                    @NotNull final String... options)
          throws LDAPException
  {
    final int maxNumberLength = String.valueOf(options.length).length();
    final int subsequentIndent = maxNumberLength + 3;

    tool.out();
    tool.wrapStandardOut(0, 0, wrapColumn, true, prompt);

    int optionNumber = 1;
    for (final String option : options)
    {
      tool.wrapStandardOut(0, subsequentIndent, wrapColumn, true,
           rightAlign(String.valueOf(optionNumber), maxNumberLength), " - ",
           option);
      optionNumber++;
    }

    tool.out();
    if (allowUndefined)
    {
      tool.wrapStandardOut((maxNumberLength - 1), subsequentIndent, wrapColumn,
           true, "u - ", INFO_INTERACTIVE_MENU_OPTION_UNDEFINED.get());
    }
    tool.wrapStandardOut((maxNumberLength - 1), subsequentIndent, wrapColumn,
         true, "q - ", INFO_INTERACTIVE_MENU_OPTION_QUIT.get());

    final String message;
    if (defaultOptionString == null)
    {
      message = INFO_INTERACTIVE_MENU_ENTER_CHOICE_WITHOUT_DEFAULT.get() + ' ';
    }
    else
    {
      message = INFO_INTERACTIVE_MENU_ENTER_CHOICE_WITH_DEFAULT.get(
           defaultOptionString) + ' ';
    }

    try
    {
      while (true)
      {
        tool.out();
        tool.wrapStandardOut(0, 0, wrapColumn, false, message);

        String line = systemInReader.readLine().trim();
        if (line.equalsIgnoreCase("q"))
        {
          throw new LDAPException(ResultCode.SUCCESS, "");
        }
        else if (allowUndefined && line.equalsIgnoreCase("u"))
        {
          return -1;
        }

        if (line.isEmpty() && (defaultOptionString != null))
        {
          line = defaultOptionString;
        }

        int selectedValue = -1;
        try
        {
          selectedValue = Integer.parseInt(line);
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
        }

        if ((selectedValue < 1) || (selectedValue > options.length))
        {
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_MENU_INVALID_CHOICE.get());
        }
        else
        {
          return selectedValue - 1;
        }
      }
    }
    catch (final LDAPException le)
    {
      Debug.debugException(le);
      throw le;
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      throw new LDAPException(ResultCode.LOCAL_ERROR,
           ERR_INTERACTIVE_MENU_CANNOT_READ_CHOICE.get(
                StaticUtils.getExceptionMessage(e)),
           e);
    }
  }



  /**
   * Prompts the user to enter a string value.
   *
   * @param  prompt        The prompt to display to the user.
   * @param  defaultValue  The value that should be selected if the user
   *                       presses ENTER without entering a value.
   * @param  requireValue  Indicates whether a value is required.
   *
   * @return  The string obtained from the user, or {@code null} if the user did
   *          not provide a value, there is no default value, and no value is
   *          required.
   *
   * @throws  LDAPException  If an error occurs while obtaining the value from
   *                         the user.
   */
  @Nullable()
  private String promptForString(@NotNull final String prompt,
                                 @Nullable final String defaultValue,
                                 final boolean requireValue)
          throws LDAPException
  {
    tool.out();

    final String promptStr;
    if (defaultValue == null)
    {
      promptStr = prompt + ": ";
    }
    else
    {
      promptStr = prompt + " [" + defaultValue + "]: ";
    }

    tool.wrapStandardOut(0, 0, wrapColumn, false, promptStr);

    try
    {
      String line = systemInReader.readLine().trim();
      if (line.isEmpty() && (defaultValue != null))
      {
        line = defaultValue;
      }

      if (! line.isEmpty())
      {
        return line;
      }
      else if (requireValue)
      {
        tool.wrapErr(0, wrapColumn,
             ERR_INTERACTIVE_PROMPT_VALUE_REQUIRED.get());
        return promptForString(prompt, defaultValue, requireValue);
      }
      else
      {
        return null;
      }
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      throw new LDAPException(ResultCode.LOCAL_ERROR,
           ERR_INTERACTIVE_PROMPT_ERROR_READING_RESPONSE.get(
                StaticUtils.getExceptionMessage(e)),
           e);
    }
  }



  /**
   * Prompts the user to enter a Boolean value.
   *
   * @param  prompt        The prompt to display to the user.
   * @param  defaultValue  The value that should be selected if the user
   *                       presses ENTER without entering a value.
   * @param  requireValue  Indicates whether a value is required.
   *
   * @return  The Boolean value obtained from the user, or {@code null} if the
   *          user did not provide a value, there is no default value, and no
   *          value is required.
   *
   * @throws  LDAPException  If an error occurs while obtaining the value from
   *                         the user.
   */
  @Nullable()
  private Boolean promptForBoolean(@NotNull final String prompt,
                                   @Nullable final Boolean defaultValue,
                                   final boolean requireValue)
          throws LDAPException
  {
    final String[] choices =
    {
      "true",
      "false"
    };

    tool.out();
    if (defaultValue != null)
    {
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUE.get());
      tool.wrapStandardOut(5, 10, wrapColumn, true,
           String.valueOf(defaultValue));
    }

    final int newIntValue = getNumberedMenuChoice(prompt, (! requireValue),
         null, choices);
    switch (newIntValue)
    {
      case 0:
        return Boolean.TRUE;
      case 1:
        return Boolean.FALSE;
      default:
        return null;
    }
  }



  /**
   * Prompts the user to enter a yes or no value.
   *
   * @param  prompt        The prompt to display to the user.
   * @param  defaultValue  The value that should be selected if the user
   *                       presses ENTER without entering a value.
   * @param  requireValue  Indicates whether a value is required.
   *
   * @return  The Boolean value obtained from the user, or {@code null} if the
   *          user did not provide a value, there is no default value, and no
   *          value is required.
   *
   * @throws  LDAPException  If an error occurs while obtaining the value from
   *                         the user.
   */
  @Nullable()
  private Boolean promptForYesNo(@NotNull final String prompt,
                                 @Nullable final Boolean defaultValue,
                                 final boolean requireValue)
          throws LDAPException
  {
    final String[] choices =
    {
      INFO_INTERACTIVE_CHOICE_YES.get(),
      INFO_INTERACTIVE_CHOICE_NO.get()
    };

    tool.out();
    String defaultOptionString = null;
    if (defaultValue != null)
    {
      tool.wrapStandardOut(0, 0, wrapColumn, true,
           INFO_INTERACTIVE_ARG_DESC_CURRENT_VALUE.get());
      if (defaultValue)
      {
        tool.wrapStandardOut(5, 10, wrapColumn, true,
             INFO_INTERACTIVE_CHOICE_YES.get());
        defaultOptionString = "1";
      }
      else
      {
        tool.wrapStandardOut(5, 10, wrapColumn, true,
             INFO_INTERACTIVE_CHOICE_NO.get());
        defaultOptionString = "2";
      }
    }

    final int newIntValue = getNumberedMenuChoice(prompt, (! requireValue),
         defaultOptionString, choices);
    switch (newIntValue)
    {
      case 0:
        return Boolean.TRUE;
      case 1:
        return Boolean.FALSE;
      default:
        return null;
    }
  }



  /**
   * Prompts the user to enter a distinguished name value.
   *
   * @param  prompt         The prompt to display to the user.
   * @param  defaultValue   The value that should be selected if the user
   *                        presses ENTER without entering a value.
   * @param  nullDNAllowed  Indicates whether the user is allowed to select the
   *                        null DN by pressing ENTER without entering a value.
   *                        Note that it will not be possible to specify the
   *                        null DN if a default value is supplied, since the
   *                        default value will be chosen instead.
   *
   * @return  The DN value obtained from the user.  It may be the null DN if
   *          the user pressed ENTER without specifying a value.
   *
   * @throws  LDAPException  If an error occurs while obtaining the value from
   *                         the user.
   */
  @NotNull()
  private DN promptForDN(@NotNull final String prompt,
                         @Nullable final String defaultValue,
                         final boolean nullDNAllowed)
          throws LDAPException
  {
    tool.out();

    final String promptStr;
    if (defaultValue == null)
    {
      promptStr = prompt + ": ";
    }
    else
    {
      promptStr = prompt + " [" + defaultValue + "]: ";
    }

    tool.wrapStandardOut(0, 0, wrapColumn, false, promptStr);

    try
    {
      String line = systemInReader.readLine().trim();
      if (line.isEmpty())
      {
        if (defaultValue != null)
        {
          line = defaultValue;
        }

        if (line.isEmpty())
        {
          if (nullDNAllowed)
          {
            return DN.NULL_DN;
          }
          else
          {
            tool.wrapErr(0, wrapColumn,
                 ERR_INTERACTIVE_PROMPT_NULL_DN_NOT_ALLOWED.get());
            return promptForDN(prompt, defaultValue, nullDNAllowed);
          }
        }
      }

      try
      {
        return new DN(line);
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
        tool.wrapErr(0, wrapColumn, ERR_INTERACTIVE_PROMPT_INVALID_DN.get());
        return promptForDN(prompt, defaultValue, nullDNAllowed);
      }
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      throw new LDAPException(ResultCode.LOCAL_ERROR,
           ERR_INTERACTIVE_PROMPT_ERROR_READING_RESPONSE.get(
                StaticUtils.getExceptionMessage(e)),
           e);
    }
  }



  /**
   * Prompts the user to enter a filter.
   *
   * @param  prompt        The prompt to display to the user.
   * @param  defaultValue  The value that should be selected if the user
   *                       presses ENTER without entering a value.
   * @param  requireValue  Indicates whether a value is required.
   *
   * @return  The filter obtained from the user, or {@code null} if the user did
   *          not provide a value, there is no default value, and no value is
   *          required.
   *
   * @throws  LDAPException  If an error occurs while obtaining the value from
   *                         the user.
   */
  @Nullable()
  private Filter promptForFilter(@NotNull final String prompt,
                                 @Nullable final Filter defaultValue,
                                 final boolean requireValue)
          throws LDAPException
  {
    tool.out();

    final String promptStr;
    if (defaultValue == null)
    {
      promptStr = prompt + ": ";
    }
    else
    {
      promptStr = prompt + " [" + defaultValue + "]: ";
    }

    tool.wrapStandardOut(0, 0, wrapColumn, false, promptStr);

    try
    {
      String line = systemInReader.readLine().trim();
      if (line.isEmpty() && (defaultValue != null))
      {
        line = String.valueOf(defaultValue);
      }

      if (line.isEmpty())
      {
        if (requireValue)
        {
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_PROMPT_VALUE_REQUIRED.get());
          return promptForFilter(prompt, defaultValue, requireValue);
        }
        else
        {
          return null;
        }
      }

      try
      {
        return Filter.create(line);
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
        tool.wrapErr(0, wrapColumn,
             ERR_INTERACTIVE_PROMPT_INVALID_FILTER.get());
        return promptForFilter(prompt, defaultValue, requireValue);
      }
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      throw new LDAPException(ResultCode.LOCAL_ERROR,
           ERR_INTERACTIVE_PROMPT_ERROR_READING_RESPONSE.get(
                StaticUtils.getExceptionMessage(e)),
           e);
    }
  }



  /**
   * Prompts the user to enter an integer value.
   *
   * @param  prompt        The prompt to display to the user.
   * @param  defaultValue  The value that should be selected if the user
   *                       presses ENTER without entering a value.
   * @param  lowerBound    The lower bound for valid values.
   * @param  upperBound    The upper bound for valid values.
   * @param  requireValue  Indicates whether a value is required.
   *
   * @return  The value obtained from the user, or {@code null} if the user did
   *          not provide a value, there is no default value, and no value is
   *          required.
   *
   * @throws  LDAPException  If an error occurs while obtaining the value from
   *                         the user.
   */
  @Nullable()
  private Integer promptForInteger(@NotNull final String prompt,
                                   @Nullable final Integer defaultValue,
                                   @Nullable final Integer lowerBound,
                                   @Nullable final Integer upperBound,
                                   final boolean requireValue)
          throws LDAPException
  {
    tool.out();

    final int max;
    if (upperBound == null)
    {
      max = Integer.MAX_VALUE;
    }
    else
    {
      max = upperBound;
    }

    final int min;
    if (lowerBound == null)
    {
      min = Integer.MIN_VALUE;
    }
    else
    {
      min = lowerBound;
    }

    final String promptStr;
    if (defaultValue == null)
    {
      promptStr = prompt + ": ";
    }
    else
    {
      promptStr = prompt + " [" + defaultValue + "]: ";
    }

    tool.wrapStandardOut(0, 0, wrapColumn, false, promptStr);

    try
    {
      String line = systemInReader.readLine().trim();
      if (line.isEmpty() && (defaultValue != null))
      {
        line = String.valueOf(defaultValue);
      }

      if (line.isEmpty())
      {
        if (requireValue)
        {
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_PROMPT_VALUE_REQUIRED.get());
          return promptForInteger(prompt, defaultValue, lowerBound, upperBound,
               requireValue);
        }
        else
        {
          return null;
        }
      }

      final int intValue;
      try
      {
        intValue = Integer.parseInt(line);
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
        tool.wrapErr(0, wrapColumn,
             ERR_INTERACTIVE_PROMPT_INVALID_INTEGER_WITH_RANGE.get(min, max));
        return promptForInteger(prompt, defaultValue, lowerBound, upperBound,
             requireValue);
      }

      if ((intValue > max) || (intValue < min))
      {
        tool.wrapErr(0, wrapColumn,
             ERR_INTERACTIVE_PROMPT_INVALID_INTEGER_WITH_RANGE.get(min, max));
        return promptForInteger(prompt, defaultValue, lowerBound, upperBound,
             requireValue);
      }

      return intValue;
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      throw new LDAPException(ResultCode.LOCAL_ERROR,
           ERR_INTERACTIVE_PROMPT_ERROR_READING_RESPONSE.get(
                StaticUtils.getExceptionMessage(e)),
           e);
    }
  }



  /**
   * Prompts the user to enter a path.
   *
   * @param  prompt           The prompt to display to the user.
   * @param  defaultValue     The value that should be selected if the user
   *                          presses ENTER without entering a value.
   * @param  requireValue     Indicates whether a value is required.
   * @param  fileMustExist    Indicates whether the value must represent a path
   *                          that exists.
   * @param  parentMustExist  Indicates whether the parent directory containing
   *                          the specified path must exist.
   * @param  mustBeFile       Indicates whether the path must represent a file.
   * @param  mustBeDirectory  Indicates whether the path must represent a
   *                          directory.
   *
   * @return  The file obtained from the user, or {@code null} if the user did
   *          not provide a value, there is no default value, and no value is
   *          required.
   *
   * @throws  LDAPException  If an error occurs while obtaining the value from
   *                         the user.
   */
  @Nullable()
  private File promptForPath(@NotNull final String prompt,
                             @Nullable final String defaultValue,
                             final boolean requireValue,
                             final boolean fileMustExist,
                             final boolean parentMustExist,
                             final boolean mustBeFile,
                             final boolean mustBeDirectory)
       throws LDAPException
  {
    tool.out();

    final String promptStr;
    if (defaultValue == null)
    {
      promptStr = prompt + ": ";
    }
    else
    {
      promptStr = prompt + " [" + defaultValue + "]: ";
    }

    tool.wrapStandardOut(0, 0, wrapColumn, false, promptStr);

    try
    {
      String line = systemInReader.readLine().trim();
      if (line.isEmpty() && (defaultValue != null))
      {
        line = String.valueOf(defaultValue);
      }

      if (line.isEmpty())
      {
        if (requireValue)
        {
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_PROMPT_VALUE_REQUIRED.get());
          return promptForPath(prompt, defaultValue, requireValue,
               fileMustExist, parentMustExist, mustBeFile, mustBeDirectory);
        }
        else
        {
          return null;
        }
      }

      final File f = new File(line).getAbsoluteFile();
      if (! f.exists())
      {
        if (fileMustExist)
        {
          if (mustBeDirectory)
          {
            tool.wrapErr(0, wrapColumn,
                 ERR_INTERACTIVE_PROMPT_DIR_DOES_NOT_EXIST.get(
                      f.getAbsolutePath()));
            return promptForPath(prompt, defaultValue, requireValue,
                 fileMustExist, parentMustExist, mustBeFile, mustBeDirectory);
          }
          else
          {
            tool.wrapErr(0, wrapColumn,
                 ERR_INTERACTIVE_PROMPT_FILE_DOES_NOT_EXIST.get(
                      f.getAbsolutePath()));
            return promptForPath(prompt, defaultValue, requireValue,
                 fileMustExist, parentMustExist, mustBeFile, mustBeDirectory);
          }
        }
        else if (parentMustExist)
        {
          final File parent = f.getParentFile();
          if ((parent == null) || (! parent.exists()))
          {
            tool.wrapErr(0, wrapColumn,
                 ERR_INTERACTIVE_PROMPT_PARENT_DOES_NOT_EXIST.get(
                      f.getAbsolutePath()));
            return promptForPath(prompt, defaultValue, requireValue,
                 fileMustExist, parentMustExist, mustBeFile, mustBeDirectory);
          }
        }

        return f;
      }

      if (f.isDirectory())
      {
        if (mustBeFile)
        {
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_PROMPT_PATH_MUST_BE_FILE.get(
                    f.getAbsolutePath()));
          return promptForPath(prompt, defaultValue, requireValue,
               fileMustExist, parentMustExist, mustBeFile, mustBeDirectory);
        }
      }
      else if (mustBeDirectory)
      {
        tool.wrapErr(0, wrapColumn,
             ERR_INTERACTIVE_PROMPT_PATH_MUST_BE_DIR.get(
                  f.getAbsolutePath()));
        return promptForPath(prompt, defaultValue, requireValue,
             fileMustExist, parentMustExist, mustBeFile, mustBeDirectory);
      }

      return f;
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      throw new LDAPException(ResultCode.LOCAL_ERROR,
           ERR_INTERACTIVE_PROMPT_ERROR_READING_RESPONSE.get(
                StaticUtils.getExceptionMessage(e)),
           e);
    }
  }



  /**
   * Prompts the user to enter a password.
   *
   * @param  prompt         The prompt to display to the user.
   * @param  confirmPrompt  A prompt that should be displayed before prompting
   *                        for the password a second time (to confirm that the
   *                        user entered it correctly the first time).  If this
   *                        is {@code null}, then there will be no prompt for
   *                        confirmation.
   * @param  requireValue   Indicates whether a value is required.
   *
   * @return  The password obtained from the user, or {@code null} if the user
   *          did not provide a value and no value is required.
   *
   * @throws  LDAPException  If an error occurs while obtaining the value from
   *                         the user.
   */
  @Nullable()
  private byte[] promptForPassword(@NotNull final String prompt,
                                   @Nullable final String confirmPrompt,
                                   final boolean requireValue)
          throws LDAPException
  {
    tool.out();

    tool.wrapStandardOut(0, 0, wrapColumn, false, prompt, ": ");


    try
    {
      final byte[] pwBytes;
      try
      {
        if (IN_UNIT_TEST)
        {
          PasswordReader.setTestReader(systemInReader);
        }
        pwBytes = PasswordReader.readPassword();
      }
      finally
      {
        PasswordReader.setTestReader(null);
      }

      if ((pwBytes == null) || (pwBytes.length == 0))
      {
        if (requireValue)
        {
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_PROMPT_VALUE_REQUIRED.get());
          return promptForPassword(prompt, confirmPrompt, requireValue);
        }
        else
        {
          return null;
        }
      }
      else
      {
        if (confirmPrompt != null)
        {
          final byte[] confirmedPWBytes;
          try
          {
            if (IN_UNIT_TEST)
            {
              PasswordReader.setTestReader(systemInReader);
            }

            tool.wrapStandardOut(0, 0, wrapColumn, false, confirmPrompt, ": ");
            confirmedPWBytes = PasswordReader.readPassword();

            if (! Arrays.equals(pwBytes, confirmedPWBytes))
            {
              tool.wrapErr(0, wrapColumn,
                   ERR_INTERACTIVE_PROMPT_CONFIRM_MISMATCH.get());
              return promptForPassword(prompt, confirmPrompt, requireValue);
            }
          }
          finally
          {
            PasswordReader.setTestReader(null);
          }
        }

        return pwBytes;
      }
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      throw new LDAPException(ResultCode.LOCAL_ERROR,
           ERR_INTERACTIVE_PROMPT_ERROR_READING_RESPONSE.get(
                StaticUtils.getExceptionMessage(e)),
           e);
    }
  }



  /**
   * Prompts the user to enter a timestamp.
   *
   * @param  prompt        The prompt to display to the user.
   * @param  defaultValue  The value that should be selected if the user
   *                       presses ENTER without entering a value.
   * @param  requireValue  Indicates whether a value is required.
   *
   * @return  An object pair that contains both the parsed date and the string
   *          representation provided by the user, or {@code null} if the user
   *          did not provide a value, there is no default value, and no value
   *          is required.
   *
   * @throws  LDAPException  If an error occurs while obtaining the value from
   *                         the user.
   */
  @Nullable()
  private ObjectPair promptForTimestamp(
                                       @NotNull final String prompt,
                                       @Nullable final Date defaultValue,
                                       final boolean requireValue)
          throws LDAPException
  {
    tool.out();

    final String promptStr;
    if (defaultValue == null)
    {
      promptStr = prompt + ": ";
    }
    else
    {
      promptStr = prompt + " [" + defaultValue + "]: ";
    }

    tool.wrapStandardOut(0, 0, wrapColumn, false, promptStr);

    try
    {
      String line = systemInReader.readLine().trim();
      if (line.isEmpty() && (defaultValue != null))
      {
        line = String.valueOf(defaultValue);
      }

      if (line.isEmpty())
      {
        if (requireValue)
        {
          tool.wrapErr(0, wrapColumn,
               ERR_INTERACTIVE_PROMPT_VALUE_REQUIRED.get());
          return promptForTimestamp(prompt, defaultValue, requireValue);
        }
        else
        {
          return null;
        }
      }

      try
      {
        return new ObjectPair<>(TimestampArgument.parseTimestamp(line), line);
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
        tool.wrapErr(0, wrapColumn,
             ERR_INTERACTIVE_PROMPT_INVALID_TIMESTAMP.get());
        return promptForTimestamp(prompt, defaultValue, requireValue);
      }
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      throw new LDAPException(ResultCode.LOCAL_ERROR,
           ERR_INTERACTIVE_PROMPT_ERROR_READING_RESPONSE.get(
                StaticUtils.getExceptionMessage(e)),
           e);
    }
  }



  /**
   * Creates a new string with enough initial spaces to ensure that the last
   * character of the returned string is also the last character of the provided
   * string.
   *
   * @param  s  The string to be right-aligned.
   * @param  w  The number of characters to include in the string that is
   *            returned.
   *
   * @return  A right-aligned representation of the provided string in the given
   *          width.
   */
  @NotNull()
  private static String rightAlign(@NotNull final String s, final int w)
  {
    final int l = s.length();
    if (l >= w)
    {
      return s;
    }

    final StringBuilder buffer = new StringBuilder(w);
    for (int i=0; i < (w-l); i++)
    {
      buffer.append(' ');
    }

    buffer.append(s);
    return buffer.toString();
  }



  /**
   * Creates a new string with enough trailing spaces to ensure that the
   * returned string has the specified width.
   *
   * @param  s  The string to be left-aligned.
   * @param  w  The number of characters to include in the string that is
   *            returned.
   *
   * @return  A left-aligned representation of the provided string in the given
   *          width.
   */
  @NotNull()
  private static String leftAlign(@NotNull final String s, final int w)
  {
    final int l = s.length();
    if (l >= w)
    {
      return s;
    }

    final StringBuilder buffer = new StringBuilder(w);
    buffer.append(s);
    while (buffer.length() < w)
    {
      buffer.append(' ');
    }
    return buffer.toString();
  }



  /**
   * Examines the arguments provided to the tool to ensure that all required,
   * exclusive, and dependent argument set constraints have been satisfied.
   *
   * @throws  ArgumentException  If any required or exclusive argument
   *                             constraints are not satisfied.
   */
  private void validateRequiredExclusiveAndDependentArgumentSets()
          throws ArgumentException
  {
    validateRequiredExclusiveAndDependentArgumentSets(parser);

    final SubCommand selectedSubCommand = parser.getSelectedSubCommand();
    if (selectedSubCommand != null)
    {
      validateRequiredExclusiveAndDependentArgumentSets(
           selectedSubCommand.getArgumentParser());
    }
  }



  /**
   * Examines the arguments provided to the tool to ensure that all required,
   * exclusive, and dependent argument set constraints have been satisfied.
   *
   * @param  parser The argument parser to examine.
   *
   * @throws  ArgumentException  If any required or exclusive argument
   *                             constraints are not satisfied.
   */
  private static void validateRequiredExclusiveAndDependentArgumentSets(
                           @NotNull final ArgumentParser parser)
          throws ArgumentException
  {
    // Iterate through the required argument sets and make sure that at least
    // one argument from each set is present.
    for (final Set requiredArgumentsSet :
         parser.getRequiredArgumentSets())
    {
      boolean found = false;
      for (final Argument a : requiredArgumentsSet)
      {
        if (a.getNumOccurrences() > 0)
        {
          found = true;
          break;
        }
      }

      if (! found)
      {
        final StringBuilder buffer = new StringBuilder();
        for (final Argument a : requiredArgumentsSet)
        {
          if (buffer.length() > 0)
          {
            buffer.append(", ");
          }

          buffer.append(a.getIdentifierString());
        }

        throw new ArgumentException(
             ERR_INTERACTIVE_REQUIRED_ARG_SET_CONFLICT.get(buffer.toString()));
      }
    }


    // Iterate through the exclusive argument sets and make sure that none of
    // them has multiple arguments that are present.
    for (final Set exclusiveArgumentsSet :
         parser.getExclusiveArgumentSets())
    {
      boolean found = false;
      for (final Argument a : exclusiveArgumentsSet)
      {
        if (a.getNumOccurrences() > 0)
        {
          if (found)
          {
            final StringBuilder buffer = new StringBuilder();
            for (final Argument exclusiveArg : exclusiveArgumentsSet)
            {
              if (buffer.length() > 0)
              {
                buffer.append(", ");
              }

              buffer.append(exclusiveArg.getIdentifierString());
            }

            throw new ArgumentException(
                 ERR_INTERACTIVE_EXCLUSIVE_ARG_SET_CONFLICT.get(
                      buffer.toString()));
          }
          else
          {
            found = true;
          }
        }
      }
    }


    // Iterate through the dependent argument sets and make sure that all of
    // those constraints are satisfied.
    for (final ObjectPair> p :
         parser.getDependentArgumentSets())
    {
      if (p.getFirst().getNumOccurrences() > 0)
      {
        boolean found = false;
        for (final Argument a : p.getSecond())
        {
          if (a.isPresent())
          {
            found = true;
            break;
          }
        }

        if (! found)
        {
          final StringBuilder buffer = new StringBuilder();
          for (final Argument arg : p.getSecond())
          {
            if (buffer.length() > 0)
            {
              buffer.append(", ");
            }

            buffer.append(arg.getIdentifierString());
          }

          throw new ArgumentException(
               ERR_INTERACTIVE_DEPENDENT_ARG_SET_CONFLICT.get(
                    p.getFirst().getIdentifierString(),
                    buffer.toString()));
        }
      }
    }
  }



  /**
   * Specifies whether this processor is being run in a unit test environment.
   *
   * @param  inUnitTest  Indicates whether this processor is being run in a unit
   *                     test environment.
   */
  static void setInUnitTest(final boolean inUnitTest)
  {
    IN_UNIT_TEST = inUnitTest;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy