/*
 * 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 2007-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS
 */
package org.opends.server.tools.status;

import static com.forgerock.opendj.cli.ArgumentConstants.*;
import static com.forgerock.opendj.cli.CliMessages.*;
import static com.forgerock.opendj.cli.Utils.*;
import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
import static org.forgerock.util.Utils.*;

import static org.opends.messages.AdminToolMessages.*;
import static org.opends.messages.QuickSetupMessages.INFO_ERROR_READING_SERVER_CONFIGURATION;
import static org.opends.messages.QuickSetupMessages.INFO_NOT_AVAILABLE_LABEL;

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;

import javax.naming.AuthenticationException;
import javax.naming.NamingException;
import javax.naming.ldap.InitialLdapContext;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.LDAPProfile;
import org.forgerock.opendj.config.client.ManagementContext;
import org.forgerock.opendj.config.client.ldap.LDAPManagementContext;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.AuthorizationException;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.LDAPConnectionFactory;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SSLContextBuilder;
import org.forgerock.opendj.ldap.TrustManagers;
import org.forgerock.util.Options;
import org.forgerock.util.time.Duration;
import org.opends.admin.ads.util.ApplicationTrustManager;
import org.opends.guitools.controlpanel.datamodel.BackendDescriptor;
import org.opends.guitools.controlpanel.datamodel.BaseDNDescriptor;
import org.opends.guitools.controlpanel.datamodel.BaseDNTableModel;
import org.opends.guitools.controlpanel.datamodel.ConfigReadException;
import org.opends.guitools.controlpanel.datamodel.ConnectionHandlerDescriptor;
import org.opends.guitools.controlpanel.datamodel.ConnectionHandlerTableModel;
import org.opends.guitools.controlpanel.datamodel.ConnectionProtocolPolicy;
import org.opends.guitools.controlpanel.datamodel.ControlPanelInfo;
import org.opends.guitools.controlpanel.datamodel.ServerDescriptor;
import org.opends.guitools.controlpanel.util.ControlPanelLog;
import org.opends.guitools.controlpanel.util.Utilities;
import org.opends.server.admin.client.cli.SecureConnectionCliArgs;
import org.opends.server.types.DN;
import org.opends.server.types.InitializationException;
import org.opends.server.types.NullOutputStream;
import org.opends.server.types.OpenDsException;
import org.opends.server.util.BuildVersion;
import org.opends.server.util.StaticUtils;
import org.opends.server.util.cli.LDAPConnectionConsoleInteraction;

import com.forgerock.opendj.cli.ArgumentException;
import com.forgerock.opendj.cli.CliConstants;
import com.forgerock.opendj.cli.ClientException;
import com.forgerock.opendj.cli.ConsoleApplication;
import com.forgerock.opendj.cli.ReturnCode;
import com.forgerock.opendj.cli.TableBuilder;
import com.forgerock.opendj.cli.TextTablePrinter;

/**
 * The class used to provide some CLI interface to display status.
 * This class basically is in charge of parsing the data provided by the
 * user in the command line.
 */
public class StatusCli extends ConsoleApplication
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  private boolean displayMustAuthenticateLegend;
  private boolean displayMustStartLegend;

  /** Prefix for log files. */
  public static final String LOG_FILE_PREFIX = "opendj-status-";
  /** Suffix for log files. */
  public static final String LOG_FILE_SUFFIX = ".log";

  private ApplicationTrustManager interactiveTrustManager;
  private boolean useInteractiveTrustManager;

  /** The argument parser. */
  private StatusCliArgumentParser argParser;

  /**
   * Constructor for the status cli object.
   *
   * @param out
   *          The print stream to use for standard output.
   * @param err
   *          The print stream to use for standard error.
   * @param in
   *          The input stream to use for standard input.
   */
  public StatusCli(PrintStream out, PrintStream err, InputStream in)
  {
    super(out, err);
  }

  /**
   * The main method for the status CLI tool.
   *
   * @param args The command-line arguments provided to this program.
   */

  public static void main(String[] args)
  {
    int retCode = mainCLI(args, true, System.out, System.err, System.in);
    if(retCode != 0)
    {
      System.exit(retCode);
    }
  }

  /**
   * Parses the provided command-line arguments and uses that information to
   * run the status tool.
   *
   * @param args the command-line arguments provided to this program.
   *
   * @return The return code.
   */

  public static int mainCLI(String[] args)
  {
    return mainCLI(args, true, System.out, System.err, System.in);
  }

  /**
   * Parses the provided command-line arguments and uses that information to run
   * the status tool.
   *
   * @param args
   *          The command-line arguments provided to this program.
   * @param initializeServer
   *          Indicates whether to initialize the server.
   * @param outStream
   *          The output stream to use for standard output, or {@code null}
   *          if standard output is not needed.
   * @param errStream
   *          The output stream to use for standard error, or {@code null}
   *          if standard error is not needed.
   * @param inStream
   *          The input stream to use for standard input.
   * @return The return code.
   */
  public static int mainCLI(String[] args, boolean initializeServer,
      OutputStream outStream, OutputStream errStream, InputStream inStream)
  {
    PrintStream out = NullOutputStream.wrapOrNullStream(outStream);
    PrintStream err = NullOutputStream.wrapOrNullStream(errStream);

    try {
      ControlPanelLog.initLogFileHandler(
              File.createTempFile(LOG_FILE_PREFIX, LOG_FILE_SUFFIX));
      ControlPanelLog.initPackage("org.opends.server.tools.status");
    } catch (Throwable t) {
      System.err.println("Unable to initialize log");
      t.printStackTrace();
    }

    final StatusCli statusCli = new StatusCli(out, err, inStream);
    int retCode = statusCli.execute(args);
    if (retCode == 0)
    {
      ControlPanelLog.closeAndDeleteLogFile();
    }
    return retCode;
  }

  /**
   * Parses the provided command-line arguments and uses that information to run
   * the status CLI.
   *
   * @param args
   *          The command-line arguments provided to this program.
   * @return The return code of the process.
   */
  public int execute(String[] args) {
    argParser = new StatusCliArgumentParser(StatusCli.class.getName());
    try {
      argParser.initializeGlobalArguments(getOutputStream());
    } catch (ArgumentException ae) {
      println(ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
      return ReturnCode.CLIENT_SIDE_PARAM_ERROR.get();
    }

    try
    {
      argParser.getSecureArgsList().initArgumentsWithConfiguration();
    }
    catch (ConfigException ce)
    {
      // Ignore.
    }

    // Validate user provided data
    try {
      argParser.parseArguments(args);
    } catch (ArgumentException ae) {
      argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
      return ReturnCode.CLIENT_SIDE_PARAM_ERROR.get();
    }

    //  If we should just display usage or version information,
    // then print it and exit.
    if (argParser.usageOrVersionDisplayed()) {
      return ReturnCode.SUCCESS.get();
    }

    // Checks the version - if upgrade required, the tool is unusable
    try
    {
      BuildVersion.checkVersionMismatch();
    }
    catch (InitializationException e)
    {
      println(e.getMessageObject());
      return 1;
    }

    int v = argParser.validateGlobalOptions(getErrorStream());
    if (v != ReturnCode.SUCCESS.get()) {
      println(LocalizableMessage.raw(argParser.getUsage()));
      return v;
    }

    final ControlPanelInfo controlInfo = ControlPanelInfo.getInstance();
    controlInfo.setTrustManager(getTrustManager());
    controlInfo.setConnectTimeout(argParser.getConnectTimeout());
    controlInfo.regenerateDescriptor();

    if (controlInfo.getServerDescriptor().getStatus() == ServerDescriptor.ServerStatus.STARTED)
    {
      String bindDn = null;
      String bindPwd = null;

      ManagementContext mContext = null;

      // This is done because we do not need to ask the user about these
      // parameters. We force their presence in the
      // LDAPConnectionConsoleInteraction, this done, it will not prompt
      // the user for them.
      final SecureConnectionCliArgs secureArgsList = argParser.getSecureArgsList();
      controlInfo.setConnectionPolicy(ConnectionProtocolPolicy.USE_ADMIN);
      int port = CliConstants.DEFAULT_ADMINISTRATION_CONNECTOR_PORT;
      controlInfo.setConnectionPolicy(ConnectionProtocolPolicy.USE_ADMIN);
      String ldapUrl = controlInfo.getURLToConnect();
      try
      {
        final URI uri = new URI(ldapUrl);
        port = uri.getPort();
      }
      catch (Throwable t)
      {
        logger.error(LocalizableMessage.raw("Error parsing url: " + ldapUrl));
      }
      secureArgsList.hostNameArg.setPresent(true);
      secureArgsList.portArg.setPresent(true);
      secureArgsList.hostNameArg.addValue(secureArgsList.hostNameArg.getDefaultValue());
      secureArgsList.portArg.addValue(Integer.toString(port));
      // We already know if SSL or StartTLS can be used.  If we cannot
      // use them we will not propose them in the connection parameters
      // and if none of them can be used we will just not ask for the
      // protocol to be used.
      final LDAPConnectionConsoleInteraction ci =
          new LDAPConnectionConsoleInteraction(this, argParser.getSecureArgsList());
      try
      {
        ci.run(false);
      }
      catch (ArgumentException e)
      {
        argParser.displayMessageAndUsageReference(getErrStream(), e.getMessageObject());
        return ReturnCode.CLIENT_SIDE_PARAM_ERROR.get();
      }
      try
      {
        if (argParser.isInteractive())
        {
          bindDn = ci.getBindDN();
          bindPwd = ci.getBindPassword();
        }
        else
        {
          bindDn = argParser.getBindDN();
          bindPwd = argParser.getBindPassword();
        }
        if (bindPwd != null && !bindPwd.isEmpty())
        {
          mContext = getManagementContextFromConnection(ci);
          interactiveTrustManager = ci.getTrustManager();
          controlInfo.setTrustManager(interactiveTrustManager);
          useInteractiveTrustManager = true;
        }
      } catch (ClientException e) {
        println(e.getMessageObject());
        return ReturnCode.CLIENT_SIDE_PARAM_ERROR.get();
      } finally {
        closeSilently(mContext);
      }

      if (mContext != null)
      {
        InitialLdapContext ctx = null;
        try {
          ctx = Utilities.getAdminDirContext(controlInfo, bindDn, bindPwd);
          controlInfo.setDirContext(ctx);
          controlInfo.regenerateDescriptor();
          writeStatus(controlInfo);

          if (!controlInfo.getServerDescriptor().getExceptions().isEmpty()) {
            return ReturnCode.ERROR_INITIALIZING_SERVER.get();
          }
        } catch (NamingException ne) {
          // This should not happen but this is useful information to
          // diagnose the error.
          println();
          println(INFO_ERROR_READING_SERVER_CONFIGURATION.get(ne));
          return ReturnCode.ERROR_INITIALIZING_SERVER.get();
        } catch (ConfigReadException cre) {
          // This should not happen but this is useful information to
          // diagnose the error.
          println();
          println(cre.getMessageObject());
          return ReturnCode.ERROR_INITIALIZING_SERVER.get();
        } finally {
          StaticUtils.close(ctx);
        }
      } else {
        // The user did not provide authentication: just display the
        // information we can get reading the config file.
        writeStatus(controlInfo);
        return ReturnCode.ERROR_USER_CANCELLED.get();
      }
    } else {
      writeStatus(controlInfo);
    }

    return ReturnCode.SUCCESS.get();
  }

  private void writeStatus(ControlPanelInfo controlInfo)
  {
    if (controlInfo.getServerDescriptor() == null)
    {
      controlInfo.regenerateDescriptor();
    }
    writeStatus(controlInfo.getServerDescriptor());
    int period = argParser.getRefreshPeriod();
    boolean first = true;
    while (period > 0)
    {
      long timeToSleep = period * 1000;
      if (!first)
      {
        long t1 = System.currentTimeMillis();
        controlInfo.regenerateDescriptor();
        long t2 = System.currentTimeMillis();

        timeToSleep = timeToSleep - t2 + t1;
      }

      if (timeToSleep > 0)
      {
        StaticUtils.sleep(timeToSleep);
      }
      println();
      println(LocalizableMessage.raw("          ---------------------"));
      println();
      writeStatus(controlInfo.getServerDescriptor());
      first = false;
    }
  }

  private void writeStatus(ServerDescriptor desc)
  {
    LocalizableMessage[] labels =
      {
        INFO_SERVER_STATUS_LABEL.get(),
        INFO_CONNECTIONS_LABEL.get(),
        INFO_HOSTNAME_LABEL.get(),
        INFO_ADMINISTRATIVE_USERS_LABEL.get(),
        INFO_INSTALLATION_PATH_LABEL.get(),
        INFO_OPENDS_VERSION_LABEL.get(),
        INFO_JAVA_VERSION_LABEL.get(),
        INFO_CTRL_PANEL_ADMIN_CONNECTOR_LABEL.get()
      };
    int labelWidth = 0;
    LocalizableMessage title = INFO_SERVER_STATUS_TITLE.get();
    if (!isScriptFriendly())
    {
      for (LocalizableMessage label : labels)
      {
        labelWidth = Math.max(labelWidth, label.length());
      }
      println();
      println(centerTitle(title));
    }
    writeStatusContents(desc, labelWidth);
    writeCurrentConnectionContents(desc, labelWidth);
    if (!isScriptFriendly())
    {
      println();
    }

    title = INFO_SERVER_DETAILS_TITLE.get();
    if (!isScriptFriendly())
    {
      println(centerTitle(title));
    }
    writeHostnameContents(desc, labelWidth);
    writeAdministrativeUserContents(desc, labelWidth);
    writeInstallPathContents(desc, labelWidth);
    boolean sameInstallAndInstance = desc.sameInstallAndInstance();
    if (!sameInstallAndInstance)
    {
      writeInstancePathContents(desc, labelWidth);
    }
    writeVersionContents(desc, labelWidth);
    writeJavaVersionContents(desc, labelWidth);
    writeAdminConnectorContents(desc, labelWidth);
    if (!isScriptFriendly())
    {
      println();
    }

    writeListenerContents(desc);
    if (!isScriptFriendly())
    {
      println();
    }

    writeBaseDNContents(desc);

    writeErrorContents(desc);

    if (!isScriptFriendly())
    {
      if (displayMustStartLegend)
      {
        println();
        println(INFO_NOT_AVAILABLE_SERVER_DOWN_CLI_LEGEND.get());
      }
      else if (displayMustAuthenticateLegend)
      {
        println();
        println(INFO_NOT_AVAILABLE_AUTHENTICATION_REQUIRED_CLI_LEGEND.get());
      }
    }
    println();
  }

  /**
   * Writes the status contents displaying with what is specified in the
   * provided ServerDescriptor object.
   *
   * @param desc
   *          The ServerStatusDescriptor object.
   */
  private void writeStatusContents(ServerDescriptor desc, int maxLabelWidth)
  {
    writeLabelValue(INFO_SERVER_STATUS_LABEL.get(), getStatus(desc).toString(), maxLabelWidth);
  }

  private LocalizableMessage getStatus(ServerDescriptor desc)
  {
    switch (desc.getStatus())
    {
    case STARTED:
      return INFO_SERVER_STARTED_LABEL.get();
    case STOPPED:
      return INFO_SERVER_STOPPED_LABEL.get();
    case STARTING:
      return INFO_SERVER_STARTING_LABEL.get();
    case STOPPING:
      return INFO_SERVER_STOPPING_LABEL.get();
    case NOT_CONNECTED_TO_REMOTE:
      return INFO_SERVER_NOT_CONNECTED_TO_REMOTE_STATUS_LABEL.get();
    case UNKNOWN:
      return INFO_SERVER_UNKNOWN_STATUS_LABEL.get();
    default:
      throw new IllegalStateException("Unknown status: "+desc.getStatus());
    }
  }

  /**
   * Writes the current connection contents displaying with what is specified in
   * the provided ServerDescriptor object.
   *
   * @param desc
   *          The ServerDescriptor object.
   */
  private void writeCurrentConnectionContents(ServerDescriptor desc, int maxLabelWidth)
  {
    writeLabelValue(INFO_CONNECTIONS_LABEL.get(), getNbConnection(desc), maxLabelWidth);
  }

  private String getNbConnection(ServerDescriptor desc)
  {
    if (desc.getStatus() == ServerDescriptor.ServerStatus.STARTED)
    {
      final int nConn = desc.getOpenConnections();
      if (nConn >= 0)
      {
        return String.valueOf(nConn);
      }
      else if (!desc.isAuthenticated() || !desc.getExceptions().isEmpty())
      {
        return getNotAvailableBecauseAuthenticationIsRequiredText();
      }
      else
      {
        return getNotAvailableText();
      }
    }
    return getNotAvailableBecauseServerIsDownText();
  }

  /**
   * Writes the host name contents.
   *
   * @param desc
   *          The ServerDescriptor object.
   * @param maxLabelWidth
   *          The maximum label width of the left label.
   */
  private void writeHostnameContents(ServerDescriptor desc, int maxLabelWidth)
  {
    writeLabelValue(INFO_HOSTNAME_LABEL.get(), desc.getHostname(), maxLabelWidth);
  }

  /**
   * Writes the administrative user contents displaying with what is specified
   * in the provided ServerStatusDescriptor object.
   *
   * @param desc
   *          The ServerStatusDescriptor object.
   * @param maxLabelWidth
   *          The maximum label width of the left label.
   */
  private void writeAdministrativeUserContents(ServerDescriptor desc, int maxLabelWidth)
  {
    Set<DN> administrators = desc.getAdministrativeUsers();
    if (!administrators.isEmpty())
    {
      TreeSet<DN> ordered = new TreeSet<>(administrators);
      for (DN dn : ordered)
      {
        writeLabelValue(INFO_ADMINISTRATIVE_USERS_LABEL.get(), dn.toString(), maxLabelWidth);
      }
    }
    else
    {
      writeLabelValue(INFO_ADMINISTRATIVE_USERS_LABEL.get(), getErrorText(desc), maxLabelWidth);
    }
  }

  private String getErrorText(ServerDescriptor desc)
  {
    if (desc.getStatus() == ServerDescriptor.ServerStatus.STARTED
        && (!desc.isAuthenticated() || !desc.getExceptions().isEmpty()))
    {
      return getNotAvailableBecauseAuthenticationIsRequiredText();
    }
    return getNotAvailableText();
  }

  /**
   * Writes the install path contents displaying with what is specified in the
   * provided ServerDescriptor object.
   *
   * @param desc
   *          The ServerDescriptor object.
   * @param maxLabelWidth
   *          The maximum label width of the left label.
   */
  private void writeInstallPathContents(ServerDescriptor desc, int maxLabelWidth)
  {
    writeLabelValue(INFO_INSTALLATION_PATH_LABEL.get(), desc.getInstallPath(), maxLabelWidth);
  }

  /**
   * Writes the instance path contents displaying with what is specified in the
   * provided ServerDescriptor object.
   *
   * @param desc
   *          The ServerDescriptor object.
   * @param maxLabelWidth
   *          The maximum label width of the left label.
   */
  private void writeInstancePathContents(ServerDescriptor desc, int maxLabelWidth)
  {
    writeLabelValue(INFO_CTRL_PANEL_INSTANCE_PATH_LABEL.get(), desc.getInstancePath(), maxLabelWidth);
  }

  /**
   * Updates the server version contents displaying with what is specified in
   * the provided ServerDescriptor object. This method must be called from the
   * event thread.
   *
   * @param desc
   *          The ServerDescriptor object.
   */
  private void writeVersionContents(ServerDescriptor desc, int maxLabelWidth)
  {
    writeLabelValue(INFO_OPENDS_VERSION_LABEL.get(), desc.getOpenDSVersion(), maxLabelWidth);
  }

  /**
   * Updates the java version contents displaying with what is specified in the
   * provided ServerDescriptor object. This method must be called from the event
   * thread.
   *
   * @param desc
   *          The ServerDescriptor object.
   * @param maxLabelWidth
   *          The maximum label width of the left label.
   */
  private void writeJavaVersionContents(ServerDescriptor desc, int maxLabelWidth)
  {
    writeLabelValue(INFO_JAVA_VERSION_LABEL.get(), getJavaVersion(desc), maxLabelWidth);
  }

  private String getJavaVersion(ServerDescriptor desc)
  {
    if (desc.getStatus() == ServerDescriptor.ServerStatus.STARTED)
    {
      if (!desc.isAuthenticated() || !desc.getExceptions().isEmpty())
      {
        return getNotAvailableBecauseAuthenticationIsRequiredText();
      }
      return desc.getJavaVersion();
    }
    return getNotAvailableBecauseServerIsDownText();
  }

  /**
   * Updates the admin connector contents displaying with what is specified in
   * the provided ServerDescriptor object. This method must be called from the
   * event thread.
   *
   * @param desc
   *          The ServerDescriptor object.
   * @param maxLabelWidth
   *          The maximum label width of the left label.
   */
  private void writeAdminConnectorContents(ServerDescriptor desc, int maxLabelWidth)
  {
    ConnectionHandlerDescriptor adminConnector = desc.getAdminConnector();
    LocalizableMessage text = adminConnector != null
        ? INFO_CTRL_PANEL_ADMIN_CONNECTOR_DESCRIPTION.get(adminConnector.getPort())
        : INFO_NOT_AVAILABLE_SHORT_LABEL.get();
    writeLabelValue(INFO_CTRL_PANEL_ADMIN_CONNECTOR_LABEL.get(), text.toString(), maxLabelWidth);
  }

  /**
   * Writes the listeners contents displaying with what is specified in the
   * provided ServerDescriptor object.
   *
   * @param desc
   *          The ServerDescriptor object.
   */
  private void writeListenerContents(ServerDescriptor desc)
  {
    if (!isScriptFriendly())
    {
      LocalizableMessage title = INFO_LISTENERS_TITLE.get();
      println(centerTitle(title));
    }

    Set<ConnectionHandlerDescriptor> allHandlers = desc.getConnectionHandlers();
    if (allHandlers.isEmpty())
    {
      if (desc.getStatus() == ServerDescriptor.ServerStatus.STARTED)
      {
        if (!desc.isAuthenticated())
        {
          println(INFO_NOT_AVAILABLE_AUTHENTICATION_REQUIRED_CLI_LABEL.get());
        }
        else
        {
          println(INFO_NO_LISTENERS_FOUND.get());
        }
      }
      else
      {
        println(INFO_NO_LISTENERS_FOUND.get());
      }
    }
    else
    {
      ConnectionHandlerTableModel connHandlersTableModel =
        new ConnectionHandlerTableModel(false);
      connHandlersTableModel.setData(allHandlers);
      writeConnectionHandlersTableModel(connHandlersTableModel, desc);
    }
  }

  /**
   * Writes the base DN contents displaying with what is specified in the
   * provided ServerDescriptor object.
   *
   * @param desc
   *          The ServerDescriptor object.
   */
  private void writeBaseDNContents(ServerDescriptor desc)
  {
    LocalizableMessage title = INFO_DATABASES_TITLE.get();
    if (!isScriptFriendly())
    {
      println(centerTitle(title));
    }

    Set<BaseDNDescriptor> replicas = new HashSet<>();
    Set<BackendDescriptor> bs = desc.getBackends();
    for (BackendDescriptor backend: bs)
    {
      if (!backend.isConfigBackend())
      {
        replicas.addAll(backend.getBaseDns());
      }
    }
    if (replicas.isEmpty())
    {
      if (desc.getStatus() == ServerDescriptor.ServerStatus.STARTED)
      {
        if (!desc.isAuthenticated())
        {
          println(INFO_NOT_AVAILABLE_AUTHENTICATION_REQUIRED_CLI_LABEL.get());
        }
        else
        {
          println(INFO_NO_DBS_FOUND.get());
        }
      }
      else
      {
        println(INFO_NO_DBS_FOUND.get());
      }
    }
    else
    {
      BaseDNTableModel baseDNTableModel = new BaseDNTableModel(true, false);
      baseDNTableModel.setData(replicas, desc.getStatus(), desc.isAuthenticated());

      writeBaseDNTableModel(baseDNTableModel, desc);
    }
  }

  /**
   * Writes the error label contents displaying with what is specified in the
   * provided ServerDescriptor object.
   *
   * @param desc
   *          The ServerDescriptor object.
   */
  private void writeErrorContents(ServerDescriptor desc)
  {
    for (OpenDsException ex : desc.getExceptions())
    {
      LocalizableMessage errorMsg = ex.getMessageObject();
      if (errorMsg != null)
      {
        println();
        println(errorMsg);
      }
    }
  }

  /**
   * Returns the not available text explaining that the data is not available
   * because the server is down.
   *
   * @return the text.
   */
  private String getNotAvailableBecauseServerIsDownText()
  {
    displayMustStartLegend = true;
    return INFO_NOT_AVAILABLE_SERVER_DOWN_CLI_LABEL.get().toString();
  }

  /**
   * Returns the not available text explaining that the data is not available
   * because authentication is required.
   *
   * @return the text.
   */
  private String getNotAvailableBecauseAuthenticationIsRequiredText()
  {
    displayMustAuthenticateLegend = true;
    return INFO_NOT_AVAILABLE_AUTHENTICATION_REQUIRED_CLI_LABEL.get().toString();
  }

  /**
   * Returns the not available text explaining that the data is not available.
   *
   * @return the text.
   */
  private String getNotAvailableText()
  {
    return INFO_NOT_AVAILABLE_LABEL.get().toString();
  }

  /**
   * Writes the contents of the provided table model simulating a table layout
   * using text.
   *
   * @param tableModel
   *          The connection handler table model.
   * @param desc
   *          The Server Status descriptor.
   */
  private void writeConnectionHandlersTableModel(
      ConnectionHandlerTableModel tableModel,
      ServerDescriptor desc)
  {
    if (isScriptFriendly())
    {
      for (int i=0; i<tableModel.getRowCount(); i++)
      {
        // Get the host name, it can be multivalued.
        String[] hostNames = getHostNames(tableModel, i);
        for (String hostName : hostNames)
        {
          println(LocalizableMessage.raw("-"));
          for (int j=0; j<tableModel.getColumnCount(); j++)
          {
            LocalizableMessageBuilder line = new LocalizableMessageBuilder();
            line.append(tableModel.getColumnName(j)).append(": ");
            if (j == 0)
            {
              // It is the hostName
              line.append(getCellValue(hostName, desc));
            }
            else
            {
              line.append(getCellValue(tableModel.getValueAt(i, j), desc));
            }
            println(line.toMessage());
          }
        }
      }
    }
    else
    {
      TableBuilder table = new TableBuilder();
      for (int i=0; i< tableModel.getColumnCount(); i++)
      {
        table.appendHeading(LocalizableMessage.raw(tableModel.getColumnName(i)));
      }
      for (int i=0; i<tableModel.getRowCount(); i++)
      {
        // Get the host name, it can be multivalued.
        String[] hostNames = getHostNames(tableModel, i);
        for (String hostName : hostNames)
        {
          table.startRow();
          for (int j=0; j<tableModel.getColumnCount(); j++)
          {
            if (j == 0)
            {
              // It is the hostName
              table.appendCell(getCellValue(hostName, desc));
            }
            else
            {
              table.appendCell(getCellValue(tableModel.getValueAt(i, j), desc));
            }
          }
        }
      }
      TextTablePrinter printer = new TextTablePrinter(getOutputStream());
      printer.setColumnSeparator(LIST_TABLE_SEPARATOR);
      table.print(printer);
    }
  }

  private String[] getHostNames(ConnectionHandlerTableModel tableModel, int row)
  {
   String v = (String)tableModel.getValueAt(row, 0);
   String htmlTag = "<html>";
   if (v.toLowerCase().startsWith(htmlTag))
   {
     v = v.substring(htmlTag.length());
   }
   return v.split("<br>");
  }

  private String getCellValue(Object v, ServerDescriptor desc)
  {
    if (v != null)
    {
      if (v instanceof String)
      {
        return (String) v;
      }
      else if (v instanceof Integer)
      {
        int nEntries = ((Integer)v).intValue();
        if (nEntries >= 0)
        {
          return String.valueOf(nEntries);
        }
        else if (!desc.isAuthenticated() || !desc.getExceptions().isEmpty())
        {
          return getNotAvailableBecauseAuthenticationIsRequiredText();
        }
        else
        {
          return getNotAvailableText();
        }
      }
      else
      {
        throw new IllegalStateException("Unknown object type: "+v);
      }
    }
    return getNotAvailableText();
  }

  /**
   * Writes the contents of the provided base DN table model. Every base DN is
   * written in a block containing pairs of labels and values.
   *
   * @param tableModel
   *          The TableModel.
   * @param desc
   *          The Server Status descriptor.
   */
  private void writeBaseDNTableModel(BaseDNTableModel tableModel, ServerDescriptor desc)
  {
    boolean isRunning = desc.getStatus() == ServerDescriptor.ServerStatus.STARTED;

    int labelWidth = 0;
    int labelWidthWithoutReplicated = 0;
    LocalizableMessage[] labels = new LocalizableMessage[tableModel.getColumnCount()];
    for (int i=0; i<tableModel.getColumnCount(); i++)
    {
      LocalizableMessage header = LocalizableMessage.raw(tableModel.getColumnName(i));
      labels[i] = new LocalizableMessageBuilder(header).append(":").toMessage();
      labelWidth = Math.max(labelWidth, labels[i].length());
      if (i != 4 && i != 5)
      {
        labelWidthWithoutReplicated =
          Math.max(labelWidthWithoutReplicated, labels[i].length());
      }
    }

    LocalizableMessage replicatedLabel = INFO_BASEDN_REPLICATED_LABEL.get();
    for (int i=0; i<tableModel.getRowCount(); i++)
    {
      if (isScriptFriendly())
      {
        println(LocalizableMessage.raw("-"));
      }
      else if (i > 0)
      {
        println();
      }
      for (int j=0; j<tableModel.getColumnCount(); j++)
      {
        Object v = tableModel.getValueAt(i, j);
        String value = getValue(desc, isRunning, v);

        boolean doWrite = true;
        boolean isReplicated =
          replicatedLabel.toString().equals(
              String.valueOf(tableModel.getValueAt(i, 3)));
        if (j == 4 || j == 5)
        {
          // If the suffix is not replicated we do not have to display these lines
          doWrite = isReplicated;
        }
        if (doWrite)
        {
          writeLabelValue(labels[j], value,
              isReplicated?labelWidth:labelWidthWithoutReplicated);
        }
      }
    }
  }

  private String getValue(ServerDescriptor desc, boolean isRunning, Object v)
  {
    if (v != null)
    {
      if (v == BaseDNTableModel.NOT_AVAILABLE_SERVER_DOWN)
      {
        return getNotAvailableBecauseServerIsDownText();
      }
      else if (v == BaseDNTableModel.NOT_AVAILABLE_AUTHENTICATION_REQUIRED)
      {
        return getNotAvailableBecauseAuthenticationIsRequiredText();
      }
      else if (v == BaseDNTableModel.NOT_AVAILABLE)
      {
        return getNotAvailableText(desc, isRunning);
      }
      else if (v instanceof String)
      {
        return (String) v;
      }
      else if (v instanceof LocalizableMessage)
      {
        return ((LocalizableMessage) v).toString();
      }
      else if (v instanceof Integer)
      {
        final int nEntries = ((Integer) v).intValue();
        if (nEntries >= 0)
        {
          return String.valueOf(nEntries);
        }
        return getNotAvailableText(desc, isRunning);
      }
      else
      {
        throw new IllegalStateException("Unknown object type: " + v);
      }
    }
    return "";
  }

  private String getNotAvailableText(ServerDescriptor desc, boolean isRunning)
  {
    if (!isRunning)
    {
      return getNotAvailableBecauseServerIsDownText();
    }
    if (!desc.isAuthenticated() || !desc.getExceptions().isEmpty())
    {
      return getNotAvailableBecauseAuthenticationIsRequiredText();
    }
    return getNotAvailableText();
  }

  private void writeLabelValue(final LocalizableMessage label, final String value, final int maxLabelWidth)
  {
    final LocalizableMessageBuilder buf = new LocalizableMessageBuilder();
    buf.append(label);

    int extra = maxLabelWidth - label.length();
    for (int i = 0; i<extra; i++)
    {
      buf.append(" ");
    }
    buf.append(" ").append(value);
    println(buf.toMessage());
  }

  private LocalizableMessage centerTitle(final LocalizableMessage text)
  {
    if (text.length() <= MAX_LINE_WIDTH - 8)
    {
      final LocalizableMessageBuilder buf = new LocalizableMessageBuilder();
      int extra = Math.min(10,
          (MAX_LINE_WIDTH - 8 - text.length()) / 2);
      for (int i=0; i<extra; i++)
      {
        buf.append(" ");
      }
      buf.append("--- ").append(text).append(" ---");
      return buf.toMessage();
    }
    return text;
  }

  /**
   * Returns the trust manager to be used by this application.
   *
   * @return the trust manager to be used by this application.
   */
  private ApplicationTrustManager getTrustManager()
  {
    if (useInteractiveTrustManager)
    {
      return interactiveTrustManager;
    }
    return argParser.getTrustManager();
  }

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

  /** {@inheritDoc} */
  @Override
  public boolean isInteractive() {
    return argParser.isInteractive();
  }

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

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

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

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

  /** FIXME Common code with DSConfigand tools*. This method needs to be moved. */
  private ManagementContext getManagementContextFromConnection(
      final LDAPConnectionConsoleInteraction ci) throws ClientException
  {
    // Interact with the user though the console to get
    // LDAP connection information
    final String hostName = getHostNameForLdapUrl(ci.getHostName());
    final Integer portNumber = ci.getPortNumber();
    final String bindDN = ci.getBindDN();
    final String bindPassword = ci.getBindPassword();
    TrustManager trustManager = ci.getTrustManager();
    final KeyManager keyManager = ci.getKeyManager();

    // This connection should always be secure. useSSL = true.
    Connection connection = null;
    final Options options = Options.defaultOptions();
    options.set(CONNECT_TIMEOUT, new Duration((long) ci.getConnectTimeout(), TimeUnit.MILLISECONDS));
    LDAPConnectionFactory factory = null;
    while (true)
    {
      try
      {
        final SSLContextBuilder sslBuilder = new SSLContextBuilder();
        sslBuilder.setTrustManager(trustManager == null ? TrustManagers.trustAll() : trustManager);
        sslBuilder.setKeyManager(keyManager);
        options.set(SSL_USE_STARTTLS, ci.useStartTLS());
        options.set(SSL_CONTEXT, sslBuilder.getSSLContext());

        factory = new LDAPConnectionFactory(hostName, portNumber, options);
        connection = factory.getConnection();
        connection.bind(bindDN, bindPassword.toCharArray());
        break;
      }
      catch (LdapException e)
      {
        if (ci.isTrustStoreInMemory() && e.getCause() instanceof SSLException
            && e.getCause().getCause() instanceof CertificateException)
        {
          String authType = null;
          if (trustManager instanceof ApplicationTrustManager)
          { // FIXME use PromptingTrustManager
            ApplicationTrustManager appTrustManager =
                (ApplicationTrustManager) trustManager;
            authType = appTrustManager.getLastRefusedAuthType();
            X509Certificate[] cert = appTrustManager.getLastRefusedChain();

            if (ci.checkServerCertificate(cert, authType, hostName))
            {
              // If the certificate is trusted, update the trust manager.
              trustManager = ci.getTrustManager();
              // Try to connect again.
              continue;
            }
          }
        }
        if (e.getCause() instanceof SSLException)
        {
          LocalizableMessage message =
              ERR_FAILED_TO_CONNECT_NOT_TRUSTED.get(hostName, portNumber);
          throw new ClientException(ReturnCode.CLIENT_SIDE_CONNECT_ERROR,
              message);
        }
        if (e.getCause() instanceof AuthorizationException)
        {
          throw new ClientException(ReturnCode.AUTH_METHOD_NOT_SUPPORTED,
              ERR_SIMPLE_BIND_NOT_SUPPORTED.get());
        }
        else if (e.getCause() instanceof AuthenticationException
            || e.getResult().getResultCode() == ResultCode.INVALID_CREDENTIALS)
        {
          // Status Cli must not fail when un-authenticated.
          return null;
        }
        throw new ClientException(ReturnCode.CLIENT_SIDE_CONNECT_ERROR,
            ERR_FAILED_TO_CONNECT.get(hostName, portNumber));
      }
      catch (GeneralSecurityException e)
      {
        LocalizableMessage message =
            ERR_FAILED_TO_CONNECT.get(hostName, portNumber);
        throw new ClientException(ReturnCode.CLIENT_SIDE_CONNECT_ERROR, message);
      }
      finally
      {
        closeSilently(factory, connection);
      }
    }

    return LDAPManagementContext.newManagementContext(connection, LDAPProfile.getInstance());
  }
}
