/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2014-2015 ForgeRock AS
 */
package org.opends.guitools.uninstaller;

import static org.forgerock.util.Utils.*;
import static org.opends.admin.ads.util.ConnectionUtils.*;
import static org.opends.messages.AdminToolMessages.*;
import static org.opends.messages.QuickSetupMessages.*;

import static com.forgerock.opendj.cli.ArgumentConstants.*;
import static com.forgerock.opendj.cli.Utils.*;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;

import javax.naming.NamingException;
import javax.naming.NoPermissionException;
import javax.naming.ldap.InitialLdapContext;
import javax.net.ssl.TrustManager;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.admin.ads.ADSContext;
import org.opends.admin.ads.ServerDescriptor;
import org.opends.admin.ads.TopologyCache;
import org.opends.admin.ads.TopologyCacheException;
import org.opends.admin.ads.util.ApplicationTrustManager;
import org.opends.admin.ads.util.ConnectionUtils;
import org.opends.guitools.controlpanel.datamodel.ConnectionProtocolPolicy;
import org.opends.guitools.controlpanel.datamodel.ControlPanelInfo;
import org.opends.quicksetup.Application;
import org.opends.quicksetup.ApplicationException;
import org.opends.quicksetup.Configuration;
import org.opends.quicksetup.Constants;
import org.opends.quicksetup.Installation;
import org.opends.quicksetup.ProgressStep;
import org.opends.quicksetup.Step;
import org.opends.quicksetup.UserDataException;
import org.opends.quicksetup.event.ProgressUpdateEvent;
import org.opends.quicksetup.event.ProgressUpdateListener;
import org.opends.quicksetup.util.PlainTextProgressMessageFormatter;
import org.opends.quicksetup.util.ServerController;
import org.opends.quicksetup.util.Utils;
import org.opends.server.admin.client.cli.SecureConnectionCliArgs;
import org.opends.server.util.StaticUtils;
import org.opends.server.util.cli.LDAPConnectionConsoleInteraction;

import com.forgerock.opendj.cli.ArgumentException;
import com.forgerock.opendj.cli.ClientException;
import com.forgerock.opendj.cli.ConsoleApplication;
import com.forgerock.opendj.cli.Menu;
import com.forgerock.opendj.cli.MenuBuilder;
import com.forgerock.opendj.cli.MenuResult;
import com.forgerock.opendj.cli.ReturnCode;

/**
 * The class used to provide some CLI interface in the uninstall.
 *
 * This class basically is in charge of parsing the data provided by the user
 * in the command line and displaying messages asking the user for information.
 *
 * Once the user has provided all the required information it calls Uninstaller
 * and launches it.
 *
 */
public class UninstallCliHelper extends ConsoleApplication {

  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  private UninstallerArgumentParser parser;
  private LDAPConnectionConsoleInteraction ci;
  private ControlPanelInfo info;

  private boolean forceNonInteractive;
  private boolean useSSL = true;
  private boolean useStartTLS;

  /**
   * Default constructor.
   */
  public UninstallCliHelper()
  {
    // Nothing to do.
  }

  /**
   * Creates a UserData based in the arguments provided. It asks user for
   * additional information if what is provided in the arguments is not enough.
   *
   * @param args
   *          the ArgumentParser with the allowed arguments of the command line.
   *          The code assumes that the arguments have already been parsed.
   * @param rawArguments
   *          the arguments provided in the command line.
   * @return the UserData object with what the user wants to uninstall and null
   *         if the user cancels the uninstallation.
   * @throws UserDataException
   *           if there is an error with the data in the arguments.
   * @throws ClientException
   *           If there is an error processing data in non-interactive mode and
   *           an error must be thrown (not in force on error mode).
   */
  public UninstallUserData createUserData(UninstallerArgumentParser args,
      String[] rawArguments)
  throws UserDataException, ClientException
  {
    parser = args;
    UninstallUserData userData = new UninstallUserData();
    try
    {
      boolean isInteractive;
      boolean isQuiet;
      boolean isVerbose;
      boolean isCanceled = false;

      /* Step 1: analyze the arguments.
       */

      isInteractive = args.isInteractive();

      isQuiet = args.isQuiet();

      isVerbose = args.isVerbose();

      userData.setQuiet(isQuiet);
      userData.setVerbose(isVerbose);
      userData.setForceOnError(args.isForceOnError());
      userData.setTrustManager(args.getTrustManager());

      userData.setConnectTimeout(getConnectTimeout());

      /*
       * Step 2: check that the provided parameters are compatible.
       */
      LocalizableMessageBuilder buf = new LocalizableMessageBuilder();
      int v = args.validateGlobalOptions(buf);
      if (v != ReturnCode.SUCCESS.get())
      {
        throw new UserDataException(null, buf.toMessage());
      }

      /* Step 3: If this is an interactive uninstall ask for confirmation to
       * delete the different parts of the installation if the user did not
       * specify anything to delete.  If we are not in interactive mode
       * check that the user specified something to be deleted.
       */
      Set<String> outsideDbs;
      Set<String> outsideLogs;
      Configuration config =
        Installation.getLocal().getCurrentConfiguration();
      try {
        outsideDbs = config.getOutsideDbs();
      } catch (IOException ioe) {
        outsideDbs = Collections.emptySet();
        logger.info(LocalizableMessage.raw("error determining outside databases", ioe));
      }

      try {
        outsideLogs = config.getOutsideLogs();
      } catch (IOException ioe) {
        outsideLogs = Collections.emptySet();
        logger.info(LocalizableMessage.raw("error determining outside logs", ioe));
      }

      boolean somethingSpecifiedToDelete =
        args.removeAll() ||
        args.removeBackupFiles() ||
        args.removeDatabases() ||
        args.removeLDIFFiles() ||
        args.removeConfigurationFiles() ||
        args.removeLogFiles() ||
        args.removeServerLibraries();

      if (somethingSpecifiedToDelete)
      {
        userData.setRemoveBackups(args.removeAll() || args.removeBackupFiles());
        userData.setRemoveConfigurationAndSchema(args.removeAll() ||
            args.removeConfigurationFiles());
        userData.setRemoveDatabases(args.removeAll() || args.removeDatabases());
        userData.setRemoveLDIFs(args.removeAll() || args.removeLDIFFiles());
        userData.setRemoveLibrariesAndTools(args.removeAll() ||
            args.removeServerLibraries());
        userData.setRemoveLogs(args.removeAll() || args.removeLogFiles());

        userData.setExternalDbsToRemove(outsideDbs);
        userData.setExternalLogsToRemove(outsideLogs);
      }
      else if (!isInteractive)
      {
        throw new UserDataException(null,
           ERR_CLI_UNINSTALL_NOTHING_TO_BE_UNINSTALLED_NON_INTERACTIVE.get());
      }
      else
      {
        isCanceled = askWhatToDelete(userData, outsideDbs, outsideLogs);
      }
      String adminUid = args.getAdministratorUID();
      if (adminUid == null && !args.isInteractive())
      {
        adminUid = args.getDefaultAdministratorUID();
      }
      userData.setAdminUID(adminUid);
      userData.setAdminPwd(args.getBindPassword());
      String referencedHostName = args.getReferencedHostName();
      if (referencedHostName == null && !args.isInteractive())
      {
        referencedHostName = args.getDefaultReferencedHostName();
      }
      try
      {
        UninstallData d = new UninstallData(Installation.getLocal());
        userData.setReplicationServer(
            referencedHostName+":"+d.getReplicationServerPort());
      }
      catch (Throwable t)
      {
        logger.error(LocalizableMessage.raw("Could not create UninstallData: "+t, t));
        userData.setReplicationServer(
            referencedHostName+":8989");
      }
      info = ControlPanelInfo.getInstance();
      info.setTrustManager(userData.getTrustManager());
      info.setConnectTimeout(getConnectTimeout());
      info.regenerateDescriptor();
      info.setConnectionPolicy(ConnectionProtocolPolicy.USE_ADMIN);

      String adminConnectorUrl = info.getAdminConnectorURL();

      if (adminConnectorUrl == null)
      {
        logger.warn(LocalizableMessage.raw(
        "Error retrieving a valid LDAP URL in conf file."));
        if (!parser.isInteractive())
        {
          LocalizableMessage msg = ERR_COULD_NOT_FIND_VALID_LDAPURL.get();
          throw new ClientException(ReturnCode.APPLICATION_ERROR, msg);
        }
      }
      userData.setLocalServerUrl(adminConnectorUrl);
      userData.setReferencedHostName(referencedHostName);

      /*
       * Step 4: check if server is running.  Depending if it is running and the
       * OS we are running, ask for authentication information.
       */
      if (!isCanceled)
      {
        isCanceled = checkServerState(userData);
      }

      if (isCanceled && !userData.isForceOnError())
      {
        logger.info(LocalizableMessage.raw("User cancelled uninstall."));
        userData = null;
      }

      if (userData != null && !args.isQuiet())
      {
        println();
      }
    }
    catch (Throwable t)
    {
      logger.warn(LocalizableMessage.raw("Exception: "+t, t));
      if (t instanceof UserDataException)
      {
        throw (UserDataException)t;
      }
      else if (t instanceof ClientException)
      {
        throw (ClientException)t;
      }
      else
      {
        throw new IllegalStateException("Unexpected error: "+t, t);
      }
    }
    logger.info(LocalizableMessage.raw("Successfully created user data"));
    return userData;
  }

  /**
   * Commodity method used to ask the user to confirm the deletion of certain
   * parts of the server.  It updates the provided UserData object
   * accordingly.  Returns <CODE>true</CODE> if the user cancels and <CODE>
   * false</CODE> otherwise.
   * @param userData the UserData object to be updated.
   * @param outsideDbs the set of relative paths of databases located outside
   * the installation path of the server.
   * @param outsideLogs the set of relative paths of log files located outside
   * the installation path of the server.
   * @return <CODE>true</CODE> if the user cancels and <CODE>false</CODE>
   * otherwise.
   */
  private boolean askWhatToDelete(UninstallUserData userData,
      Set<String> outsideDbs, Set<String> outsideLogs) throws UserDataException
  {
    boolean cancelled = false;
    final int REMOVE_ALL = 1;
    final int SPECIFY_TO_REMOVE = 2;
    int[] indexes = {REMOVE_ALL, SPECIFY_TO_REMOVE};
    LocalizableMessage[] msgs = new LocalizableMessage[] {
        INFO_CLI_UNINSTALL_REMOVE_ALL.get(),
        INFO_CLI_UNINSTALL_SPECIFY_WHAT_REMOVE.get()
      };

    MenuBuilder<Integer> builder = new MenuBuilder<>(this);
    builder.setPrompt(INFO_CLI_UNINSTALL_WHAT_TO_DELETE.get());

    for (int i=0; i<indexes.length; i++)
    {
      builder.addNumberedOption(msgs[i], MenuResult.success(indexes[i]));
    }

    builder.addQuitOption();

    builder.setDefault(LocalizableMessage.raw(String.valueOf(REMOVE_ALL)),
        MenuResult.success(REMOVE_ALL));

    builder.setMaxTries(CONFIRMATION_MAX_TRIES);

    Menu<Integer> menu = builder.toMenu();
    int choice;
    try
    {
      MenuResult<Integer> m = menu.run();
      if (m.isSuccess())
      {
        choice = m.getValue();
      }
      else if (m.isQuit())
      {
        choice = REMOVE_ALL;
        cancelled = true;
      }
      else
      {
        // Should never happen.
        throw new RuntimeException();
      }
    }
    catch (ClientException ce)
    {
      logger.warn(LocalizableMessage.raw("Error reading input: "+ce, ce));
      throw new UserDataException(null, ce.getMessageObject(), ce);
    }

    if (cancelled)
    {
      // Nothing to do
    }
    else if (choice == REMOVE_ALL)
    {
      userData.setRemoveBackups(true);
      userData.setRemoveConfigurationAndSchema(true);
      userData.setRemoveDatabases(true);
      userData.setRemoveLDIFs(true);
      userData.setRemoveLibrariesAndTools(true);
      userData.setRemoveLogs(true);

      userData.setExternalDbsToRemove(outsideDbs);
      userData.setExternalLogsToRemove(outsideLogs);
    }
    else
    {
      boolean somethingSelected = false;
      while (!somethingSelected && !cancelled)
      {
        println();
//      Ask for confirmation for the different items
        msgs = new LocalizableMessage [] {
                INFO_CLI_UNINSTALL_CONFIRM_LIBRARIES_BINARIES.get(),
                INFO_CLI_UNINSTALL_CONFIRM_DATABASES.get(),
                INFO_CLI_UNINSTALL_CONFIRM_LOGS.get(),
                INFO_CLI_UNINSTALL_CONFIRM_CONFIGURATION_SCHEMA.get(),
                INFO_CLI_UNINSTALL_CONFIRM_BACKUPS.get(),
                INFO_CLI_UNINSTALL_CONFIRM_LDIFS.get(),
                INFO_CLI_UNINSTALL_CONFIRM_OUTSIDEDBS.get(
                        joinAsString(Constants.LINE_SEPARATOR, outsideDbs)),
                INFO_CLI_UNINSTALL_CONFIRM_OUTSIDELOGS.get(
                        joinAsString(Constants.LINE_SEPARATOR, outsideLogs)
                )
        };

        boolean[] answers = new boolean[msgs.length];
        try
        {
          for (int i=0; i<msgs.length; i++)
          {
            boolean ignore = (i == 6 && outsideDbs.isEmpty())
                || (i == 7 && outsideLogs.isEmpty());
            if (!ignore)
            {
              answers[i] = askConfirmation(msgs[i], true, logger);
            }
            else
            {
              answers[i] = false;
            }
          }
        }
        catch (ClientException ce)
        {
          throw new UserDataException(null, ce.getMessageObject(), ce);
        }

        if (!cancelled)
        {
          for (int i=0; i<answers.length; i++)
          {
            switch (i)
            {
            case 0:
              userData.setRemoveLibrariesAndTools(answers[i]);
              break;

            case 1:
              userData.setRemoveDatabases(answers[i]);
              break;

            case 2:
              userData.setRemoveLogs(answers[i]);
              break;

            case 3:
              userData.setRemoveConfigurationAndSchema(answers[i]);
              break;

            case 4:
              userData.setRemoveBackups(answers[i]);
              break;

            case 5:
              userData.setRemoveLDIFs(answers[i]);
              break;

            case 6:
              if (answers[i])
              {
                userData.setExternalDbsToRemove(outsideDbs);
              }
              break;

            case 7:
              if (answers[i])
              {
                userData.setExternalLogsToRemove(outsideLogs);
              }
              break;
            }
          }
          if (userData.getExternalDbsToRemove().isEmpty() &&
              userData.getExternalLogsToRemove().isEmpty() &&
              !userData.getRemoveLibrariesAndTools() &&
              !userData.getRemoveDatabases() &&
              !userData.getRemoveConfigurationAndSchema() &&
              !userData.getRemoveBackups() &&
              !userData.getRemoveLDIFs() &&
              !userData.getRemoveLogs())
          {
            somethingSelected = false;
            println();
            printErrorMessage(
                ERR_CLI_UNINSTALL_NOTHING_TO_BE_UNINSTALLED.get());
          }
          else
          {
            somethingSelected = true;
          }
        }
      }
    }

    return cancelled;
  }

  /**
   * Commodity method used to ask the user (when necessary) if the server must
   * be stopped or not. It also prompts (if required) for authentication.
   *
   * @param userData
   *          the UserData object to be updated with the authentication of the
   *          user.
   * @return <CODE>true</CODE> if the user wants to continue with uninstall and
   *         <CODE>false</CODE> otherwise.
   * @throws UserDataException
   *           if there is a problem with the data provided by the user (in the
   *           particular case where we are on non-interactive uninstall and
   *           some data is missing or not valid).
   * @throws ClientException
   *           If there is an error processing data in non-interactive mode and
   *           an error must be thrown (not in force on error mode).
   */
  private boolean checkServerState(UninstallUserData userData)
  throws UserDataException, ClientException
  {
    boolean cancelled = false;
    boolean interactive = parser.isInteractive();
    boolean forceOnError = parser.isForceOnError();
    UninstallData conf = null;
    try
    {
      conf = new UninstallData(Installation.getLocal());
    }
    catch (Throwable t)
    {
      logger.warn(LocalizableMessage.raw("Error processing task: "+t, t));
      throw new UserDataException(Step.CONFIRM_UNINSTALL,
          getThrowableMsg(INFO_BUG_MSG.get(), t));
    }
    logger.info(LocalizableMessage.raw("interactive: "+interactive));
    logger.info(LocalizableMessage.raw("forceOnError: "+forceOnError));
    logger.info(LocalizableMessage.raw("conf.isADS(): "+conf.isADS()));
    logger.info(LocalizableMessage.raw("conf.isReplicationServer(): "+
        conf.isReplicationServer()));
    logger.info(LocalizableMessage.raw("conf.isServerRunning(): "+conf.isServerRunning()));
    if (conf.isADS() && conf.isReplicationServer())
    {
      if (conf.isServerRunning())
      {
        if (interactive)
        {
          try
          {
            println();
            if (confirmToUpdateRemote())
            {
              cancelled = !askForAuthenticationIfNeeded(userData);
              if (cancelled)
              {
                /* Ask for confirmation to stop server */
                println();
                cancelled = !confirmToStopServer();
              }
              else
              {
                cancelled = !updateUserUninstallDataWithRemoteServers(userData);
                if (cancelled)
                {
                  println();
                  /* Ask for confirmation to stop server */
                  cancelled = !confirmToStopServer();
                }
              }
            }
            else
            {
              /* Ask for confirmation to stop server */
              cancelled = !confirmToStopServer();
            }
          }
          catch (ClientException ce)
          {
            throw new UserDataException(null, ce.getMessageObject(), ce);
          }
        }
        else
        {
          boolean errorWithRemote =
            !updateUserUninstallDataWithRemoteServers(userData);
          cancelled = errorWithRemote && !parser.isForceOnError();
          logger.info(LocalizableMessage.raw("Non interactive mode.  errorWithRemote: "+
              errorWithRemote));
        }
      }
      else if (interactive)
      {
        println();
        try
        {
          if (confirmToUpdateRemoteAndStart())
          {
            boolean startWorked = startServer(userData.isQuiet());
            // Ask for authentication if needed, etc.
            if (startWorked)
            {
              cancelled = !askForAuthenticationIfNeeded(userData);
              if (cancelled)
              {
                println();
                /* Ask for confirmation to stop server */
                cancelled = !confirmToStopServer();
              }
              else
              {
                cancelled =
                  !updateUserUninstallDataWithRemoteServers(userData);
                if (cancelled)
                {
                  println();
                  /* Ask for confirmation to stop server */
                  cancelled = !confirmToStopServer();
                }
              }
              userData.setStopServer(true);
            }
            else
            {
              userData.setStopServer(false);
              println();
              /* Ask for confirmation to delete files */
              cancelled = !confirmDeleteFiles();
            }
          }
          else
          {
            println();
            /* Ask for confirmation to delete files */
            cancelled = !confirmDeleteFiles();
          }
        }
        catch (ClientException ce)
        {
          throw new UserDataException(null, ce.getMessageObject(), ce);
        }
      }
      else
      {
        boolean startWorked = startServer(userData.isQuiet());
        // Ask for authentication if needed, etc.
        if (startWorked)
        {
          userData.setStopServer(true);
          boolean errorWithRemote =
            !updateUserUninstallDataWithRemoteServers(userData);
          cancelled = errorWithRemote && !parser.isForceOnError();
        }
        else
        {
          cancelled  = !forceOnError;
          userData.setStopServer(false);
        }
      }
      if (!cancelled || parser.isForceOnError())
      {
        /* During all the confirmations, the server might be stopped. */
        userData.setStopServer(
            Installation.getLocal().getStatus().isServerRunning());
        logger.info(LocalizableMessage.raw("Must stop the server after confirmations? "+
            userData.getStopServer()));
      }
    }
    else if (conf.isServerRunning())
    {
      try
      {
        if (interactive)
        {
          println();
          /* Ask for confirmation to stop server */
          cancelled = !confirmToStopServer();
        }

        if (!cancelled)
        {
          /* During all the confirmations, the server might be stopped. */
          userData.setStopServer(
              Installation.getLocal().getStatus().isServerRunning());
          logger.info(LocalizableMessage.raw("Must stop the server after confirmations? "+
              userData.getStopServer()));
        }
      }
      catch (ClientException ce)
      {
        throw new UserDataException(null, ce.getMessageObject(), ce);
      }
    }
    else
    {
      userData.setStopServer(false);
      if (interactive)
      {
        println();
        /* Ask for confirmation to delete files */
        try
        {
          cancelled = !confirmDeleteFiles();
        }
        catch (ClientException ce)
        {
          throw new UserDataException(null, ce.getMessageObject(), ce);
        }
      }
    }
    logger.info(LocalizableMessage.raw("cancelled: "+cancelled));
    return cancelled;
  }

  /**
   *  Ask for confirmation to stop server.
   *  @return <CODE>true</CODE> if the user wants to continue and stop the
   *  server.  <CODE>false</CODE> otherwise.
   *  @throws ClientException if the user reached the confirmation limit.
   */
  private boolean confirmToStopServer() throws ClientException
  {
    return askConfirmation(INFO_CLI_UNINSTALL_CONFIRM_STOP.get(), true, logger);
  }

  /**
   *  Ask for confirmation to delete files.
   *  @return <CODE>true</CODE> if the user wants to continue and delete the
   *  files.  <CODE>false</CODE> otherwise.
   *  @throws ClientException if the user reached the confirmation limit.
   */
  private boolean confirmDeleteFiles() throws ClientException
  {
    return askConfirmation(INFO_CLI_UNINSTALL_CONFIRM_DELETE_FILES.get(), true,
        logger);
  }

  /**
   *  Ask for confirmation to update configuration on remote servers.
   *  @return <CODE>true</CODE> if the user wants to continue and stop the
   *  server.  <CODE>false</CODE> otherwise.
   *  @throws ClientException if the user reached the confirmation limit.
   */
  private boolean confirmToUpdateRemote() throws ClientException
  {
    return askConfirmation(INFO_CLI_UNINSTALL_CONFIRM_UPDATE_REMOTE.get(), true,
        logger);
  }

  /**
   *  Ask for confirmation to update configuration on remote servers.
   *  @return <CODE>true</CODE> if the user wants to continue and stop the
   *  server.  <CODE>false</CODE> otherwise.
   *  @throws ClientException if the user reached the confirmation limit.
   */
  private boolean confirmToUpdateRemoteAndStart() throws ClientException
  {
    return askConfirmation(
        INFO_CLI_UNINSTALL_CONFIRM_UPDATE_REMOTE_AND_START.get(), true, logger);
  }

  /**
   *  Ask for confirmation to provide again authentication.
   *  @return <CODE>true</CODE> if the user wants to provide authentication
   *  again.  <CODE>false</CODE> otherwise.
   *  @throws ClientException if the user reached the confirmation limit.
   */
  private boolean promptToProvideAuthenticationAgain() throws ClientException
  {
    return askConfirmation(
        INFO_UNINSTALL_CONFIRM_PROVIDE_AUTHENTICATION_AGAIN.get(), true, logger);
  }

  /**
   * Ask for data required to update configuration on remote servers. If all the
   * data is provided and validated, we assume that the user wants to update the
   * remote servers.
   *
   * @return <CODE>true</CODE> if the user wants to continue and update the
   *         remote servers. <CODE>false</CODE> otherwise.
   * @throws UserDataException
   *           if there is a problem with the information provided by the user.
   * @throws ClientException
   *           If there is an error processing data.
   */
  private boolean askForAuthenticationIfNeeded(UninstallUserData userData)
  throws UserDataException, ClientException
  {
    boolean accepted = true;
    String uid = userData.getAdminUID();
    String pwd = userData.getAdminPwd();

    boolean couldConnect = false;

    while (!couldConnect && accepted)
    {

      // This is done because we do not need to ask the user about these
      // parameters.  If we force their presence the class
      // LDAPConnectionConsoleInteraction will not prompt the user for
      // them.
      SecureConnectionCliArgs secureArgsList = parser.getSecureArgsList();

      secureArgsList.hostNameArg.setPresent(true);
      secureArgsList.portArg.setPresent(true);
      secureArgsList.hostNameArg.clearValues();
      secureArgsList.hostNameArg.addValue(
          secureArgsList.hostNameArg.getDefaultValue());
      secureArgsList.portArg.clearValues();
      secureArgsList.portArg.addValue(
          secureArgsList.portArg.getDefaultValue());
      secureArgsList.bindDnArg.clearValues();
      if (uid != null)
      {
        secureArgsList.bindDnArg.addValue(ADSContext.getAdministratorDN(uid));
        secureArgsList.bindDnArg.setPresent(true);
      }
      else
      {
        secureArgsList.bindDnArg.setPresent(false);
      }
      secureArgsList.bindPasswordArg.clearValues();
      if (pwd != null)
      {
        secureArgsList.bindPasswordArg.addValue(pwd);
        secureArgsList.bindPasswordArg.setPresent(true);
      }
      else
      {
        secureArgsList.bindPasswordArg.setPresent(false);
      }

      if (ci == null)
      {
        ci =
        new LDAPConnectionConsoleInteraction(this, parser.getSecureArgsList());
        ci.setDisplayLdapIfSecureParameters(true);
      }

      try
      {
        ci.run(false);
        userData.setAdminUID(ci.getAdministratorUID());
        userData.setAdminPwd(ci.getBindPassword());

        info.setConnectionPolicy(ConnectionProtocolPolicy.USE_ADMIN);

        String adminConnectorUrl = info.getAdminConnectorURL();
        if (adminConnectorUrl == null)
        {
          logger.warn(LocalizableMessage.raw(
         "Error retrieving a valid Administration Connector URL in conf file."));
          LocalizableMessage msg = ERR_COULD_NOT_FIND_VALID_LDAPURL.get();
            throw new ClientException(ReturnCode.APPLICATION_ERROR, msg);
        }
        try
        {
          URI uri = new URI(adminConnectorUrl);
          int port = uri.getPort();
          secureArgsList.portArg.clearValues();
          secureArgsList.portArg.addValue(String.valueOf(port));
          ci.setPortNumber(port);
        }
        catch (Throwable t)
        {
          logger.error(LocalizableMessage.raw("Error parsing url: "+adminConnectorUrl));
        }
        updateTrustManager(userData, ci);

        info.setConnectionPolicy(ConnectionProtocolPolicy.USE_ADMIN);

        adminConnectorUrl = info.getAdminConnectorURL();

        if (adminConnectorUrl == null)
        {
          logger.warn(LocalizableMessage.raw(
         "Error retrieving a valid Administration Connector URL in conf file."));
          LocalizableMessage msg = ERR_COULD_NOT_FIND_VALID_LDAPURL.get();
          throw new ClientException(ReturnCode.APPLICATION_ERROR, msg);
        }

        userData.setLocalServerUrl(adminConnectorUrl);
        couldConnect = true;
      }
      catch (ArgumentException e)
      {
        parser.displayMessageAndUsageReference(getErrStream(), e.getMessageObject());
      }
      catch (ClientException e) {
        printErrorMessage(e.getMessageObject());
        println();
      }

      if (!couldConnect)
      {
        try
        {
          accepted = promptToProvideAuthenticationAgain();
          if (accepted)
          {
            uid = null;
            pwd = null;
          }
        }
        catch (ClientException ce)
        {
          throw new UserDataException(null, ce.getMessageObject(), ce);
        }
      }
    }

    if (accepted)
    {
      String referencedHostName = parser.getReferencedHostName();
      while (referencedHostName == null)
      {
        println();
        referencedHostName = askForReferencedHostName(userData.getHostName());
      }
      try
      {
        UninstallData d = new UninstallData(Installation.getLocal());
        userData.setReplicationServer(
            referencedHostName+":"+d.getReplicationServerPort());
        userData.setReferencedHostName(referencedHostName);
      }
      catch (Throwable t)
      {
        logger.error(LocalizableMessage.raw("Could not create UninstallData: "+t, t));
      }
    }
    userData.setUpdateRemoteReplication(accepted);
    return accepted;
  }

  private String askForReferencedHostName(String defaultHostName)
  {
    String s = defaultHostName;
    try
    {
      s = readInput(INFO_UNINSTALL_CLI_REFERENCED_HOSTNAME_PROMPT.get(),
          defaultHostName);
    }
    catch (ClientException ce)
    {
      logger.warn(LocalizableMessage.raw("Error reading input: %s", ce), ce);
    }
    return s;
  }

  private boolean startServer(boolean suppressOutput)
  {
    logger.info(LocalizableMessage.raw("startServer, suppressOutput: " + suppressOutput));
    boolean serverStarted = false;
    Application application = new Application()
    {
      /** {@inheritDoc} */
      @Override
      public String getInstallationPath()
      {
        return Installation.getLocal().getRootDirectory().getAbsolutePath();
      }
      /** {@inheritDoc} */
      @Override
      public String getInstancePath()
      {
        String installPath =  getInstallationPath();

        // look for <installPath>/lib/resource.loc
        String instancePathFileName = installPath + File.separator + "lib"
        + File.separator + "resource.loc";
        File f = new File(instancePathFileName);

        if (! f.exists())
        {
          return installPath;
        }

        BufferedReader reader;
        try
        {
          reader = new BufferedReader(new FileReader(instancePathFileName));
        }
        catch (Exception e)
        {
          return installPath;
        }


        // Read the first line and close the file.
        String line;
        try
        {
          line = reader.readLine();
          return new File(line).getAbsolutePath();
        }
        catch (Exception e)
        {
          return installPath;
        }
        finally
        {
          StaticUtils.close(reader);
        }
      }
      /** {@inheritDoc} */
      @Override
      public ProgressStep getCurrentProgressStep()
      {
        return UninstallProgressStep.NOT_STARTED;
      }
      /** {@inheritDoc} */
      @Override
      public Integer getRatio(ProgressStep step)
      {
        return 0;
      }
      /** {@inheritDoc} */
      @Override
      public LocalizableMessage getSummary(ProgressStep step)
      {
        return null;
      }
      /** {@inheritDoc} */
      @Override
      public boolean isFinished()
      {
        return false;
      }
      /** {@inheritDoc} */
      @Override
      public boolean isCancellable()
      {
        return false;
      }
      /** {@inheritDoc} */
      @Override
      public void cancel()
      {
      }
      /** {@inheritDoc} */
      @Override
      public void run()
      {
      }
    };
    application.setProgressMessageFormatter(
        new PlainTextProgressMessageFormatter());
    if (!suppressOutput)
    {
      application.addProgressUpdateListener(
          new ProgressUpdateListener() {
            @Override
            public void progressUpdate(ProgressUpdateEvent ev) {
              System.out.print(ev.getNewLogs().toString());
              System.out.flush();
            }
          });
    }
    ServerController controller = new ServerController(application,
        Installation.getLocal());
    try
    {
      if (!suppressOutput)
      {
        println();
      }
      controller.startServer(suppressOutput);
      if (!suppressOutput)
      {
        println();
      }
      serverStarted = Installation.getLocal().getStatus().isServerRunning();
      logger.info(LocalizableMessage.raw("server started successfully. serverStarted: "+
          serverStarted));
    }
    catch (ApplicationException ae)
    {
      logger.warn(LocalizableMessage.raw("ApplicationException: "+ae, ae));
      if (!suppressOutput)
      {
        printErrorMessage(ae.getMessageObject());
      }
    }
    catch (Throwable t)
    {
      logger.error(LocalizableMessage.raw("Unexpected error: "+t, t));
      throw new IllegalStateException("Unexpected error: "+t, t);
    }
    return serverStarted;
  }

  /**
   * Returns an InitialLdapContext using the provided parameters. We try to
   * guarantee that the connection is able to read the configuration.
   *
   * @param host
   *          the host name.
   * @param port
   *          the port to connect.
   * @param useSSL
   *          whether to use SSL or not.
   * @param useStartTLS
   *          whether to use StartTLS or not.
   * @param bindDn
   *          the bind dn to be used.
   * @param pwd
   *          the password.
   * @param connectTimeout
   *          the timeout in milliseconds to connect to the server.
   * @param trustManager
   *          the trust manager.
   * @return an InitialLdapContext connected.
   * @throws NamingException
   *           if there was an error establishing the connection.
   */
  private InitialLdapContext createAdministrativeContext(String host,
      int port, boolean useSSL, boolean useStartTLS, String bindDn, String pwd,
      int connectTimeout, ApplicationTrustManager trustManager)
      throws NamingException
  {
    InitialLdapContext ctx;
    String ldapUrl = ConnectionUtils.getLDAPUrl(host, port, useSSL);
    if (useSSL)
    {
      ctx = createLdapsContext(ldapUrl, bindDn, pwd, connectTimeout, null, trustManager, null);
    }
    else if (useStartTLS)
    {
      ctx =
          Utils.createStartTLSContext(ldapUrl, bindDn, pwd, connectTimeout,
              null, trustManager, null);
    }
    else
    {
      ctx = createLdapContext(ldapUrl, bindDn, pwd, connectTimeout, null);
    }
    if (!ConnectionUtils.connectedAsAdministrativeUser(ctx))
    {
      throw new NoPermissionException(ERR_NOT_ADMINISTRATIVE_USER.get()
          .toString());
    }
    return ctx;
  }

  /**
   * Updates the contents of the UninstallUserData while trying to connect to
   * the remote servers. It returns <CODE>true</CODE> if we could connect to the
   * remote servers and all the presented certificates were accepted and
   * <CODE>false</CODE> otherwise. continue if
   *
   * @param userData
   *          the user data to be updated.
   * @return <CODE>true</CODE> if we could connect to the remote servers and all
   *         the presented certificates were accepted and <CODE>false</CODE>
   *         otherwise.
   * @throws UserDataException
   *           if were are not in interactive mode and not in force on error
   *           mode and the operation must be stopped.
   * @throws ClientException
   *           If there is an error processing data in non-interactive mode and
   *           an error must be thrown (not in force on error mode).
   */
  private boolean updateUserUninstallDataWithRemoteServers(
      UninstallUserData userData) throws UserDataException, ClientException
  {
    boolean accepted = false;
    boolean interactive = parser.isInteractive();
    boolean forceOnError = parser.isForceOnError();

    boolean exceptionOccurred = true;

    LocalizableMessage exceptionMsg = null;

    logger.info(LocalizableMessage.raw("Updating user data with remote servers."));

    InitialLdapContext ctx = null;
    try
    {
      info.setTrustManager(userData.getTrustManager());
      info.setConnectTimeout(getConnectTimeout());
      String host = "localhost";
      int port = 389;
      String adminUid = userData.getAdminUID();
      String pwd = userData.getAdminPwd();
      String dn = ADSContext.getAdministratorDN(adminUid);

      info.setConnectionPolicy(ConnectionProtocolPolicy.USE_ADMIN);
      String adminConnectorUrl = info.getAdminConnectorURL();
      try
      {
        URI uri = new URI(adminConnectorUrl);
        host = uri.getHost();
        port = uri.getPort();
      }
      catch (Throwable t)
      {
        logger.error(LocalizableMessage.raw("Error parsing url: "+adminConnectorUrl));
      }
      ctx = createAdministrativeContext(host, port, useSSL, useStartTLS, dn,
          pwd, getConnectTimeout(),
          userData.getTrustManager());

      ADSContext adsContext = new ADSContext(ctx);
      if (interactive && userData.getTrustManager() == null)
      {
        // This is required when the user did  connect to the server using SSL
        // or Start TLS in interactive mode.  In this case
        // LDAPConnectionInteraction.run does not initialize the keystore and
        // the trust manager is null.
        forceTrustManagerInitialization();
        updateTrustManager(userData, ci);
      }
      logger.info(LocalizableMessage.raw("Reloading topology"));
      TopologyCache cache = new TopologyCache(adsContext,
          userData.getTrustManager(), getConnectTimeout());
      cache.getFilter().setSearchMonitoringInformation(false);
      cache.reloadTopology();

      accepted = handleTopologyCache(cache, userData);

      exceptionOccurred = false;
    }
    catch (NamingException ne)
    {
      logger.warn(LocalizableMessage.raw("Error connecting to server: "+ne, ne));
      if (isCertificateException(ne))
      {
        String details = ne.getMessage() != null ?
            ne.getMessage() : ne.toString();
        exceptionMsg = INFO_ERROR_READING_CONFIG_LDAP_CERTIFICATE.get(details);
      }
      else
      {
        exceptionMsg = getThrowableMsg(INFO_ERROR_CONNECTING_TO_LOCAL.get(), ne);
      }
    } catch (TopologyCacheException te)
    {
      logger.warn(LocalizableMessage.raw("Error connecting to server: "+te, te));
      exceptionMsg = Utils.getMessage(te);

    } catch (ClientException ce)
    {
      throw ce;

    } catch (Throwable t)
    {
      logger.warn(LocalizableMessage.raw("Error connecting to server: "+t, t));
      exceptionMsg = getThrowableMsg(INFO_BUG_MSG.get(), t);
    }
    finally
    {
      StaticUtils.close(ctx);
    }
    if (exceptionOccurred)
    {
      if (!interactive)
      {
        if (forceOnError)
        {
          println();
          printErrorMessage(ERR_UNINSTALL_ERROR_UPDATING_REMOTE_FORCE.get(
              "--"+parser.getSecureArgsList().adminUidArg.getLongIdentifier(),
              "--"+OPTION_LONG_BINDPWD,
              "--"+OPTION_LONG_BINDPWD_FILE,
              exceptionMsg));
        }
        else
        {
          println();
          throw new UserDataException(null,
              ERR_UNINSTALL_ERROR_UPDATING_REMOTE_NO_FORCE.get(
                  "--"+
                  parser.getSecureArgsList().adminUidArg.getLongIdentifier(),
                  "--"+OPTION_LONG_BINDPWD,
                  "--"+OPTION_LONG_BINDPWD_FILE,
                  "--"+parser.forceOnErrorArg.getLongIdentifier(),
                  exceptionMsg));
        }
      }
      else
      {
        try
        {
          accepted = askConfirmation(
              ERR_UNINSTALL_NOT_UPDATE_REMOTE_PROMPT.get(),
              false, logger);
        }
        catch (ClientException ce)
        {
          throw new UserDataException(null, ce.getMessageObject(), ce);
        }
      }
    }
    userData.setUpdateRemoteReplication(accepted);
    logger.info(LocalizableMessage.raw("accepted: "+accepted));
    return accepted;
  }

  /**
   * Method that interacts with the user depending on what errors where
   * encountered in the TopologyCache object.  This method assumes that the
   * TopologyCache has been reloaded.
   * Returns <CODE>true</CODE> if the user accepts all the problems encountered
   * and <CODE>false</CODE> otherwise.
   * @param userData the user data.
   * @throws UserDataException if there is an error with the information
   * provided by the user when we are in non-interactive mode.
   * @throws ClientException if there is an error processing data in
   * non-interactive mode and an error must be thrown (not in force on error
   * mode).
   */
  private boolean handleTopologyCache(TopologyCache cache,
      UninstallUserData userData) throws UserDataException, ClientException
  {
    boolean returnValue;
    boolean stopProcessing = false;
    boolean reloadTopologyCache = false;

    logger.info(LocalizableMessage.raw("Handle topology cache."));

    Set<TopologyCacheException> exceptions = new HashSet<>();
    /* Analyze if we had any exception while loading servers.  For the moment
     * only throw the exception found if the user did not provide the
     * Administrator DN and this caused a problem authenticating in one server
     * or if there is a certificate problem.
     */
    Set<ServerDescriptor> servers = cache.getServers();
    userData.setRemoteServers(servers);
    for (ServerDescriptor server : servers)
    {
      TopologyCacheException e = server.getLastException();
      if (e != null)
      {
        exceptions.add(e);
      }
    }
    Set<LocalizableMessage> exceptionMsgs = new LinkedHashSet<>();
    /* Check the exceptions and see if we throw them or not. */
    for (TopologyCacheException e : exceptions)
    {
      logger.info(LocalizableMessage.raw("Analyzing exception: "+e, e));
      if (stopProcessing)
      {
        break;
      }
      switch (e.getType())
      {
      case NOT_GLOBAL_ADMINISTRATOR:
        println();
        printErrorMessage(INFO_NOT_GLOBAL_ADMINISTRATOR_PROVIDED.get());
        stopProcessing = true;
        break;
      case GENERIC_CREATING_CONNECTION:
        if (isCertificateException(e.getCause()))
        {
          if (isInteractive())
          {
            println();
            stopProcessing = true;
            if (ci.promptForCertificateConfirmation(e.getCause(),
                e.getTrustManager(), e.getLdapUrl(), logger))
            {
              reloadTopologyCache = true;
              updateTrustManager(userData, ci);
            }
          }
          else
          {
            exceptionMsgs.add(
                INFO_ERROR_READING_CONFIG_LDAP_CERTIFICATE_SERVER.get(
                e.getHostPort(), e.getCause().getMessage()));
          }
        }
        else
        {
          exceptionMsgs.add(Utils.getMessage(e));
        }
        break;
      default:
        exceptionMsgs.add(Utils.getMessage(e));
      }
    }
    if (isInteractive())
    {
      if (!stopProcessing && !exceptionMsgs.isEmpty())
      {
        println();
        try
        {
          returnValue = askConfirmation(
            ERR_UNINSTALL_READING_REGISTERED_SERVERS_CONFIRM_UPDATE_REMOTE.get(
                Utils.getMessageFromCollection(exceptionMsgs,
                  Constants.LINE_SEPARATOR)), true, logger);
        }
        catch (ClientException ce)
        {
          throw new UserDataException(null, ce.getMessageObject(), ce);
        }
      }
      else if (reloadTopologyCache)
      {
       returnValue = updateUserUninstallDataWithRemoteServers(userData);
      }
      else
      {
        returnValue = !stopProcessing;
      }
    }
    else
    {
      logger.info(LocalizableMessage.raw("exceptionMsgs: "+exceptionMsgs));
      if (!exceptionMsgs.isEmpty())
      {
        if (parser.isForceOnError())
        {
          LocalizableMessage msg = Utils.getMessageFromCollection(exceptionMsgs,
              Constants.LINE_SEPARATOR);
          println();
          printErrorMessage(msg);
          returnValue = false;
        }
        else
        {
          LocalizableMessage msg =
            ERR_UNINSTALL_ERROR_UPDATING_REMOTE_NO_FORCE.get(
              "--"+
              parser.getSecureArgsList().adminUidArg.getLongIdentifier(),
              "--"+OPTION_LONG_BINDPWD,
              "--"+OPTION_LONG_BINDPWD_FILE,
              "--"+parser.forceOnErrorArg.getLongIdentifier(),
              Utils.getMessageFromCollection(exceptionMsgs,
                  Constants.LINE_SEPARATOR));
          throw new ClientException(ReturnCode.APPLICATION_ERROR, msg);
        }
      }
      else
      {
        returnValue = true;
      }
    }
    logger.info(LocalizableMessage.raw("Return value: "+returnValue));
    return returnValue;
  }

  /** {@inheritDoc} */
  @Override
  public boolean isAdvancedMode() {
    return false;
  }



  /** {@inheritDoc} */
  @Override
  public boolean isInteractive() {
    return !forceNonInteractive && parser.isInteractive();
  }



  /** {@inheritDoc} */
  @Override
  public boolean isMenuDrivenMode() {
    return true;
  }



  /** {@inheritDoc} */
  @Override
  public boolean isQuiet() {
    return false;
  }



  /** {@inheritDoc} */
  @Override
  public boolean isScriptFriendly() {
    return false;
  }



  /** {@inheritDoc} */
  @Override
  public boolean isVerbose() {
    return true;
  }

  /**
   * Commodity method to update the user data with the trust manager in the
   * LDAPConnectionConsoleInteraction object.
   * @param userData the user data to be updated.
   * @param ci the LDAPConnectionConsoleInteraction object to be used to update
   * the user data object.
   */
   private void updateTrustManager(UninstallUserData userData,
       LDAPConnectionConsoleInteraction ci)
   {
     ApplicationTrustManager trust = null;
     TrustManager t = ci.getTrustManager();
     if (t != null)
     {
       if (t instanceof ApplicationTrustManager)
       {
         trust = (ApplicationTrustManager)t;
       }
       else
       {
         trust = new ApplicationTrustManager(ci.getKeyStore());
       }
     }
     userData.setTrustManager(trust);
   }



   /**
    * Forces the initialization of the trust manager in the
    * LDAPConnectionInteraction object.
    */
   private void forceTrustManagerInitialization()
   {
     forceNonInteractive = true;
     try
     {
       ci.initializeTrustManagerIfRequired();
     }
     catch (ArgumentException ae)
     {
       logger.warn(LocalizableMessage.raw("Error initializing trust store: "+ae, ae));
     }
     forceNonInteractive = false;
   }

   private void printErrorMessage(LocalizableMessage msg)
   {
     super.println(msg);
     logger.warn(LocalizableMessage.raw(msg));
   }

   /**
    * Returns the timeout to be used to connect in milliseconds.  The method
    * must be called after parsing the arguments.
    * @return the timeout to be used to connect in milliseconds.  Returns
    * {@code 0} if there is no timeout.
    * @throw {@code IllegalStateException} if the method is called before
    * parsing the arguments.
    */
   private int getConnectTimeout()
   {
     try
     {
       return parser.getSecureArgsList().connectTimeoutArg.getIntValue();
     }
     catch (ArgumentException ae)
     {
       throw new IllegalStateException("Argument parser is not parsed: "+ae,
           ae);
     }
   }
}
