mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Violette Roche-Montane
12.11.2014 769a4f06af790ddd713bb280ffd5f657886ae90a
Checkpoint commit for OPENDJ-1343 Migrate dsconfig / OPENDJ-1303 "opendj-cli"
- added classes needeed by DSConfig.

5 files modified
11 files added
2669 ■■■■■ changed files
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommandBuilder.java 272 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java 5 ●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConsoleApplication.java 84 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/HelpCallback.java 41 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Menu.java 48 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuBuilder.java 732 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuCallback.java 51 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuResult.java 261 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableBuilder.java 328 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TablePrinter.java 50 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableSerializer.java 134 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TextTablePrinter.java 497 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java 48 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ValidationCallback.java 53 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties 21 ●●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/ConsoleApplicationTestCase.java 44 ●●●● patch | view | raw | blame | history
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommandBuilder.java
New file
@@ -0,0 +1,272 @@
/*
 * 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 2008-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2014 ForgeRock AS
 */
package com.forgerock.opendj.cli;
import static com.forgerock.opendj.cli.Utils.OBFUSCATED_VALUE;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import com.forgerock.opendj.util.OperatingSystem;
/**
 * Class used to be able to generate the non interactive mode.
 */
public class CommandBuilder {
    private String commandName;
    private String subcommandName;
    private ArrayList<Argument> args;
    private HashSet<Argument> obfuscatedArgs;
    /**
     * The separator used to link the lines of the resulting command-lines.
     */
    public final static String LINE_SEPARATOR;
    static {
        if (OperatingSystem.isWindows()) {
            LINE_SEPARATOR = " ";
        } else {
            LINE_SEPARATOR = " \\\n          ";
        }
    }
    /**
     * The separator used to link the lines of the resulting command-lines in HTML format.
     */
    public final static String HTML_LINE_SEPARATOR;
    static {
        if (OperatingSystem.isWindows()) {
            HTML_LINE_SEPARATOR = "&nbsp;";
        } else {
            HTML_LINE_SEPARATOR = "&nbsp;\\<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
        }
    }
    /**
     * The constructor for the CommandBuilder.
     *
     * @param commandName
     *            The command name.
     * @param subcommandName
     *            The sub command name.
     */
    public CommandBuilder(String commandName, String subcommandName) {
        this.commandName = commandName;
        this.subcommandName = subcommandName;
        args = new ArrayList<Argument>();
        obfuscatedArgs = new HashSet<Argument>();
    }
    /**
     * Adds an argument to the list of the command builder.
     *
     * @param argument
     *            The argument to be added.
     */
    public void addArgument(final Argument argument) {
        // We use an ArrayList to be able to provide the possibility of updating
        // the position of the attributes.
        if (!args.contains(argument)) {
            args.add(argument);
        }
    }
    /**
     * Adds an argument whose values must be obfuscated (passwords for instance).
     *
     * @param argument
     *            The argument to be added.
     */
    public void addObfuscatedArgument(final Argument argument) {
        addArgument(argument);
        obfuscatedArgs.add(argument);
    }
    /**
     * Removes the provided argument from this CommandBuilder.
     *
     * @param argument
     *            The argument to be removed.
     * @return <CODE>true</CODE> if the attribute was present and removed and <CODE>false</CODE> otherwise.
     */
    public boolean removeArgument(final Argument argument) {
        obfuscatedArgs.remove(argument);
        return args.remove(argument);
    }
    /**
     * Appends the arguments of another command builder to this command builder.
     *
     * @param builder
     *            The CommandBuilder to append.
     */
    public void append(final CommandBuilder builder) {
        for (final Argument arg : builder.args) {
            if (builder.isObfuscated(arg)) {
                addObfuscatedArgument(arg);
            } else {
                addArgument(arg);
            }
        }
    }
    /**
     * Returns the String representation of this command builder (i.e. what we want to show to the user).
     *
     * @return The String representation of this command builder (i.e. what we want to show to the user).
     */
    public String toString() {
        return toString(false, LINE_SEPARATOR);
    }
    /**
     * Returns the String representation of this command builder (i.e. what we want to show to the user).
     *
     * @param lineSeparator
     *            The String to be used to separate lines of the command-builder.
     * @return The String representation of this command builder (i.e. what we want to show to the user).
     */
    public String toString(final String lineSeparator) {
        return toString(false, lineSeparator);
    }
    /**
     * Returns the String representation of this command builder (i.e. what we want to show to the user).
     *
     * @param showObfuscated
     *            Displays in clear the obfuscated values.
     * @param lineSeparator
     *            The String to be used to separate lines of the command-builder.
     * @return The String representation of this command builder (i.e. what we want to show to the user).
     */
    private String toString(final boolean showObfuscated, final String lineSeparator) {
        final StringBuilder builder = new StringBuilder();
        builder.append(commandName);
        if (subcommandName != null) {
            builder.append(" " + subcommandName);
        }
        for (final Argument arg : args) {
            // This CLI is always using SSL, and the argument has been removed from
            // the user interface
            if (arg.getName().equals("useSSL")) {
                continue;
            }
            String argName;
            if (arg.getLongIdentifier() != null) {
                argName = "--" + arg.getLongIdentifier();
            } else {
                argName = "-" + arg.getShortIdentifier();
            }
            if (arg instanceof BooleanArgument) {
                builder.append(lineSeparator + argName);
            } else if (arg instanceof FileBasedArgument) {
                for (String value : ((FileBasedArgument) arg).getNameToValueMap().keySet()) {
                    builder.append(lineSeparator + argName + " ");
                    if (isObfuscated(arg) && !showObfuscated) {
                        value = OBFUSCATED_VALUE;
                    } else {
                        value = escapeValue(value);
                    }
                    builder.append(value);
                }
            } else {
                for (String value : arg.getValues()) {
                    builder.append(lineSeparator + argName + " ");
                    if (isObfuscated(arg) && !showObfuscated) {
                        value = OBFUSCATED_VALUE;
                    } else {
                        value = escapeValue(value);
                    }
                    builder.append(value);
                }
            }
        }
        return builder.toString();
    }
    /**
     * Clears the arguments.
     */
    public void clearArguments() {
        args.clear();
        obfuscatedArgs.clear();
    }
    /**
     * Returns the list of arguments.
     *
     * @return The list of arguments.
     */
    public List<Argument> getArguments() {
        return args;
    }
    /**
     * Tells whether the provided argument's values must be obfuscated or not.
     *
     * @param argument
     *            The argument to handle.
     * @return <CODE>true</CODE> if the attribute's values must be obfuscated and <CODE>false</CODE> otherwise.
     */
    public boolean isObfuscated(final Argument argument) {
        return obfuscatedArgs.contains(argument);
    }
    // Chars that require special treatment when passing them to command-line.
    private final static char[] CHARSTOESCAPE =
    { ' ', '\t', '\n', '|', ';', '<', '>', '(', ')', '$', '`', '\\', '"', '\'' };
    /**
     * This method simply takes a value and tries to transform it (with escape or '"') characters so that it can be used
     * in a command line.
     *
     * @param value
     *            The String to be treated.
     * @return The transformed value.
     */
    public static String escapeValue(String value) {
        final StringBuilder b = new StringBuilder();
        if (OperatingSystem.isUnix()) {
            for (int i = 0; i < value.length(); i++) {
                final char c = value.charAt(i);
                boolean charToEscapeFound = false;
                for (int j = 0; j < CHARSTOESCAPE.length && !charToEscapeFound; j++) {
                    charToEscapeFound = c == CHARSTOESCAPE[j];
                }
                if (charToEscapeFound) {
                    b.append('\\');
                }
                b.append(c);
            }
        } else {
            b.append('"').append(value).append('"');
        }
        return b.toString();
    }
}
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java
@@ -123,8 +123,11 @@
     *             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,
        final BooleanArgument quiet = new BooleanArgument(OPTION_LONG_QUIET, OPTION_SHORT_QUIET, OPTION_LONG_QUIET,
                INFO_DESCRIPTION_QUIET.get());
        quiet.setPropertyName(OPTION_LONG_QUIET);
        return quiet;
    }
    /**
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConsoleApplication.java
@@ -30,6 +30,7 @@
import static com.forgerock.opendj.cli.CliMessages.INFO_ERROR_EMPTY_RESPONSE;
import static com.forgerock.opendj.cli.CliMessages.INFO_MENU_PROMPT_RETURN_TO_CONTINUE;
import static com.forgerock.opendj.cli.CliMessages.INFO_PROMPT_SINGLE_DEFAULT;
import static com.forgerock.opendj.cli.CliMessages.ERR_TRIES_LIMIT_REACHED;
import static com.forgerock.opendj.cli.Utils.MAX_LINE_WIDTH;
import static com.forgerock.opendj.cli.Utils.wrapText;
@@ -144,6 +145,15 @@
    }
    /**
     * Indicates whether or not the user has requested advanced mode.
     *
     * @return Returns <code>true</code> if the user has requested advanced mode.
     */
    public boolean isAdvancedMode() {
        return false;
    }
    /**
     * Interactively prompts the user to press return to continue. This method should be called in situations where a
     * user needs to be given a chance to read some documentation before continuing (continuing may cause the
     * documentation to be scrolled out of view).
@@ -215,14 +225,18 @@
     *            The message.
     */
    public final void print(final LocalizableMessage msg) {
        out.print(wrap(msg));
        if (!isQuiet()) {
            out.print(wrap(msg));
        }
    }
    /**
     * Displays a blank line to the output stream.
     */
    public final void println() {
        out.println();
        if (!isQuiet()) {
            out.println();
        }
    }
    /**
@@ -232,7 +246,9 @@
     *            The message.
     */
    public final void println(final LocalizableMessage msg) {
        out.println(wrap(msg));
        if (!isQuiet()) {
            out.println(wrap(msg));
        }
    }
    /**
@@ -244,7 +260,9 @@
     *            The number of columns to indent.
     */
    public final void println(final LocalizableMessage msg, final int indent) {
        out.println(wrapText(msg, MAX_LINE_WIDTH, indent));
        if (!isQuiet()) {
            out.println(wrapText(msg, MAX_LINE_WIDTH, indent));
        }
    }
    /**
@@ -254,7 +272,7 @@
     *            The verbose message.
     */
    public final void printVerboseMessage(final LocalizableMessage msg) {
        if (isVerbose() || isInteractive()) {
        if (isVerbose()) {
            out.println(wrap(msg));
        }
    }
@@ -330,7 +348,7 @@
     * @throws ClientException
     *             If the line of input could not be retrieved for some reason.
     */
    private final String readLineOfInput(final LocalizableMessage prompt) throws ClientException {
    final String readLineOfInput(final LocalizableMessage prompt) throws ClientException {
        if (prompt != null) {
            err.print(wrap(prompt));
            err.print(" ");
@@ -348,6 +366,60 @@
    }
    /**
     * Interactively prompts for user input and continues until valid input is provided.
     *
     * @param <T>
     *            The type of decoded user input.
     * @param prompt
     *            The interactive prompt which should be displayed on each input attempt.
     * @param validator
     *            An input validator responsible for validating and decoding the user's response.
     * @return Returns the decoded user's response.
     * @throws ClientException
     *             If an unexpected error occurred which prevented validation.
     */
    public final <T> T readValidatedInput(final LocalizableMessage prompt, final ValidationCallback<T> validator)
            throws ClientException {
        while (true) {
            final String response = readLineOfInput(prompt);
            final T value = validator.validate(this, response);
            if (value != null) {
                return value;
            }
        }
    }
    /**
     * Interactively prompts for user input and continues until valid input is provided.
     *
     * @param <T>
     *            The type of decoded user input.
     * @param prompt
     *            The interactive prompt which should be displayed on each input attempt.
     * @param validator
     *            An input validator responsible for validating and decoding the user's response.
     * @param maxTries
     *            The maximum number of tries that we can make.
     * @return Returns the decoded user's response.
     * @throws ClientException
     *             If an unexpected error occurred which prevented validation or
     *             if the maximum number of tries was reached.
     */
    public final <T> T readValidatedInput(final LocalizableMessage prompt, final ValidationCallback<T> validator,
            final int maxTries) throws ClientException {
        int nTries = 0;
        while (nTries < maxTries) {
            final String response = readLineOfInput(prompt);
            final T value = validator.validate(this, response);
            if (value != null) {
                return value;
            }
            nTries++;
        }
        throw new ClientException(ReturnCode.ERROR_USER_DATA, ERR_TRIES_LIMIT_REACHED.get(maxTries));
    }
    /**
     * Inserts line breaks into the provided buffer to wrap text at no more than the specified column width (80).
     *
     * @param msg
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/HelpCallback.java
New file
@@ -0,0 +1,41 @@
/*
 * 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 2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014 ForgeRock AS
 */
package com.forgerock.opendj.cli;
/**
 * An interface for displaying help interactively.
 */
public interface HelpCallback {
    /**
     * Displays help to the provided application console.
     *
     * @param app
     *            The console application.
     */
    void display(ConsoleApplication app);
}
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Menu.java
New file
@@ -0,0 +1,48 @@
/*
 * 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 2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014 ForgeRock AS
 */
package com.forgerock.opendj.cli;
/**
 * An interactive console-based menu.
 *
 * @param <T>
 *          The type of success result value(s) returned by the
 *          call-back. Use <code>Void</code> if the call-backs do
 *          not return any values.
 */
public interface Menu<T> {
    /**
     * Displays the menu and waits for the user to select a valid option. When the user selects an option, the call-back
     * associated with the option will be invoked and its result returned.
     *
     * @return Returns the result of invoking the chosen menu call-back.
     * @throws ClientException
     *             If an I/O exception occurred or if one of the menu option call-backs failed for some reason.
     */
    MenuResult<T> run() throws ClientException;
}
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuBuilder.java
New file
@@ -0,0 +1,732 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2007-2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014 ForgeRock AS
 */
package com.forgerock.opendj.cli;
import static com.forgerock.opendj.cli.CliMessages.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.forgerock.i18n.LocalizableMessage;
/**
 * An interface for incrementally building a command-line menu.
 *
 * @param <T>
 *            The type of value returned by the call-backs. Use <code>Void</code> if the call-backs do not return a
 *            value.
 */
public final class MenuBuilder<T> {
    /**
     * A simple menu option call-back which is a composite of zero or more underlying call-backs.
     *
     * @param <T>
     *            The type of value returned by the call-back.
     */
    private static final class CompositeCallback<T> implements MenuCallback<T> {
        // The list of underlying call-backs.
        private final Collection<MenuCallback<T>> callbacks;
        /**
         * Creates a new composite call-back with the specified set of call-backs.
         *
         * @param callbacks
         *            The set of call-backs.
         */
        public CompositeCallback(Collection<MenuCallback<T>> callbacks) {
            this.callbacks = callbacks;
        }
        /**
         * {@inheritDoc}
         */
        public MenuResult<T> invoke(ConsoleApplication app) throws ClientException {
            List<T> values = new ArrayList<T>();
            for (MenuCallback<T> callback : callbacks) {
                MenuResult<T> result = callback.invoke(app);
                if (!result.isSuccess()) {
                    // Throw away all the other results.
                    return result;
                } else {
                    values.addAll(result.getValues());
                }
            }
            return MenuResult.success(values);
        }
    }
    /**
     * Underlying menu implementation generated by this menu builder.
     *
     * @param <T>
     *            The type of value returned by the call-backs. Use <code>Void</code> if the call-backs do not return a
     *            value.
     */
    private static final class MenuImpl<T> implements Menu<T> {
        // Indicates whether the menu will allow selection of multiple
        // numeric options.
        private final boolean allowMultiSelect;
        // The application console.
        private final ConsoleApplication app;
        // The call-back lookup table.
        private final Map<String, MenuCallback<T>> callbacks;
        // The char options table builder.
        private final TableBuilder cbuilder;
        // The call-back for the optional default action.
        private final MenuCallback<T> defaultCallback;
        // The description of the optional default action.
        private final LocalizableMessage defaultDescription;
        // The numeric options table builder.
        private final TableBuilder nbuilder;
        // The table printer.
        private final TablePrinter printer;
        // The menu prompt.
        private final LocalizableMessage prompt;
        // The menu title.
        private final LocalizableMessage title;
        // The maximum number of times we display the menu if the user provides
        // bad input (-1 for unlimited).
        private int nMaxTries;
        // Private constructor.
        private MenuImpl(ConsoleApplication app, LocalizableMessage title, LocalizableMessage prompt,
                TableBuilder ntable, TableBuilder ctable, TablePrinter printer, Map<String, MenuCallback<T>> callbacks,
                boolean allowMultiSelect, MenuCallback<T> defaultCallback, LocalizableMessage defaultDescription,
                int nMaxTries) {
            this.app = app;
            this.title = title;
            this.prompt = prompt;
            this.nbuilder = ntable;
            this.cbuilder = ctable;
            this.printer = printer;
            this.callbacks = callbacks;
            this.allowMultiSelect = allowMultiSelect;
            this.defaultCallback = defaultCallback;
            this.defaultDescription = defaultDescription;
            this.nMaxTries = nMaxTries;
        }
        /**
         * {@inheritDoc}
         */
        public MenuResult<T> run() throws ClientException {
            // The validation call-back which will be used to determine the
            // action call-back.
            ValidationCallback<MenuCallback<T>> validator = new ValidationCallback<MenuCallback<T>>() {
                public MenuCallback<T> validate(ConsoleApplication app, String input) {
                    String ninput = input.trim();
                    if (ninput.length() == 0) {
                        if (defaultCallback != null) {
                            return defaultCallback;
                        } else if (allowMultiSelect) {
                            app.println();
                            app.println(ERR_MENU_BAD_CHOICE_MULTI.get());
                            app.println();
                            return null;
                        } else {
                            app.println();
                            app.println(ERR_MENU_BAD_CHOICE_SINGLE.get());
                            app.println();
                            return null;
                        }
                    } else if (allowMultiSelect) {
                        // Use a composite call-back to collect all the results.
                        List<MenuCallback<T>> cl = new ArrayList<MenuCallback<T>>();
                        for (String value : ninput.split(",")) {
                            // Make sure that there are no duplicates.
                            String nvalue = value.trim();
                            Set<String> choices = new HashSet<String>();
                            if (choices.contains(nvalue)) {
                                app.println();
                                app.println(ERR_MENU_BAD_CHOICE_MULTI_DUPE.get(value));
                                app.println();
                                return null;
                            } else if (!callbacks.containsKey(nvalue)) {
                                app.println();
                                app.println(ERR_MENU_BAD_CHOICE_MULTI.get());
                                app.println();
                                return null;
                            } else {
                                cl.add(callbacks.get(nvalue));
                                choices.add(nvalue);
                            }
                        }
                        return new CompositeCallback<T>(cl);
                    } else if (!callbacks.containsKey(ninput)) {
                        app.println();
                        app.println(ERR_MENU_BAD_CHOICE_SINGLE.get());
                        app.println();
                        return null;
                    } else {
                        return callbacks.get(ninput);
                    }
                }
            };
            // Determine the correct choice prompt.
            LocalizableMessage promptMsg;
            if (allowMultiSelect) {
                if (defaultDescription != null) {
                    promptMsg = INFO_MENU_PROMPT_MULTI_DEFAULT.get(defaultDescription);
                } else {
                    promptMsg = INFO_MENU_PROMPT_MULTI.get();
                }
            } else {
                if (defaultDescription != null) {
                    promptMsg = INFO_MENU_PROMPT_SINGLE_DEFAULT.get(defaultDescription);
                } else {
                    promptMsg = INFO_MENU_PROMPT_SINGLE.get();
                }
            }
            // If the user selects help then we need to loop around and
            // display the menu again.
            while (true) {
                // Display the menu.
                if (title != null) {
                    app.println(title);
                    app.println();
                }
                if (prompt != null) {
                    app.println(prompt);
                    app.println();
                }
                if (nbuilder.getTableHeight() > 0) {
                    nbuilder.print(printer);
                    app.println();
                }
                if (cbuilder.getTableHeight() > 0) {
                    TextTablePrinter cprinter = new TextTablePrinter(app.getErrorStream());
                    cprinter.setDisplayHeadings(false);
                    int sz = String.valueOf(nbuilder.getTableHeight()).length() + 1;
                    cprinter.setIndentWidth(4);
                    cprinter.setColumnWidth(0, sz);
                    cprinter.setColumnWidth(1, 0);
                    cbuilder.print(cprinter);
                    app.println();
                }
                // Get the user's choice.
                MenuCallback<T> choice;
                if (nMaxTries != -1) {
                    choice = app.readValidatedInput(promptMsg, validator, nMaxTries);
                } else {
                    choice = app.readValidatedInput(promptMsg, validator);
                }
                // Invoke the user's selected choice.
                MenuResult<T> result = choice.invoke(app);
                // Determine if the help needs to be displayed, display it and
                // start again.
                if (!result.isAgain()) {
                    return result;
                } else {
                    app.println();
                    app.println();
                }
            }
        }
    }
    /**
     * A simple menu option call-back which does nothing but return the provided menu result.
     *
     * @param <T>
     *            The type of result returned by the call-back.
     */
    private static final class ResultCallback<T> implements MenuCallback<T> {
        // The result to be returned by this call-back.
        private final MenuResult<T> result;
        // Private constructor.
        private ResultCallback(MenuResult<T> result) {
            this.result = result;
        }
        /**
         * {@inheritDoc}
         */
        public MenuResult<T> invoke(ConsoleApplication app) throws ClientException {
            return result;
        }
    }
    // The multiple column display threshold.
    private int threshold = -1;
    // Indicates whether the menu will allow selection of multiple
    // numeric options.
    private boolean allowMultiSelect = false;
    // The application console.
    private final ConsoleApplication app;
    // The char option call-backs.
    private final List<MenuCallback<T>> charCallbacks = new ArrayList<MenuCallback<T>>();
    // The char option keys (must be single-character messages).
    private final List<LocalizableMessage> charKeys = new ArrayList<LocalizableMessage>();
    // The synopsis of char options.
    private final List<LocalizableMessage> charSynopsis = new ArrayList<LocalizableMessage>();
    // Optional column headings.
    private final List<LocalizableMessage> columnHeadings = new ArrayList<LocalizableMessage>();
    // Optional column widths.
    private final List<Integer> columnWidths = new ArrayList<Integer>();
    // The call-back for the optional default action.
    private MenuCallback<T> defaultCallback = null;
    // The description of the optional default action.
    private LocalizableMessage defaultDescription = null;
    // The numeric option call-backs.
    private final List<MenuCallback<T>> numericCallbacks = new ArrayList<MenuCallback<T>>();
    // The numeric option fields.
    private final List<List<LocalizableMessage>> numericFields = new ArrayList<List<LocalizableMessage>>();
    // The menu title.
    private LocalizableMessage title = null;
    // The menu prompt.
    private LocalizableMessage prompt = null;
    // The maximum number of times that we allow the user to provide an invalid
    // answer (-1 if unlimited).
    private int nMaxTries = -1;
    /**
     * Creates a new menu.
     *
     * @param app
     *            The application console.
     */
    public MenuBuilder(ConsoleApplication app) {
        this.app = app;
    }
    /**
     * Creates a "back" menu option. When invoked, this option will return a {@code MenuResult.cancel()} result.
     *
     * @param isDefault
     *            Indicates whether this option should be made the menu default.
     */
    public void addBackOption(boolean isDefault) {
        addCharOption(INFO_MENU_OPTION_BACK_KEY.get(), INFO_MENU_OPTION_BACK.get(), MenuResult.<T> cancel());
        if (isDefault) {
            setDefault(INFO_MENU_OPTION_BACK_KEY.get(), MenuResult.<T> cancel());
        }
    }
    /**
     * Creates a "cancel" menu option. When invoked, this option will return a {@code MenuResult.cancel()} result.
     *
     * @param isDefault
     *            Indicates whether this option should be made the menu default.
     */
    public void addCancelOption(boolean isDefault) {
        addCharOption(INFO_MENU_OPTION_CANCEL_KEY.get(), INFO_MENU_OPTION_CANCEL.get(), MenuResult.<T> cancel());
        if (isDefault) {
            setDefault(INFO_MENU_OPTION_CANCEL_KEY.get(), MenuResult.<T> cancel());
        }
    }
    /**
     * Adds a menu choice to the menu which will have a single letter as its key.
     *
     * @param c
     *            The single-letter message which will be used as the key for this option.
     * @param description
     *            The menu option description.
     * @param callback
     *            The call-back associated with this option.
     */
    public void addCharOption(LocalizableMessage c, LocalizableMessage description, MenuCallback<T> callback) {
        charKeys.add(c);
        charSynopsis.add(description);
        charCallbacks.add(callback);
    }
    /**
     * Adds a menu choice to the menu which will have a single letter as its key and which returns the provided result.
     *
     * @param c
     *            The single-letter message which will be used as the key for this option.
     * @param description
     *            The menu option description.
     * @param result
     *            The menu result which should be returned by this menu choice.
     */
    public void addCharOption(LocalizableMessage c, LocalizableMessage description, MenuResult<T> result) {
        addCharOption(c, description, new ResultCallback<T>(result));
    }
    /**
     * Creates a "help" menu option which will use the provided help call-back to display help relating to the other
     * menu options. When the help menu option is selected help will be displayed and then the user will be shown the
     * menu again and prompted to enter a choice.
     *
     * @param callback
     *            The help call-back.
     */
    public void addHelpOption(final HelpCallback callback) {
        MenuCallback<T> wrapper = new MenuCallback<T>() {
            public MenuResult<T> invoke(ConsoleApplication app) throws ClientException {
                app.println();
                callback.display(app);
                return MenuResult.again();
            }
        };
        addCharOption(INFO_MENU_OPTION_HELP_KEY.get(), INFO_MENU_OPTION_HELP.get(), wrapper);
    }
    /**
     * Adds a menu choice to the menu which will have a numeric key.
     *
     * @param description
     *            The menu option description.
     * @param callback
     *            The call-back associated with this option.
     * @param extraFields
     *            Any additional fields associated with this menu option.
     * @return Returns the number associated with menu choice.
     */
    public int addNumberedOption(LocalizableMessage description, MenuCallback<T> callback,
            LocalizableMessage... extraFields) {
        List<LocalizableMessage> fields = new ArrayList<LocalizableMessage>();
        fields.add(description);
        if (extraFields != null) {
            fields.addAll(Arrays.asList(extraFields));
        }
        numericFields.add(fields);
        numericCallbacks.add(callback);
        return numericCallbacks.size();
    }
    /**
     * Adds a menu choice to the menu which will have a numeric key and which returns the provided result.
     *
     * @param description
     *            The menu option description.
     * @param result
     *            The menu result which should be returned by this menu choice.
     * @param extraFields
     *            Any additional fields associated with this menu option.
     * @return Returns the number associated with menu choice.
     */
    public int addNumberedOption(LocalizableMessage description, MenuResult<T> result,
            LocalizableMessage... extraFields) {
        return addNumberedOption(description, new ResultCallback<T>(result), extraFields);
    }
    /**
     * Creates a "quit" menu option. When invoked, this option will return a {@code MenuResult.quit()} result.
     */
    public void addQuitOption() {
        addCharOption(INFO_MENU_OPTION_QUIT_KEY.get(), INFO_MENU_OPTION_QUIT.get(), MenuResult.<T> quit());
    }
    /**
     * Sets the flag which indicates whether or not the menu will permit multiple numeric options to be selected at
     * once. Users specify multiple choices by separating them with a comma. The default is <code>false</code>.
     *
     * @param allowMultiSelect
     *            Indicates whether or not the menu will permit multiple numeric options to be selected at once.
     */
    public void setAllowMultiSelect(boolean allowMultiSelect) {
        this.allowMultiSelect = allowMultiSelect;
    }
    /**
     * Sets the optional column headings. The column headings will be displayed above the menu options.
     *
     * @param headings
     *            The optional column headings.
     */
    public void setColumnHeadings(LocalizableMessage... headings) {
        this.columnHeadings.clear();
        if (headings != null) {
            this.columnHeadings.addAll(Arrays.asList(headings));
        }
    }
    /**
     * Sets the optional column widths. A value of zero indicates that the column should be expandable, a value of
     * <code>null</code> indicates that the column should use its default width.
     *
     * @param widths
     *            The optional column widths.
     */
    public void setColumnWidths(Integer... widths) {
        this.columnWidths.clear();
        if (widths != null) {
            this.columnWidths.addAll(Arrays.asList(widths));
        }
    }
    /**
     * Sets the optional default action for this menu. The default action call-back will be invoked if the user does not
     * specify an option and just presses enter.
     *
     * @param description
     *            A short description of the default action.
     * @param callback
     *            The call-back associated with the default action.
     */
    public void setDefault(LocalizableMessage description, MenuCallback<T> callback) {
        defaultCallback = callback;
        defaultDescription = description;
    }
    /**
     * Sets the optional default action for this menu. The default action call-back will be invoked if the user does not
     * specify an option and just presses enter.
     *
     * @param description
     *            A short description of the default action.
     * @param result
     *            The menu result which should be returned by default.
     */
    public void setDefault(LocalizableMessage description, MenuResult<T> result) {
        setDefault(description, new ResultCallback<T>(result));
    }
    /**
     * Sets the number of numeric options required to trigger multiple-column display. A negative value (the default)
     * indicates that the numeric options will always be displayed in a single column. A value of 0 indicates that
     * numeric options will always be displayed in multiple columns.
     *
     * @param threshold
     *            The number of numeric options required to trigger multiple-column display.
     */
    public void setMultipleColumnThreshold(int threshold) {
        this.threshold = threshold;
    }
    /**
     * Sets the optional menu prompt. The prompt will be displayed above the menu. Menus do not have a prompt by
     * default.
     *
     * @param prompt
     *            The menu prompt, or <code>null</code> if there is not prompt.
     */
    public void setPrompt(LocalizableMessage prompt) {
        this.prompt = prompt;
    }
    /**
     * Sets the optional menu title. The title will be displayed above the menu prompt. Menus do not have a title by
     * default.
     *
     * @param title
     *            The menu title, or <code>null</code> if there is not title.
     */
    public void setTitle(LocalizableMessage title) {
        this.title = title;
    }
    /**
     * Creates a menu from this menu builder.
     *
     * @return Returns the new menu.
     */
    public Menu<T> toMenu() {
        TableBuilder nbuilder = new TableBuilder();
        Map<String, MenuCallback<T>> callbacks = new HashMap<String, MenuCallback<T>>();
        // Determine whether multiple columns should be used for numeric
        // options.
        boolean useMultipleColumns = false;
        if (threshold >= 0 && numericCallbacks.size() >= threshold) {
            useMultipleColumns = true;
        }
        // Create optional column headers.
        if (!columnHeadings.isEmpty()) {
            nbuilder.appendHeading();
            for (LocalizableMessage heading : columnHeadings) {
                if (heading != null) {
                    nbuilder.appendHeading(heading);
                } else {
                    nbuilder.appendHeading();
                }
            }
            if (useMultipleColumns) {
                nbuilder.appendHeading();
                for (LocalizableMessage heading : columnHeadings) {
                    if (heading != null) {
                        nbuilder.appendHeading(heading);
                    } else {
                        nbuilder.appendHeading();
                    }
                }
            }
        }
        // Add the numeric options first.
        int sz = numericCallbacks.size();
        int rows = sz;
        if (useMultipleColumns) {
            // Display in two columns the first column should contain half
            // the options. If there are an odd number of columns then the
            // first column should contain an additional option (e.g. if
            // there are 23 options, the first column should contain 12
            // options and the second column 11 options).
            rows /= 2;
            rows += sz % 2;
        }
        for (int i = 0, j = rows; i < rows; i++, j++) {
            nbuilder.startRow();
            nbuilder.appendCell(INFO_MENU_NUMERIC_OPTION.get(i + 1));
            for (LocalizableMessage field : numericFields.get(i)) {
                if (field != null) {
                    nbuilder.appendCell(field);
                } else {
                    nbuilder.appendCell();
                }
            }
            callbacks.put(String.valueOf(i + 1), numericCallbacks.get(i));
            // Second column.
            if (useMultipleColumns && (j < sz)) {
                nbuilder.appendCell(INFO_MENU_NUMERIC_OPTION.get(j + 1));
                for (LocalizableMessage field : numericFields.get(j)) {
                    if (field != null) {
                        nbuilder.appendCell(field);
                    } else {
                        nbuilder.appendCell();
                    }
                }
                callbacks.put(String.valueOf(j + 1), numericCallbacks.get(j));
            }
        }
        // Add the char options last.
        TableBuilder cbuilder = new TableBuilder();
        for (int i = 0; i < charCallbacks.size(); i++) {
            char c = charKeys.get(i).charAt(0);
            LocalizableMessage option = INFO_MENU_CHAR_OPTION.get(c);
            cbuilder.startRow();
            cbuilder.appendCell(option);
            cbuilder.appendCell(charSynopsis.get(i));
            callbacks.put(String.valueOf(c), charCallbacks.get(i));
        }
        // Configure the table printer.
        TextTablePrinter printer = new TextTablePrinter(app.getErrorStream());
        if (columnHeadings.isEmpty()) {
            printer.setDisplayHeadings(false);
        } else {
            printer.setDisplayHeadings(true);
            printer.setHeadingSeparatorStartColumn(1);
        }
        printer.setIndentWidth(4);
        if (columnWidths.isEmpty()) {
            printer.setColumnWidth(1, 0);
            if (useMultipleColumns) {
                printer.setColumnWidth(3, 0);
            }
        } else {
            for (int i = 0; i < columnWidths.size(); i++) {
                Integer j = columnWidths.get(i);
                if (j != null) {
                    // Skip the option key column.
                    printer.setColumnWidth(i + 1, j);
                    if (useMultipleColumns) {
                        printer.setColumnWidth(i + 2 + columnWidths.size(), j);
                    }
                }
            }
        }
        return new MenuImpl<T>(app, title, prompt, nbuilder, cbuilder, printer, callbacks, allowMultiSelect,
                defaultCallback, defaultDescription, nMaxTries);
    }
    /**
     * Sets the maximum number of tries that the user can provide an invalid value in the menu. -1 for unlimited tries
     * (the default). If this limit is reached a ClientException will be thrown.
     *
     * @param nTries
     *            the maximum number of tries.
     */
    public void setMaxTries(int nTries) {
        nMaxTries = nTries;
    }
}
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuCallback.java
New file
@@ -0,0 +1,51 @@
/*
 * 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 2008 Sun Microsystems
 *      Portions Copyright 2014 ForgeRock AS
 */
package com.forgerock.opendj.cli;
/**
 * A menu call-back which should be associated with each menu option.
 * When an option is selected the call-back is invoked.
 *
 * @param <T>
 *          The type of success result value(s) returned by the
 *          call-back. Use <code>Void</code> if the call-backs do
 *          not return any values.
 */
public interface MenuCallback<T> {
   /**
    * Invoke the menu call-back.
    *
    * @param app
    *          The application console.
    * @return Returns the result of invoking the menu call-back.
    * @throws ClientException
    *           If the menu call-back fails for some reason.
    */
    MenuResult<T> invoke(ConsoleApplication app) throws ClientException;
}
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuResult.java
New file
@@ -0,0 +1,261 @@
/*
 * 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 2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014 ForgeRock AS
 */
package com.forgerock.opendj.cli;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
/**
 * The result of running a {@link Menu}. The result indicates to the
 * application how it should proceed:
 * <ul>
 * <li>{@link #again()} - the menu should be displayed again. A good
 * example of this is when a user chooses to view some help. Normally,
 * after the help is displayed, the user is allowed to select another
 * option
 * <li>{@link #cancel()} - the user chose to cancel any task
 * currently in progress and go back to the previous main menu if
 * applicable
 * <li>{@link #success()} - the user chose to apply any task
 * currently in progress and go back to the previous menu if
 * applicable. Any result values applicable to the chosen option can
 * be retrieved using {@link #getValue()} or {@link #getValues()}
 * <li>{@link #quit()} - the user chose to quit the application and
 * cancel all outstanding tasks.
 * </ul>
 *
 * @param <T>
 *            The type of result value(s) contained in success results. Use <code>Void</code> if success results should
 *            not contain values.
 */
public final class MenuResult<T> {
    /**
     * The type of result returned from the menu.
     */
    private static enum Type {
        /**
         * The user selected an option which did not return a result,
         * so the menu should be displayed again.
         */
        AGAIN,
        /**
         * The user did not select an option and instead
         * chose to cancel the current task.
         */
        CANCEL,
        /**
         * The user did not select an option and instead
         * chose to quit the entire application.
         */
        QUIT,
        /**
         * The user selected an option which succeeded
         * and returned one or more result values.
         */
        SUCCESS
    }
    /**
     * Creates a new menu result indicating that the menu should be displayed again. A good example of this is when a
     * user chooses to view some help. Normally, after the help is displayed, the user is allowed to select another
     * option.
     *
     * @param <T>
     *            The type of result value(s) contained in success results. Use <code>Void</code> if success results
     *            should not contain values.
     * @return Returns a new menu result indicating that the menu should be displayed again.
     */
    public static <T> MenuResult<T> again() {
        return new MenuResult<T>(Type.AGAIN, Collections.<T> emptyList());
    }
    /**
     * Creates a new menu result indicating that the user chose to cancel any task currently in progress and go back to
     * the previous main menu if applicable.
     *
     * @param <T>
     *            The type of result value(s) contained in success results. Use <code>Void</code> if success results
     *            should not contain values.
     * @return Returns a new menu result indicating that the user chose to cancel any task currently in progress and go
     *         back to the previous main menu if applicable.
     */
    public static <T> MenuResult<T> cancel() {
        return new MenuResult<T>(Type.CANCEL, Collections.<T> emptyList());
    }
    /**
     * Creates a new menu result indicating that the user chose to quit the application and cancel all outstanding
     * tasks.
     *
     * @param <T>
     *            The type of result value(s) contained in success results. Use <code>Void</code> if success results
     *            should not contain values.
     * @return Returns a new menu result indicating that the user chose to quit the application and cancel all
     *         outstanding tasks.
     */
    public static <T> MenuResult<T> quit() {
        return new MenuResult<T>(Type.QUIT, Collections.<T> emptyList());
    }
    /**
     * Creates a new menu result indicating that the user chose to apply any task currently in progress and go back to
     * the previous menu if applicable. The menu result will not contain any result values.
     *
     * @param <T>
     *            The type of result value(s) contained in success results. Use <code>Void</code> if success results
     *            should not contain values.
     * @return Returns a new menu result indicating that the user chose to apply any task currently in progress and go
     *         back to the previous menu if applicable.The menu result will not contain any result values.
     */
    public static <T> MenuResult<T> success() {
        return success(Collections.<T> emptySet());
    }
    /**
     * Creates a new menu result indicating that the user chose to apply any task currently in progress and go back to
     * the previous menu if applicable. The menu result will contain the provided values, which can be retrieved using
     * {@link #getValue()} or {@link #getValues()}.
     *
     * @param <T>
     *            The type of the result values.
     * @param values
     *            The result values.
     * @return Returns a new menu result indicating that the user chose to apply any task currently in progress and go
     *         back to the previous menu if applicable. The menu result will contain the provided values, which can be
     *         retrieved using {@link #getValue()} or {@link #getValues()}.
     */
    public static <T> MenuResult<T> success(Collection<T> values) {
        return new MenuResult<T>(Type.SUCCESS, new ArrayList<T>(values));
    }
    /**
     * Creates a new menu result indicating that the user chose to apply any task currently in progress and go back to
     * the previous menu if applicable. The menu result will contain the provided value, which can be retrieved using
     * {@link #getValue()} or {@link #getValues()}.
     *
     * @param <T>
     *            The type of the result value.
     * @param value
     *            The result value.
     * @return Returns a new menu result indicating that the user chose to apply any task currently in progress and go
     *         back to the previous menu if applicable. The menu result will contain the provided value, which can be
     *         retrieved using {@link #getValue()} or {@link #getValues()}.
     */
    public static <T> MenuResult<T> success(T value) {
        return success(Collections.singleton(value));
    }
    // The type of result returned from the menu.
    private final Type type;
    // The menu result value(s).
    private final Collection<T> values;
    // Private constructor.
    private MenuResult(Type type, Collection<T> values) {
        this.type = type;
        this.values = values;
    }
    /**
     * Gets the menu result value if this is a menu result indicating success.
     *
     * @return Returns the menu result value, or <code>null</code> if there was no result value or if this is not a
     *         success menu result.
     * @see #isSuccess()
     */
    public T getValue() {
        if (values.isEmpty()) {
            return null;
        } else {
            return values.iterator().next();
        }
    }
    /**
     * Gets the menu result values if this is a menu result indicating success.
     *
     * @return Returns the menu result values, which may be empty if there were no result values or if this is not a
     *         success menu result.
     * @see #isSuccess()
     */
    public Collection<T> getValues() {
        return new ArrayList<T>(values);
    }
    /**
     * Determines if this menu result indicates that the menu should be displayed again. A good example of this is when
     * a user chooses to view some help. Normally, after the help is displayed, the user is allowed to select another
     * option.
     *
     * @return Returns <code>true</code> if this menu result indicates that the menu should be displayed again.
     */
    public boolean isAgain() {
        return type == Type.AGAIN;
    }
    /**
     * Determines if this menu result indicates that the user chose to cancel any task currently in progress and go back
     * to the previous main menu if applicable.
     *
     * @return Returns <code>true</code> if this menu result indicates that the user chose to cancel any task currently
     *         in progress and go back to the previous main menu if applicable.
     */
    public boolean isCancel() {
        return type == Type.CANCEL;
    }
    /**
     * Determines if this menu result indicates that the user chose to quit the application and cancel all outstanding
     * tasks.
     *
     * @return Returns <code>true</code> if this menu result indicates that the user chose to quit the application and
     *         cancel all outstanding tasks.
     */
    public boolean isQuit() {
        return type == Type.QUIT;
    }
    /**
     * Determines if this menu result indicates that the user chose to apply any task currently in progress and go back
     * to the previous menu if applicable. Any result values can be retrieved using the {@link #getValue()} or
     * {@link #getValues()} methods.
     *
     * @return Returns <code>true</code> if this menu result indicates that the user chose to apply any task currently
     *         in progress and go back to the previous menu if applicable.
     * @see #getValue()
     * @see #getValues()
     */
    public boolean isSuccess() {
        return type == Type.SUCCESS;
    }
}
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableBuilder.java
New file
@@ -0,0 +1,328 @@
/*
 * 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 2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014 ForgeRock AS
 */
package com.forgerock.opendj.cli;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.forgerock.i18n.LocalizableMessage;
/**
 * A class which can be used to construct tables of information to be displayed in a terminal.
 * Once built the table can be output using a {@link TableSerializer}.
 */
public final class TableBuilder {
    // The current column number in the current row where 0 represents
    // the left-most column in the table.
    private int column = 0;
    // The current with of each column.
    private List<Integer> columnWidths = new ArrayList<Integer>();
    // The list of column headings.
    private List<LocalizableMessage> header = new ArrayList<LocalizableMessage>();
    // The current number of rows in the table.
    private int height = 0;
    // The list of table rows.
    private List<List<String>> rows = new ArrayList<List<String>>();
    // The linked list of sort keys comparators.
    private List<Comparator<String>> sortComparators = new ArrayList<Comparator<String>>();
    // The linked list of sort keys.
    private List<Integer> sortKeys = new ArrayList<Integer>();
    // The current number of columns in the table.
    private int width = 0;
    /**
     * Creates a new table printer.
     */
    public TableBuilder() {
        // No implementation required.
    }
    /**
     * Adds a table sort key. The table will be sorted according to the case-insensitive string ordering of the cells in
     * the specified column.
     *
     * @param column
     *            The column which will be used as a sort key.
     */
    public void addSortKey(int column) {
        addSortKey(column, String.CASE_INSENSITIVE_ORDER);
    }
    /**
     * Adds a table sort key. The table will be sorted according to the provided string comparator.
     *
     * @param column
     *            The column which will be used as a sort key.
     * @param comparator
     *            The string comparator.
     */
    public void addSortKey(int column, Comparator<String> comparator) {
        sortKeys.add(column);
        sortComparators.add(comparator);
    }
    /**
     * Appends a new blank cell to the current row.
     */
    public void appendCell() {
        appendCell("");
    }
    /**
     * Appends a new cell to the current row containing the provided boolean value.
     *
     * @param value
     *            The boolean value.
     */
    public void appendCell(boolean value) {
        appendCell(String.valueOf(value));
    }
    /**
     * Appends a new cell to the current row containing the provided byte value.
     *
     * @param value
     *            The byte value.
     */
    public void appendCell(byte value) {
        appendCell(String.valueOf(value));
    }
    /**
     * Appends a new cell to the current row containing the provided char value.
     *
     * @param value
     *            The char value.
     */
    public void appendCell(char value) {
        appendCell(String.valueOf(value));
    }
    /**
     * Appends a new cell to the current row containing the provided double value.
     *
     * @param value
     *            The double value.
     */
    public void appendCell(double value) {
        appendCell(String.valueOf(value));
    }
    /**
     * Appends a new cell to the current row containing the provided float value.
     *
     * @param value
     *            The float value.
     */
    public void appendCell(float value) {
        appendCell(String.valueOf(value));
    }
    /**
     * Appends a new cell to the current row containing the provided integer value.
     *
     * @param value
     *            The boolean value.
     */
    public void appendCell(int value) {
        appendCell(String.valueOf(value));
    }
    /**
     * Appends a new cell to the current row containing the provided long value.
     *
     * @param value
     *            The long value.
     */
    public void appendCell(long value) {
        appendCell(String.valueOf(value));
    }
    /**
     * Appends a new cell to the current row containing the provided object value.
     *
     * @param value
     *            The object value.
     */
    public void appendCell(Object value) {
        // Make sure that the first row has been created.
        if (height == 0) {
            startRow();
        }
        // Create the cell.
        String s = String.valueOf(value);
        rows.get(height - 1).add(s);
        column++;
        // Update statistics.
        if (column > width) {
            width = column;
            columnWidths.add(s.length());
        } else if (columnWidths.get(column - 1) < s.length()) {
            columnWidths.set(column - 1, s.length());
        }
    }
    /**
     * Appends a new blank column heading to the header row.
     */
    public void appendHeading() {
        appendHeading(LocalizableMessage.EMPTY);
    }
    /**
     * Appends a new column heading to the header row.
     *
     * @param value
     *            The column heading value.
     */
    public void appendHeading(LocalizableMessage value) {
        header.add(value);
        // Update statistics.
        if (header.size() > width) {
            width = header.size();
            columnWidths.add(value.length());
        } else if (columnWidths.get(header.size() - 1) < value.length()) {
            columnWidths.set(header.size() - 1, value.length());
        }
    }
    /**
     * Gets the width of the current row.
     *
     * @return Returns the width of the current row.
     */
    public int getRowWidth() {
        return column;
    }
    /**
     * Gets the number of rows in table.
     *
     * @return Returns the number of rows in table.
     */
    public int getTableHeight() {
        return height;
    }
    /**
     * Gets the number of columns in table.
     *
     * @return Returns the number of columns in table.
     */
    public int getTableWidth() {
        return width;
    }
    /**
     * Prints the table in its current state using the provided table printer.
     *
     * @param printer
     *            The table printer.
     */
    public void print(TablePrinter printer) {
        // Create a new printer instance.
        TableSerializer serializer = printer.getSerializer();
        // First sort the table.
        List<List<String>> sortedRows = new ArrayList<List<String>>(rows);
        Comparator<List<String>> comparator = new Comparator<List<String>>() {
            public int compare(List<String> row1, List<String> row2) {
                for (int i = 0; i < sortKeys.size(); i++) {
                    String cell1 = row1.get(sortKeys.get(i));
                    String cell2 = row2.get(sortKeys.get(i));
                    int rc = sortComparators.get(i).compare(cell1, cell2);
                    if (rc != 0) {
                        return rc;
                    }
                }
                // Both rows are equal.
                return 0;
            }
        };
        Collections.sort(sortedRows, comparator);
        // Now output the table.
        serializer.startTable(height, width);
        for (int i = 0; i < width; i++) {
            serializer.addColumn(columnWidths.get(i));
        }
        // Column headings.
        serializer.startHeader();
        for (LocalizableMessage s : header) {
            serializer.addHeading(s.toString());
        }
        serializer.endHeader();
        // Table contents.
        serializer.startContent();
        for (List<String> row : sortedRows) {
            serializer.startRow();
            // Print each cell in the row, padding missing trailing cells.
            for (int i = 0; i < width; i++) {
                if (i < row.size()) {
                    serializer.addCell(row.get(i));
                } else {
                    serializer.addCell("");
                }
            }
            serializer.endRow();
        }
        serializer.endContent();
        serializer.endTable();
    }
    /**
     * Appends a new row to the table.
     */
    public void startRow() {
        rows.add(new ArrayList<String>());
        height++;
        column = 0;
    }
}
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TablePrinter.java
New file
@@ -0,0 +1,50 @@
/*
 * 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 2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014 ForgeRock AS
 */
package com.forgerock.opendj.cli;
/**
 * An interface for incrementally configuring a table serializer. Once
 * configured, the table printer can be used to create a new
 * {@link TableSerializer} instance using the {@link #getSerializer()}
 * method.
 */
public abstract class TablePrinter {
    /**
     * Creates a new abstract table printer.
     */
    protected TablePrinter() {
        // No implementation required.
    }
    /**
     * Creates a new table serializer based on the configuration of this table printer.
     *
     * @return Returns a new table serializer based on the configuration of this table printer.
     */
    protected abstract TableSerializer getSerializer();
}
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableSerializer.java
New file
@@ -0,0 +1,134 @@
/*
 * 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 2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014 ForgeRock AS
 */
package com.forgerock.opendj.cli;
/**
 * An interface for serializing tables.
 * <p>
 * The default implementation for each method is to do nothing.
 * Implementations must override methods as required.
 */
public abstract class TableSerializer {
    /**
     * Create a new table serializer.
     */
    protected TableSerializer() {
        // No implementation required.
    }
    /**
     * Prints a table cell.
     *
     * @param s
     *            The cell contents.
     */
    public void addCell(String s) {
        // Default implementation.
    }
    /**
     * Defines a column in the table.
     *
     * @param width
     *            The width of the column in characters.
     */
    public void addColumn(int width) {
        // Default implementation.
    }
    /**
     * Prints a column heading.
     *
     * @param s
     *            The column heading.
     */
    public void addHeading(String s) {
        // Default implementation.
    }
    /**
     * Finish printing the table contents.
     */
    public void endContent() {
        // Default implementation.
    }
    /**
     * Finish printing the column headings.
     */
    public void endHeader() {
        // Default implementation.
    }
    /**
     * Finish printing the current row of the table.
     */
    public void endRow() {
        // Default implementation.
    }
    /**
     * Finish printing the table.
     */
    public void endTable() {
        // Default implementation.
    }
    /**
     * Prepare to start printing the table contents.
     */
    public void startContent() {
        // Default implementation.
    }
    /**
     * Prepare to start printing the column headings.
     */
    public void startHeader() {
        // Default implementation.
    }
    /**
     * Prepare to start printing a new row of the table.
     */
    public void startRow() {
        // Default implementation.
    }
    /**
     * Start a new table having the specified number of rows and columns.
     *
     * @param height
     *            The number of rows in the table.
     * @param width
     *            The number of columns in the table.
     */
    public void startTable(int height, int width) {
        // Default implementation.
    }
}
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TextTablePrinter.java
New file
@@ -0,0 +1,497 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2007-2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014 ForgeRock AS
 */
package com.forgerock.opendj.cli;
import static com.forgerock.opendj.cli.Utils.MAX_LINE_WIDTH;
import java.io.BufferedWriter;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * An interface for creating a text based table.
 * Tables have configurable column widths, padding, and column separators.
 */
public final class TextTablePrinter extends TablePrinter {
    /**
     * Table serializer implementation.
     */
    private final class Serializer extends TableSerializer {
        // The current column being output.
        private int column = 0;
        /*The real column widths taking into account size constraints but
         not including padding or separators.*/
        private final List<Integer> columnWidths = new ArrayList<Integer>();
        // The cells in the current row.
        private final List<String> currentRow = new ArrayList<String>();
        // Width of the table in columns.
        private int totalColumns = 0;
        // The padding to use for indenting the table.
        private final String indentPadding;
        // Private constructor.
        private Serializer() {
            // Compute the indentation padding.
            final StringBuilder builder = new StringBuilder();
            for (int i = 0; i < indentWidth; i++) {
                builder.append(' ');
            }
            this.indentPadding = builder.toString();
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void addCell(String s) {
            currentRow.add(s);
            column++;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void addColumn(int width) {
            columnWidths.add(width);
            totalColumns++;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void addHeading(String s) {
            if (displayHeadings) {
                addCell(s);
            }
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void endHeader() {
            if (displayHeadings) {
                endRow();
                // Print the header separator.
                final StringBuilder builder = new StringBuilder(indentPadding);
                for (int i = 0; i < totalColumns; i++) {
                    int width = columnWidths.get(i);
                    if (totalColumns > 1) {
                        if (i == 0 || i == (totalColumns - 1)) {
                            // Only one lot of padding for first and last columns.
                            width += padding;
                        } else {
                            width += padding * 2;
                        }
                    }
                    for (int j = 0; j < width; j++) {
                        if (headingSeparatorStartColumn > 0) {
                            if (i < headingSeparatorStartColumn) {
                                builder.append(' ');
                            } else if (i == headingSeparatorStartColumn && j < padding) {
                                builder.append(' ');
                            } else {
                                builder.append(headingSeparator);
                            }
                        } else {
                            builder.append(headingSeparator);
                        }
                    }
                    if ((i >= headingSeparatorStartColumn) && i < (totalColumns - 1)) {
                        builder.append(columnSeparator);
                    }
                }
                writer.println(builder.toString());
            }
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void endRow() {
            boolean isRemainingText;
            do {
                StringBuilder builder = new StringBuilder(indentPadding);
                isRemainingText = false;
                for (int i = 0; i < currentRow.size(); i++) {
                    int width = columnWidths.get(i);
                    String contents = currentRow.get(i);
                    // Determine what parts of contents can be displayed on this line.
                    String head;
                    String tail = null;
                    if (contents == null) {
                        // This cell has been displayed fully.
                        head = "";
                    } else if (contents.length() > width) {
                        // We're going to have to split the cell on next word boundary.
                        int endIndex = contents.lastIndexOf(' ', width);
                        if (endIndex == -1) {
                            endIndex = width;
                            head = contents.substring(0, endIndex);
                            tail = contents.substring(endIndex);
                        } else {
                            head = contents.substring(0, endIndex);
                            tail = contents.substring(endIndex + 1);
                        }
                    } else {
                        // The contents fits ok.
                        head = contents;
                    }
                    // Add this cell's contents to the current line.
                    if (i > 0) {
                        // Add right padding for previous cell.
                        for (int j = 0; j < padding; j++) {
                            builder.append(' ');
                        }
                        // Add separator.
                        builder.append(columnSeparator);
                        // Add left padding for this cell.
                        for (int j = 0; j < padding; j++) {
                            builder.append(' ');
                        }
                    }
                    // Add cell contents.
                    builder.append(head);
                    // Now pad with extra space to make up the width.
                    // Only if it's not the last cell (see issue #3210)
                    if (i != currentRow.size() - 1) {
                        for (int j = head.length(); j < width; j++) {
                            builder.append(' ');
                        }
                    }
                    // Update the row contents.
                    currentRow.set(i, tail);
                    if (tail != null) {
                        isRemainingText = true;
                    }
                }
                // Output the line.
                writer.println(builder.toString());
            } while (isRemainingText);
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void endTable() {
            writer.flush();
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void startHeader() {
            determineColumnWidths();
            column = 0;
            currentRow.clear();
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public void startRow() {
            column = 0;
            currentRow.clear();
        }
        // We need to calculate the effective width of each column.
        private void determineColumnWidths() {
            // First calculate the minimum width so that we know how much
            // expandable columns can expand.
            int minWidth = indentWidth;
            int expandableColumnSize = 0;
            for (int i = 0; i < totalColumns; i++) {
                int actualSize = columnWidths.get(i);
                if (fixedColumns.containsKey(i)) {
                    int requestedSize = fixedColumns.get(i);
                    if (requestedSize == 0) {
                        expandableColumnSize += actualSize;
                    } else {
                        columnWidths.set(i, requestedSize);
                        minWidth += requestedSize;
                    }
                } else {
                    minWidth += actualSize;
                }
                // Must also include padding and separators.
                if (i > 0) {
                    minWidth += padding * 2 + columnSeparator.length();
                }
            }
            if (minWidth > totalWidth) {
                // The table is too big: leave expandable columns at their
                // requested width, as there's not much else that can be done.
            } else {
                int available = totalWidth - minWidth;
                if (expandableColumnSize > available) {
                    // Only modify column sizes if necessary.
                    for (int i = 0; i < totalColumns; i++) {
                        int actualSize = columnWidths.get(i);
                        if (fixedColumns.containsKey(i)) {
                            int requestedSize = fixedColumns.get(i);
                            if (requestedSize == 0) {
                                // Calculate size based on requested actual size as a
                                // proportion of the total.
                                requestedSize = ((actualSize * available) / expandableColumnSize);
                                columnWidths.set(i, requestedSize);
                            }
                        }
                    }
                }
            }
        }
    }
    /**
     * The default string which should be used to separate one column from the next (not including padding).
     */
    private static final String DEFAULT_COLUMN_SEPARATOR = "";
    /**
     * The default character which should be used to separate the table heading row from the rows beneath.
     */
    private static final char DEFAULT_HEADING_SEPARATOR = '-';
    /**
     * The default padding which will be used to separate a cell's contents from its adjacent column separators.
     */
    private static final int DEFAULT_PADDING = 1;
    // The string which should be used to separate one column
    // from the next (not including padding).
    private String columnSeparator = DEFAULT_COLUMN_SEPARATOR;
    // Indicates whether or not the headings should be output.
    private boolean displayHeadings = true;
    // Table indicating whether or not a column is fixed width.
    private final Map<Integer, Integer> fixedColumns = new HashMap<Integer, Integer>();
    // The number of characters the table should be indented.
    private int indentWidth = 0;
    // The character which should be used to separate the table
    // heading row from the rows beneath.
    private char headingSeparator = DEFAULT_HEADING_SEPARATOR;
    // The column where the heading separator should begin.
    private int headingSeparatorStartColumn = 0;
    // The padding which will be used to separate a cell's
    // contents from its adjacent column separators.
    private int padding = DEFAULT_PADDING;
    // Total permitted width for the table which expandable columns
    // can use up.
    private int totalWidth = MAX_LINE_WIDTH;
    // The output destination.
    private PrintWriter writer = null;
    /**
     * Creates a new text table printer for the specified output stream. The text table printer will have the following
     * initial settings:
     * <ul>
     * <li>headings will be displayed
     * <li>no separators between columns
     * <li>columns are padded by one character
     * </ul>
     *
     * @param stream
     *            The stream to output tables to.
     */
    public TextTablePrinter(OutputStream stream) {
        this(new BufferedWriter(new OutputStreamWriter(stream)));
    }
    /**
     * Creates a new text table printer for the specified writer. The text table printer will have the following initial
     * settings:
     * <ul>
     * <li>headings will be displayed
     * <li>no separators between columns
     * <li>columns are padded by one character
     * </ul>
     *
     * @param writer
     *            The writer to output tables to.
     */
    public TextTablePrinter(Writer writer) {
        this.writer = new PrintWriter(writer);
    }
    /**
     * Sets the column separator which should be used to separate one column from the next (not including padding).
     *
     * @param columnSeparator
     *            The column separator.
     */
    public void setColumnSeparator(String columnSeparator) {
        this.columnSeparator = columnSeparator;
    }
    /**
     * Set the maximum width for a column. If a cell is too big to fit in its column then it will be wrapped.
     *
     * @param column
     *            The column to make fixed width (0 is the first column).
     * @param width
     *            The width of the column (this should not include column separators or padding), or <code>0</code> to
     *            indicate that this column should be expandable.
     * @throws IllegalArgumentException
     *             If column is less than 0.
     */
    public void setColumnWidth(int column, int width) {
        if (column < 0) {
            throw new IllegalArgumentException("Negative column " + column);
        }
        if (width < 0) {
            throw new IllegalArgumentException("Negative width " + width);
        }
        fixedColumns.put(column, width);
    }
    /**
     * Specify whether the column headings should be displayed or not.
     *
     * @param displayHeadings
     *            <code>true</code> if column headings should be displayed.
     */
    public void setDisplayHeadings(boolean displayHeadings) {
        this.displayHeadings = displayHeadings;
    }
    /**
     * Sets the heading separator which should be used to separate the table heading row from the rows beneath.
     *
     * @param headingSeparator
     *            The heading separator.
     */
    public void setHeadingSeparator(char headingSeparator) {
        this.headingSeparator = headingSeparator;
    }
    /**
     * Sets the heading separator start column. The heading separator will only be display in the specified column and
     * all subsequent columns. Usually this should be left at zero (the default) but sometimes it useful to indent the
     * heading separate in order to provide additional emphasis (for example in menus).
     *
     * @param startColumn
     *            The heading separator start column.
     */
    public void setHeadingSeparatorStartColumn(int startColumn) {
        if (startColumn < 0) {
            throw new IllegalArgumentException("Negative start column " + startColumn);
        }
        this.headingSeparatorStartColumn = startColumn;
    }
    /**
     * Sets the amount of characters that the table should be indented. By default the table is not indented.
     *
     * @param indentWidth
     *            The number of characters the table should be indented.
     * @throws IllegalArgumentException
     *             If indentWidth is less than 0.
     */
    public void setIndentWidth(int indentWidth) {
        if (indentWidth < 0) {
            throw new IllegalArgumentException("Negative indentation width " + indentWidth);
        }
        this.indentWidth = indentWidth;
    }
    /**
     * Sets the padding which will be used to separate a cell's contents from its adjacent column separators.
     *
     * @param padding
     *            The padding.
     */
    public void setPadding(int padding) {
        this.padding = padding;
    }
    /**
     * Sets the total permitted width for the table which expandable columns can use up.
     *
     * @param totalWidth
     *            The total width.
     */
    public void setTotalWidth(int totalWidth) {
        this.totalWidth = totalWidth;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    protected TableSerializer getSerializer() {
        return new Serializer();
    }
}
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java
@@ -26,7 +26,12 @@
 */
package com.forgerock.opendj.cli;
import static com.forgerock.opendj.cli.CliMessages.*;
import static com.forgerock.opendj.cli.CliMessages.ERR_INCOMPATIBLE_JAVA_VERSION;
import static com.forgerock.opendj.cli.CliMessages.INFO_TIME_IN_DAYS_HOURS_MINUTES_SECONDS;
import static com.forgerock.opendj.cli.CliMessages.INFO_TIME_IN_HOURS_MINUTES_SECONDS;
import static com.forgerock.opendj.cli.CliMessages.INFO_TIME_IN_MINUTES_SECONDS;
import static com.forgerock.opendj.cli.CliMessages.INFO_TIME_IN_SECONDS;
import com.forgerock.opendj.util.OperatingSystem;
import static com.forgerock.opendj.util.StaticUtils.EOL;
import java.io.File;
@@ -34,7 +39,10 @@
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.StringTokenizer;
import java.util.TimeZone;
import org.forgerock.i18n.LocalizableMessage;
@@ -42,10 +50,32 @@
 * 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 value used to display arguments that must be obfuscated (such as passwords). This does not require
     * localization (since the output of command builder by its nature is not localized).
     */
    public final static String OBFUSCATED_VALUE = "******";
    /**
     * The date format string that will be used to construct and parse dates represented using generalized time. It is
     * assumed that the provided date formatter will be set to UTC.
     */
    public static final String DATE_FORMAT_LOCAL_TIME = "dd/MMM/yyyy:HH:mm:ss Z";
    private static final String COMMENT_SHELL_UNIX = "# ";
    private static final String COMMENT_BATCH_WINDOWS = "rem ";
    /**
     * The String used to write comments in a shell (or batch) script.
     */
    public static final String SHELL_COMMENT_SEPARATOR = OperatingSystem.isWindows() ? COMMENT_BATCH_WINDOWS
            : COMMENT_SHELL_UNIX;
    /**
     * The column at which to wrap long lines of output in the command-line
     * tools.
     */
@@ -65,6 +95,22 @@
    }
    /**
     * Formats a Date to String representation in "dd/MMM/yyyy:HH:mm:ss Z".
     *
     * @param date
     *            to format; null if <code>date</code> is null
     * @return string representation of the date
     */
    public String formatDateTimeStringForEquivalentCommand(Date date) {
        String timeStr = null;
        if (date != null) {
            SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_LOCAL_TIME);
            dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
            timeStr = dateFormat.format(date);
        }
        return timeStr;
    }
    /**
     * Filters the provided value to ensure that it is appropriate for use as an
     * exit code. Exit code values are generally only allowed to be between 0
     * and 255, so any value outside of this range will be converted to 255,
opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ValidationCallback.java
New file
@@ -0,0 +1,53 @@
/*
 * 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 2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014 ForgeRock AS
 */
package com.forgerock.opendj.cli;
/**
 * An interface for validating user input.
 *
 * @param <T>
 *            The type of the decoded input.
 */
public interface ValidationCallback<T> {
    /**
     * Validates and decodes the user-provided input. Implementations must validate
     * <code>input</code> and return the decoded value if the input is acceptable.
     * If the input is unacceptable, implementations must return
     * <code>null</code> and output a user friendly error message to the provided
     * application console.
     *
     * @param app
     *            The console application.
     * @param input
     *            The user input to be validated.
     * @return Returns the decoded input if the input is valid, or <code>null</code> if it is not.
     * @throws ClientException
     *             If an unexpected error occurred which prevented validation.
     */
    T validate(ConsoleApplication app, String input) throws ClientException;
}
opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties
@@ -715,3 +715,24 @@
INFO_DESCRIPTION_CONNECTION_TIMEOUT=Maximum length of time (in \
 milliseconds) that can be taken to establish a connection.  Use '0' to \
 specify no time out
ERR_MENU_BAD_CHOICE_MULTI=Invalid response. Please enter one or \
more valid menu options
ERR_MENU_BAD_CHOICE_SINGLE=Invalid response. Please enter a valid \
menu option
ERR_MENU_BAD_CHOICE_MULTI_DUPE=The option "%s" was specified \
more than once. Please enter one or more valid menu options
INFO_MENU_PROMPT_MULTI_DEFAULT=Enter one or more choices separated by commas [%s]:
INFO_MENU_PROMPT_MULTI=Enter one or more choices separated by commas:
INFO_MENU_PROMPT_SINGLE_DEFAULT=Enter choice [%s]:
INFO_MENU_PROMPT_SINGLE=Enter choice:
INFO_MENU_OPTION_HELP=help
INFO_MENU_OPTION_HELP_KEY=?
INFO_MENU_OPTION_CANCEL=cancel
INFO_MENU_OPTION_CANCEL_KEY=c
INFO_MENU_OPTION_QUIT=quit
INFO_MENU_OPTION_QUIT_KEY=q
INFO_MENU_NUMERIC_OPTION=%d)
INFO_MENU_CHAR_OPTION=%c)
INFO_MENU_OPTION_BACK=back
INFO_MENU_OPTION_BACK_KEY=b
ERR_TRIES_LIMIT_REACHED=Input tries limit reached (%d)
opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/ConsoleApplicationTestCase.java
@@ -41,6 +41,10 @@
 */
public class ConsoleApplicationTestCase extends CliTestCase {
    final LocalizableMessage msg = LocalizableMessage.raw("Language is the source of misunderstandings.");
    final LocalizableMessage msg2 = LocalizableMessage
            .raw("If somebody wants a sheep, that is a proof that one exists.");
    /**
     * For test purposes only.
     */
@@ -49,6 +53,7 @@
        private static ByteArrayOutputStream err;
        private boolean verbose = false;
        private boolean interactive = false;
        private boolean quiet = false;
        private MockConsoleApplication(PrintStream out, PrintStream err) {
            super(out, err);
@@ -82,6 +87,12 @@
            return interactive;
        }
        /** {@inheritDoc} */
        @Override
        public boolean isQuiet() {
            return quiet;
        }
        public void setVerbose(boolean v) {
            verbose = v;
        }
@@ -89,12 +100,14 @@
        public void setInteractive(boolean inter) {
            interactive = inter;
        }
        public void setQuiet(boolean q) {
            quiet = q;
        }
    }
    @Test()
    public void testWriteLineInOutputStream() throws UnsupportedEncodingException {
        final LocalizableMessage msg = LocalizableMessage
                .raw("If somebody wants a sheep, that is a proof that one exists.");
        final MockConsoleApplication ca = MockConsoleApplication.getDefault();
        ca.print(msg);
        assertThat(ca.getOut()).contains(msg.toString());
@@ -103,7 +116,6 @@
    @Test()
    public void testWriteLineInErrorStream() throws UnsupportedEncodingException {
        final LocalizableMessage msg = LocalizableMessage.raw("Language is the source of misunderstandings.");
        final MockConsoleApplication ca = MockConsoleApplication.getDefault();
        ca.errPrintln(msg);
        assertThat(ca.getOut()).isEmpty();
@@ -112,8 +124,6 @@
    @Test()
    public void testWriteOutputStreamVerbose() throws UnsupportedEncodingException {
        final LocalizableMessage msg = LocalizableMessage
                .raw("If somebody wants a sheep, that is a proof that one exists.");
        final MockConsoleApplication ca = MockConsoleApplication.getDefault();
        ca.printVerboseMessage(msg);
        assertThat(ca.isVerbose()).isFalse();
@@ -128,7 +138,6 @@
    @Test()
    public void testWriteErrorStreamVerbose() throws UnsupportedEncodingException {
        final LocalizableMessage msg = LocalizableMessage.raw("Language is the source of misunderstandings.");
        final MockConsoleApplication ca = MockConsoleApplication.getDefault();
        ca.errPrintVerboseMessage(msg);
        assertThat(ca.isVerbose()).isFalse();
@@ -149,9 +158,6 @@
     */
    @Test()
    public void testNonInteractiveApplicationShouldNotStdoutErrors() throws UnsupportedEncodingException {
        final LocalizableMessage msg = LocalizableMessage.raw("Language is the source of misunderstandings.");
        final LocalizableMessage msg2 = LocalizableMessage
                .raw("If somebody wants a sheep, that is a proof that one exists.");
        final MockConsoleApplication ca = MockConsoleApplication.getDefault();
        assertFalse(ca.isInteractive());
@@ -170,10 +176,6 @@
     */
    @Test()
    public void testInteractiveApplicationShouldStdoutErrors() throws UnsupportedEncodingException {
        final LocalizableMessage msg = LocalizableMessage.raw("Language is the source of misunderstandings.");
        final LocalizableMessage msg2 = LocalizableMessage
                .raw("If somebody wants a sheep, that is a proof that one exists.");
        final MockConsoleApplication ca = MockConsoleApplication.getDefault();
        assertFalse(ca.isInteractive());
@@ -186,4 +188,20 @@
        assertThat(ca.getOut()).contains(msg2.toString());
        assertThat(ca.getErr()).isEmpty();
    }
    /**
     * In quiet mode, only the stderr should contain lines.
     * @throws UnsupportedEncodingException
     */
    @Test()
    public void testQuietMode() throws UnsupportedEncodingException {
        final MockConsoleApplication ca = MockConsoleApplication.getDefault();
        ca.setQuiet(true);
        assertTrue(ca.isQuiet());
        ca.println(msg);
        ca.errPrintln(msg2);
        assertThat(ca.getOut()).isEmpty();
        assertThat(ca.getErr()).contains(msg2.toString());
        assertThat(ca.getErr()).doesNotContain(msg.toString());
    }
}