/*
 * 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 2007-2010 Sun Microsystems, Inc.
 */
package org.opends.server.tools.dsconfig;



import java.io.BufferedReader;
import static org.opends.messages.DSConfigMessages.*;
import static org.opends.messages.ToolMessages.*;
import static org.opends.server.loggers.debug.DebugLogger.*;
import static org.opends.server.tools.ToolConstants.*;
import static org.opends.server.tools.dsconfig.ArgumentExceptionFactory.*;
import static org.opends.server.util.ServerConstants.PROPERTY_SCRIPT_NAME;
import static org.opends.server.util.StaticUtils.*;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

import org.opends.messages.Message;
import org.opends.quicksetup.util.Utils;
import org.opends.server.admin.AttributeTypePropertyDefinition;
import org.opends.server.admin.ClassLoaderProvider;
import org.opends.server.admin.ClassPropertyDefinition;
import org.opends.server.admin.InstantiableRelationDefinition;
import org.opends.server.admin.RelationDefinition;
import org.opends.server.admin.SetRelationDefinition;
import org.opends.server.admin.Tag;
import org.opends.server.admin.client.ManagedObjectDecodingException;
import org.opends.server.admin.client.MissingMandatoryPropertiesException;
import org.opends.server.admin.client.OperationRejectedException;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.tools.ClientException;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.InitializationException;
import org.opends.server.util.EmbeddedUtils;
import org.opends.server.util.ServerConstants;
import org.opends.server.util.StaticUtils;
import org.opends.server.util.args.ArgumentException;
import org.opends.server.util.args.BooleanArgument;
import org.opends.server.util.args.StringArgument;
import org.opends.server.util.args.SubCommand;
import org.opends.server.util.args.SubCommandArgumentParser;
import org.opends.server.util.args.ArgumentGroup;
import org.opends.server.util.cli.CLIException;
import org.opends.server.util.cli.CommandBuilder;
import org.opends.server.util.cli.ConsoleApplication;
import org.opends.server.util.cli.Menu;
import org.opends.server.util.cli.MenuBuilder;
import org.opends.server.util.cli.MenuCallback;
import org.opends.server.util.cli.MenuResult;
import org.opends.server.util.cli.OutputStreamConsoleApplication;



/**
 * This class provides a command-line tool which enables
 * administrators to configure the Directory Server.
 */
public final class DSConfig extends ConsoleApplication {

  /**
   * A menu call-back which runs a sub-command interactively.
   */
  private class SubCommandHandlerMenuCallback implements MenuCallback<Integer> {

    // The sub-command handler.
    private final SubCommandHandler handler;



    /**
     * Creates a new sub-command handler call-back.
     *
     * @param handler
     *          The sub-command handler.
     */
    public SubCommandHandlerMenuCallback(SubCommandHandler handler) {
      this.handler = handler;
    }



    /**
     * {@inheritDoc}
     */
    public MenuResult<Integer> invoke(ConsoleApplication app)
    throws CLIException {
      try {
        MenuResult<Integer> result = handler.run(app, factory);

        if (result.isQuit()) {
          return result;
        } else {
          if (result.isSuccess() && isInteractive() &&
              handler.isCommandBuilderUseful())
          {
            printCommandBuilder(getCommandBuilder(handler));
          }
          // Success or cancel.
          app.println();
          app.pressReturnToContinue();
          return MenuResult.again();
        }
      } catch (ArgumentException e) {
        app.println(e.getMessageObject());
        return MenuResult.success(1);
      } catch (ClientException e) {
        app.println(e.getMessageObject());
        return MenuResult.success(e.getExitCode());
      }
    }
  }



  /**
   * The interactive mode sub-menu implementation.
   */
  private class SubMenuCallback implements MenuCallback<Integer> {

    // The menu.
    private final Menu<Integer> menu;



    /**
     * Creates a new sub-menu implementation.
     *
     * @param app
     *          The console application.
     * @param rd
     *          The relation definition.
     * @param ch
     *          The optional create sub-command.
     * @param dh
     *          The optional delete sub-command.
     * @param lh
     *          The optional list sub-command.
     * @param sh
     *          The option set-prop sub-command.
     */
    public SubMenuCallback(ConsoleApplication app, RelationDefinition<?, ?> rd,
        CreateSubCommandHandler<?, ?> ch, DeleteSubCommandHandler dh,
        ListSubCommandHandler lh, SetPropSubCommandHandler sh) {
      Message ufn = rd.getUserFriendlyName();

      Message ufpn = null;
      if (rd instanceof InstantiableRelationDefinition<?,?>) {
        InstantiableRelationDefinition<?, ?> ir =
          (InstantiableRelationDefinition<?, ?>) rd;
        ufpn = ir.getUserFriendlyPluralName();
      } else if (rd instanceof SetRelationDefinition<?,?>) {
        SetRelationDefinition<?, ?> sr =
          (SetRelationDefinition<?, ?>) rd;
        ufpn = sr.getUserFriendlyPluralName();
      }

      MenuBuilder<Integer> builder = new MenuBuilder<Integer>(app);

      builder.setTitle(INFO_DSCFG_HEADING_COMPONENT_MENU_TITLE.get(ufn));
      builder.setPrompt(INFO_DSCFG_HEADING_COMPONENT_MENU_PROMPT.get());

      if (lh != null) {
        SubCommandHandlerMenuCallback callback =
          new SubCommandHandlerMenuCallback(lh);
        if (ufpn != null) {
          builder.addNumberedOption(
              INFO_DSCFG_OPTION_COMPONENT_MENU_LIST_PLURAL.get(ufpn), callback);
        } else {
          builder
          .addNumberedOption(INFO_DSCFG_OPTION_COMPONENT_MENU_LIST_SINGULAR
              .get(ufn), callback);
        }
      }

      if (ch != null) {
        SubCommandHandlerMenuCallback callback =
          new SubCommandHandlerMenuCallback(ch);
        builder.addNumberedOption(INFO_DSCFG_OPTION_COMPONENT_MENU_CREATE
            .get(ufn), callback);
      }

      if (sh != null) {
        SubCommandHandlerMenuCallback callback =
          new SubCommandHandlerMenuCallback(sh);
        if (ufpn != null) {
          builder
          .addNumberedOption(INFO_DSCFG_OPTION_COMPONENT_MENU_MODIFY_PLURAL
              .get(ufn), callback);
        } else {
          builder.addNumberedOption(
              INFO_DSCFG_OPTION_COMPONENT_MENU_MODIFY_SINGULAR.get(ufn),
              callback);
        }
      }

      if (dh != null) {
        SubCommandHandlerMenuCallback callback =
          new SubCommandHandlerMenuCallback(dh);
        builder.addNumberedOption(INFO_DSCFG_OPTION_COMPONENT_MENU_DELETE
            .get(ufn), callback);
      }

      builder.addBackOption(true);
      builder.addQuitOption();

      this.menu = builder.toMenu();
    }



    /**
     * {@inheritDoc}
     */
    public final MenuResult<Integer> invoke(ConsoleApplication app)
    throws CLIException {
      try {
        app.println();
        app.println();

        MenuResult<Integer> result = menu.run();

        if (result.isCancel()) {
          return MenuResult.again();
        }

        return result;
      } catch (CLIException e) {
        app.println(e.getMessageObject());
        return MenuResult.success(1);
      }
    }

  }

  /**
   * The type name which will be used for the most generic managed
   * object types when they are instantiable and intended for
   * customization only.
   */
  public static final String CUSTOM_TYPE = "custom";

  /**
   * The type name which will be used for the most generic managed
   * object types when they are instantiable and not intended for
   * customization.
   */
  public static final String GENERIC_TYPE = "generic";

  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();

  // This CLI is always using the administration connector with SSL
  private static final boolean alwaysSSL = true;

  private long sessionStartTime;
  private boolean sessionStartTimePrinted = false;
  private int sessionEquivalentOperationNumber = 0;

  /**
   * Provides the command-line arguments to the main application for
   * processing.
   *
   * @param args
   *          The set of command-line arguments provided to this
   *          program.
   */
  public static void main(String[] args) {
    int exitCode = main(args, true, System.out, System.err);
    if (exitCode != 0) {
      System.exit(filterExitCode(exitCode));
    }
  }



  /**
   * Provides the command-line arguments to the main application for
   * processing and returns the exit code as an integer.
   *
   * @param args
   *          The set of command-line arguments provided to this
   *          program.
   * @param initializeServer
   *          Indicates whether to perform basic initialization (which
   *          should not be done if the tool is running in the same
   *          JVM as the server).
   * @param outStream
   *          The output stream for standard output.
   * @param errStream
   *          The output stream for standard error.
   * @return Zero to indicate that the program completed successfully,
   *         or non-zero to indicate that an error occurred.
   */
  public static int main(String[] args, boolean initializeServer,
      OutputStream outStream, OutputStream errStream) {
    DSConfig app = new DSConfig(System.in, outStream, errStream,
        new LDAPManagementContextFactory(alwaysSSL));
    app.sessionStartTime = System.currentTimeMillis();
    // Only initialize the client environment when run as a standalone
    // application.
    if (initializeServer) {
      try {
        app.initializeClientEnvironment();
      } catch (InitializationException e) {
        // TODO: is this ok as an error message?
        app.println(e.getMessageObject());
        return 1;
      }
    }

    // Run the application.
    return app.run(args);
  }

  // The argument which should be used to request advanced mode.
  private BooleanArgument advancedModeArgument;

  // Flag indicating whether or not the application environment has
  // already been initialized.
  private boolean environmentInitialized = false;

  // The factory which the application should use to retrieve its
  // management context.
  private final ManagementContextFactory factory;

  // Flag indicating whether or not the global arguments have
  // already been initialized.
  private boolean globalArgumentsInitialized = false;

  // The sub-command handler factory.
  private SubCommandHandlerFactory handlerFactory = null;

  // Mapping of sub-commands to their implementations;
  private final Map<SubCommand, SubCommandHandler> handlers =
    new HashMap<SubCommand, SubCommandHandler>();

  // Indicates whether or not a sub-command was provided.
  private boolean hasSubCommand = true;

  // The argument which should be used to read dsconfig commands from a file.
  private StringArgument batchFileArgument;

  // The argument which should be used to request non interactive
  // behavior.
  private BooleanArgument noPromptArgument;

  // The argument that the user must set to display the equivalent
  // non-interactive mode argument
  private BooleanArgument displayEquivalentArgument;

  // The argument that allows the user to dump the equivalent non-interactive
  // command to a file.
  private StringArgument equivalentCommandFileArgument;

  // The command-line argument parser.
  private final SubCommandArgumentParser parser;

  // The argument which should be used to request quiet output.
  private BooleanArgument quietArgument;

  // The argument which should be used to request script-friendly
  // output.
  private BooleanArgument scriptFriendlyArgument;

  // The argument which should be used to request usage information.
  private BooleanArgument showUsageArgument;

  // The argument which should be used to request verbose output.
  private BooleanArgument verboseArgument;

  // The argument which should be used to indicate the properties file.
  private StringArgument propertiesFileArgument;

  // The argument which should be used to indicate that we will not look for
  // properties file.
  private BooleanArgument noPropertiesFileArgument;

  /**
   * Creates a new dsconfig application instance.
   *
   * @param in
   *          The application input stream.
   * @param out
   *          The application output stream.
   * @param err
   *          The application error stream.
   * @param factory
   *          The factory which this application instance should use
   *          for obtaining management contexts.
   */
  private DSConfig(InputStream in, OutputStream out, OutputStream err,
      ManagementContextFactory factory) {
    super(in, out, err);

    this.parser = new SubCommandArgumentParser(this.getClass().getName(),
        INFO_CONFIGDS_TOOL_DESCRIPTION.get(), false);

    this.factory = factory;
  }



  /**
   * Initializes core APIs for use when dsconfig will be run as a
   * standalone application.
   *
   * @throws InitializationException
   *           If the core APIs could not be initialized.
   */
  private void initializeClientEnvironment() throws InitializationException {
    if (environmentInitialized == false) {
      EmbeddedUtils.initializeForClientUse();

      // Bootstrap definition classes.
      ClassLoaderProvider.getInstance().enable();

      // Switch off class name validation in client.
      ClassPropertyDefinition.setAllowClassValidation(false);

      // Switch off attribute type name validation in client.
      AttributeTypePropertyDefinition.setCheckSchema(false);

      environmentInitialized = true;
    }
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isAdvancedMode() {
    return advancedModeArgument.isPresent();
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isInteractive() {
    return !noPromptArgument.isPresent();
  }



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



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isQuiet() {
    return quietArgument.isPresent();
  }



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



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isVerbose() {
    return verboseArgument.isPresent();
  }



  // Displays the provided message followed by a help usage reference.
  private void displayMessageAndUsageReference(Message message) {
    println(message);
    println();
    println(parser.getHelpUsageReference());
  }



  /**
   * Registers the global arguments with the argument parser.
   *
   * @throws ArgumentException
   *           If a global argument could not be registered.
   */
  private void initializeGlobalArguments(String[] args)
  throws ArgumentException {
    if (globalArgumentsInitialized == false) {
      verboseArgument = new BooleanArgument("verbose", 'v', "verbose",
          INFO_DESCRIPTION_VERBOSE.get());

      quietArgument = new BooleanArgument(
          OPTION_LONG_QUIET,
          OPTION_SHORT_QUIET,
          OPTION_LONG_QUIET,
          INFO_DESCRIPTION_QUIET.get());
      quietArgument.setPropertyName(OPTION_LONG_QUIET);

      scriptFriendlyArgument = new BooleanArgument("script-friendly",
          OPTION_SHORT_SCRIPT_FRIENDLY, OPTION_LONG_SCRIPT_FRIENDLY,
          INFO_DESCRIPTION_SCRIPT_FRIENDLY.get());
      scriptFriendlyArgument.setPropertyName(OPTION_LONG_SCRIPT_FRIENDLY);

      noPromptArgument = new BooleanArgument(
          OPTION_LONG_NO_PROMPT,
          OPTION_SHORT_NO_PROMPT,
          OPTION_LONG_NO_PROMPT,
          INFO_DESCRIPTION_NO_PROMPT.get());

      advancedModeArgument = new BooleanArgument(OPTION_DSCFG_LONG_ADVANCED,
          OPTION_DSCFG_SHORT_ADVANCED, OPTION_DSCFG_LONG_ADVANCED,
          INFO_DSCFG_DESCRIPTION_ADVANCED.get());
      advancedModeArgument.setPropertyName(OPTION_DSCFG_LONG_ADVANCED);

      showUsageArgument = new BooleanArgument("showUsage", OPTION_SHORT_HELP,
          OPTION_LONG_HELP, INFO_DSCFG_DESCRIPTION_SHOW_GROUP_USAGE_SUMMARY
          .get());

      batchFileArgument = new StringArgument(OPTION_LONG_BATCH_FILE_PATH,
          OPTION_SHORT_BATCH_FILE_PATH, OPTION_LONG_BATCH_FILE_PATH,
          false, false, true, INFO_BATCH_FILE_PATH_PLACEHOLDER.get(),
          null, null,
          INFO_DESCRIPTION_BATCH_FILE_PATH.get());

      displayEquivalentArgument = new BooleanArgument(
          OPTION_DSCFG_LONG_DISPLAY_EQUIVALENT,
          null, OPTION_DSCFG_LONG_DISPLAY_EQUIVALENT,
          INFO_DSCFG_DESCRIPTION_DISPLAY_EQUIVALENT.get());
      advancedModeArgument.setPropertyName(
          OPTION_DSCFG_LONG_DISPLAY_EQUIVALENT);

      equivalentCommandFileArgument = new StringArgument(
          OPTION_LONG_EQUIVALENT_COMMAND_FILE_PATH, null,
          OPTION_LONG_EQUIVALENT_COMMAND_FILE_PATH, false, false, true,
          INFO_PATH_PLACEHOLDER.get(), null, null,
          INFO_DSCFG_DESCRIPTION_EQUIVALENT_COMMAND_FILE_PATH.get());

      propertiesFileArgument = new StringArgument("propertiesFilePath",
          null, OPTION_LONG_PROP_FILE_PATH,
          false, false, true, INFO_PROP_FILE_PATH_PLACEHOLDER.get(), null, null,
          INFO_DESCRIPTION_PROP_FILE_PATH.get());

      noPropertiesFileArgument = new BooleanArgument(
          "noPropertiesFileArgument", null, OPTION_LONG_NO_PROP_FILE,
          INFO_DESCRIPTION_NO_PROP_FILE.get());

      // Register the global arguments.

      ArgumentGroup toolOptionsGroup = new ArgumentGroup(
          INFO_DESCRIPTION_CONFIG_OPTIONS_ARGS.get(), 2);
      parser.addGlobalArgument(advancedModeArgument, toolOptionsGroup);

      parser.addGlobalArgument(showUsageArgument);
      parser.setUsageArgument(showUsageArgument, getOutputStream());
      parser.addGlobalArgument(verboseArgument);
      parser.addGlobalArgument(quietArgument);
      parser.addGlobalArgument(scriptFriendlyArgument);
      parser.addGlobalArgument(noPromptArgument);
      parser.addGlobalArgument(batchFileArgument);
      parser.addGlobalArgument(displayEquivalentArgument);
      parser.addGlobalArgument(equivalentCommandFileArgument);
      parser.addGlobalArgument(propertiesFileArgument);
      parser.setFilePropertiesArgument(propertiesFileArgument);
      parser.addGlobalArgument(noPropertiesFileArgument);
      parser.setNoPropertiesFileArgument(noPropertiesFileArgument);

      // Register any global arguments required by the management
      // context factory.
      factory.setRawArguments(args);
      factory.registerGlobalArguments(parser);

      globalArgumentsInitialized = true;
    }
  }



  /**
   * Registers the sub-commands with the argument parser. This method
   * uses the administration framework introspection APIs to determine
   * the overall structure of the command-line.
   *
   * @throws ArgumentException
   *           If a sub-command could not be created.
   */
  private void initializeSubCommands() throws ArgumentException {
    if (handlerFactory == null) {
      handlerFactory = new SubCommandHandlerFactory(parser);

      Comparator<SubCommand> c = new Comparator<SubCommand>() {

        public int compare(SubCommand o1, SubCommand o2) {
          return o1.getName().compareTo(o2.getName());
        }
      };

      Map<Tag, SortedSet<SubCommand>> groups =
        new TreeMap<Tag, SortedSet<SubCommand>>();
      SortedSet<SubCommand> allSubCommands = new TreeSet<SubCommand>(c);
      for (SubCommandHandler handler : handlerFactory
          .getAllSubCommandHandlers()) {
        SubCommand sc = handler.getSubCommand();

        handlers.put(sc, handler);
        allSubCommands.add(sc);

        // Add the sub-command to its groups.
        for (Tag tag : handler.getTags()) {
          SortedSet<SubCommand> group = groups.get(tag);
          if (group == null) {
            group = new TreeSet<SubCommand>(c);
            groups.put(tag, group);
          }
          group.add(sc);
        }
      }

      // Register the usage arguments.
      for (Map.Entry<Tag, SortedSet<SubCommand>> group : groups.entrySet()) {
        Tag tag = group.getKey();
        SortedSet<SubCommand> subCommands = group.getValue();

        String option = OPTION_LONG_HELP + "-" + tag.getName();
        String synopsis = tag.getSynopsis().toString().toLowerCase();
        BooleanArgument arg = new BooleanArgument(option, null, option,
            INFO_DSCFG_DESCRIPTION_SHOW_GROUP_USAGE.get(synopsis));

        parser.addGlobalArgument(arg);
        parser.setUsageGroupArgument(arg, subCommands);
      }

      // Register the --help-all argument.
      String option = OPTION_LONG_HELP + "-all";
      BooleanArgument arg = new BooleanArgument(option, null, option,
          INFO_DSCFG_DESCRIPTION_SHOW_GROUP_USAGE_ALL.get());

      parser.addGlobalArgument(arg);
      parser.setUsageGroupArgument(arg, allSubCommands);
    }
  }



  /**
   * Parses the provided command-line arguments and makes the
   * appropriate changes to the Directory Server configuration.
   *
   * @param args
   *          The command-line arguments provided to this program.
   * @return The exit code from the configuration processing. A
   *         nonzero value indicates that there was some kind of
   *         problem during the configuration processing.
   */
  private int run(String[] args) {
    // Register global arguments and sub-commands.
    try {
      initializeGlobalArguments(args);
      initializeSubCommands();
    } catch (ArgumentException e) {
      Message message = ERR_CANNOT_INITIALIZE_ARGS.get(e.getMessage());
      println(message);
      return 1;
    }

    // Parse the command-line arguments provided to this program.
    try {
      parser.parseArguments(args);
    } catch (ArgumentException ae) {
      Message message = ERR_ERROR_PARSING_ARGS.get(ae.getMessage());
      displayMessageAndUsageReference(message);
      return 1;
    }

    // If the usage/version argument was provided, then we don't need
    // to do anything else.
    if (parser.usageOrVersionDisplayed()) {
      return 0;
    }

    // Check for conflicting arguments.
    if (quietArgument.isPresent() && verboseArgument.isPresent()) {
      Message message = ERR_TOOL_CONFLICTING_ARGS.get(quietArgument
          .getLongIdentifier(), verboseArgument.getLongIdentifier());
      displayMessageAndUsageReference(message);
      return 1;
    }

    if (batchFileArgument.isPresent() && !noPromptArgument.isPresent()) {
      Message message =
        ERR_DSCFG_ERROR_BATCH_FILE_AND_INTERACTIVE_INCOMPATIBLE.get(
            batchFileArgument.getLongIdentifier(), noPromptArgument
            .getLongIdentifier());
      displayMessageAndUsageReference(message);
      return 1;
    }

    if (quietArgument.isPresent() && !noPromptArgument.isPresent()) {
      Message message = ERR_DSCFG_ERROR_QUIET_AND_INTERACTIVE_INCOMPATIBLE.get(
          quietArgument.getLongIdentifier(), noPromptArgument
          .getLongIdentifier());
      displayMessageAndUsageReference(message);
      return 1;
    }

    if (scriptFriendlyArgument.isPresent() && verboseArgument.isPresent()) {
      Message message = ERR_TOOL_CONFLICTING_ARGS.get(scriptFriendlyArgument
          .getLongIdentifier(), verboseArgument.getLongIdentifier());
      displayMessageAndUsageReference(message);
      return 1;
    }

    if (noPropertiesFileArgument.isPresent()
        && propertiesFileArgument.isPresent())
    {
      Message message = ERR_TOOL_CONFLICTING_ARGS.get(
          noPropertiesFileArgument.getLongIdentifier(),
          propertiesFileArgument.getLongIdentifier());
      displayMessageAndUsageReference(message);
      return 1;
    }

    // Check that we can write on the provided path where we write the
    // equivalent non-interactive commands.
    if (equivalentCommandFileArgument.isPresent())
    {
      String file = equivalentCommandFileArgument.getValue();
      if (!Utils.canWrite(file))
      {
        println(ERR_DSCFG_CANNOT_WRITE_EQUIVALENT_COMMAND_LINE_FILE.get(file));
        return 1;
      }
      else
      {
        File f = new File(file);
        if (f.isDirectory())
        {
          println(ERR_DSCFG_EQUIVALENT_COMMAND_LINE_FILE_DIRECTORY.get(file));
          return 1;
        }
      }
    }

    // Make sure that management context's arguments are valid.
    try {
      factory.validateGlobalArguments();
    } catch (ArgumentException e) {
      println(e.getMessageObject());
      return 1;
    }

    // Handle batch file if any
    if (batchFileArgument.isPresent()) {
      handleBatchFile(args);
      // don't need to do anything else
      return 0;
    }

    int retCode = 0;
    if (parser.getSubCommand() == null) {
      hasSubCommand = false;

      if (isInteractive()) {
        // Top-level interactive mode.
        retCode = runInteractiveMode();
      } else {
        Message message = ERR_ERROR_PARSING_ARGS
        .get(ERR_DSCFG_ERROR_MISSING_SUBCOMMAND.get());
        displayMessageAndUsageReference(message);
        retCode = 1;
      }
    } else {
      hasSubCommand = true;

      // Retrieve the sub-command implementation and run it.
      SubCommandHandler handler = handlers.get(parser.getSubCommand());
      retCode = runSubCommand(handler);
    }

    try {
      // Close the Management context ==> an LDAP UNBIND is sent
      factory.close();
    } catch (Exception e) {
      // Nothing to report in this case
    }

    return retCode;
  }



  // Run the top-level interactive console.
  private int runInteractiveMode() {
    // In interactive mode, redirect all output to stdout.
    ConsoleApplication app = new OutputStreamConsoleApplication(this);

    // Build menu structure.
    Comparator<RelationDefinition<?, ?>> c =
      new Comparator<RelationDefinition<?, ?>>() {

      public int compare(RelationDefinition<?, ?> rd1,
          RelationDefinition<?, ?> rd2) {
        String s1 = rd1.getUserFriendlyName().toString();
        String s2 = rd2.getUserFriendlyName().toString();

        return s1.compareToIgnoreCase(s2);
      }

    };

    Set<RelationDefinition<?, ?>> relations;
    Map<RelationDefinition<?, ?>, CreateSubCommandHandler<?, ?>> createHandlers;
    Map<RelationDefinition<?, ?>, DeleteSubCommandHandler> deleteHandlers;
    Map<RelationDefinition<?, ?>, ListSubCommandHandler> listHandlers;
    Map<RelationDefinition<?, ?>, GetPropSubCommandHandler> getPropHandlers;
    Map<RelationDefinition<?, ?>, SetPropSubCommandHandler> setPropHandlers;

    relations = new TreeSet<RelationDefinition<?, ?>>(c);
    createHandlers =
      new HashMap<RelationDefinition<?, ?>, CreateSubCommandHandler<?, ?>>();
    deleteHandlers =
      new HashMap<RelationDefinition<?, ?>, DeleteSubCommandHandler>();
    listHandlers =
      new HashMap<RelationDefinition<?, ?>, ListSubCommandHandler>();
    getPropHandlers =
      new HashMap<RelationDefinition<?, ?>, GetPropSubCommandHandler>();
    setPropHandlers =
      new HashMap<RelationDefinition<?, ?>, SetPropSubCommandHandler>();

    for (CreateSubCommandHandler<?, ?> ch : handlerFactory
        .getCreateSubCommandHandlers()) {
      relations.add(ch.getRelationDefinition());
      createHandlers.put(ch.getRelationDefinition(), ch);
    }

    for (DeleteSubCommandHandler dh : handlerFactory
        .getDeleteSubCommandHandlers()) {
      relations.add(dh.getRelationDefinition());
      deleteHandlers.put(dh.getRelationDefinition(), dh);
    }

    for (ListSubCommandHandler lh :
      handlerFactory.getListSubCommandHandlers()) {
      relations.add(lh.getRelationDefinition());
      listHandlers.put(lh.getRelationDefinition(), lh);
    }

    for (GetPropSubCommandHandler gh : handlerFactory
        .getGetPropSubCommandHandlers()) {
      relations.add(gh.getRelationDefinition());
      getPropHandlers.put(gh.getRelationDefinition(), gh);
    }

    for (SetPropSubCommandHandler sh : handlerFactory
        .getSetPropSubCommandHandlers()) {
      relations.add(sh.getRelationDefinition());
      setPropHandlers.put(sh.getRelationDefinition(), sh);
    }

    // Main menu.
    MenuBuilder<Integer> builder = new MenuBuilder<Integer>(app);

    builder.setTitle(INFO_DSCFG_HEADING_MAIN_MENU_TITLE.get());
    builder.setPrompt(INFO_DSCFG_HEADING_MAIN_MENU_PROMPT.get());
    builder.setMultipleColumnThreshold(0);

    for (RelationDefinition<?, ?> rd : relations) {
      MenuCallback<Integer> callback = new SubMenuCallback(app, rd,
          createHandlers.get(rd), deleteHandlers.get(rd), listHandlers.get(rd),
          setPropHandlers.get(rd));
      builder.addNumberedOption(rd.getUserFriendlyName(), callback);
    }

    builder.addQuitOption();

    Menu<Integer> menu = builder.toMenu();

    try {
      // Force retrieval of management context.
      factory.getManagementContext(app);
    } catch (ArgumentException e) {
      app.println(e.getMessageObject());
      return 1;
    } catch (ClientException e) {
      app.println(e.getMessageObject());
      return 1;
    }

    try {
      app.println();
      app.println();

      MenuResult<Integer> result = menu.run();

      if (result.isQuit()) {
        return 0;
      } else {
        return result.getValue();
      }
    } catch (CLIException e) {
      app.println(e.getMessageObject());
      return 1;
    }
  }



  // Run the provided sub-command handler.
  private int runSubCommand(SubCommandHandler handler) {
    try {
      MenuResult<Integer> result = handler.run(this, factory);

      if (result.isSuccess()) {
        if (isInteractive() &&
            handler.isCommandBuilderUseful())
        {
          printCommandBuilder(getCommandBuilder(handler));
        }
        return result.getValue();
      } else {
        // User must have quit.
        return 1;
      }
    } catch (ArgumentException e) {
      println(e.getMessageObject());
      return 1;
    } catch (CLIException e) {
      println(e.getMessageObject());
      return 1;
    } catch (ClientException e) {
      Throwable cause = e.getCause();
      if (cause instanceof ManagedObjectDecodingException) {
        ManagedObjectDecodingException de =
          (ManagedObjectDecodingException) cause;
        println();
        displayManagedObjectDecodingException(this, de);
        println();
      } else if (cause instanceof MissingMandatoryPropertiesException) {
        MissingMandatoryPropertiesException mmpe =
          (MissingMandatoryPropertiesException) cause;
        println();
        displayMissingMandatoryPropertyException(this, mmpe);
        println();
      } else if (cause instanceof OperationRejectedException) {
        OperationRejectedException ore = (OperationRejectedException) cause;
        println();
        displayOperationRejectedException(this, ore);
        println();
      } else {
        // Just display the default message.
        println(e.getMessageObject());
      }

      return 1;
    } catch (Exception e) {
      if (debugEnabled()) {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      println(Message.raw(StaticUtils.stackTraceToString(e)));
      return 1;
    }
  }

  /**
   * Updates the command builder with the global options: script friendly,
   * verbose, etc. for a given subcommand.  It also adds systematically the
   * no-prompt option.
   * @param handler the subcommand handler.
   */
  private CommandBuilder getCommandBuilder(SubCommandHandler handler)
  {
    String commandName =
      System.getProperty(ServerConstants.PROPERTY_SCRIPT_NAME);
    if (commandName == null)
    {
      commandName = "dsconfig";
    }

    CommandBuilder commandBuilder =
      new CommandBuilder(commandName, handler.getSubCommand().getName());

    commandBuilder.append(handler.getCommandBuilder());

    if ((factory != null) && (factory.getContextCommandBuilder() != null))
    {
      commandBuilder.append(factory.getContextCommandBuilder());
    }

    if (verboseArgument.isPresent())
    {
      commandBuilder.addArgument(verboseArgument);
    }

    if (scriptFriendlyArgument.isPresent())
    {
      commandBuilder.addArgument(scriptFriendlyArgument);
    }

    commandBuilder.addArgument(noPromptArgument);

    if (propertiesFileArgument.isPresent())
    {
      commandBuilder.addArgument(propertiesFileArgument);
    }

    if (noPropertiesFileArgument.isPresent())
    {
      commandBuilder.addArgument(noPropertiesFileArgument);
    }

    return commandBuilder;
  }

  /**
   * Creates a command builder with the global options: script friendly,
   * verbose, etc. for a given subcommand name.  It also adds systematically the
   * no-prompt option.
   * @param subcommandName the subcommand name.
   * @return the command builder that has been created with the specified
   * subcommandName.
   */
  CommandBuilder getCommandBuilder(String subcommandName)
  {
    String commandName =
      System.getProperty(ServerConstants.PROPERTY_SCRIPT_NAME);
    if (commandName == null)
    {
      commandName = "dsconfig";
    }

    CommandBuilder commandBuilder =
      new CommandBuilder(commandName, subcommandName);

    if ((factory != null) && (factory.getContextCommandBuilder() != null))
    {
      commandBuilder.append(factory.getContextCommandBuilder());
    }

    if (verboseArgument.isPresent())
    {
      commandBuilder.addArgument(verboseArgument);
    }

    if (scriptFriendlyArgument.isPresent())
    {
      commandBuilder.addArgument(scriptFriendlyArgument);
    }

    commandBuilder.addArgument(noPromptArgument);

    if (propertiesFileArgument.isPresent())
    {
      commandBuilder.addArgument(propertiesFileArgument);
    }

    if (noPropertiesFileArgument.isPresent())
    {
      commandBuilder.addArgument(noPropertiesFileArgument);
    }

    return commandBuilder;
  }

  /**
   * Prints the contents of a command builder.  This method has been created
   * since SetPropSubCommandHandler calls it.  All the logic of DSConfig is on
   * this method.  It writes the content of the CommandBuilder to the standard
   * output, or to a file depending on the options provided by the user.
   * @param commandBuilder the command builder to be printed.
   */
  void printCommandBuilder(CommandBuilder commandBuilder)
  {
    if (displayEquivalentArgument.isPresent())
    {
      println();
      // We assume that the app we are running is this one.
      println(INFO_DSCFG_NON_INTERACTIVE.get(commandBuilder.toString()));
    }
    if (equivalentCommandFileArgument.isPresent())
    {
      String file = equivalentCommandFileArgument.getValue();
      try
      {
        BufferedWriter writer =
          new BufferedWriter(new FileWriter(file, true));

        if (!sessionStartTimePrinted)
        {
          writer.write(SHELL_COMMENT_SEPARATOR+getSessionStartTimeMessage());
          writer.newLine();
          sessionStartTimePrinted = true;
        }

        sessionEquivalentOperationNumber++;
        writer.newLine();
        writer.write(SHELL_COMMENT_SEPARATOR+
            INFO_DSCFG_EQUIVALENT_COMMAND_LINE_SESSION_OPERATION_NUMBER.get(
                sessionEquivalentOperationNumber));
        writer.newLine();

        writer.write(SHELL_COMMENT_SEPARATOR+getCurrentOperationDateMessage());
        writer.newLine();

        writer.write(commandBuilder.toString());
        writer.newLine();
        writer.newLine();

        writer.flush();
        writer.close();
      }
      catch (IOException ioe)
      {
        println(ERR_DSCFG_ERROR_WRITING_EQUIVALENT_COMMAND_LINE.get(file,
            ioe.toString()));
      }
    }
  }

  /**
   * Returns the message to be displayed in the file with the equivalent
   * command-line with information about when the session started.
   * @return  the message to be displayed in the file with the equivalent
   * command-line with information about when the session started.
   */
  private String getSessionStartTimeMessage()
  {
    String scriptName = System.getProperty(PROPERTY_SCRIPT_NAME);
    if ((scriptName == null) || (scriptName.length() == 0))
    {
      scriptName = "dsconfig";
    }
    String date = formatDateTimeStringForEquivalentCommand(
        new Date(sessionStartTime));
    return INFO_DSCFG_SESSION_START_TIME_MESSAGE.get(scriptName, date).
    toString();
  }

  private void handleBatchFile(String[] args) {
    BufferedReader reader = null;
    try {
      // Build a list of initial arguments,
      // removing the batch file option + its value
      List<String> initialArgs = new ArrayList<String>();
      Collections.addAll(initialArgs, args);
      int batchFileArgIndex = -1;
      for (String elem : initialArgs) {
        if (elem.startsWith("-" + OPTION_SHORT_BATCH_FILE_PATH)
                || elem.contains(OPTION_LONG_BATCH_FILE_PATH)) {
          batchFileArgIndex = initialArgs.indexOf(elem);
          break;
        }
      }
      if (batchFileArgIndex != -1) {
        // Remove both the batch file arg and its value
        initialArgs.remove(batchFileArgIndex);
        initialArgs.remove(batchFileArgIndex);
      }
      String batchFilePath = batchFileArgument.getValue().trim();
      reader =
              new BufferedReader(new FileReader(batchFilePath));
      String line;
      String command = "";
      //
      // Split the CLI string into arguments array
      //
      while ((line = reader.readLine()) != null) {
         if (line.equals("") || line.startsWith("#")) {
          // Empty line or comment
          continue;
        }
       // command split in several line support
        if (line.endsWith("\\")) {
          // command is split into several lines
          command += line.substring(0, line.length() - 1);
          continue;
        } else {
          command += line;
        }
        command = command.trim();
        // string between quotes support
        command = replaceSpacesInQuotes(command);
        // "\ " support
        command = command.replace("\\ ", "##");
        String[] fileArguments = command.split("\\s+");
        // reset command
        command = "";
        for (int ii = 0; ii < fileArguments.length; ii++) {
          fileArguments[ii] = fileArguments[ii].replace("##", " ");
          System.out.println(fileArguments[ii]);
        }

        // Append initial arguments to the file line
        List<String> allArguments = new ArrayList<String>();
        Collections.addAll(allArguments, fileArguments);
        allArguments.addAll(initialArgs);
        String[] allArgsArray = allArguments.toArray(new String[]{});

        // Build a single CLI string for display
        StringBuffer cli = new StringBuffer();
        for (String arg : allArgsArray) {
          cli.append(arg + " ");
        }

        int exitCode =
                main(allArgsArray, false, getOutputStream(), getErrorStream());
        if (exitCode != 0) {
          reader.close();
          System.exit(filterExitCode(exitCode));
        }
      }

      reader.close();

    } catch (IOException ex) {
      println(ERR_DSCFG_ERROR_READING_BATCH_FILE.get(ex.toString()));
    }
  }

  // Replace spaces in quotes by "\ "
  private String replaceSpacesInQuotes(String line) {
    String newLine = "";
    boolean inQuotes = false;
    for (int ii = 0; ii < line.length(); ii++) {
      char ch = line.charAt(ii);
      if ((ch == '\"') || (ch == '\'')) {
        if (!inQuotes) {
          // enter in a quoted string
          inQuotes = true;
        } else {
          // end of a quoted string
          inQuotes = false;
        }
        continue;
      }
      if (inQuotes && (ch == ' ')) {
        newLine += "\\ ";
      } else {
        newLine += ch;
      }
    }
    return newLine;
  }
}
