/*
|
* 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;
|
}
|
}
|