From 4bbf391109a162bbe313564bae130bbab359989e Mon Sep 17 00:00:00 2001
From: Violette Roche-Montane <violette.roche-montane@forgerock.com>
Date: Wed, 12 Feb 2014 16:11:06 +0000
Subject: [PATCH] Checkpoint commit for OPENDJ-1343 Migrate dsconfig / OPENDJ-1303 "opendj-cli" - added classes needeed by DSConfig.
---
opendj-cli/src/main/java/com/forgerock/opendj/cli/TableBuilder.java | 328 +++++++
opendj-cli/src/main/java/com/forgerock/opendj/cli/ValidationCallback.java | 53 +
opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuResult.java | 261 +++++
opendj-cli/src/main/java/com/forgerock/opendj/cli/Menu.java | 48 +
opendj-cli/src/main/java/com/forgerock/opendj/cli/ConsoleApplication.java | 84 +
opendj-cli/src/test/java/com/forgerock/opendj/cli/ConsoleApplicationTestCase.java | 44
opendj-cli/src/main/java/com/forgerock/opendj/cli/TablePrinter.java | 50 +
opendj-cli/src/main/java/com/forgerock/opendj/cli/TextTablePrinter.java | 497 +++++++++++
opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java | 48 +
opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuBuilder.java | 732 ++++++++++++++++
opendj-cli/src/main/java/com/forgerock/opendj/cli/CommandBuilder.java | 272 ++++++
opendj-cli/src/main/java/com/forgerock/opendj/cli/HelpCallback.java | 41
opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuCallback.java | 51 +
opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java | 5
opendj-cli/src/main/java/com/forgerock/opendj/cli/TableSerializer.java | 134 ++
opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties | 21
16 files changed, 2,648 insertions(+), 21 deletions(-)
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommandBuilder.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommandBuilder.java
new file mode 100644
index 0000000..a4da3f1
--- /dev/null
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommandBuilder.java
@@ -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 = " ";
+ } else {
+ HTML_LINE_SEPARATOR = " \\<br> ";
+ }
+ }
+
+ /**
+ * 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();
+ }
+}
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java
index e57d835..f400649 100644
--- a/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java
@@ -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;
+
}
/**
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConsoleApplication.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConsoleApplication.java
index 3811989..a17ca6f 100755
--- a/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConsoleApplication.java
+++ b/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
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/HelpCallback.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/HelpCallback.java
new file mode 100644
index 0000000..05659ce
--- /dev/null
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/HelpCallback.java
@@ -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);
+}
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/Menu.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/Menu.java
new file mode 100644
index 0000000..abd62b2
--- /dev/null
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/Menu.java
@@ -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;
+}
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuBuilder.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuBuilder.java
new file mode 100644
index 0000000..5da49e6
--- /dev/null
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuBuilder.java
@@ -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;
+ }
+}
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuCallback.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuCallback.java
new file mode 100644
index 0000000..a7bc29d
--- /dev/null
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuCallback.java
@@ -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;
+}
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuResult.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuResult.java
new file mode 100644
index 0000000..433b634
--- /dev/null
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuResult.java
@@ -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;
+ }
+}
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableBuilder.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableBuilder.java
new file mode 100644
index 0000000..06692c7
--- /dev/null
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableBuilder.java
@@ -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;
+ }
+}
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/TablePrinter.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/TablePrinter.java
new file mode 100644
index 0000000..b54cd4c
--- /dev/null
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/TablePrinter.java
@@ -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();
+}
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableSerializer.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableSerializer.java
new file mode 100644
index 0000000..3846725
--- /dev/null
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableSerializer.java
@@ -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.
+ }
+}
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/TextTablePrinter.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/TextTablePrinter.java
new file mode 100644
index 0000000..ca8906c
--- /dev/null
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/TextTablePrinter.java
@@ -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();
+ }
+}
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java
index 0c48ba0..0ee1a38 100644
--- a/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java
@@ -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,
diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/ValidationCallback.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/ValidationCallback.java
new file mode 100644
index 0000000..da88811
--- /dev/null
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/ValidationCallback.java
@@ -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;
+}
diff --git a/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties b/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties
index 9656f31..7fe48b8 100755
--- a/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties
+++ b/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties
@@ -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)
diff --git a/opendj-cli/src/test/java/com/forgerock/opendj/cli/ConsoleApplicationTestCase.java b/opendj-cli/src/test/java/com/forgerock/opendj/cli/ConsoleApplicationTestCase.java
index f0aa277..4b291a6 100644
--- a/opendj-cli/src/test/java/com/forgerock/opendj/cli/ConsoleApplicationTestCase.java
+++ b/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());
+ }
}
--
Gitblit v1.10.0