/*
* 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.
*/
package org.opends.server.util.cli;
import static org.opends.messages.UtilityMessages.*;
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.opends.messages.Message;
import org.opends.server.util.table.TableBuilder;
import org.opends.server.util.table.TablePrinter;
import org.opends.server.util.table.TextTablePrinter;
/**
* 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 CLIException {
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 Message defaultDescription;
// The numeric options table builder.
private final TableBuilder nbuilder;
// The table printer.
private final TablePrinter printer;
// The menu prompt.
private final Message prompt;
// The menu title.
private final Message 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, Message title, Message prompt,
TableBuilder ntable, TableBuilder ctable, TablePrinter printer,
Map<String, MenuCallback<T>> callbacks, boolean allowMultiSelect,
MenuCallback<T> defaultCallback, Message 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 CLIException {
// 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.
Message 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 CLIException {
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<Message> charKeys = new ArrayList<Message>();
// The synopsis of char options.
private final List<Message> charSynopsis = new ArrayList<Message>();
// Optional column headings.
private final List<Message> columnHeadings = new ArrayList<Message>();
// 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 Message 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<Message>> numericFields =
new ArrayList<List<Message>>();
// The menu title.
private Message title = null;
// The menu prompt.
private Message 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(Message c, Message 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(Message c, Message 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 CLIException {
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(Message description, MenuCallback<T> callback,
Message... extraFields) {
List<Message> fields = new ArrayList<Message>();
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(Message description, MenuResult<T> result,
Message... 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(Message... 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(Message 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(Message 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(Message 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(Message 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 (Message heading : columnHeadings) {
if (heading != null) {
nbuilder.appendHeading(heading);
} else {
nbuilder.appendHeading();
}
}
if (useMultipleColumns) {
nbuilder.appendHeading();
for (Message 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 (Message 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 (Message 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);
Message 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 CLIException will be thrown.
* @param nTries the maximum number of tries.
*/
public void setMaxTries(int nTries)
{
nMaxTries = nTries;
}
}