/* * 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 * The type of value returned by the call-backs. Use Void if the call-backs do not return a * value. */ public final class MenuBuilder { /** * A simple menu option call-back which is a composite of zero or more underlying call-backs. * * @param * The type of value returned by the call-back. */ private static final class CompositeCallback implements MenuCallback { // The list of underlying call-backs. private final Collection> callbacks; /** * Creates a new composite call-back with the specified set of call-backs. * * @param callbacks * The set of call-backs. */ public CompositeCallback(Collection> callbacks) { this.callbacks = callbacks; } /** {@inheritDoc} */ public MenuResult invoke(ConsoleApplication app) throws ClientException { List values = new ArrayList(); for (MenuCallback callback : callbacks) { MenuResult 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 * The type of value returned by the call-backs. Use Void if the call-backs do not return a * value. */ private static final class MenuImpl implements Menu { // 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> callbacks; // The char options table builder. private final TableBuilder cbuilder; // The call-back for the optional default action. private final MenuCallback 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> callbacks, boolean allowMultiSelect, MenuCallback 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 run() throws ClientException { // The validation call-back which will be used to determine the // action call-back. ValidationCallback> validator = new ValidationCallback>() { public MenuCallback 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> cl = new ArrayList>(); for (String value : ninput.split(",")) { // Make sure that there are no duplicates. String nvalue = value.trim(); Set choices = new HashSet(); 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(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 choice; if (nMaxTries != -1) { choice = app.readValidatedInput(promptMsg, validator, nMaxTries); } else { choice = app.readValidatedInput(promptMsg, validator); } // Invoke the user's selected choice. MenuResult 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 * The type of result returned by the call-back. */ private static final class ResultCallback implements MenuCallback { // The result to be returned by this call-back. private final MenuResult result; // Private constructor. private ResultCallback(MenuResult result) { this.result = result; } /** {@inheritDoc} */ public MenuResult 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> charCallbacks = new ArrayList>(); // The char option keys (must be single-character messages). private final List charKeys = new ArrayList(); // The synopsis of char options. private final List charSynopsis = new ArrayList(); // Optional column headings. private final List columnHeadings = new ArrayList(); // Optional column widths. private final List columnWidths = new ArrayList(); // The call-back for the optional default action. private MenuCallback defaultCallback = null; // The description of the optional default action. private LocalizableMessage defaultDescription = null; // The numeric option call-backs. private final List> numericCallbacks = new ArrayList>(); // The numeric option fields. private final List> numericFields = new ArrayList>(); // 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. cancel()); if (isDefault) { setDefault(INFO_MENU_OPTION_BACK_KEY.get(), MenuResult. 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. cancel()); if (isDefault) { setDefault(INFO_MENU_OPTION_CANCEL_KEY.get(), MenuResult. 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 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 result) { addCharOption(c, description, new ResultCallback(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 wrapper = new MenuCallback() { public MenuResult 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 callback, LocalizableMessage... extraFields) { List fields = new ArrayList(); 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 result, LocalizableMessage... extraFields) { return addNumberedOption(description, new ResultCallback(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. 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 false. * * @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 * null 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 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 result) { setDefault(description, new ResultCallback(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 null 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 null 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 toMenu() { TableBuilder nbuilder = new TableBuilder(); Map> callbacks = new HashMap>(); // 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(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; } }