/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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 2008-2010 Sun Microsystems, Inc.
 */

package org.opends.server.util.args;

import org.opends.messages.Message;
import static org.opends.messages.ToolMessages.*;
import org.opends.server.tools.LDAPConnection;
import org.opends.server.tools.LDAPConnectionOptions;
import org.opends.server.tools.SSLConnectionFactory;
import org.opends.server.tools.SSLConnectionException;
import org.opends.server.tools.LDAPConnectionException;
import static org.opends.server.util.ServerConstants.MAX_LINE_WIDTH;
import static org.opends.server.util.StaticUtils.wrapText;
import org.opends.server.util.cli.LDAPConnectionConsoleInteraction;
import org.opends.server.admin.client.cli.SecureConnectionCliArgs;
import org.opends.server.types.OpenDsException;

import java.util.LinkedList;
import java.util.LinkedHashSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.io.PrintStream;
import javax.net.ssl.SSLException;

/**
 * Creates an argument parser pre-populated with arguments for specifying
 * information for openning and LDAPConnection an LDAP connection.
 */
public class LDAPConnectionArgumentParser extends ArgumentParser {

  private SecureConnectionCliArgs args;

  /**
   * Creates a new instance of this argument parser with no arguments.
   * Unnamed trailing arguments will not be allowed.
   *
   * @param  mainClassName               The fully-qualified name of the Java
   *                                     class that should be invoked to launch
   *                                     the program with which this argument
   *                                     parser is associated.
   * @param  toolDescription             A human-readable description for the
   *                                     tool, which will be included when
   *                                     displaying usage information.
   * @param  longArgumentsCaseSensitive  Indicates whether long arguments should
   * @param  argumentGroup               Group to which LDAP arguments will be
   *                                     added to the parser.  May be null to
   *                                     indicate that arguments should be
   *                                     added to the default group
   * @param alwaysSSL If true, always use the SSL connection type. In this case,
   * the arguments useSSL and startTLS are not present.
    */
  public LDAPConnectionArgumentParser(String mainClassName,
                                      Message toolDescription,
                                      boolean longArgumentsCaseSensitive,
                                      ArgumentGroup argumentGroup,
                                      boolean alwaysSSL) {
    super(mainClassName, toolDescription, longArgumentsCaseSensitive);
    addLdapConnectionArguments(argumentGroup, alwaysSSL);
  }

  /**
   * Creates a new instance of this argument parser with no arguments that may
   * or may not be allowed to have unnamed trailing arguments.
   *
   * @param  mainClassName               The fully-qualified name of the Java
   *                                     class that should be invoked to launch
   *                                     the program with which this argument
   *                                     parser is associated.
   * @param  toolDescription             A human-readable description for the
   *                                     tool, which will be included when
   *                                     displaying usage information.
   * @param  longArgumentsCaseSensitive  Indicates whether long arguments should
   *                                     be treated in a case-sensitive manner.
   * @param  allowsTrailingArguments     Indicates whether this parser allows
   *                                     unnamed trailing arguments to be
   *                                     provided.
   * @param  minTrailingArguments        The minimum number of unnamed trailing
   *                                     arguments that must be provided.  A
   *                                     value less than or equal to zero
   *                                     indicates that no minimum will be
   *                                     enforced.
   * @param  maxTrailingArguments        The maximum number of unnamed trailing
   *                                     arguments that may be provided.  A
   *                                     value less than or equal to zero
   *                                     indicates that no maximum will be
   *                                     enforced.
   * @param  trailingArgsDisplayName     The display name that should be used
   *                                     as a placeholder for unnamed trailing
   *                                     arguments in the generated usage
   *                                     information.
   * @param  argumentGroup               Group to which LDAP arguments will be
   *                                     added to the parser.  May be null to
   *                                     indicate that arguments should be
   *                                     added to the default group
   * @param alwaysSSL If true, always use the SSL connection type. In this case,
   * the arguments useSSL and startTLS are not present.
    */
  public LDAPConnectionArgumentParser(String mainClassName,
                                      Message toolDescription,
                                      boolean longArgumentsCaseSensitive,
                                      boolean allowsTrailingArguments,
                                      int minTrailingArguments,
                                      int maxTrailingArguments,
                                      String trailingArgsDisplayName,
                                      ArgumentGroup argumentGroup,
                                      boolean alwaysSSL) {
    super(mainClassName, toolDescription, longArgumentsCaseSensitive,
            allowsTrailingArguments, minTrailingArguments, maxTrailingArguments,
            trailingArgsDisplayName);
    addLdapConnectionArguments(argumentGroup, alwaysSSL);
  }

  /**
   * Indicates whether or not the user has indicated that they would like
   * to perform a remote operation based on the arguments.
   *
   * @return true if the user wants to perform a remote operation;
   *         false otherwise
   */
  public boolean connectionArgumentsPresent() {
    return args != null && args.argumentsPresent();
  }

  /**
   * Creates a new LDAPConnection and invokes a connect operation using
   * information provided in the parsed set of arguments that were provided
   * by the user.
   *
   * @param out stream to write messages
   * @param err stream to write messages
   * @return LDAPConnection created by this class from parsed arguments
   * @throws LDAPConnectionException if there was a problem connecting
   *         to the server indicated by the input arguments
   * @throws ArgumentException if there was a problem processing the input
   *         arguments
   */
  public LDAPConnection connect(PrintStream out, PrintStream err)
          throws LDAPConnectionException, ArgumentException
  {
    return connect(this.args, out, err);
  }


  /**
   * Creates a new LDAPConnection and invokes a connect operation using
   * information provided in the parsed set of arguments that were provided
   * by the user.
   *
   * @param args with which to connect
   * @param out stream to write messages
   * @param err stream to write messages
   * @return LDAPConnection created by this class from parsed arguments
   * @throws LDAPConnectionException if there was a problem connecting
   *         to the server indicated by the input arguments
   * @throws ArgumentException if there was a problem processing the input
   *         arguments
   */
  private LDAPConnection connect(SecureConnectionCliArgs args,
                                PrintStream out, PrintStream err)
          throws LDAPConnectionException, ArgumentException
  {
    // If both a bind password and bind password file were provided, then return
    // an error.
    if (args.bindPasswordArg.isPresent() &&
            args.bindPasswordFileArg.isPresent())
    {
      Message message = ERR_LDAP_CONN_MUTUALLY_EXCLUSIVE_ARGUMENTS.get(
              args.bindPasswordArg.getLongIdentifier(),
              args.bindPasswordFileArg.getLongIdentifier());
      err.println(wrapText(message, MAX_LINE_WIDTH));
      throw new ArgumentException(message);
    }


    // If both a key store password and key store password file were provided,
    // then return an error.
    if (args.keyStorePasswordArg.isPresent() &&
            args.keyStorePasswordFileArg.isPresent())
    {
      Message message = ERR_LDAP_CONN_MUTUALLY_EXCLUSIVE_ARGUMENTS.get(
              args.keyStorePasswordArg.getLongIdentifier(),
              args.keyStorePasswordFileArg.getLongIdentifier());
      throw new ArgumentException(message);
    }


    // If both a trust store password and trust store password file were
    // provided, then return an error.
    if (args.trustStorePasswordArg.isPresent() &&
            args.trustStorePasswordFileArg.isPresent())
    {
      Message message = ERR_LDAP_CONN_MUTUALLY_EXCLUSIVE_ARGUMENTS.get(
              args.trustStorePasswordArg.getLongIdentifier(),
              args.trustStorePasswordFileArg.getLongIdentifier());
      err.println(wrapText(message, MAX_LINE_WIDTH));
      throw new ArgumentException(message);
    }

    // Create the LDAP connection options object, which will be used to
    // customize the way that we connect to the server and specify a set of
    // basic defaults.
    LDAPConnectionOptions connectionOptions = new LDAPConnectionOptions();
    connectionOptions.setVersionNumber(3);


    // See if we should use SSL or StartTLS when establishing the connection.
    // If so, then make sure only one of them was specified.
    if (args.useSSLArg.isPresent())
    {
      if (args.useStartTLSArg.isPresent())
      {
        Message message = ERR_LDAP_CONN_MUTUALLY_EXCLUSIVE_ARGUMENTS.get(
                args.useSSLArg.getLongIdentifier(),
                args.useSSLArg.getLongIdentifier());
        err.println(wrapText(message, MAX_LINE_WIDTH));
        throw new ArgumentException(message);
      }
      else
      {
        connectionOptions.setUseSSL(true);
      }
    }
    else if (args.useStartTLSArg.isPresent())
    {
      connectionOptions.setStartTLS(true);
    }


    // If we should blindly trust any certificate, then install the appropriate
    // SSL connection factory.
    if (args.useSSLArg.isPresent() || args.useStartTLSArg.isPresent())
    {
      try
      {
        String clientAlias;
        if (args.certNicknameArg.isPresent())
        {
          clientAlias = args.certNicknameArg.getValue();
        }
        else
        {
          clientAlias = null;
        }

        SSLConnectionFactory sslConnectionFactory = new SSLConnectionFactory();
        sslConnectionFactory.init(args.trustAllArg.isPresent(),
                args.keyStorePathArg.getValue(),
                args.keyStorePasswordArg.getValue(),
                clientAlias,
                args.trustStorePathArg.getValue(),
                args.trustStorePasswordArg.getValue());

        connectionOptions.setSSLConnectionFactory(sslConnectionFactory);
      }
      catch (SSLConnectionException sce)
      {
        Message message =
                ERR_LDAP_CONN_CANNOT_INITIALIZE_SSL.get(sce.getMessage());
        err.println(wrapText(message, MAX_LINE_WIDTH));
      }
    }


    // If one or more SASL options were provided, then make sure that one of
    // them was "mech" and specified a valid SASL mechanism.
    if (args.saslOptionArg.isPresent())
    {
      String             mechanism = null;
      LinkedList<String> options   = new LinkedList<String>();

      for (String s : args.saslOptionArg.getValues())
      {
        int equalPos = s.indexOf('=');
        if (equalPos <= 0)
        {
          Message message = ERR_LDAP_CONN_CANNOT_PARSE_SASL_OPTION.get(s);
          err.println(wrapText(message, MAX_LINE_WIDTH));
          throw new ArgumentException(message);
        }
        else
        {
          String name  = s.substring(0, equalPos);

          if (name.equalsIgnoreCase("mech"))
          {
            mechanism = s;
          }
          else
          {
            options.add(s);
          }
        }
      }

      if (mechanism == null)
      {
        Message message = ERR_LDAP_CONN_NO_SASL_MECHANISM.get();
        err.println(wrapText(message, MAX_LINE_WIDTH));
        throw new ArgumentException(message);
      }

      connectionOptions.setSASLMechanism(mechanism);

      for (String option : options)
      {
        connectionOptions.addSASLProperty(option);
      }
    }

    int timeout = args.connectTimeoutArg.getIntValue();

    return connect(
            args.hostNameArg.getValue(),
            args.portArg.getIntValue(),
            args.bindDnArg.getValue(),
            getPasswordValue(args.bindPasswordArg, args.bindPasswordFileArg),
            connectionOptions, timeout, out, err);
  }

  /**
   * Creates a connection using a console interaction that will be used
   * to potientially interact with the user to prompt for necessary
   * information for establishing the connection.
   *
   * @param ui user interaction for prompting the user
   * @param out stream to write messages
   * @param err stream to write messages
   * @return LDAPConnection created by this class from parsed arguments
   * @throws LDAPConnectionException if there was a problem connecting
   *         to the server indicated by the input arguments
   */
  public LDAPConnection connect(LDAPConnectionConsoleInteraction ui,
                                PrintStream out, PrintStream err)
          throws LDAPConnectionException
  {
    LDAPConnection connection = null;
    try {
      ui.run();
      LDAPConnectionOptions options = new LDAPConnectionOptions();
      options.setVersionNumber(3);
      connection = connect(
              ui.getHostName(),
              ui.getPortNumber(),
              ui.getBindDN(),
              ui.getBindPassword(),
              ui.populateLDAPOptions(options),
              ui.getConnectTimeout(),
              out, err);
    } catch (OpenDsException e) {
      if ((e.getCause() != null) && (e.getCause().getCause() != null) &&
        e.getCause().getCause() instanceof SSLException) {
        err.println(ERR_TASKINFO_LDAP_EXCEPTION_SSL.get(ui.getHostName(),
          String.valueOf(ui.getPortNumber())));
      } else {
        err.println(e.getMessageObject());
      }
    }
    return connection;
  }


  /**
   * Creates a connection from information provided.
   *
   * @param host of the server
   * @param port of the server
   * @param bindDN with which to connect
   * @param bindPw with which to connect
   * @param options with which to connect
   * @param out stream to write messages
   * @param err stream to write messages
   * @return LDAPConnection created by this class from parsed arguments
   * @throws LDAPConnectionException if there was a problem connecting
   *         to the server indicated by the input arguments
   */
  public LDAPConnection connect(String host, int port,
                                String bindDN, String bindPw,
                                LDAPConnectionOptions options,
                                PrintStream out,
                                PrintStream err)
          throws LDAPConnectionException
  {
    return connect(host, port, bindDN, bindPw, options, 0, out, err);
  }


  /**
   * Creates a connection from information provided.
   *
   * @param host of the server
   * @param port of the server
   * @param bindDN with which to connect
   * @param bindPw with which to connect
   * @param options with which to connect
   * @param timeout the timeout to establish the connection in milliseconds.
   *        Use {@code 0} to express no timeout
   * @param out stream to write messages
   * @param err stream to write messages
   * @return LDAPConnection created by this class from parsed arguments
   * @throws LDAPConnectionException if there was a problem connecting
   *         to the server indicated by the input arguments
   */
  public LDAPConnection connect(String host, int port,
                                String bindDN, String bindPw,
                                LDAPConnectionOptions options,
                                int timeout,
                                PrintStream out,
                                PrintStream err)
  throws LDAPConnectionException
  {
    // Attempt to connect and authenticate to the Directory Server.
    AtomicInteger nextMessageID = new AtomicInteger(1);

    LDAPConnection connection = new LDAPConnection(
            host, port, options, out, err);

    connection.connectToHost(bindDN, bindPw, nextMessageID, timeout);

    return connection;
  }

  /**
   * Gets the arguments associated with this parser.
   *
   * @return arguments for this parser.
   */
  public SecureConnectionCliArgs getArguments() {
    return args;
  }

  /**
   * Commodity method that retrieves the password value analyzing the contents
   * of a string argument and of a file based argument.  It assumes that the
   * arguments have already been parsed and validated.
   * @param bindPwdArg the string argument.
   * @param bindPwdFileArg the file based argument.
   * @return the password value.
   */
  public static String getPasswordValue(StringArgument bindPwdArg,
      FileBasedArgument bindPwdFileArg)
  {
    String pwd = bindPwdArg.getValue();
    if ((pwd == null) && bindPwdFileArg.isPresent())
    {
      pwd = bindPwdFileArg.getValue();
    }
    return pwd;
  }

  private void addLdapConnectionArguments(ArgumentGroup argGroup,
    boolean alwaysSSL) {
    args = new SecureConnectionCliArgs(alwaysSSL);
    try {
      LinkedHashSet<Argument> argSet = args.createGlobalArguments();
      for (Argument arg : argSet) {
        addArgument(arg, argGroup);
      }
    }
    catch (ArgumentException ae) {
      ae.printStackTrace(); // Should never happen
    }

  }

}
