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 = " "; } 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(); } } 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()); } }