From 67fe39c9b91686bb1a8d2fc5feaf5de096061a58 Mon Sep 17 00:00:00 2001
From: Violette Roche-Montane <violette.roche-montane@forgerock.com>
Date: Tue, 04 Feb 2014 13:51:34 +0000
Subject: [PATCH] Checkpoint commit for OPENDJ-1303 Split out CLI support from opendj-ldap-toolkit into a separate Maven module, "opendj-cli" - Added SubCommand && SubCommandParser. - Added more commonsArguments. - Replaced "double" in IntegerArgument to "int". - Added LINE_SEPARATOR in Utils. - Updated Constants. - Updated messages.

---
 opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java                    |    3 
 opendj-cli/src/main/java/com/forgerock/opendj/cli/Argument.java                 |    2 
 opendj-cli/src/main/java/com/forgerock/opendj/cli/IntegerArgument.java          |  149 --
 opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java          |  427 +++++++++
 opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommand.java               |  359 +++++++
 opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties           |  198 ++++
 opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommandArgumentParser.java | 1531 +++++++++++++++++++++++++++++++++
 opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentParser.java           |   42 
 opendj-cli/src/main/java/com/forgerock/opendj/cli/CliConstants.java             |   16 
 9 files changed, 2,582 insertions(+), 145 deletions(-)

diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/Argument.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/Argument.java
index e75df99..6e3cb89 100644
--- a/opendj-cli/src/main/java/com/forgerock/opendj/cli/Argument.java
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/Argument.java
@@ -40,7 +40,7 @@
  * for an application. This is an abstract class that must be subclassed in
  * order to provide specific functionality.
  */
-abstract class Argument {
+public abstract class Argument {
     // Indicates whether this argument should be hidden in the usage
     // information.
     private boolean isHidden;
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentParser.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentParser.java
index 0b1a392..264ead1 100644
--- a/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentParser.java
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentParser.java
@@ -44,6 +44,7 @@
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.LinkedList;
+import java.util.List;
 import java.util.Properties;
 import java.util.Set;
 import java.util.SortedSet;
@@ -62,7 +63,7 @@
  * file to obtain default values for arguments there if they are not specified
  * on the command-line.
  */
-public final class ArgumentParser {
+public class ArgumentParser {
     /**
      * The argument that will be used to indicate the file properties.
      */
@@ -143,7 +144,7 @@
     private String[] rawArguments;
 
     /** Set of argument groups. */
-    private Set<ArgumentGroup> argumentGroups;
+    protected Set<ArgumentGroup> argumentGroups;
 
     /**
      * Group for arguments that have not been explicitly grouped. These will
@@ -1593,4 +1594,41 @@
             buffer.append(EOL);
         }
     }
+
+    void normalizeArguments(final Properties argumentProperties, final List<Argument> arguments)
+            throws ArgumentException {
+        for (final Argument a : arguments) {
+            if (!a.isPresent()
+            // See if there is a value in the properties that can be used
+                    && argumentProperties != null && a.getPropertyName() != null) {
+                final String value = argumentProperties.getProperty(a.getPropertyName().toLowerCase());
+                final LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
+                if (value != null) {
+                    Boolean addValue = true;
+                    if (!(a instanceof BooleanArgument)) {
+                        addValue = a.valueIsAcceptable(value, invalidReason);
+                    }
+                    if (addValue) {
+                        a.addValue(value);
+                        if (a.needsValue()) {
+                            a.setPresent(true);
+                        }
+                        a.setValueSetByProperty(true);
+                    }
+                }
+            }
+
+            if (!a.isPresent() && a.needsValue()) {
+                // See if the argument defines a default.
+                if (a.getDefaultValue() != null) {
+                    a.addValue(a.getDefaultValue());
+                }
+
+                // If there is still no value and the argument is required, then that's a problem.
+                if (!a.hasValue() && a.isRequired()) {
+                    throw new ArgumentException(ERR_ARGPARSER_NO_VALUE_FOR_REQUIRED_ARG.get(a.getName()));
+                }
+            }
+        }
+    }
 }
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/CliConstants.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/CliConstants.java
index 70354ce..bf34149 100755
--- a/opendj-cli/src/main/java/com/forgerock/opendj/cli/CliConstants.java
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/CliConstants.java
@@ -617,6 +617,22 @@
      */
     public static final String OPTION_LONG_OUTPUT_LDIF_FILENAME = "outputLDIF";
 
+    /**
+     * The value for the long option to automatically accept the license
+     * if present.
+     */
+    public static final String OPTION_LONG_ACCEPT_LICENSE = "acceptLicense";
+
+    /**
+     * The value for the short option rootUserDN.
+     */
+    public static final char OPTION_SHORT_ROOT_USER_DN = 'D';
+
+    /**
+     * The value for the long option rootUserDN.
+     */
+    public static final String OPTION_LONG_ROOT_USER_DN = "rootUserDN";
+
     // Prevent instantiation.
     private CliConstants() {
 
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java
index 038ed2a..d9b4d40 100644
--- a/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java
@@ -46,8 +46,7 @@
      *             If there is a problem with any of the parameters used to create this argument.
      */
     public static final BooleanArgument getShowUsage() throws ArgumentException {
-        return new BooleanArgument("showUsage", OPTION_SHORT_HELP, OPTION_LONG_HELP,
-                INFO_DESCRIPTION_SHOWUSAGE.get());
+        return new BooleanArgument("showUsage", OPTION_SHORT_HELP, OPTION_LONG_HELP, INFO_DESCRIPTION_SHOWUSAGE.get());
     }
 
     /**
@@ -71,9 +70,8 @@
      *             If there is a problem with any of the parameters used to create this argument.
      */
     public static final StringArgument getPropertiesFileArgument() throws ArgumentException {
-        return 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());
+        return 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());
     }
 
     /**
@@ -117,4 +115,423 @@
         return version;
     }
 
+    /**
+     * Returns the "quiet" boolean argument.
+     *
+     * @return The "quiet" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final BooleanArgument getQuiet() throws ArgumentException {
+        return new BooleanArgument(OPTION_LONG_QUIET, OPTION_SHORT_QUIET, OPTION_LONG_QUIET,
+                INFO_DESCRIPTION_QUIET.get());
+    }
+
+    /**
+     * Returns the "no-prompt" boolean argument.
+     *
+     * @return The "no-prompt" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final BooleanArgument getNoPrompt() throws ArgumentException {
+        return new BooleanArgument(OPTION_LONG_NO_PROMPT, OPTION_SHORT_NO_PROMPT, OPTION_LONG_NO_PROMPT,
+                INFO_DESCRIPTION_NO_PROMPT.get());
+    }
+
+    /**
+     * Returns the "acceptLicense" boolean argument.
+     *
+     * @return The "acceptLicense" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final BooleanArgument getAcceptLicense() throws ArgumentException {
+        return new BooleanArgument(OPTION_LONG_ACCEPT_LICENSE, null, OPTION_LONG_ACCEPT_LICENSE,
+                INFO_OPTION_ACCEPT_LICENSE.get());
+    }
+
+    /**
+     * Returns the "test only" boolean argument.
+     *
+     * @return The "test only" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final BooleanArgument getTestOnly() throws ArgumentException {
+        final BooleanArgument testOnly = new BooleanArgument("testOnly".toLowerCase(), 't', "testOnly",
+                INFO_ARGUMENT_DESCRIPTION_TESTONLY.get());
+        testOnly.setHidden(true);
+        testOnly.setPropertyName("testOnly");
+        return testOnly;
+    }
+
+    /**
+     * Returns the "CLI" boolean argument.
+     *
+     * @return The "CLI" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final BooleanArgument getCLI() throws ArgumentException {
+        final BooleanArgument cli = new BooleanArgument(OPTION_LONG_CLI.toLowerCase(), OPTION_SHORT_CLI,
+                OPTION_LONG_CLI, INFO_ARGUMENT_DESCRIPTION_CLI.get());
+        cli.setPropertyName(OPTION_LONG_CLI);
+        return cli;
+    }
+
+    /**
+     * Returns the "baseDN" string argument.
+     *
+     * @return The "baseDN" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final StringArgument getBaseDN() throws ArgumentException {
+        return new StringArgument(OPTION_LONG_BASEDN.toLowerCase(), OPTION_SHORT_BASEDN, OPTION_LONG_BASEDN, false,
+                true, true, INFO_BASEDN_PLACEHOLDER.get(), null, OPTION_LONG_BASEDN,
+                INFO_ARGUMENT_DESCRIPTION_BASEDN.get());
+    }
+
+    /**
+     * Returns the "add base entry" boolean argument.
+     *
+     * @return The "addBaseEntry" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final BooleanArgument getAddBaseEntry() throws ArgumentException {
+        final BooleanArgument addBaseEntryArg = new BooleanArgument("addBaseEntry".toLowerCase(), 'a', "addBaseEntry",
+                INFO_ARGUMENT_DESCRIPTION_ADDBASE.get());
+        addBaseEntryArg.setPropertyName("addBaseEntry");
+        return addBaseEntryArg;
+    }
+
+    /**
+     * Returns the "import LDIF" string argument.
+     *
+     * @return The "import LDIF" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final StringArgument getImportLDIF() throws ArgumentException {
+        return new StringArgument(OPTION_LONG_LDIF_FILE.toLowerCase(), OPTION_SHORT_LDIF_FILE, OPTION_LONG_LDIF_FILE,
+                false, true, true, INFO_LDIFFILE_PLACEHOLDER.get(), null, OPTION_LONG_LDIF_FILE,
+                INFO_ARGUMENT_DESCRIPTION_IMPORTLDIF.get());
+    }
+
+    /**
+     * Returns the "rejected import ldif file" string argument.
+     *
+     * @return The "rejectFile" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final StringArgument getRejectedImportLdif() throws ArgumentException {
+        return new StringArgument("rejectFile".toLowerCase(), 'R', "rejectFile", false, false, true,
+                INFO_REJECT_FILE_PLACEHOLDER.get(), null, "rejectFile", INFO_GENERAL_DESCRIPTION_REJECTED_FILE.get());
+    }
+
+    /**
+     * Returns the "skip file" string argument.
+     *
+     * @return The "skipFile" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final StringArgument getSkippedImportFile() throws ArgumentException {
+        return new StringArgument("skipFile".toLowerCase(), null, "skipFile", false, false, true,
+                INFO_SKIP_FILE_PLACEHOLDER.get(), null, "skipFile", INFO_GENERAL_DESCRIPTION_SKIPPED_FILE.get());
+    }
+
+    /**
+     * Returns the "sample data" integer argument.
+     *
+     * @return The "sampleData" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final IntegerArgument getSampleData() throws ArgumentException {
+        return new IntegerArgument("sampleData".toLowerCase(), 'd', "sampleData", false, false, true,
+                INFO_NUM_ENTRIES_PLACEHOLDER.get(), 0, "sampleData", true, 0, false, 0,
+                INFO_SETUP_DESCRIPTION_SAMPLE_DATA.get());
+    }
+
+    /**
+     * Returns the "LDAP port" integer argument.
+     *
+     * @param defaultLdapPort
+     *            Default LDAP Connector port.
+     * @return The "ldapPort" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final IntegerArgument getLDAPPort(final int defaultLdapPort) throws ArgumentException {
+        return new IntegerArgument("ldapPort".toLowerCase(), OPTION_SHORT_PORT, "ldapPort", false, false, true,
+                INFO_PORT_PLACEHOLDER.get(), defaultLdapPort, "ldapPort", true, 1, true, 65535,
+                INFO_ARGUMENT_DESCRIPTION_LDAPPORT.get());
+    }
+
+    /**
+     * Returns the "Admin port" integer argument.
+     *
+     * @param defaultAdminPort
+     *            Default Administration Connector port.
+     * @return The "adminConnectorPort" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final IntegerArgument getAdminLDAPPort(final int defaultAdminPort) throws ArgumentException {
+        return new IntegerArgument("adminConnectorPort".toLowerCase(), null, "adminConnectorPort", false, false, true,
+                INFO_PORT_PLACEHOLDER.get(), defaultAdminPort, "adminConnectorPort", true, 1, true, 65535,
+                INFO_ARGUMENT_DESCRIPTION_ADMINCONNECTORPORT.get());
+    }
+
+    /**
+     * Returns the "JMX port" integer argument.
+     *
+     * @param defaultJMXPort
+     *            Default JMX port.
+     * @return The "jmxPort" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final IntegerArgument getJMXPort(final int defaultJMXPort) throws ArgumentException {
+        return new IntegerArgument("jmxPort".toLowerCase(), 'x', "jmxPort", false, false, true,
+                INFO_JMXPORT_PLACEHOLDER.get(), defaultJMXPort, "jmxPort", true, 1, true, 65535,
+                INFO_ARGUMENT_DESCRIPTION_SKIPPORT.get());
+    }
+
+    /**
+     * Returns the "skip port check" boolean argument.
+     *
+     * @return The "getSkipPortCheck" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final BooleanArgument getSkipPortCheck() throws ArgumentException {
+        final BooleanArgument skipPortCheck = new BooleanArgument("skipPortCheck".toLowerCase(), 'S', "skipPortCheck",
+                INFO_ARGUMENT_DESCRIPTION_SKIPPORT.get());
+        skipPortCheck.setPropertyName("skipPortCheck");
+        return skipPortCheck;
+    }
+
+    /**
+     * Returns the "directory manager DN" string argument.
+     *
+     * @return The "rootUserDN" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final StringArgument getRootDN() throws ArgumentException {
+        return new StringArgument(OPTION_LONG_ROOT_USER_DN.toLowerCase(), OPTION_SHORT_ROOT_USER_DN,
+                OPTION_LONG_ROOT_USER_DN, false, false, true, INFO_ROOT_USER_DN_PLACEHOLDER.get(),
+                "cn=Directory Manager", OPTION_LONG_ROOT_USER_DN, INFO_ARGUMENT_DESCRIPTION_ROOTDN.get());
+    }
+
+    /**
+     * Returns the "directory manager DN password" string argument.
+     *
+     * @return The "rootUserPassword" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final StringArgument getRootDNPwd() throws ArgumentException {
+        return new StringArgument("rootUserPassword".toLowerCase(), OPTION_SHORT_BINDPWD, "rootUserPassword", false,
+                false, true, INFO_ROOT_USER_PWD_PLACEHOLDER.get(), null, "rootUserPassword",
+                INFO_ROOT_USER_PWD_PLACEHOLDER.get());
+    }
+
+    /**
+     * Returns the "directory manager DN password file" file argument.
+     *
+     * @return The "rootUserPasswordFile" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final FileBasedArgument getRootDNPwdFile() throws ArgumentException {
+        return new FileBasedArgument("rootUserPasswordFile".toLowerCase(), OPTION_SHORT_BINDPWD_FILE,
+                "rootUserPasswordFile", false, false, INFO_ROOT_USER_PWD_FILE_PLACEHOLDER.get(), null,
+                "rootUserPasswordFile", INFO_ARGUMENT_DESCRIPTION_ROOTPWFILE.get());
+    }
+
+    /**
+     * Returns the "enable window service" integer argument.
+     *
+     * @return The "enableWindowsService" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final BooleanArgument getEnableWindowsService() throws ArgumentException {
+        final BooleanArgument enableWindowsServiceArg = new BooleanArgument("enableWindowsService".toLowerCase(), 'e',
+                "enableWindowsService", INFO_ARGUMENT_DESCRIPTION_ENABLE_WINDOWS_SERVICE.get());
+        enableWindowsServiceArg.setPropertyName("enableWindowsService");
+        return enableWindowsServiceArg;
+    }
+
+    /**
+     * Returns the "do not start" boolean argument.
+     *
+     * @return The "doNotStart" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final BooleanArgument getDoNotStart() throws ArgumentException {
+        final BooleanArgument doNotStartArg = new BooleanArgument("doNotStart".toLowerCase(), 'O', "doNotStart",
+                INFO_SETUP_DESCRIPTION_DO_NOT_START.get());
+        doNotStartArg.setPropertyName("doNotStart");
+        return doNotStartArg;
+    }
+
+    /**
+     * Returns the "enable start TLS" boolean argument.
+     *
+     * @return The "enableStartTLS" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final BooleanArgument getEnableTLS() throws ArgumentException {
+        final BooleanArgument enableStartTLS = new BooleanArgument("enableStartTLS".toLowerCase(),
+                OPTION_SHORT_START_TLS, "enableStartTLS", INFO_SETUP_DESCRIPTION_ENABLE_STARTTLS.get());
+        enableStartTLS.setPropertyName("enableStartTLS");
+        return enableStartTLS;
+    }
+
+    /**
+     * Returns the "ldaps port" integer argument.
+     *
+     * @param defaultSecurePort
+     *            Default value for the LDAPS port.
+     * @return The "ldapsPort" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final IntegerArgument getLDAPSPort(final int defaultSecurePort) throws ArgumentException {
+        return new IntegerArgument("ldapsPort".toLowerCase(), OPTION_SHORT_USE_SSL, "ldapsPort", false, false, true,
+                INFO_PORT_PLACEHOLDER.get(), defaultSecurePort, "ldapsPort", true, 1, true, 65535,
+                INFO_ARGUMENT_DESCRIPTION_LDAPSPORT.get());
+    }
+
+    /**
+     * Returns the "generate self certificate" boolean argument.
+     *
+     * @return The "generateSelfSignedCertificate" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final BooleanArgument getGenerateSelfSigned() throws ArgumentException {
+        final BooleanArgument generateSelfSigned = new BooleanArgument("generateSelfSignedCertificate".toLowerCase(),
+                null, "generateSelfSignedCertificate", INFO_ARGUMENT_DESCRIPTION_USE_SELF_SIGNED_CERTIFICATE.get());
+        generateSelfSigned.setPropertyName("generateSelfSignedCertificate");
+        return generateSelfSigned;
+    }
+
+    /**
+     * Returns the "host name" string argument.
+     *
+     * @param defaultHostName
+     *            The default host name value.
+     * @return The "hostname" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final StringArgument getHostName(final String defaultHostName) throws ArgumentException {
+        final StringArgument hostName = new StringArgument(OPTION_LONG_HOST.toLowerCase(), OPTION_SHORT_HOST,
+                OPTION_LONG_HOST, false, false, true, INFO_HOST_PLACEHOLDER.get(), defaultHostName, null,
+                INFO_ARGUMENT_DESCRIPTION_HOST_NAME.get());
+        hostName.setPropertyName(OPTION_LONG_HOST);
+        return hostName;
+    }
+
+    /**
+     * Returns the "use PKCS11 key store" boolean argument.
+     *
+     * @return The "usePkcs11Keystore" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final BooleanArgument getUsePKCS11Keystore() throws ArgumentException {
+        final BooleanArgument usePkcs11 = new BooleanArgument("usePkcs11Keystore".toLowerCase(), null,
+                "usePkcs11Keystore", INFO_ARGUMENT_DESCRIPTION_USE_PKCS11.get());
+        usePkcs11.setPropertyName("usePkcs11Keystore");
+        return usePkcs11;
+    }
+
+    /**
+     * Returns the "use java key store" string argument.
+     *
+     * @return The "useJavaKeystore" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final StringArgument getUseJavaKeyStore() throws ArgumentException {
+        return new StringArgument("useJavaKeystore".toLowerCase(), null, "useJavaKeystore", false, false, true,
+                INFO_KEYSTOREPATH_PLACEHOLDER.get(), null, "useJavaKeystore",
+                INFO_ARGUMENT_DESCRIPTION_USE_JAVAKEYSTORE.get());
+    }
+
+    /**
+     * Returns the "use JCEKS" string argument.
+     *
+     * @return The "useJCEKS" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final StringArgument getUseJCEKS() throws ArgumentException {
+        return new StringArgument("useJCEKS".toLowerCase(), null, "useJCEKS", false, false, true,
+                INFO_KEYSTOREPATH_PLACEHOLDER.get(), null, "useJCEKS", INFO_ARGUMENT_DESCRIPTION_USE_JCEKS.get());
+    }
+
+    /**
+     * Returns the "use PKCS12 key store" string argument.
+     *
+     * @return The "usePkcs12keyStore" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final StringArgument getUsePKCS12KeyStore() throws ArgumentException {
+        return new StringArgument("usePkcs12keyStore".toLowerCase(), null, "usePkcs12keyStore", false, false, true,
+                INFO_KEYSTOREPATH_PLACEHOLDER.get(), null, "usePkcs12keyStore",
+                INFO_ARGUMENT_DESCRIPTION_USE_PKCS12.get());
+    }
+
+    /**
+     * Returns the "key store password" string argument.
+     *
+     * @return The "keyStorePassword" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final StringArgument getKeyStorePassword() throws ArgumentException {
+        return new StringArgument(OPTION_LONG_KEYSTORE_PWD.toLowerCase(), OPTION_SHORT_KEYSTORE_PWD,
+                OPTION_LONG_KEYSTORE_PWD, false, false, true, INFO_KEYSTORE_PWD_PLACEHOLDER.get(), null,
+                OPTION_LONG_KEYSTORE_PWD, INFO_ARGUMENT_DESCRIPTION_KEYSTOREPASSWORD.get());
+    }
+
+    /**
+     * Returns the "key store password file" file argument.
+     *
+     * @return The "keyStorePassword" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final FileBasedArgument getKeyStorePasswordFile() throws ArgumentException {
+        return new FileBasedArgument(OPTION_LONG_KEYSTORE_PWD_FILE.toLowerCase(), OPTION_SHORT_KEYSTORE_PWD_FILE,
+                OPTION_LONG_KEYSTORE_PWD_FILE, false, false, INFO_KEYSTORE_PWD_FILE_PLACEHOLDER.get(), null,
+                OPTION_LONG_KEYSTORE_PWD_FILE, INFO_ARGUMENT_DESCRIPTION_KEYSTOREPASSWORD_FILE.get());
+    }
+
+    /**
+     * Returns the "key store password file" string argument.
+     *
+     * @return The "keyStorePassword" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static final StringArgument getCertNickName() throws ArgumentException {
+        return new StringArgument(OPTION_LONG_CERT_NICKNAME.toLowerCase(), OPTION_SHORT_CERT_NICKNAME,
+                OPTION_LONG_CERT_NICKNAME, false, false, true, INFO_NICKNAME_PLACEHOLDER.get(), null,
+                OPTION_LONG_CERT_NICKNAME, INFO_ARGUMENT_DESCRIPTION_CERT_NICKNAME.get());
+    }
+
 }
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/IntegerArgument.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/IntegerArgument.java
index 5ede248..cb57056 100644
--- a/opendj-cli/src/main/java/com/forgerock/opendj/cli/IntegerArgument.java
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/IntegerArgument.java
@@ -44,133 +44,10 @@
     private final boolean hasUpperBound;
 
     // The lower bound that will be enforced for this argument.
-    private final double lowerBound;
+    private final int lowerBound;
 
     // The upper bound that will be enforced for this argument.
-    private final double upperBound;
-
-    /**
-     * Creates a new integer argument with the provided information.
-     *
-     * @param name
-     *            The generic name that should be used to refer to this
-     *            argument.
-     * @param shortIdentifier
-     *            The single-character identifier for this argument, or
-     *            <CODE>null</CODE> if there is none.
-     * @param longIdentifier
-     *            The long identifier for this argument, or <CODE>null</CODE> if
-     *            there is none.
-     * @param isRequired
-     *            Indicates whether this argument must be specified on the
-     *            command line.
-     * @param isMultiValued
-     *            Indicates whether this argument may be specified more than
-     *            once to provide multiple values.
-     * @param needsValue
-     *            Indicates whether this argument requires a value.
-     * @param valuePlaceholder
-     *            The placeholder for the argument value that will be displayed
-     *            in usage information, or <CODE>null</CODE> if this argument
-     *            does not require a value.
-     * @param defaultValue
-     *            The default value that should be used for this argument if
-     *            none is provided in a properties file or on the command line.
-     *            This may be <CODE>null</CODE> if there is no generic default.
-     * @param propertyName
-     *            The name of the property in a property file that may be used
-     *            to override the default value but will be overridden by a
-     *            command-line argument.
-     * @param hasLowerBound
-     *            Indicates whether a lower bound should be enforced for values
-     *            of this argument.
-     * @param lowerBound
-     *            The lower bound that should be enforced for values of this
-     *            argument.
-     * @param hasUpperBound
-     *            Indicates whether an upperbound should be enforced for values
-     *            of this argument.
-     * @param upperBound
-     *            The upper bound that should be enforced for values of this
-     *            argument.
-     * @param description
-     *            LocalizableMessage for the description of this argument.
-     * @throws ArgumentException
-     *             If there is a problem with any of the parameters used to
-     *             create this argument.
-     */
-    public IntegerArgument(final String name, final Character shortIdentifier,
-            final String longIdentifier, final boolean isRequired, final boolean isMultiValued,
-            final boolean needsValue, final LocalizableMessage valuePlaceholder,
-            final double defaultValue, final String propertyName, final boolean hasLowerBound,
-            final double lowerBound, final boolean hasUpperBound, final double upperBound,
-            final LocalizableMessage description) throws ArgumentException {
-        super(name, shortIdentifier, longIdentifier, isRequired, isMultiValued, needsValue,
-                valuePlaceholder, String.valueOf(defaultValue), propertyName, description);
-
-        this.hasLowerBound = hasLowerBound;
-        this.hasUpperBound = hasUpperBound;
-        this.lowerBound = lowerBound;
-        this.upperBound = upperBound;
-
-        if (hasLowerBound && hasUpperBound && (lowerBound > upperBound)) {
-            final LocalizableMessage message =
-                    ERR_INTARG_LOWER_BOUND_ABOVE_UPPER_BOUND.get(name, lowerBound, upperBound);
-            throw new ArgumentException(message);
-        }
-    }
-
-    /**
-     * Creates a new integer argument with the provided information.
-     *
-     * @param name
-     *            The generic name that should be used to refer to this
-     *            argument.
-     * @param shortIdentifier
-     *            The single-character identifier for this argument, or
-     *            <CODE>null</CODE> if there is none.
-     * @param longIdentifier
-     *            The long identifier for this argument, or <CODE>null</CODE> if
-     *            there is none.
-     * @param isRequired
-     *            Indicates whether this argument must be specified on the
-     *            command line.
-     * @param isMultiValued
-     *            Indicates whether this argument may be specified more than
-     *            once to provide multiple values.
-     * @param needsValue
-     *            Indicates whether this argument requires a value.
-     * @param valuePlaceholder
-     *            The placeholder for the argument value that will be displayed
-     *            in usage information, or <CODE>null</CODE> if this argument
-     *            does not require a value.
-     * @param defaultValue
-     *            The default value that should be used for this argument if
-     *            none is provided in a properties file or on the command line.
-     *            This may be <CODE>null</CODE> if there is no generic default.
-     * @param propertyName
-     *            The name of the property in a property file that may be used
-     *            to override the default value but will be overridden by a
-     *            command-line argument.
-     * @param description
-     *            LocalizableMessage for the description of this argument.
-     * @throws ArgumentException
-     *             If there is a problem with any of the parameters used to
-     *             create this argument.
-     */
-    public IntegerArgument(final String name, final Character shortIdentifier,
-            final String longIdentifier, final boolean isRequired, final boolean isMultiValued,
-            final boolean needsValue, final LocalizableMessage valuePlaceholder,
-            final double defaultValue, final String propertyName,
-            final LocalizableMessage description) throws ArgumentException {
-        super(name, shortIdentifier, longIdentifier, isRequired, isMultiValued, needsValue,
-                valuePlaceholder, String.format("%f", defaultValue), propertyName, description);
-
-        hasLowerBound = false;
-        hasUpperBound = false;
-        lowerBound = Integer.MIN_VALUE;
-        upperBound = Integer.MAX_VALUE;
-    }
+    private final int upperBound;
 
     /**
      * Creates a new integer argument with the provided information.
@@ -226,7 +103,7 @@
             final String longIdentifier, final boolean isRequired, final boolean isMultiValued,
             final boolean needsValue, final LocalizableMessage valuePlaceholder,
             final int defaultValue, final String propertyName, final boolean hasLowerBound,
-            final double lowerBound, final boolean hasUpperBound, final double upperBound,
+            final int lowerBound, final boolean hasUpperBound, final int upperBound,
             final LocalizableMessage description) throws ArgumentException {
         super(name, shortIdentifier, longIdentifier, isRequired, isMultiValued, needsValue,
                 valuePlaceholder, String.valueOf(defaultValue), propertyName, description);
@@ -337,7 +214,7 @@
     public IntegerArgument(final String name, final Character shortIdentifier,
             final String longIdentifier, final boolean isRequired, final boolean needsValue,
             final LocalizableMessage valuePlaceholder, final boolean hasLowerBound,
-            final double lowerBound, final boolean hasUpperBound, final double upperBound,
+            final int lowerBound, final boolean hasUpperBound, final int upperBound,
             final LocalizableMessage description) throws ArgumentException {
         super(name, shortIdentifier, longIdentifier, isRequired, false, needsValue,
                 valuePlaceholder, null, null, description);
@@ -390,8 +267,8 @@
 
         hasLowerBound = false;
         hasUpperBound = false;
-        lowerBound = Double.MIN_VALUE;
-        upperBound = Double.MAX_VALUE;
+        lowerBound = Integer.MIN_VALUE;
+        upperBound = Integer.MAX_VALUE;
     }
 
     /**
@@ -400,7 +277,7 @@
      *
      * @return The lower bound that may be enforced for values of this argument.
      */
-    public double getLowerBound() {
+    public int getLowerBound() {
         return lowerBound;
     }
 
@@ -410,7 +287,7 @@
      *
      * @return The upper bound that may be enforced for values of this argument.
      */
-    public double getUpperBound() {
+    public int getUpperBound() {
         return upperBound;
     }
 
@@ -452,18 +329,18 @@
     public boolean valueIsAcceptable(final String valueString,
             final LocalizableMessageBuilder invalidReason) {
         // First, the value must be decodable as an integer.
-        double intValue;
+        int intValue;
         try {
-            intValue = Double.parseDouble(valueString);
+            intValue = Integer.parseInt(valueString);
         } catch (final Exception e) {
-            invalidReason.append(ERR_ARG_CANNOT_DECODE_AS_INT.get(valueString, getName()));
+            invalidReason.append(ERR_ARG_CANNOT_DECODE_AS_INT.get(valueString, getPropertyName()));
             return false;
         }
 
         // If there is a lower bound, then the value must be greater than or
         // equal to it.
         if (hasLowerBound && (intValue < lowerBound)) {
-            invalidReason.append(ERR_INTARG_VALUE_BELOW_LOWER_BOUND.get(getName(), intValue,
+            invalidReason.append(ERR_INTARG_VALUE_BELOW_LOWER_BOUND.get(getPropertyName(), intValue,
                     lowerBound));
             return false;
         }
@@ -472,7 +349,7 @@
         // equal to it.
         if (hasUpperBound && (intValue > upperBound)) {
 
-            invalidReason.append(ERR_INTARG_VALUE_ABOVE_UPPER_BOUND.get(getName(), intValue,
+            invalidReason.append(ERR_INTARG_VALUE_ABOVE_UPPER_BOUND.get(getPropertyName(), intValue,
                     upperBound));
             return false;
         }
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommand.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommand.java
new file mode 100644
index 0000000..f320edc
--- /dev/null
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommand.java
@@ -0,0 +1,359 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at legal-notices/CDDLv1_0.txt.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Copyright 2006-2008 Sun Microsystems, Inc.
+ *      Portions Copyright 2014 ForgeRock AS
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.util.StaticUtils.toLowerCase;
+import static com.forgerock.opendj.cli.CliMessages.*;
+
+import org.forgerock.i18n.LocalizableMessage;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+
+/**
+ * This class defines a data structure for holding information about a subcommand that may be used with the subcommand
+ * argument parser. The subcommand has a name, a description, and a set of arguments.
+ */
+public class SubCommand {
+    // Indicates whether this subCommand should be hidden in the usage
+    // information.
+    private boolean isHidden;
+
+    // The mapping between the short argument IDs and the arguments for this
+    // subcommand.
+    private HashMap<Character, Argument> shortIDMap;
+
+    // The mapping between the long argument IDs and the arguments for this
+    // subcommand.
+    private HashMap<String, Argument> longIDMap;
+
+    // The list of arguments associated with this subcommand.
+    private LinkedList<Argument> arguments;
+
+    // The description for this subcommand.
+    private LocalizableMessage description;
+
+    // The name of this subcommand.
+    private String name;
+
+    // The argument parser with which this subcommand is associated.
+    private SubCommandArgumentParser parser;
+
+    // Indicates whether this parser will allow additional unnamed
+    // arguments at the end of the list.
+    private boolean allowsTrailingArguments;
+
+    // The maximum number of unnamed trailing arguments that may be
+    // provided.
+    private int maxTrailingArguments;
+
+    // The minimum number of unnamed trailing arguments that may be
+    // provided.
+    private int minTrailingArguments;
+
+    // The display name that will be used for the trailing arguments in
+    // the usage information.
+    private String trailingArgsDisplayName;
+
+    /**
+     * Creates a new subcommand with the provided information. The subcommand will be automatically registered with the
+     * associated parser.
+     *
+     * @param parser
+     *            The argument parser with which this subcommand is associated.
+     * @param name
+     *            The name of this subcommand.
+     * @param description
+     *            The description of this subcommand.
+     * @throws ArgumentException
+     *             If the associated argument parser already has a subcommand with the same name.
+     */
+    public SubCommand(SubCommandArgumentParser parser, String name, LocalizableMessage description)
+            throws ArgumentException {
+        this(parser, name, false, 0, 0, null, description);
+    }
+
+    /**
+     * Creates a new subcommand with the provided information. The subcommand will be automatically registered with the
+     * associated parser.
+     *
+     * @param parser
+     *            The argument parser with which this subcommand is associated.
+     * @param name
+     *            The name of this subcommand.
+     * @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 description
+     *            The description of this subcommand.
+     * @throws ArgumentException
+     *             If the associated argument parser already has a subcommand with the same name.
+     */
+    public SubCommand(SubCommandArgumentParser parser, String name, boolean allowsTrailingArguments,
+            int minTrailingArguments, int maxTrailingArguments, String trailingArgsDisplayName,
+            LocalizableMessage description) throws ArgumentException {
+        this.parser = parser;
+        this.name = name;
+        this.description = description;
+        this.allowsTrailingArguments = allowsTrailingArguments;
+        this.minTrailingArguments = minTrailingArguments;
+        this.maxTrailingArguments = maxTrailingArguments;
+        this.trailingArgsDisplayName = trailingArgsDisplayName;
+        this.isHidden = false;
+
+        String nameToCheck = name;
+        if (parser.longArgumentsCaseSensitive()) {
+            nameToCheck = toLowerCase(name);
+        }
+
+        if (parser.hasSubCommand(nameToCheck)) {
+            LocalizableMessage message = ERR_ARG_SUBCOMMAND_DUPLICATE_SUBCOMMAND.get(name);
+            throw new ArgumentException(message);
+        }
+
+        parser.addSubCommand(this);
+        shortIDMap = new HashMap<Character, Argument>();
+        longIDMap = new HashMap<String, Argument>();
+        arguments = new LinkedList<Argument>();
+    }
+
+    /**
+     * Retrieves the name of this subcommand.
+     *
+     * @return The name of this subcommand.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Retrieves the description for this subcommand.
+     *
+     * @return The description for this subcommand.
+     */
+    public LocalizableMessage getDescription() {
+        return description;
+    }
+
+    /**
+     * Retrieves the set of arguments for this subcommand.
+     *
+     * @return The set of arguments for this subcommand.
+     */
+    public LinkedList<Argument> getArguments() {
+        return arguments;
+    }
+
+    /**
+     * Retrieves the subcommand argument with the specified short identifier.
+     *
+     * @param shortID
+     *            The short identifier of the argument to retrieve.
+     * @return The subcommand argument with the specified short identifier, or <CODE>null</CODE> if there is none.
+     */
+    public Argument getArgument(Character shortID) {
+        return shortIDMap.get(shortID);
+    }
+
+    /**
+     * Retrieves the subcommand argument with the specified long identifier.
+     *
+     * @param longID
+     *            The long identifier of the argument to retrieve.
+     * @return The subcommand argument with the specified long identifier, or <CODE>null</CODE> if there is none.
+     */
+    public Argument getArgument(String longID) {
+        return longIDMap.get(longID);
+    }
+
+    /**
+     * Retrieves the subcommand argument with the specified name.
+     *
+     * @param name
+     *            The name of the argument to retrieve.
+     * @return The subcommand argument with the specified name, or <CODE>null</CODE> if there is no such argument.
+     */
+    public Argument getArgumentForName(String name) {
+        for (Argument a : arguments) {
+            if (a.getName().equals(name)) {
+                return a;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Adds the provided argument for use with this subcommand.
+     *
+     * @param argument
+     *            The argument to add for use with this subcommand.
+     * @throws ArgumentException
+     *             If either the short ID or long ID for the argument conflicts with that of another argument already
+     *             associated with this subcommand.
+     */
+    public void addArgument(Argument argument) throws ArgumentException {
+        String argumentName = argument.getName();
+        for (Argument a : arguments) {
+            if (argumentName.equals(a.getName())) {
+                LocalizableMessage message = ERR_ARG_SUBCOMMAND_DUPLICATE_ARGUMENT_NAME.get(name, argumentName);
+                throw new ArgumentException(message);
+            }
+        }
+
+        if (parser.hasGlobalArgument(argumentName)) {
+            LocalizableMessage message = ERR_ARG_SUBCOMMAND_ARGUMENT_GLOBAL_CONFLICT.get(argumentName, name);
+            throw new ArgumentException(message);
+        }
+
+        Character shortID = argument.getShortIdentifier();
+        if (shortID != null) {
+            if (shortIDMap.containsKey(shortID)) {
+                LocalizableMessage message = ERR_ARG_SUBCOMMAND_DUPLICATE_SHORT_ID.get(argumentName, name,
+                        String.valueOf(shortID), shortIDMap.get(shortID).getName());
+                throw new ArgumentException(message);
+            }
+
+            Argument arg = parser.getGlobalArgumentForShortID(shortID);
+            if (arg != null) {
+                LocalizableMessage message = ERR_ARG_SUBCOMMAND_ARGUMENT_SHORT_ID_GLOBAL_CONFLICT.get(argumentName,
+                        name, String.valueOf(shortID), arg.getName());
+                throw new ArgumentException(message);
+            }
+        }
+
+        String longID = argument.getLongIdentifier();
+        if (longID != null) {
+            if (!parser.longArgumentsCaseSensitive()) {
+                longID = toLowerCase(longID);
+            }
+
+            if (longIDMap.containsKey(longID)) {
+                LocalizableMessage message = ERR_ARG_SUBCOMMAND_DUPLICATE_LONG_ID.get(argumentName, name,
+                        argument.getLongIdentifier(), longIDMap.get(longID).getName());
+                throw new ArgumentException(message);
+            }
+
+            Argument arg = parser.getGlobalArgumentForLongID(longID);
+            if (arg != null) {
+                LocalizableMessage message = ERR_ARG_SUBCOMMAND_ARGUMENT_LONG_ID_GLOBAL_CONFLICT.get(argumentName,
+                        name, argument.getLongIdentifier(), arg.getName());
+                throw new ArgumentException(message);
+            }
+        }
+
+        arguments.add(argument);
+
+        if (shortID != null) {
+            shortIDMap.put(shortID, argument);
+        }
+
+        if (longID != null) {
+            longIDMap.put(longID, argument);
+        }
+    }
+
+    /**
+     * Indicates whether this sub-command will allow unnamed trailing arguments. These will be arguments at the end of
+     * the list that are not preceded by either a long or short identifier and will need to be manually parsed by the
+     * application using this parser. Note that once an unnamed trailing argument has been identified, all remaining
+     * arguments will be classified as such.
+     *
+     * @return <CODE>true</CODE> if this sub-command allows unnamed trailing arguments, or <CODE>false</CODE> if it does
+     *         not.
+     */
+    public boolean allowsTrailingArguments() {
+        return allowsTrailingArguments;
+    }
+
+    /**
+     * Retrieves the minimum number of unnamed trailing arguments that must be provided.
+     *
+     * @return The minimum number of unnamed trailing arguments that must be provided, or a value less than or equal to
+     *         zero if no minimum will be enforced.
+     */
+    public int getMinTrailingArguments() {
+        return minTrailingArguments;
+    }
+
+    /**
+     * Retrieves the maximum number of unnamed trailing arguments that may be provided.
+     *
+     * @return The maximum number of unnamed trailing arguments that may be provided, or a value less than or equal to
+     *         zero if no maximum will be enforced.
+     */
+    public int getMaxTrailingArguments() {
+        return maxTrailingArguments;
+    }
+
+    /**
+     * Retrieves the trailing arguments display name.
+     *
+     * @return Returns the trailing arguments display name.
+     */
+    public String getTrailingArgumentsDisplayName() {
+        return trailingArgsDisplayName;
+    }
+
+    /**
+     * Retrieves the set of unnamed trailing arguments that were provided on the command line.
+     *
+     * @return The set of unnamed trailing arguments that were provided on the command line.
+     */
+    public ArrayList<String> getTrailingArguments() {
+        return parser.getTrailingArguments();
+    }
+
+    /**
+     * Indicates whether this subcommand should be hidden from the usage information.
+     *
+     * @return <CODE>true</CODE> if this subcommand should be hidden from the usage information, or <CODE>false</CODE>
+     *         if not.
+     */
+    public boolean isHidden() {
+        return isHidden;
+    }
+
+    /**
+     * Specifies whether this subcommand should be hidden from the usage information.
+     *
+     * @param isHidden
+     *            Indicates whether this subcommand should be hidden from the usage information.
+     */
+    public void setHidden(boolean isHidden) {
+        this.isHidden = isHidden;
+    }
+}
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommandArgumentParser.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommandArgumentParser.java
new file mode 100644
index 0000000..2ca9dcf
--- /dev/null
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommandArgumentParser.java
@@ -0,0 +1,1531 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at legal-notices/CDDLv1_0.txt.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Copyright 2006-2010 Sun Microsystems, Inc.
+ *      Portions Copyright 2011-2014 ForgeRock AS
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.util.StaticUtils.toLowerCase;
+import static com.forgerock.opendj.util.StaticUtils.EOL;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+
+import static com.forgerock.opendj.cli.CliMessages.*;
+import static com.forgerock.opendj.cli.CliConstants.*;
+import static com.forgerock.opendj.cli.Utils.*;
+
+/**
+ * This class defines a variant of the argument parser that can be used with applications that use subcommands to
+ * customize their behavior and that have a different set of options per subcommand (e.g, "cvs checkout" takes different
+ * options than "cvs commit"). This parser also has the ability to use global options that will always be applicable
+ * regardless of the subcommand in addition to the subcommand-specific arguments. There must not be any conflicts
+ * between the global options and the option for any subcommand, but it is allowed to re-use subcommand-specific options
+ * for different purposes between different subcommands.
+ */
+public class SubCommandArgumentParser extends ArgumentParser {
+    /**
+     * The argument that will be used to trigger the display of usage information.
+     */
+    private Argument usageArgument;
+
+    /**
+     * The arguments that will be used to trigger the display of usage information for groups of sub-commands.
+     */
+    private final Map<Argument, Collection<SubCommand>> usageGroupArguments;
+
+    /**
+     * The set of unnamed trailing arguments that were provided for this parser.
+     */
+    private ArrayList<String> trailingArguments;
+
+    /**
+     * Indicates whether subcommand and long argument names should be treated in a case-sensitive manner.
+     */
+    private final boolean longArgumentsCaseSensitive;
+
+    /** Indicates whether the usage information has been displayed. */
+    private boolean usageOrVersionDisplayed;
+
+    /**
+     * The set of global arguments defined for this parser, referenced by short ID.
+     */
+    private final Map<Character, Argument> globalShortIDMap;
+
+    /**
+     * The set of global arguments defined for this parser, referenced by argument name.
+     */
+    private final Map<String, Argument> globalArgumentMap;
+
+    /**
+     * The set of global arguments defined for this parser, referenced by long ID.
+     */
+    private final Map<String, Argument> globalLongIDMap;
+
+    /**
+     * The set of subcommands defined for this parser, referenced by subcommand name.
+     */
+    private final SortedMap<String, SubCommand> subCommands;
+
+    /** The total set of global arguments defined for this parser. */
+    private final List<Argument> globalArgumentList;
+
+    /** The output stream to which usage information should be printed. */
+    private OutputStream usageOutputStream;
+
+    /**
+     * The fully-qualified name of the Java class that should be invoked to launch the program with which this argument
+     * parser is associated.
+     */
+    private final String mainClassName;
+
+    /**
+     * A human-readable description for the tool, which will be included when displaying usage information.
+     */
+    private final LocalizableMessage toolDescription;
+
+    /** The raw set of command-line arguments that were provided. */
+    private String[] rawArguments;
+
+    /**
+     * The subcommand requested by the user as part of the command-line arguments.
+     */
+    private SubCommand subCommand;
+
+    /** Indicates whether the version argument was provided. */
+    private boolean versionPresent;
+
+    private static final String INDENT = "    ";
+    private final static int MAX_LENGTH = 80;
+
+    /**
+     * Creates a new instance of this subcommand argument parser with no 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 subcommand and long argument names should be treated in a case-sensitive manner.
+     */
+    public SubCommandArgumentParser(String mainClassName, LocalizableMessage toolDescription,
+            boolean longArgumentsCaseSensitive) {
+        super(mainClassName, toolDescription, longArgumentsCaseSensitive);
+        this.mainClassName = mainClassName;
+        this.toolDescription = toolDescription;
+        this.longArgumentsCaseSensitive = longArgumentsCaseSensitive;
+
+        trailingArguments = new ArrayList<String>();
+        globalArgumentList = new LinkedList<Argument>();
+        globalArgumentMap = new HashMap<String, Argument>();
+        globalShortIDMap = new HashMap<Character, Argument>();
+        globalLongIDMap = new HashMap<String, Argument>();
+        usageGroupArguments = new HashMap<Argument, Collection<SubCommand>>();
+        subCommands = new TreeMap<String, SubCommand>();
+        usageOrVersionDisplayed = false;
+        rawArguments = null;
+        subCommand = null;
+        usageArgument = null;
+        usageOutputStream = null;
+    }
+
+    /**
+     * Retrieves the fully-qualified name of the Java class that should be invoked to launch the program with which this
+     * argument parser is associated.
+     *
+     * @return The fully-qualified name of the Java class that should be invoked to launch the program with which this
+     *         argument parser is associated.
+     */
+    @Override
+    public String getMainClassName() {
+        return mainClassName;
+    }
+
+    /**
+     * Retrieves a human-readable description for this tool, which should be included at the top of the command-line
+     * usage information.
+     *
+     * @return A human-readable description for this tool, or {@code null} if none is available.
+     */
+    @Override
+    public LocalizableMessage getToolDescription() {
+        return toolDescription;
+    }
+
+    /**
+     * Indicates whether subcommand names and long argument strings should be treated in a case-sensitive manner.
+     *
+     * @return <CODE>true</CODE> if subcommand names and long argument strings should be treated in a case-sensitive
+     *         manner, or <CODE>false</CODE> if they should not.
+     */
+    public boolean longArgumentsCaseSensitive() {
+        return longArgumentsCaseSensitive;
+    }
+
+    /**
+     * Retrieves the list of all global arguments that have been defined for this argument parser.
+     *
+     * @return The list of all global arguments that have been defined for this argument parser.
+     */
+    public List<Argument> getGlobalArgumentList() {
+        return globalArgumentList;
+    }
+
+    /**
+     * Indicates whether this argument parser contains a global argument with the specified name.
+     *
+     * @param argumentName
+     *            The name for which to make the determination.
+     * @return <CODE>true</CODE> if a global argument exists with the specified name, or <CODE>false</CODE> if not.
+     */
+    public boolean hasGlobalArgument(String argumentName) {
+        return globalArgumentMap.containsKey(argumentName);
+    }
+
+    /**
+     * Retrieves the global argument with the specified name.
+     *
+     * @param name
+     *            The name of the global argument to retrieve.
+     * @return The global argument with the specified name, or <CODE>null</CODE> if there is no such argument.
+     */
+    public Argument getGlobalArgument(String name) {
+        return globalArgumentMap.get(name);
+    }
+
+    /**
+     * Retrieves the set of global arguments mapped by the short identifier that may be used to reference them. Note
+     * that arguments that do not have a short identifier will not be present in this list.
+     *
+     * @return The set of global arguments mapped by the short identifier that may be used to reference them.
+     */
+    public Map<Character, Argument> getGlobalArgumentsByShortID() {
+        return globalShortIDMap;
+    }
+
+    /**
+     * Indicates whether this argument parser has a global argument with the specified short ID.
+     *
+     * @param shortID
+     *            The short ID character for which to make the determination.
+     * @return <CODE>true</CODE> if a global argument exists with the specified short ID, or <CODE>false</CODE> if not.
+     */
+    public boolean hasGlobalArgumentWithShortID(Character shortID) {
+        return globalShortIDMap.containsKey(shortID);
+    }
+
+    /**
+     * Retrieves the global argument with the specified short identifier.
+     *
+     * @param shortID
+     *            The short identifier for the global argument to retrieve.
+     * @return The global argument with the specified short identifier, or <CODE>null</CODE> if there is no such
+     *         argument.
+     */
+    public Argument getGlobalArgumentForShortID(Character shortID) {
+        return globalShortIDMap.get(shortID);
+    }
+
+    /**
+     * Retrieves the set of global arguments mapped by the long identifier that may be used to reference them. Note that
+     * arguments that do not have a long identifier will not be present in this list.
+     *
+     * @return The set of global arguments mapped by the long identifier that may be used to reference them.
+     */
+    public Map<String, Argument> getGlobalArgumentsByLongID() {
+        return globalLongIDMap;
+    }
+
+    /**
+     * Indicates whether this argument parser has a global argument with the specified long ID.
+     *
+     * @param longID
+     *            The long ID string for which to make the determination.
+     * @return <CODE>true</CODE> if a global argument exists with the specified long ID, or <CODE>false</CODE> if not.
+     */
+    public boolean hasGlobalArgumentWithLongID(String longID) {
+        return globalLongIDMap.containsKey(longID);
+    }
+
+    /**
+     * Retrieves the global argument with the specified long identifier.
+     *
+     * @param longID
+     *            The long identifier for the global argument to retrieve.
+     * @return The global argument with the specified long identifier, or <CODE>null</CODE> if there is no such
+     *         argument.
+     */
+    public Argument getGlobalArgumentForLongID(String longID) {
+        return globalLongIDMap.get(longID);
+    }
+
+    /**
+     * Retrieves the set of subcommands defined for this argument parser, referenced by subcommand name.
+     *
+     * @return The set of subcommands defined for this argument parser, referenced by subcommand name.
+     */
+    public SortedMap<String, SubCommand> getSubCommands() {
+        return subCommands;
+    }
+
+    /**
+     * Indicates whether this argument parser has a subcommand with the specified name.
+     *
+     * @param name
+     *            The subcommand name for which to make the determination.
+     * @return <CODE>true</CODE> if this argument parser has a subcommand with the specified name, or <CODE>false</CODE>
+     *         if it does not.
+     */
+    public boolean hasSubCommand(String name) {
+        return subCommands.containsKey(name);
+    }
+
+    /**
+     * Retrieves the subcommand with the specified name.
+     *
+     * @param name
+     *            The name of the subcommand to retrieve.
+     * @return The subcommand with the specified name, or <CODE>null</CODE> if no such subcommand is defined.
+     */
+    public SubCommand getSubCommand(String name) {
+        return subCommands.get(name);
+    }
+
+    /**
+     * Retrieves the subcommand that was selected in the set of command-line arguments.
+     *
+     * @return The subcommand that was selected in the set of command-line arguments, or <CODE>null</CODE> if none was
+     *         selected.
+     */
+    public SubCommand getSubCommand() {
+        return subCommand;
+    }
+
+    /**
+     * Retrieves the raw set of arguments that were provided.
+     *
+     * @return The raw set of arguments that were provided, or <CODE>null</CODE> if the argument list has not yet been
+     *         parsed.
+     */
+    @Override
+    public String[] getRawArguments() {
+        return rawArguments;
+    }
+
+    /**
+     * Adds the provided argument to the set of global arguments handled by this parser.
+     *
+     * @param argument
+     *            The argument to be added.
+     * @throws ArgumentException
+     *             If the provided argument conflicts with another global or subcommand argument that has already been
+     *             defined.
+     */
+    public void addGlobalArgument(Argument argument) throws ArgumentException {
+        addGlobalArgument(argument, null);
+    }
+
+    /**
+     * Adds the provided argument to the set of global arguments handled by this parser.
+     *
+     * @param argument
+     *            The argument to be added.
+     * @param group
+     *            The argument group to which the argument belongs.
+     * @throws ArgumentException
+     *             If the provided argument conflicts with another global or subcommand argument that has already been
+     *             defined.
+     */
+    public void addGlobalArgument(Argument argument, ArgumentGroup group) throws ArgumentException {
+
+        String argumentName = argument.getName();
+        if (globalArgumentMap.containsKey(argumentName)) {
+            LocalizableMessage message = ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_NAME.get(argumentName);
+            throw new ArgumentException(message);
+        }
+        for (SubCommand s : subCommands.values()) {
+            if (s.getArgumentForName(argumentName) != null) {
+                LocalizableMessage message = ERR_SUBCMDPARSER_GLOBAL_ARG_NAME_SUBCMD_CONFLICT.get(argumentName,
+                        s.getName());
+                throw new ArgumentException(message);
+            }
+        }
+
+        Character shortID = argument.getShortIdentifier();
+        if (shortID != null) {
+            if (globalShortIDMap.containsKey(shortID)) {
+                String name = globalShortIDMap.get(shortID).getName();
+
+                LocalizableMessage message = ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_SHORT_ID.get(
+                        String.valueOf(shortID), argumentName, name);
+                throw new ArgumentException(message);
+            }
+
+            for (SubCommand s : subCommands.values()) {
+                if (s.getArgument(shortID) != null) {
+                    String cmdName = s.getName();
+                    String name = s.getArgument(shortID).getName();
+
+                    LocalizableMessage message = ERR_SUBCMDPARSER_GLOBAL_ARG_SHORT_ID_CONFLICT.get(
+                            String.valueOf(shortID), argumentName, name, cmdName);
+                    throw new ArgumentException(message);
+                }
+            }
+        }
+
+        String longID = argument.getLongIdentifier();
+        if (longID != null) {
+            if (!longArgumentsCaseSensitive) {
+                longID = toLowerCase(longID);
+            }
+
+            if (globalLongIDMap.containsKey(longID)) {
+                String name = globalLongIDMap.get(longID).getName();
+
+                LocalizableMessage message = ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_LONG_ID.get(
+                        argument.getLongIdentifier(), argumentName, name);
+                throw new ArgumentException(message);
+            }
+
+            for (SubCommand s : subCommands.values()) {
+                if (s.getArgument(longID) != null) {
+                    String cmdName = s.getName();
+                    String name = s.getArgument(longID).getName();
+
+                    LocalizableMessage message = ERR_SUBCMDPARSER_GLOBAL_ARG_LONG_ID_CONFLICT.get(
+                            argument.getLongIdentifier(), argumentName, name, cmdName);
+                    throw new ArgumentException(message);
+                }
+            }
+        }
+
+        if (shortID != null) {
+            globalShortIDMap.put(shortID, argument);
+        }
+
+        if (longID != null) {
+            globalLongIDMap.put(longID, argument);
+        }
+
+        globalArgumentList.add(argument);
+
+        if (group == null) {
+            group = getStandardGroup(argument);
+        }
+        group.addArgument(argument);
+        argumentGroups.add(group);
+    }
+
+    /**
+     * Removes the provided argument from the set of global arguments handled by this parser.
+     *
+     * @param argument
+     *            The argument to be removed.
+     */
+    protected void removeGlobalArgument(Argument argument) {
+        String argumentName = argument.getName();
+        globalArgumentMap.remove(argumentName);
+
+        Character shortID = argument.getShortIdentifier();
+        if (shortID != null) {
+            globalShortIDMap.remove(shortID);
+        }
+
+        String longID = argument.getLongIdentifier();
+        if (longID != null) {
+            if (!longArgumentsCaseSensitive) {
+                longID = toLowerCase(longID);
+            }
+
+            globalLongIDMap.remove(longID);
+        }
+
+        globalArgumentList.remove(argument);
+    }
+
+    /**
+     * Sets the provided argument as one which will automatically trigger the output of full usage information if it is
+     * provided on the command line and no further argument validation will be performed.
+     * <p>
+     * If sub-command groups are defined using the {@link #setUsageGroupArgument(Argument, Collection)} method, then
+     * this usage argument, when specified, will result in usage information being displayed which does not include
+     * information on sub-commands.
+     * <p>
+     * Note that the caller will still need to add this argument to the parser with the
+     * {@link #addGlobalArgument(Argument)} method, and the argument should not be required and should not take a value.
+     * Also, the caller will still need to check for the presence of the usage argument after calling
+     * {@link #parseArguments(String[])} to know that no further processing will be required.
+     *
+     * @param argument
+     *            The argument whose presence should automatically trigger the display of full usage information.
+     * @param outputStream
+     *            The output stream to which the usage information should be written.
+     */
+    @Override
+    public void setUsageArgument(Argument argument, OutputStream outputStream) {
+        usageArgument = argument;
+        usageOutputStream = outputStream;
+
+        usageGroupArguments.put(argument, Collections.<SubCommand> emptySet());
+    }
+
+    /**
+     * Sets the provided argument as one which will automatically trigger the output of partial usage information if it
+     * is provided on the command line and no further argument validation will be performed.
+     * <p>
+     * Partial usage information will include a usage synopsis, a summary of each of the sub-commands listed in the
+     * provided sub-commands collection, and a summary of the global options.
+     * <p>
+     * Note that the caller will still need to add this argument to the parser with the
+     * {@link #addGlobalArgument(Argument)} method, and the argument should not be required and should not take a value.
+     * Also, the caller will still need to check for the presence of the usage argument after calling
+     * {@link #parseArguments(String[])} to know that no further processing will be required.
+     *
+     * @param argument
+     *            The argument whose presence should automatically trigger the display of partial usage information.
+     * @param subCommands
+     *            The list of sub-commands which should have their usage displayed.
+     */
+    public void setUsageGroupArgument(Argument argument, Collection<SubCommand> subCommands) {
+        usageGroupArguments.put(argument, subCommands);
+    }
+
+    /**
+     * Parses the provided set of arguments and updates the information associated with this parser accordingly.
+     *
+     * @param rawArguments
+     *            The raw set of arguments to parse.
+     * @throws ArgumentException
+     *             If a problem was encountered while parsing the provided arguments.
+     */
+    @Override
+    public void parseArguments(String[] rawArguments) throws ArgumentException {
+        parseArguments(rawArguments, null);
+    }
+
+    /**
+     * Parses the provided set of arguments and updates the information associated with this parser accordingly. Default
+     * values for unspecified arguments may be read from the specified properties if any are provided.
+     *
+     * @param rawArguments
+     *            The set of raw arguments to parse.
+     * @param argumentProperties
+     *            A set of properties that may be used to provide default values for arguments not included in the given
+     *            raw arguments.
+     * @throws ArgumentException
+     *             If a problem was encountered while parsing the provided arguments.
+     */
+    @Override
+    public void parseArguments(String[] rawArguments, Properties argumentProperties) throws ArgumentException {
+        this.rawArguments = rawArguments;
+        this.subCommand = null;
+        this.trailingArguments = new ArrayList<String>();
+        this.usageOrVersionDisplayed = false;
+
+        boolean inTrailingArgs = false;
+
+        int numArguments = rawArguments.length;
+        for (int i = 0; i < numArguments; i++) {
+            final String arg = rawArguments[i];
+
+            if (inTrailingArgs) {
+                trailingArguments.add(arg);
+
+                if (subCommand == null) {
+                    throw new ArgumentException(ERR_ARG_SUBCOMMAND_INVALID.get());
+                }
+
+                if (subCommand.getMaxTrailingArguments() > 0
+                        && trailingArguments.size() > subCommand.getMaxTrailingArguments()) {
+                    LocalizableMessage message = ERR_ARGPARSER_TOO_MANY_TRAILING_ARGS.get(subCommand
+                            .getMaxTrailingArguments());
+                    throw new ArgumentException(message);
+                }
+
+                continue;
+            }
+
+            if (arg.equals("--")) {
+                inTrailingArgs = true;
+            } else if (arg.startsWith("--")) {
+                // This indicates that we are using the long name to reference the
+                // argument. It may be in any of the following forms:
+                // --name
+                // --name value
+                // --name=value
+
+                String argName = arg.substring(2);
+                String argValue = null;
+                int equalPos = argName.indexOf('=');
+                if (equalPos < 0) {
+                    // This is fine. The value is not part of the argument name token.
+                } else if (equalPos == 0) {
+                    // The argument starts with "--=", which is not acceptable.
+                    LocalizableMessage message = ERR_SUBCMDPARSER_LONG_ARG_WITHOUT_NAME.get(arg);
+                    throw new ArgumentException(message);
+                } else {
+                    // The argument is in the form --name=value, so parse them both out.
+                    argValue = argName.substring(equalPos + 1);
+                    argName = argName.substring(0, equalPos);
+                }
+
+                // If we're not case-sensitive, then convert the name to lowercase.
+                String origArgName = argName;
+                if (!longArgumentsCaseSensitive) {
+                    argName = toLowerCase(argName);
+                }
+
+                // See if the specified name references a global argument. If not, then
+                // see if it references a subcommand argument.
+                Argument a = globalLongIDMap.get(argName);
+                if (a == null) {
+                    if (subCommand == null) {
+                        if (argName.equals(OPTION_LONG_HELP)) {
+                            // "--help" will always be interpreted as requesting usage
+                            // information.
+                            getUsage(usageOutputStream);
+                            return;
+                        } else if (argName.equals(OPTION_LONG_PRODUCT_VERSION)) {
+                            // "--version" will always be interpreted as requesting usage
+                            // information.
+                            versionPresent = true;
+                            usageOrVersionDisplayed = true;
+                            printVersion();
+                            return;
+                        } else {
+                            // There is no such global argument.
+                            LocalizableMessage message = ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_LONG_ID
+                                    .get(origArgName);
+                            throw new ArgumentException(message);
+                        }
+                    } else {
+                        a = subCommand.getArgument(argName);
+                        if (a == null) {
+                            if (argName.equals(OPTION_LONG_HELP)) {
+                                // "--help" will always be interpreted as requesting usage
+                                // information.
+                                getUsage(usageOutputStream);
+                                return;
+                            } else if (argName.equals(OPTION_LONG_PRODUCT_VERSION)) {
+                                // "--version" will always be interpreted as requesting usage
+                                // information.
+                                versionPresent = true;
+                                usageOrVersionDisplayed = true;
+                                printVersion();
+                                return;
+                            } else {
+                                // There is no such global or subcommand argument.
+                                LocalizableMessage message = ERR_SUBCMDPARSER_NO_ARGUMENT_FOR_LONG_ID.get(origArgName);
+                                throw new ArgumentException(message);
+                            }
+                        }
+                    }
+                }
+
+                a.setPresent(true);
+
+                // If this is a usage argument, then immediately stop and print
+                // usage information.
+                if (usageGroupArguments.containsKey(a)) {
+                    getUsage(a, usageOutputStream);
+                    return;
+                }
+
+                // See if the argument takes a value. If so, then make sure one was
+                // provided. If not, then make sure none was provided.
+                if (a.needsValue()) {
+                    if (argValue == null) {
+                        if ((i + 1) == numArguments) {
+                            LocalizableMessage message = ERR_SUBCMDPARSER_NO_VALUE_FOR_ARGUMENT_WITH_LONG_ID
+                                    .get(argName);
+                            throw new ArgumentException(message);
+                        }
+
+                        argValue = rawArguments[++i];
+                    }
+
+                    LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
+                    if (!a.valueIsAcceptable(argValue, invalidReason)) {
+                        LocalizableMessage message = ERR_SUBCMDPARSER_VALUE_UNACCEPTABLE_FOR_LONG_ID.get(argValue,
+                                argName, invalidReason.toString());
+                        throw new ArgumentException(message);
+                    }
+
+                    // If the argument already has a value, then make sure it is
+                    // acceptable to have more than one.
+                    if (a.hasValue() && !a.isMultiValued()) {
+                        LocalizableMessage message = ERR_SUBCMDPARSER_NOT_MULTIVALUED_FOR_LONG_ID.get(origArgName);
+                        throw new ArgumentException(message);
+                    }
+
+                    a.addValue(argValue);
+                } else {
+                    if (argValue != null) {
+                        LocalizableMessage message = ERR_SUBCMDPARSER_ARG_FOR_LONG_ID_DOESNT_TAKE_VALUE
+                                .get(origArgName);
+                        throw new ArgumentException(message);
+                    }
+                }
+            } else if (arg.startsWith("-")) {
+                // This indicates that we are using the 1-character name to reference
+                // the argument. It may be in any of the following forms:
+                // -n
+                // -nvalue
+                // -n value
+                if (arg.equals("-")) {
+                    LocalizableMessage message = ERR_SUBCMDPARSER_INVALID_DASH_AS_ARGUMENT.get();
+                    throw new ArgumentException(message);
+                }
+
+                char argCharacter = arg.charAt(1);
+                String argValue;
+                if (arg.length() > 2) {
+                    argValue = arg.substring(2);
+                } else {
+                    argValue = null;
+                }
+
+                // Get the argument with the specified short ID. It may be either a
+                // global argument or a subcommand-specific argument.
+                Argument a = globalShortIDMap.get(argCharacter);
+                if (a == null) {
+                    if (subCommand == null) {
+                        if (argCharacter == '?') {
+                            // "-?" will always be interpreted as requesting usage.
+                            getUsage(usageOutputStream);
+                            if (usageArgument != null) {
+                                usageArgument.setPresent(true);
+                            }
+                            return;
+                        } else if (argCharacter == OPTION_SHORT_PRODUCT_VERSION) {
+                            // "-V" will always be interpreted as requesting
+                            // version information except if it's already defined.
+                            if (dashVAccepted()) {
+                                usageOrVersionDisplayed = true;
+                                versionPresent = true;
+                                printVersion();
+                                return;
+                            } else {
+                                // -V is defined in another suncommand, so we can
+                                // accepted it as the version information argument
+                                LocalizableMessage message = ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_SHORT_ID
+                                        .get(String.valueOf(argCharacter));
+                                throw new ArgumentException(message);
+                            }
+                        } else {
+                            // There is no such argument registered.
+                            LocalizableMessage message = ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_SHORT_ID.get(String
+                                    .valueOf(argCharacter));
+                            throw new ArgumentException(message);
+                        }
+                    } else {
+                        a = subCommand.getArgument(argCharacter);
+                        if (a == null) {
+                            if (argCharacter == '?') {
+                                // "-?" will always be interpreted as requesting usage.
+                                getUsage(usageOutputStream);
+                                return;
+                            } else if (argCharacter == OPTION_SHORT_PRODUCT_VERSION) {
+                                if (dashVAccepted()) {
+                                    usageOrVersionDisplayed = true;
+                                    versionPresent = true;
+                                    printVersion();
+                                    return;
+                                }
+                            } else {
+                                // There is no such argument registered.
+                                LocalizableMessage message = ERR_SUBCMDPARSER_NO_ARGUMENT_FOR_SHORT_ID.get(String
+                                        .valueOf(argCharacter));
+                                throw new ArgumentException(message);
+                            }
+                        }
+                    }
+                }
+
+                a.setPresent(true);
+
+                // If this is the usage argument, then immediately stop and print
+                // usage information.
+                if (usageGroupArguments.containsKey(a)) {
+                    getUsage(a, usageOutputStream);
+                    return;
+                }
+
+                // See if the argument takes a value. If so, then make sure one was
+                // provided. If not, then make sure none was provided.
+                if (a.needsValue()) {
+                    if (argValue == null) {
+                        if ((i + 1) == numArguments) {
+                            LocalizableMessage message = ERR_SUBCMDPARSER_NO_VALUE_FOR_ARGUMENT_WITH_SHORT_ID
+                                    .get(String.valueOf(argCharacter));
+                            throw new ArgumentException(message);
+                        }
+
+                        argValue = rawArguments[++i];
+                    }
+
+                    LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
+                    if (!a.valueIsAcceptable(argValue, invalidReason)) {
+                        LocalizableMessage message = ERR_SUBCMDPARSER_VALUE_UNACCEPTABLE_FOR_SHORT_ID.get(argValue,
+                                String.valueOf(argCharacter), invalidReason.toString());
+                        throw new ArgumentException(message);
+                    }
+
+                    // If the argument already has a value, then make sure it is
+                    // acceptable to have more than one.
+                    if (a.hasValue() && !a.isMultiValued()) {
+                        LocalizableMessage message = ERR_SUBCMDPARSER_NOT_MULTIVALUED_FOR_SHORT_ID.get(String
+                                .valueOf(argCharacter));
+                        throw new ArgumentException(message);
+                    }
+
+                    a.addValue(argValue);
+                } else {
+                    if (argValue != null) {
+                        // If we've gotten here, then it means that we're in a scenario like
+                        // "-abc" where "a" is a valid argument that doesn't take a value.
+                        // However, this could still be valid if all remaining characters in
+                        // the value are also valid argument characters that don't take
+                        // values.
+                        int valueLength = argValue.length();
+                        for (int j = 0; j < valueLength; j++) {
+                            char c = argValue.charAt(j);
+                            Argument b = globalShortIDMap.get(c);
+                            if (b == null) {
+                                if (subCommand == null) {
+                                    LocalizableMessage message = ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_SHORT_ID
+                                            .get(String.valueOf(argCharacter));
+                                    throw new ArgumentException(message);
+                                } else {
+                                    b = subCommand.getArgument(c);
+                                    if (b == null) {
+                                        LocalizableMessage message = ERR_SUBCMDPARSER_NO_ARGUMENT_FOR_SHORT_ID
+                                                .get(String.valueOf(argCharacter));
+                                        throw new ArgumentException(message);
+                                    }
+                                }
+                            }
+
+                            if (b.needsValue()) {
+                                // This means we're in a scenario like "-abc" where b is a
+                                // valid argument that takes a value. We don't support that.
+                                LocalizableMessage message = ERR_SUBCMDPARSER_CANT_MIX_ARGS_WITH_VALUES.get(
+                                        String.valueOf(argCharacter), argValue, String.valueOf(c));
+                                throw new ArgumentException(message);
+                            } else {
+                                b.setPresent(true);
+
+                                // If this is the usage argument, then immediately stop and
+                                // print usage information.
+                                if (usageGroupArguments.containsKey(b)) {
+                                    getUsage(b, usageOutputStream);
+                                    return;
+                                }
+                            }
+                        }
+                    }
+                }
+            } else if (subCommand != null) {
+                // It's not a short or long identifier and the sub-command has
+                // already been specified, so it must be the first trailing argument.
+                if (subCommand.allowsTrailingArguments()) {
+                    trailingArguments.add(arg);
+                    inTrailingArgs = true;
+                } else {
+                    // Trailing arguments are not allowed for this sub-command.
+                    LocalizableMessage message = ERR_ARGPARSER_DISALLOWED_TRAILING_ARGUMENT.get(arg);
+                    throw new ArgumentException(message);
+                }
+            } else {
+                // It must be the sub-command.
+                String nameToCheck = arg;
+                if (!longArgumentsCaseSensitive) {
+                    nameToCheck = toLowerCase(arg);
+                }
+
+                SubCommand sc = subCommands.get(nameToCheck);
+                if (sc == null) {
+                    LocalizableMessage message = ERR_SUBCMDPARSER_INVALID_ARGUMENT.get(arg);
+                    throw new ArgumentException(message);
+                } else {
+                    subCommand = sc;
+                }
+            }
+        }
+
+        // If we have a sub-command and it allows trailing arguments and
+        // there is a minimum number, then make sure at least that many
+        // were provided.
+        if (subCommand != null) {
+            int minTrailingArguments = subCommand.getMinTrailingArguments();
+            if (subCommand.allowsTrailingArguments() && minTrailingArguments > 0
+                    && trailingArguments.size() < minTrailingArguments) {
+                LocalizableMessage message = ERR_ARGPARSER_TOO_FEW_TRAILING_ARGUMENTS.get(minTrailingArguments);
+                throw new ArgumentException(message);
+            }
+        }
+
+        // If we don't have the argumentProperties, try to load a properties file.
+        if (argumentProperties == null) {
+            argumentProperties = checkExternalProperties();
+        }
+
+        // Iterate through all the global arguments
+        normalizeArguments(argumentProperties, globalArgumentList);
+
+        // Iterate through all the subcommand-specific arguments
+        if (subCommand != null) {
+            normalizeArguments(argumentProperties, subCommand.getArguments());
+        }
+    }
+
+    private boolean dashVAccepted() {
+        if (globalShortIDMap.containsKey(OPTION_SHORT_PRODUCT_VERSION)) {
+            return false;
+        }
+        for (SubCommand subCmd : subCommands.values()) {
+            if (subCmd.getArgument(OPTION_SHORT_PRODUCT_VERSION) != null) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Appends usage information for the specified subcommand to the provided buffer.
+     *
+     * @param buffer
+     *            The buffer to which the usage information should be appended.
+     * @param subCommand
+     *            The subcommand for which to display the usage information.
+     */
+    public void getSubCommandUsage(LocalizableMessageBuilder buffer, SubCommand subCommand) {
+        usageOrVersionDisplayed = true;
+        String scriptName = System.getProperty(PROPERTY_SCRIPT_NAME);
+        if (scriptName == null || scriptName.length() == 0) {
+            scriptName = "java " + mainClassName;
+        }
+        buffer.append(INFO_ARGPARSER_USAGE_JAVA_SCRIPTNAME.get(scriptName));
+        buffer.append("  ");
+        buffer.append(scriptName);
+
+        buffer.append(" ");
+        buffer.append(subCommand.getName());
+        buffer.append(" ").append(INFO_SUBCMDPARSER_OPTIONS.get());
+        if (subCommand.allowsTrailingArguments()) {
+            buffer.append(' ');
+            buffer.append(subCommand.getTrailingArgumentsDisplayName());
+        }
+        buffer.append(EOL);
+        buffer.append(subCommand.getDescription());
+        buffer.append(EOL);
+
+        if (!globalArgumentList.isEmpty()) {
+            buffer.append(EOL);
+            buffer.append(INFO_GLOBAL_OPTIONS.get());
+            buffer.append(EOL);
+            buffer.append("    ");
+            buffer.append(INFO_GLOBAL_OPTIONS_REFERENCE.get(scriptName));
+            buffer.append(EOL);
+        }
+
+        if (!subCommand.getArguments().isEmpty()) {
+            buffer.append(EOL);
+            buffer.append(INFO_SUBCMD_OPTIONS.get());
+            buffer.append(EOL);
+        }
+        for (Argument a : subCommand.getArguments()) {
+            // If this argument is hidden, then skip it.
+            if (a.isHidden()) {
+                continue;
+            }
+
+            // Write a line with the short and/or long identifiers that may be used
+            // for the argument.
+            Character shortID = a.getShortIdentifier();
+            String longID = a.getLongIdentifier();
+            if (shortID != null) {
+                int currentLength = buffer.length();
+
+                if (a.equals(usageArgument)) {
+                    buffer.append("-?, ");
+                }
+
+                buffer.append("-");
+                buffer.append(shortID.charValue());
+
+                if (a.needsValue() && longID == null) {
+                    buffer.append(" ");
+                    buffer.append(a.getValuePlaceholder());
+                }
+
+                if (longID != null) {
+                    StringBuilder newBuffer = new StringBuilder();
+                    newBuffer.append(", --");
+                    newBuffer.append(longID);
+
+                    if (a.needsValue()) {
+                        newBuffer.append(" ");
+                        newBuffer.append(a.getValuePlaceholder());
+                    }
+
+                    int lineLength = (buffer.length() - currentLength) + newBuffer.length();
+                    if (lineLength > MAX_LENGTH) {
+                        buffer.append(EOL);
+                    }
+                    buffer.append(newBuffer.toString());
+                }
+
+                buffer.append(EOL);
+            } else {
+                if (longID != null) {
+                    if (a.equals(usageArgument)) {
+                        buffer.append("-?, ");
+                    }
+                    buffer.append("--");
+                    buffer.append(longID);
+
+                    if (a.needsValue()) {
+                        buffer.append(" ");
+                        buffer.append(a.getValuePlaceholder());
+                    }
+
+                    buffer.append(EOL);
+                }
+            }
+
+            indentAndWrap2(INDENT, a.getDescription(), buffer);
+            if (a.needsValue() && a.getDefaultValue() != null && a.getDefaultValue().length() > 0) {
+                indentAndWrap2(INDENT, INFO_ARGPARSER_USAGE_DEFAULT_VALUE.get(a.getDefaultValue()), buffer);
+            }
+        }
+    }
+
+    /**
+     * Write one or more lines with the description of the argument. We will indent the description five characters and
+     * try our best to wrap at or before column 79 so it will be friendly to 80-column displays.
+     * <p>
+     * FIXME Try to merge with #indentAndWrap(LocalizableMessage, LocalizableMessage, LocalizableMessageBuilder).
+     */
+    private void indentAndWrap2(String indent, LocalizableMessage text, LocalizableMessageBuilder buffer) {
+        int actualSize = MAX_LENGTH - indent.length() - 1;
+        if (text.length() <= actualSize) {
+            buffer.append(indent);
+            buffer.append(text);
+            buffer.append(EOL);
+        } else {
+            String s = text.toString();
+            while (s.length() > actualSize) {
+                int spacePos = s.lastIndexOf(' ', actualSize);
+                if (spacePos > 0) {
+                    buffer.append(indent);
+                    buffer.append(s.substring(0, spacePos).trim());
+                    s = s.substring(spacePos + 1).trim();
+                    buffer.append(EOL);
+                } else {
+                    // There are no spaces in the first 74 columns.
+                    // See if there is one after that point.
+                    // If so, then break there. If not, then don't break at all.
+                    spacePos = s.indexOf(' ');
+                    if (spacePos > 0) {
+                        buffer.append(indent);
+                        buffer.append(s.substring(0, spacePos).trim());
+                        s = s.substring(spacePos + 1).trim();
+                        buffer.append(EOL);
+                    } else {
+                        buffer.append(indent);
+                        buffer.append(s);
+                        s = "";
+                        buffer.append(EOL);
+                    }
+                }
+            }
+
+            if (s.length() > 0) {
+                buffer.append(indent);
+                buffer.append(s);
+                buffer.append(EOL);
+            }
+        }
+    }
+
+    /**
+     * Retrieves a string containing usage information based on the defined arguments.
+     *
+     * @return A string containing usage information based on the defined arguments.
+     */
+    @Override
+    public String getUsage() {
+        LocalizableMessageBuilder buffer = new LocalizableMessageBuilder();
+
+        if (subCommand == null) {
+            if (System.getProperty("org.forgerock.opendj.gendoc") != null) {
+                // Generate reference documentation for dsconfig subcommands
+                for (SubCommand s : subCommands.values()) {
+                    buffer.append(toRefSect2(s));
+                }
+            } else if (usageGroupArguments.size() > 1) {
+                // We have sub-command groups, so don't display any
+                // sub-commands by default.
+                getFullUsage(Collections.<SubCommand> emptySet(), true, buffer);
+            } else {
+                // No grouping, so display all sub-commands.
+                getFullUsage(subCommands.values(), true, buffer);
+            }
+        } else {
+            getSubCommandUsage(buffer, subCommand);
+        }
+
+        return buffer.toMessage().toString();
+    }
+
+    /**
+     * Retrieves a string describing how the user can get more help.
+     *
+     * @return A string describing how the user can get more help.
+     */
+    public LocalizableMessage getHelpUsageReference() {
+        usageOrVersionDisplayed = true;
+        String scriptName = System.getProperty(PROPERTY_SCRIPT_NAME);
+        if (scriptName == null || scriptName.length() == 0) {
+            scriptName = "java " + mainClassName;
+        }
+
+        LocalizableMessageBuilder buffer = new LocalizableMessageBuilder();
+        buffer.append(INFO_GLOBAL_HELP_REFERENCE.get(scriptName));
+        buffer.append(EOL);
+        return buffer.toMessage();
+    }
+
+    /**
+     * Retrieves the set of unnamed trailing arguments that were provided on the command line.
+     *
+     * @return The set of unnamed trailing arguments that were provided on the command line.
+     */
+    @Override
+    public ArrayList<String> getTrailingArguments() {
+        return trailingArguments;
+    }
+
+    /**
+     * Indicates whether the usage information has been displayed to the end user either by an explicit argument like
+     * "-H" or "--help", or by a built-in argument like "-?".
+     *
+     * @return {@code true} if the usage information has been displayed, or {@code false} if not.
+     */
+    @Override
+    public boolean usageOrVersionDisplayed() {
+        return usageOrVersionDisplayed;
+    }
+
+    /**
+     * Adds the provided subcommand to this argument parser. This is only intended for use by the
+     * <CODE>SubCommand</CODE> constructor and does not do any validation of its own to ensure that there are no
+     * conflicts with the subcommand or any of its arguments.
+     *
+     * @param subCommand
+     *            The subcommand to add to this argument parser.
+     */
+    void addSubCommand(SubCommand subCommand) {
+        subCommands.put(toLowerCase(subCommand.getName()), subCommand);
+    }
+
+    /** Get usage for a specific usage argument. */
+    private void getUsage(Argument a, OutputStream outputStream) {
+        LocalizableMessageBuilder buffer = new LocalizableMessageBuilder();
+
+        if (a.equals(usageArgument) && subCommand != null) {
+            getSubCommandUsage(buffer, subCommand);
+        } else if (a.equals(usageArgument) && usageGroupArguments.size() <= 1) {
+            // No groups - so display all sub-commands.
+            getFullUsage(subCommands.values(), true, buffer);
+        } else if (a.equals(usageArgument)) {
+            // Using groups - so display all sub-commands group help.
+            getFullUsage(Collections.<SubCommand> emptySet(), true, buffer);
+        } else {
+            // Requested help on specific group - don't display global
+            // options.
+            getFullUsage(usageGroupArguments.get(a), false, buffer);
+        }
+
+        try {
+            outputStream.write(buffer.toString().getBytes());
+        } catch (Exception e) {
+            // TODO empty catch
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void getUsage(OutputStream outputStream) {
+        try {
+            outputStream.write(getUsage().getBytes());
+        } catch (IOException e) {
+            // TODO empty catch
+        }
+    }
+
+    /**
+     * Appends complete usage information for the specified set of sub-commands.
+     */
+    private void getFullUsage(Collection<SubCommand> c, boolean showGlobalOptions, LocalizableMessageBuilder buffer) {
+        usageOrVersionDisplayed = true;
+        if (toolDescription != null && toolDescription.length() > 0) {
+            buffer.append(wrapText(toolDescription, MAX_LENGTH - 1));
+            buffer.append(EOL);
+            buffer.append(EOL);
+        }
+
+        String scriptName = System.getProperty(PROPERTY_SCRIPT_NAME);
+        if (scriptName == null || scriptName.length() == 0) {
+            scriptName = "java " + mainClassName;
+        }
+        buffer.append(INFO_ARGPARSER_USAGE.get());
+        buffer.append("  ");
+        buffer.append(scriptName);
+
+        if (subCommands.isEmpty()) {
+            buffer.append(" ").append(INFO_SUBCMDPARSER_OPTIONS.get());
+        } else {
+            buffer.append(" ").append(INFO_SUBCMDPARSER_SUBCMD_AND_OPTIONS.get());
+        }
+
+        if (!subCommands.isEmpty()) {
+            buffer.append(EOL);
+            buffer.append(EOL);
+            if (c.isEmpty()) {
+                buffer.append(INFO_SUBCMDPARSER_SUBCMD_HELP_HEADING.get());
+            } else {
+                buffer.append(INFO_SUBCMDPARSER_SUBCMD_HEADING.get());
+            }
+            buffer.append(EOL);
+        }
+
+        if (c.isEmpty()) {
+            // Display usage arguments (except the default one).
+            for (Argument a : globalArgumentList) {
+                if (a.isHidden()) {
+                    continue;
+                }
+
+                if (usageGroupArguments.containsKey(a) && !a.equals(usageArgument)) {
+                    printArgumentUsage(a, buffer);
+                }
+            }
+        } else {
+            boolean isFirst = true;
+            for (SubCommand sc : c) {
+                if (sc.isHidden()) {
+                    continue;
+                }
+                if (isFirst) {
+                    buffer.append(EOL);
+                }
+                buffer.append(sc.getName());
+                buffer.append(EOL);
+                indentAndWrap(LocalizableMessage.raw(INDENT), sc.getDescription(), buffer);
+                buffer.append(EOL);
+                isFirst = false;
+            }
+        }
+
+        buffer.append(EOL);
+
+        if (showGlobalOptions) {
+            if (subCommands.isEmpty()) {
+                buffer.append(INFO_SUBCMDPARSER_WHERE_OPTIONS_INCLUDE.get());
+            } else {
+                buffer.append(INFO_SUBCMDPARSER_GLOBAL_HEADING.get());
+            }
+            buffer.append(EOL);
+            buffer.append(EOL);
+
+            boolean printGroupHeaders = printUsageGroupHeaders();
+
+            // Display non-usage arguments.
+            for (ArgumentGroup argGroup : argumentGroups) {
+                if (argGroup.containsArguments() && printGroupHeaders) {
+                    // Print the groups description if any
+                    LocalizableMessage groupDesc = argGroup.getDescription();
+                    if (groupDesc != null && !LocalizableMessage.EMPTY.equals(groupDesc)) {
+                        buffer.append(EOL);
+                        buffer.append(wrapText(groupDesc.toString(), MAX_LENGTH - 1));
+                        buffer.append(EOL);
+                        buffer.append(EOL);
+                    }
+                }
+
+                for (Argument a : argGroup.getArguments()) {
+                    if (a.isHidden()) {
+                        continue;
+                    }
+
+                    if (!usageGroupArguments.containsKey(a)) {
+                        printArgumentUsage(a, buffer);
+                    }
+                }
+            }
+
+            // Finally print default usage argument.
+            if (usageArgument != null) {
+                printArgumentUsage(usageArgument, buffer);
+            } else {
+                buffer.append("-?");
+            }
+            buffer.append(EOL);
+        }
+    }
+
+    /**
+     * Appends argument usage information to the provided buffer.
+     *
+     * @param a
+     *            The argument to handle.
+     * @param buffer
+     *            The buffer to which the usage information should be appended.
+     */
+    private void printArgumentUsage(Argument a, LocalizableMessageBuilder buffer) {
+        String value;
+        if (a.needsValue()) {
+            LocalizableMessage pHolder = a.getValuePlaceholder();
+            if (pHolder == null) {
+                value = " {value}";
+            } else {
+                value = " " + pHolder;
+            }
+        } else {
+            value = "";
+        }
+
+        Character shortIDChar = a.getShortIdentifier();
+        if (shortIDChar != null) {
+            if (a.equals(usageArgument)) {
+                buffer.append("-?, ");
+            }
+            buffer.append("-");
+            buffer.append(shortIDChar);
+
+            String longIDString = a.getLongIdentifier();
+            if (longIDString != null) {
+                buffer.append(", --");
+                buffer.append(longIDString);
+            }
+            buffer.append(value);
+        } else {
+            String longIDString = a.getLongIdentifier();
+            if (longIDString != null) {
+                if (a.equals(usageArgument)) {
+                    buffer.append("-?, ");
+                }
+                buffer.append("--");
+                buffer.append(longIDString);
+                buffer.append(value);
+            }
+        }
+
+        buffer.append(EOL);
+
+        indentAndWrap(LocalizableMessage.raw(INDENT), a.getDescription(), buffer);
+        if (a.needsValue() && a.getDefaultValue() != null && a.getDefaultValue().length() > 0) {
+            indentAndWrap(LocalizableMessage.raw(INDENT), INFO_ARGPARSER_USAGE_DEFAULT_VALUE.get(a.getDefaultValue()),
+                    buffer);
+        }
+    }
+
+    /**
+     * Write one or more lines with the description of the argument. We will indent the description five characters and
+     * try our best to wrap at or before column 79 so it will be friendly to 80-column displays.
+     */
+    private void indentAndWrap(LocalizableMessage indent, LocalizableMessage text, LocalizableMessageBuilder buffer) {
+        int actualSize = MAX_LENGTH - indent.length();
+        if (text.length() <= actualSize) {
+            buffer.append(indent);
+            buffer.append(text);
+            buffer.append(EOL);
+        } else {
+            String s = text.toString();
+            while (s.length() > actualSize) {
+                int spacePos = s.lastIndexOf(' ', actualSize);
+                if (spacePos > 0) {
+                    buffer.append(indent);
+                    buffer.append(s.substring(0, spacePos).trim());
+                    s = s.substring(spacePos + 1).trim();
+                    buffer.append(EOL);
+                } else {
+                    // There are no spaces in the first actualSize -1 columns.
+                    // See if there is one after that point.
+                    // If so, then break there. If not, then don't break at all.
+                    spacePos = s.indexOf(' ');
+                    if (spacePos > 0) {
+                        buffer.append(indent);
+                        buffer.append(s.substring(0, spacePos).trim());
+                        s = s.substring(spacePos + 1).trim();
+                        buffer.append(EOL);
+                    } else {
+                        buffer.append(indent);
+                        buffer.append(s);
+                        s = "";
+                        buffer.append(EOL);
+                    }
+                }
+            }
+
+            if (s.length() > 0) {
+                buffer.append(indent);
+                buffer.append(s);
+                buffer.append(EOL);
+            }
+        }
+    }
+
+    /**
+     * Returns whether the usage argument was provided or not. This method should be called after a call to
+     * parseArguments.
+     *
+     * @return <CODE>true</CODE> if the usage argument was provided and <CODE>false</CODE> otherwise.
+     */
+    @Override
+    public boolean isUsageArgumentPresent() {
+        return usageArgument != null && usageArgument.isPresent();
+    }
+
+    /**
+     * Returns whether the version argument was provided or not. This method should be called after a call to
+     * parseArguments.
+     *
+     * @return <CODE>true</CODE> if the version argument was provided and <CODE>false</CODE> otherwise.
+     */
+    @Override
+    public boolean isVersionArgumentPresent() {
+        return super.isVersionArgumentPresent() && !versionPresent;
+    }
+
+    /**
+     * Generate reference documentation for dsconfig subcommands in DocBook 5 XML format. As the number of categories is
+     * large, the subcommand entries are sorted here by name for inclusion in a &lt;refsect1&gt; covering all dsconfig
+     * Subcommands as part of the &lt;refentry&gt; for dsconfig (in man-dsconfig.xml).
+     * <p>
+     * Although it would be possible to categorize subcommands in the same way as they are categorized in dsconfig
+     * interactive mode, this generator does not use the categories.
+     * <p>
+     * It would also be possible to generate the sort of information provided by the configuration reference, such that
+     * this reference would not stop at simply listing an option like --set {PROP:VAL}, but instead would also provide
+     * the list of PROPs and their possible VALs. A future improvement could no doubt merge the configuration reference
+     * with this content, though perhaps the problem calls for hypertext rather than the linear structure of a
+     * &lt;refentry&gt;.
+     * <p>
+     * Each individual subcommand results in a &lt;refsect2&gt; element similar to the following.
+     *
+     * <pre>
+     *     &lt;refsect2 xml:id=&quot;dsconfig-create-local-db-index&quot;&gt;
+     *      &lt;title&gt;dsconfig create-local-db-index&lt;/title&gt;
+     *      &lt;para&gt;Creates Local DB Indexes&lt;/para&gt;
+     *      &lt;variablelist&gt;
+     *       &lt;varlistentry&gt;
+     *        &lt;term&gt;&lt;option&gt;--backend-name {name}
+     *         &lt;/option&gt;&lt;/term&gt;
+     *        &lt;listitem&gt;
+     *         &lt;para&gt;The name of the Local DB Backend&lt;/para&gt;
+     *        &lt;/listitem&gt;
+     *       &lt;/varlistentry&gt;
+     *       &lt;varlistentry&gt;
+     *        &lt;term&gt;&lt;option&gt;--index-name {OID}&lt;/option&gt;&lt;/term&gt;
+     *        &lt;listitem&gt;
+     *         &lt;para&gt;The name of the new Local DB Index which will also be used
+     *         as the value of the &quot;attribute&quot; property: Specifies the name
+     *         of the attribute for which the index is to be maintained.&lt;/para&gt;
+     *        &lt;/listitem&gt;
+     *       &lt;/varlistentry&gt;
+     *       &lt;varlistentry&gt;
+     *        &lt;term&gt;&lt;option&gt;--set {PROP:VALUE}&lt;/option&gt;&lt;/term&gt;
+     *        &lt;listitem&gt;
+     *         &lt;para&gt;Assigns a value to a property where PROP is the name of the
+     *         property and VALUE is the single value to be assigned. Specify the same
+     *         property multiple times in order to assign more than one value to
+     *         it&lt;/para&gt;
+     *        &lt;/listitem&gt;
+     *       &lt;/varlistentry&gt;
+     *      &lt;/variablelist&gt;
+     *      &lt;/refsect2&gt;
+     * </pre>
+     *
+     * @param sc
+     *            The SubCommand containing reference information.
+     * @return Refsect2 representation of the subcommand.
+     */
+    private String toRefSect2(SubCommand sc) {
+        final StringBuilder options = new StringBuilder();
+        if (!sc.getArguments().isEmpty()) {
+            options.append(" <variablelist>").append(EOL);
+            for (Argument a : sc.getArguments()) {
+                options.append("  <varlistentry>").append(EOL);
+                options.append("   <term><option>");
+                Character shortID = a.getShortIdentifier();
+                if (shortID != null) {
+                    options.append("-").append(shortID.charValue());
+                }
+                String longID = a.getLongIdentifier();
+                if (shortID != null && longID != null) {
+                    options.append(" | ");
+                }
+                if (longID != null) {
+                    options.append("--").append(longID);
+                }
+                if (a.needsValue()) {
+                    options.append(" ").append(a.getValuePlaceholder());
+                }
+                options.append("</option></term>").append(EOL);
+                options.append("   <listitem>").append(EOL);
+                options.append("    <para>");
+                options.append(a.getDescription());
+                options.append("</para>").append(EOL);
+                options.append("   </listitem>").append(EOL);
+                options.append("  </varlistentry>").append(EOL);
+            }
+            options.append(" </variablelist>").append(EOL);
+        }
+
+        return "<refsect2 xml:id=\"dsconfig-" + sc.getName() + "\">" + EOL + " <title>dsconfig " + sc.getName()
+                + "</title>" + EOL + " <para>" + sc.getDescription() + "</para>" + EOL + options + "</refsect2>" + EOL;
+    }
+
+    void printVersion() {
+        // TODO
+        // DirectoryServer.printVersion(usageOutputStream);
+    }
+}
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java
index a5c7012..3d99f61 100644
--- a/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java
@@ -40,6 +40,9 @@
  * This class provides utility functions for all the client side tools.
  */
 final public class Utils {
+    /** Platform appropriate line separator. */
+    static public final String LINE_SEPARATOR = System.getProperty("line.separator");
+
     /**
      * The name of a command-line script used to launch a tool.
      */
diff --git a/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties b/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties
index bfb49e9..1395801 100755
--- a/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties
+++ b/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties
@@ -137,6 +137,17 @@
 INFO_ARGPARSER_USAGE_JAVA_SCRIPTNAME=Usage:  %s  {options}
 INFO_ARGPARSER_USAGE_TRAILINGARGS={trailing-arguments}
 INFO_ARGPARSER_USAGE_DEFAULT_VALUE=Default value: %s
+INFO_SUBCMDPARSER_OPTIONS={options}
+INFO_GLOBAL_OPTIONS=Global Options:
+INFO_GLOBAL_OPTIONS_REFERENCE=See "%s --help"
+INFO_GLOBAL_HELP_REFERENCE=See "%s --help" to get more usage help
+INFO_SUBCMD_OPTIONS=SubCommand Options:
+INFO_ARGPARSER_USAGE=Usage:
+INFO_SUBCMDPARSER_SUBCMD_AND_OPTIONS={subcommand} {options}
+INFO_SUBCMDPARSER_SUBCMD_HELP_HEADING=To get the list of subcommands use:
+INFO_SUBCMDPARSER_SUBCMD_HEADING=Available subcommands:
+ERR_ARG_SUBCOMMAND_INVALID=Invalid subcommand
+INFO_SUBCMDPARSER_GLOBAL_HEADING=The global options are:
 #
 # Extension messages
 #
@@ -146,7 +157,7 @@
 ERR_CANNOT_INITIALIZE_ARGS=An unexpected error occurred while \
  attempting to initialize the command-line arguments:  %s
 ERR_ERROR_PARSING_ARGS=An error occurred while parsing the \
- command-line arguments:  %s
+ command-line arguments: %s
 INFO_PROCESSING_OPERATION=Processing %s request for %s
 INFO_OPERATION_FAILED=%s operation failed
 INFO_OPERATION_SUCCESSFUL=%s operation successful for DN %s
@@ -501,3 +512,188 @@
  processing :  %s
 ERR_CONSTANT_ARG_CANNOT_DECODE=Unable to parse a constant argument, \
  expecting name=value but got %s
+INFO_DESCRIPTION_QUIET=Use quiet mode
+INFO_DESCRIPTION_NO_PROMPT=Use non-interactive mode.  If data in \
+the command is missing, the user is not prompted and the tool will fail
+INFO_OPTION_ACCEPT_LICENSE=Automatically accepts the product license \
+(if present)
+#
+# Setup messages
+#
+INFO_SETUP_TITLE=OPENDJ3 Setup tool
+INFO_SETUP_DESCRIPTION=This utility can be used to setup the Directory Server
+INFO_ARGUMENT_DESCRIPTION_TESTONLY=Just verify that the JVM can be \
+ started properly
+INFO_ARGUMENT_DESCRIPTION_CLI=Use the command line install. \
+ If not specified the graphical interface will be launched.  The rest of the \
+ options (excluding help and version) will only be taken into account if this \
+ option is specified
+INFO_ARGUMENT_DESCRIPTION_BASEDN=Base DN for user \
+ information in the Directory Server.  Multiple base DNs may be provided by \
+ using this option multiple times
+INFO_ARGUMENT_DESCRIPTION_ADDBASE=Indicates whether to create the base \
+ entry in the Directory Server database
+INFO_ARGUMENT_DESCRIPTION_IMPORTLDIF=Path to an LDIF file \
+ containing data that should be added to the Directory Server database. \
+ Multiple LDIF files may be provided by using this option multiple times
+INFO_LDIFFILE_PLACEHOLDER={ldifFile}
+INFO_REJECT_FILE_PLACEHOLDER={rejectFile}
+INFO_SKIP_FILE_PLACEHOLDER={skipFile}
+INFO_JMXPORT_PLACEHOLDER={jmxPort}
+INFO_ROOT_USER_DN_PLACEHOLDER={rootUserDN}
+INFO_ROOT_USER_PWD_PLACEHOLDER={rootUserPassword}
+INFO_ROOT_USER_PWD_FILE_PLACEHOLDER={rootUserPasswordFile}
+INFO_HOST_PLACEHOLDER={host}
+INFO_GENERAL_DESCRIPTION_REJECTED_FILE=Write rejected entries to the \
+ specified file
+INFO_GENERAL_DESCRIPTION_SKIPPED_FILE=Write skipped entries to the \
+ specified file
+INFO_SETUP_DESCRIPTION_SAMPLE_DATA=Specifies that the database should \
+ be populated with the specified number of sample entries
+INFO_ARGUMENT_DESCRIPTION_LDAPPORT=Port on which the \
+ Directory Server should listen for LDAP communication
+INFO_ARGUMENT_DESCRIPTION_ADMINCONNECTORPORT=Port on which the \
+ Administration Connector should listen for communication
+INFO_ARGUMENT_DESCRIPTION_JMXPORT=Port on which the \
+ Directory Server should listen for JMX communication
+INFO_ARGUMENT_DESCRIPTION_SKIPPORT=Skip the check to determine whether \
+ the specified ports are usable
+INFO_ARGUMENT_DESCRIPTION_ROOTDN=DN for the initial root \
+ user for the Directory Server
+INFO_ARGUMENT_DESCRIPTION_ROOTPW=Password for the initial \
+ root user for the Directory Server
+INFO_ARGUMENT_DESCRIPTION_ROOTPWFILE=Path to a file \
+ containing the password for the initial root user for the Directory Server
+ INFO_ARGUMENT_DESCRIPTION_ENABLE_WINDOWS_SERVICE=Enable the server to run \
+ as a Windows Service
+INFO_ARGUMENT_DESCRIPTION_LDAPSPORT=Port on which the \
+ Directory Server should listen for LDAPS communication.  The LDAPS port will \
+ be configured and SSL will be enabled only if this argument is explicitly \
+ specified
+INFO_ARGUMENT_DESCRIPTION_HOST_NAME=The fully-qualified directory server \
+ host name that will be used when generating self-signed \
+ certificates for LDAP SSL/StartTLS, the administration connector, and \
+ replication
+INFO_ARGUMENT_DESCRIPTION_USE_SELF_SIGNED_CERTIFICATE=Generate a \
+ self-signed certificate that the server should use when accepting SSL-based \
+ connections or performing StartTLS negotiation
+INFO_ARGUMENT_DESCRIPTION_USE_PKCS11=Use a certificate in a \
+ PKCS#11 token that the server should use when accepting SSL-based \
+ connections or performing StartTLS negotiation
+INFO_ARGUMENT_DESCRIPTION_USE_JAVAKEYSTORE=Path of a Java \
+ Key Store (JKS) containing a certificate to be used as the server certificate
+INFO_ARGUMENT_DESCRIPTION_USE_JCEKS=Path of a JCEKS containing a \
+ certificate to be used as the server certificate
+INFO_ARGUMENT_DESCRIPTION_USE_PKCS12=Path of a PKCS#12 key \
+ store containing the certificate that the server should use when accepting \
+ SSL-based connections or performing StartTLS negotiation
+INFO_ARGUMENT_CERT_OPTION_JCEKS=Use an existing certificate located on a \
+ JCEKS key store
+INFO_ARGUMENT_DESCRIPTION_CERT_NICKNAME=Nickname of the \
+ certificate that the server should use when accepting SSL-based \
+ connections or performing StartTLS negotiation
+INFO_ARGUMENT_DESCRIPTION_KEYSTOREPASSWORD=Certificate key store PIN.  \
+ A PIN is required when you specify to use an existing certificate (JKS, \
+ JCEKS, PKCS#12 or PKCS#11) as server certificate
+INFO_ARGUMENT_DESCRIPTION_KEYSTOREPASSWORD_FILE=Certificate key store \
+ PIN file.  A PIN is required when you specify to use an existing certificate \
+ (JKS, JCEKS, PKCS#12 or PKCS#11) as server certificate
+INFO_SETUP_DESCRIPTION_DO_NOT_START=Do not start the server when the \
+ configuration is completed
+INFO_SETUP_DESCRIPTION_ENABLE_STARTTLS=Enable StartTLS to allow \
+ secure communication with the server using the LDAP port
+#
+# SubCommandes messages
+#
+ERR_ARG_SUBCOMMAND_DUPLICATE_SUBCOMMAND=The argument parser already \
+ has a %s subcommand
+ERR_ARG_SUBCOMMAND_DUPLICATE_ARGUMENT_NAME=There are multiple \
+ arguments for subcommand %s with name %s
+ERR_ARG_SUBCOMMAND_ARGUMENT_GLOBAL_CONFLICT=Argument %s for \
+ subcommand %s conflicts with a global argument with the same name
+ERR_ARG_SUBCOMMAND_DUPLICATE_SHORT_ID=Argument %s for subcommand %s \
+ has a short identifier -%s that conflicts with that of argument %s
+ERR_ARG_SUBCOMMAND_ARGUMENT_SHORT_ID_GLOBAL_CONFLICT=Argument %s \
+ for subcommand %s has a short ID -%s that conflicts with that of global \
+ argument %s
+ERR_ARG_SUBCOMMAND_DUPLICATE_LONG_ID=Argument %s for subcommand %s \
+ has a long identifier --%s that conflicts with that of argument %s
+ERR_ARG_SUBCOMMAND_ARGUMENT_LONG_ID_GLOBAL_CONFLICT=Argument %s for \
+ subcommand %s has a long ID --%s that conflicts with that of global argument \
+ %s
+ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_NAME=There is already another \
+ global argument named "%s"
+ERR_SUBCMDPARSER_GLOBAL_ARG_NAME_SUBCMD_CONFLICT=The argument name \
+ %s conflicts with the name of another argument associated with the %s \
+ subcommand
+ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_SHORT_ID=Short ID -%s for \
+ global argument %s conflicts with the short ID of another global argument %s
+ERR_SUBCMDPARSER_GLOBAL_ARG_SHORT_ID_CONFLICT=Short ID -%s for \
+ global argument %s conflicts with the short ID for the %s argument associated \
+ with subcommand %s
+ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_LONG_ID=Long ID --%s for \
+ global argument %s conflicts with the long ID of another global argument %s
+ERR_SUBCMDPARSER_GLOBAL_ARG_LONG_ID_CONFLICT=Long ID --%s for \
+ global argument %s conflicts with the long ID for the %s argument associated \
+ with subcommand %s
+ERR_SUBCMDPARSER_CANNOT_READ_PROPERTIES_FILE=An error occurred \
+ while attempting to read the contents of the argument properties file %s:  %s
+ERR_SUBCMDPARSER_LONG_ARG_WITHOUT_NAME=The provided command-line \
+ argument %s does not contain an argument name
+ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_LONG_ID=The provided \
+ argument --%s is not a valid global argument identifier
+ERR_SUBCMDPARSER_NO_ARGUMENT_FOR_LONG_ID=The provided argument --%s \
+ is not a valid global or subcommand argument identifier
+ERR_SUBCMDPARSER_NO_VALUE_FOR_ARGUMENT_WITH_LONG_ID=Command-line \
+ argument --%s requires a value but none was given
+ERR_SUBCMDPARSER_VALUE_UNACCEPTABLE_FOR_LONG_ID=The provided value \
+ "%s" for argument --%s is not acceptable:  %s
+ERR_SUBCMDPARSER_NOT_MULTIVALUED_FOR_LONG_ID=The argument --%s was \
+ included multiple times in the provided set of arguments but it does not \
+ allow multiple values
+ERR_SUBCMDPARSER_ARG_FOR_LONG_ID_DOESNT_TAKE_VALUE=A value was \
+ provided for argument --%s but that argument does not take a value
+ERR_SUBCMDPARSER_INVALID_DASH_AS_ARGUMENT=The dash character by \
+ itself is invalid for use as an argument name
+ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_SHORT_ID=The provided \
+ argument -%s is not a valid global argument identifier
+ERR_SUBCMDPARSER_NO_ARGUMENT_FOR_SHORT_ID=The provided argument \
+ -%s is not a valid global or subcommand argument identifier
+ERR_SUBCMDPARSER_NO_VALUE_FOR_ARGUMENT_WITH_SHORT_ID=Argument -%s \
+ requires a value but none was provided
+ERR_SUBCMDPARSER_VALUE_UNACCEPTABLE_FOR_SHORT_ID=The provided \
+ value "%s" for argument -%s is not acceptable:  %s
+ERR_SUBCMDPARSER_NOT_MULTIVALUED_FOR_SHORT_ID=The argument -%s was \
+ included multiple times in the provided set of arguments but it does not \
+ allow multiple values
+ERR_SUBCMDPARSER_CANT_MIX_ARGS_WITH_VALUES=The provided argument \
+ block '-%s%s' is illegal because the '%s' argument requires a value but is in \
+ the same block as at least one other argument that doesn't require a value
+ERR_SUBCMDPARSER_INVALID_ARGUMENT=The provided argument "%s" is \
+ not recognized
+ERR_SUBCMDPARSER_NO_VALUE_FOR_REQUIRED_ARG=The argument %s is \
+ required to have a value but none was provided in the argument list and no \
+ default value is available
+ERR_ARGUMENT_NO_BASE_DN_SPECIFIED=You have specified \
+ not to create a base DN.  If no base DN is to be created you cannot specify \
+ argument '%s'
+ERR_PORT_ALREADY_SPECIFIED=ERROR:  You have specified \
+ the value %s for different ports
+ERR_SEVERAL_CERTIFICATE_TYPE_SPECIFIED=You have \
+ specified several certificate types to be used.  Only one certificate type \
+ (self-signed, JKS, JCEKS, PKCS#12 or PCKS#11) is allowed
+ERR_CERTIFICATE_REQUIRED_FOR_SSL_OR_STARTTLS=You have \
+ chosen to enable SSL or StartTLS.  You must specify which type of certificate \
+ you want the server to use
+ERR_TWO_CONFLICTING_ARGUMENTS=ERROR:  You may not \
+ provide both the %s and the %s arguments at the same time
+ERR_NO_KEYSTORE_PASSWORD=You must provide the PIN of the \
+ keystore to retrieve the certificate to be used by the server.  You can use \
+ {%s} or {%s}
+ERR_SSL_OR_STARTTLS_REQUIRED=You have specified to use a \
+ certificate as server certificate.  You must enable SSL (using option {%s}) \
+ or Start TLS (using option %s)
+ERR_NO_ROOT_PASSWORD=ERROR:  No password was provided \
+ for the initial root user.  When performing a non-interactive installation, \
+ this must be provided using either the %s or the %s argument
+ 

--
Gitblit v1.10.0