001 /* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at 010 * trunk/opends/resource/legal-notices/OpenDS.LICENSE 011 * or https://OpenDS.dev.java.net/OpenDS.LICENSE. 012 * See the License for the specific language governing permissions 013 * and limitations under the License. 014 * 015 * When distributing Covered Code, include this CDDL HEADER in each 016 * file and include the License file at 017 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, 018 * add the following below this CDDL HEADER, with the fields enclosed 019 * by brackets "[]" replaced with your own identifying information: 020 * Portions Copyright [yyyy] [name of copyright owner] 021 * 022 * CDDL HEADER END 023 * 024 * 025 * Copyright 2007-2008 Sun Microsystems, Inc. 026 */ 027 package org.opends.server.util.cli; 028 029 030 031 import static org.opends.messages.UtilityMessages.*; 032 033 import java.util.ArrayList; 034 import java.util.Arrays; 035 import java.util.Collection; 036 import java.util.HashMap; 037 import java.util.HashSet; 038 import java.util.List; 039 import java.util.Map; 040 import java.util.Set; 041 042 import org.opends.messages.Message; 043 import org.opends.server.util.table.TableBuilder; 044 import org.opends.server.util.table.TablePrinter; 045 import org.opends.server.util.table.TextTablePrinter; 046 047 048 049 /** 050 * An interface for incrementally building a command-line menu. 051 * 052 * @param <T> 053 * The type of value returned by the call-backs. Use 054 * <code>Void</code> if the call-backs do not return a 055 * value. 056 */ 057 public final class MenuBuilder<T> { 058 059 /** 060 * A simple menu option call-back which is a composite of zero or 061 * more underlying call-backs. 062 * 063 * @param <T> 064 * The type of value returned by the call-back. 065 */ 066 private static final class CompositeCallback<T> implements MenuCallback<T> { 067 068 // The list of underlying call-backs. 069 private final Collection<MenuCallback<T>> callbacks; 070 071 072 073 /** 074 * Creates a new composite call-back with the specified set of 075 * call-backs. 076 * 077 * @param callbacks 078 * The set of call-backs. 079 */ 080 public CompositeCallback(Collection<MenuCallback<T>> callbacks) { 081 this.callbacks = callbacks; 082 } 083 084 085 086 /** 087 * {@inheritDoc} 088 */ 089 public MenuResult<T> invoke(ConsoleApplication app) throws CLIException { 090 List<T> values = new ArrayList<T>(); 091 for (MenuCallback<T> callback : callbacks) { 092 MenuResult<T> result = callback.invoke(app); 093 094 if (!result.isSuccess()) { 095 // Throw away all the other results. 096 return result; 097 } else { 098 values.addAll(result.getValues()); 099 } 100 } 101 return MenuResult.success(values); 102 } 103 } 104 105 106 107 /** 108 * Underlying menu implementation generated by this menu builder. 109 * 110 * @param <T> 111 * The type of value returned by the call-backs. Use 112 * <code>Void</code> if the call-backs do not return a 113 * value. 114 */ 115 private static final class MenuImpl<T> implements Menu<T> { 116 117 // Indicates whether the menu will allow selection of multiple 118 // numeric options. 119 private final boolean allowMultiSelect; 120 121 // The application console. 122 private final ConsoleApplication app; 123 124 // The call-back lookup table. 125 private final Map<String, MenuCallback<T>> callbacks; 126 127 // The char options table builder. 128 private final TableBuilder cbuilder; 129 130 // The call-back for the optional default action. 131 private final MenuCallback<T> defaultCallback; 132 133 // The description of the optional default action. 134 private final Message defaultDescription; 135 136 // The numeric options table builder. 137 private final TableBuilder nbuilder; 138 139 // The table printer. 140 private final TablePrinter printer; 141 142 // The menu prompt. 143 private final Message prompt; 144 145 // The menu title. 146 private final Message title; 147 148 // The maximum number of times we display the menu if the user provides 149 // bad input (-1 for unlimited). 150 private int nMaxTries; 151 152 // Private constructor. 153 private MenuImpl(ConsoleApplication app, Message title, Message prompt, 154 TableBuilder ntable, TableBuilder ctable, TablePrinter printer, 155 Map<String, MenuCallback<T>> callbacks, boolean allowMultiSelect, 156 MenuCallback<T> defaultCallback, Message defaultDescription, 157 int nMaxTries) { 158 this.app = app; 159 this.title = title; 160 this.prompt = prompt; 161 this.nbuilder = ntable; 162 this.cbuilder = ctable; 163 this.printer = printer; 164 this.callbacks = callbacks; 165 this.allowMultiSelect = allowMultiSelect; 166 this.defaultCallback = defaultCallback; 167 this.defaultDescription = defaultDescription; 168 this.nMaxTries = nMaxTries; 169 } 170 171 172 173 /** 174 * {@inheritDoc} 175 */ 176 public MenuResult<T> run() throws CLIException { 177 // The validation call-back which will be used to determine the 178 // action call-back. 179 ValidationCallback<MenuCallback<T>> validator = 180 new ValidationCallback<MenuCallback<T>>() { 181 182 public MenuCallback<T> validate(ConsoleApplication app, String input) { 183 String ninput = input.trim(); 184 185 if (ninput.length() == 0) { 186 if (defaultCallback != null) { 187 return defaultCallback; 188 } else if (allowMultiSelect) { 189 app.println(); 190 app.println(ERR_MENU_BAD_CHOICE_MULTI.get()); 191 app.println(); 192 return null; 193 } else { 194 app.println(); 195 app.println(ERR_MENU_BAD_CHOICE_SINGLE.get()); 196 app.println(); 197 return null; 198 } 199 } else if (allowMultiSelect) { 200 // Use a composite call-back to collect all the results. 201 List<MenuCallback<T>> cl = new ArrayList<MenuCallback<T>>(); 202 for (String value : ninput.split(",")) { 203 // Make sure that there are no duplicates. 204 String nvalue = value.trim(); 205 Set<String> choices = new HashSet<String>(); 206 207 if (choices.contains(nvalue)) { 208 app.println(); 209 app.println(ERR_MENU_BAD_CHOICE_MULTI_DUPE.get(value)); 210 app.println(); 211 return null; 212 } else if (!callbacks.containsKey(nvalue)) { 213 app.println(); 214 app.println(ERR_MENU_BAD_CHOICE_MULTI.get()); 215 app.println(); 216 return null; 217 } else { 218 cl.add(callbacks.get(nvalue)); 219 choices.add(nvalue); 220 } 221 } 222 223 return new CompositeCallback<T>(cl); 224 } else if (!callbacks.containsKey(ninput)) { 225 app.println(); 226 app.println(ERR_MENU_BAD_CHOICE_SINGLE.get()); 227 app.println(); 228 return null; 229 } else { 230 return callbacks.get(ninput); 231 } 232 } 233 }; 234 235 // Determine the correct choice prompt. 236 Message promptMsg; 237 if (allowMultiSelect) { 238 if (defaultDescription != null) { 239 promptMsg = INFO_MENU_PROMPT_MULTI_DEFAULT.get(defaultDescription); 240 } else { 241 promptMsg = INFO_MENU_PROMPT_MULTI.get(); 242 } 243 } else { 244 if (defaultDescription != null) { 245 promptMsg = INFO_MENU_PROMPT_SINGLE_DEFAULT.get(defaultDescription); 246 } else { 247 promptMsg = INFO_MENU_PROMPT_SINGLE.get(); 248 } 249 } 250 251 // If the user selects help then we need to loop around and 252 // display the menu again. 253 while (true) { 254 // Display the menu. 255 if (title != null) { 256 app.println(title); 257 app.println(); 258 } 259 260 if (prompt != null) { 261 app.println(prompt); 262 app.println(); 263 } 264 265 if (nbuilder.getTableHeight() > 0) { 266 nbuilder.print(printer); 267 app.println(); 268 } 269 270 if (cbuilder.getTableHeight() > 0) { 271 TextTablePrinter cprinter = 272 new TextTablePrinter(app.getErrorStream()); 273 cprinter.setDisplayHeadings(false); 274 int sz = String.valueOf(nbuilder.getTableHeight()).length() + 1; 275 cprinter.setIndentWidth(4); 276 cprinter.setColumnWidth(0, sz); 277 cprinter.setColumnWidth(1, 0); 278 cbuilder.print(cprinter); 279 app.println(); 280 } 281 282 // Get the user's choice. 283 MenuCallback<T> choice; 284 285 if (nMaxTries != -1) 286 { 287 choice = app.readValidatedInput(promptMsg, validator, nMaxTries); 288 } 289 else 290 { 291 choice = app.readValidatedInput(promptMsg, validator); 292 } 293 294 // Invoke the user's selected choice. 295 MenuResult<T> result = choice.invoke(app); 296 297 // Determine if the help needs to be displayed, display it and 298 // start again. 299 if (!result.isAgain()) { 300 return result; 301 } else { 302 app.println(); 303 app.println(); 304 } 305 } 306 } 307 } 308 309 310 311 /** 312 * A simple menu option call-back which does nothing but return the 313 * provided menu result. 314 * 315 * @param <T> 316 * The type of result returned by the call-back. 317 */ 318 private static final class ResultCallback<T> implements MenuCallback<T> { 319 320 // The result to be returned by this call-back. 321 private final MenuResult<T> result; 322 323 324 325 // Private constructor. 326 private ResultCallback(MenuResult<T> result) { 327 this.result = result; 328 } 329 330 331 332 /** 333 * {@inheritDoc} 334 */ 335 public MenuResult<T> invoke(ConsoleApplication app) throws CLIException { 336 return result; 337 } 338 339 } 340 341 // The multiple column display threshold. 342 private int threshold = -1; 343 344 // Indicates whether the menu will allow selection of multiple 345 // numeric options. 346 private boolean allowMultiSelect = false; 347 348 // The application console. 349 private final ConsoleApplication app; 350 351 // The char option call-backs. 352 private final List<MenuCallback<T>> charCallbacks = 353 new ArrayList<MenuCallback<T>>(); 354 355 // The char option keys (must be single-character messages). 356 private final List<Message> charKeys = new ArrayList<Message>(); 357 358 // The synopsis of char options. 359 private final List<Message> charSynopsis = new ArrayList<Message>(); 360 361 // Optional column headings. 362 private final List<Message> columnHeadings = new ArrayList<Message>(); 363 364 // Optional column widths. 365 private final List<Integer> columnWidths = new ArrayList<Integer>(); 366 367 // The call-back for the optional default action. 368 private MenuCallback<T> defaultCallback = null; 369 370 // The description of the optional default action. 371 private Message defaultDescription = null; 372 373 // The numeric option call-backs. 374 private final List<MenuCallback<T>> numericCallbacks = 375 new ArrayList<MenuCallback<T>>(); 376 377 // The numeric option fields. 378 private final List<List<Message>> numericFields = 379 new ArrayList<List<Message>>(); 380 381 // The menu title. 382 private Message title = null; 383 384 // The menu prompt. 385 private Message prompt = null; 386 387 // The maximum number of times that we allow the user to provide an invalid 388 // answer (-1 if unlimited). 389 private int nMaxTries = -1; 390 391 /** 392 * Creates a new menu. 393 * 394 * @param app 395 * The application console. 396 */ 397 public MenuBuilder(ConsoleApplication app) { 398 this.app = app; 399 } 400 401 402 403 /** 404 * Creates a "back" menu option. When invoked, this option will 405 * return a {@code MenuResult.cancel()} result. 406 * 407 * @param isDefault 408 * Indicates whether this option should be made the menu 409 * default. 410 */ 411 public void addBackOption(boolean isDefault) { 412 addCharOption(INFO_MENU_OPTION_BACK_KEY.get(), INFO_MENU_OPTION_BACK.get(), 413 MenuResult.<T> cancel()); 414 415 if (isDefault) { 416 setDefault(INFO_MENU_OPTION_BACK_KEY.get(), MenuResult.<T> cancel()); 417 } 418 } 419 420 421 422 /** 423 * Creates a "cancel" menu option. When invoked, this option will 424 * return a {@code MenuResult.cancel()} result. 425 * 426 * @param isDefault 427 * Indicates whether this option should be made the menu 428 * default. 429 */ 430 public void addCancelOption(boolean isDefault) { 431 addCharOption(INFO_MENU_OPTION_CANCEL_KEY.get(), INFO_MENU_OPTION_CANCEL 432 .get(), MenuResult.<T> cancel()); 433 434 if (isDefault) { 435 setDefault(INFO_MENU_OPTION_CANCEL_KEY.get(), MenuResult.<T> cancel()); 436 } 437 } 438 439 440 441 /** 442 * Adds a menu choice to the menu which will have a single letter as 443 * its key. 444 * 445 * @param c 446 * The single-letter message which will be used as the key 447 * for this option. 448 * @param description 449 * The menu option description. 450 * @param callback 451 * The call-back associated with this option. 452 */ 453 public void addCharOption(Message c, Message description, 454 MenuCallback<T> callback) { 455 charKeys.add(c); 456 charSynopsis.add(description); 457 charCallbacks.add(callback); 458 } 459 460 461 462 /** 463 * Adds a menu choice to the menu which will have a single letter as 464 * its key and which returns the provided result. 465 * 466 * @param c 467 * The single-letter message which will be used as the key 468 * for this option. 469 * @param description 470 * The menu option description. 471 * @param result 472 * The menu result which should be returned by this menu 473 * choice. 474 */ 475 public void addCharOption(Message c, Message description, 476 MenuResult<T> result) { 477 addCharOption(c, description, new ResultCallback<T>(result)); 478 } 479 480 481 482 /** 483 * Creates a "help" menu option which will use the provided help 484 * call-back to display help relating to the other menu options. 485 * When the help menu option is selected help will be displayed and 486 * then the user will be shown the menu again and prompted to enter 487 * a choice. 488 * 489 * @param callback 490 * The help call-back. 491 */ 492 public void addHelpOption(final HelpCallback callback) { 493 MenuCallback<T> wrapper = new MenuCallback<T>() { 494 495 public MenuResult<T> invoke(ConsoleApplication app) throws CLIException { 496 app.println(); 497 callback.display(app); 498 return MenuResult.again(); 499 } 500 501 }; 502 503 addCharOption(INFO_MENU_OPTION_HELP_KEY.get(), INFO_MENU_OPTION_HELP.get(), 504 wrapper); 505 } 506 507 508 509 /** 510 * Adds a menu choice to the menu which will have a numeric key. 511 * 512 * @param description 513 * The menu option description. 514 * @param callback 515 * The call-back associated with this option. 516 * @param extraFields 517 * Any additional fields associated with this menu option. 518 * @return Returns the number associated with menu choice. 519 */ 520 public int addNumberedOption(Message description, MenuCallback<T> callback, 521 Message... extraFields) { 522 List<Message> fields = new ArrayList<Message>(); 523 fields.add(description); 524 if (extraFields != null) { 525 fields.addAll(Arrays.asList(extraFields)); 526 } 527 528 numericFields.add(fields); 529 numericCallbacks.add(callback); 530 531 return numericCallbacks.size(); 532 } 533 534 535 536 /** 537 * Adds a menu choice to the menu which will have a numeric key and 538 * which returns the provided result. 539 * 540 * @param description 541 * The menu option description. 542 * @param result 543 * The menu result which should be returned by this menu 544 * choice. 545 * @param extraFields 546 * Any additional fields associated with this menu option. 547 * @return Returns the number associated with menu choice. 548 */ 549 public int addNumberedOption(Message description, MenuResult<T> result, 550 Message... extraFields) { 551 return addNumberedOption(description, new ResultCallback<T>(result), 552 extraFields); 553 } 554 555 556 557 /** 558 * Creates a "quit" menu option. When invoked, this option will 559 * return a {@code MenuResult.quit()} result. 560 */ 561 public void addQuitOption() { 562 addCharOption(INFO_MENU_OPTION_QUIT_KEY.get(), INFO_MENU_OPTION_QUIT.get(), 563 MenuResult.<T> quit()); 564 } 565 566 567 568 /** 569 * Sets the flag which indicates whether or not the menu will permit 570 * multiple numeric options to be selected at once. Users specify 571 * multiple choices by separating them with a comma. The default is 572 * <code>false</code>. 573 * 574 * @param allowMultiSelect 575 * Indicates whether or not the menu will permit multiple 576 * numeric options to be selected at once. 577 */ 578 public void setAllowMultiSelect(boolean allowMultiSelect) { 579 this.allowMultiSelect = allowMultiSelect; 580 } 581 582 583 584 /** 585 * Sets the optional column headings. The column headings will be 586 * displayed above the menu options. 587 * 588 * @param headings 589 * The optional column headings. 590 */ 591 public void setColumnHeadings(Message... headings) { 592 this.columnHeadings.clear(); 593 if (headings != null) { 594 this.columnHeadings.addAll(Arrays.asList(headings)); 595 } 596 } 597 598 599 600 /** 601 * Sets the optional column widths. A value of zero indicates that 602 * the column should be expandable, a value of <code>null</code> 603 * indicates that the column should use its default width. 604 * 605 * @param widths 606 * The optional column widths. 607 */ 608 public void setColumnWidths(Integer... widths) { 609 this.columnWidths.clear(); 610 if (widths != null) { 611 this.columnWidths.addAll(Arrays.asList(widths)); 612 } 613 } 614 615 616 617 /** 618 * Sets the optional default action for this menu. The default 619 * action call-back will be invoked if the user does not specify an 620 * option and just presses enter. 621 * 622 * @param description 623 * A short description of the default action. 624 * @param callback 625 * The call-back associated with the default action. 626 */ 627 public void setDefault(Message description, MenuCallback<T> callback) { 628 defaultCallback = callback; 629 defaultDescription = description; 630 } 631 632 633 634 /** 635 * Sets the optional default action for this menu. The default 636 * action call-back will be invoked if the user does not specify an 637 * option and just presses enter. 638 * 639 * @param description 640 * A short description of the default action. 641 * @param result 642 * The menu result which should be returned by default. 643 */ 644 public void setDefault(Message description, MenuResult<T> result) { 645 setDefault(description, new ResultCallback<T>(result)); 646 } 647 648 649 650 /** 651 * Sets the number of numeric options required to trigger 652 * multiple-column display. A negative value (the default) indicates 653 * that the numeric options will always be displayed in a single 654 * column. A value of 0 indicates that numeric options will always 655 * be displayed in multiple columns. 656 * 657 * @param threshold 658 * The number of numeric options required to trigger 659 * multiple-column display. 660 */ 661 public void setMultipleColumnThreshold(int threshold) { 662 this.threshold = threshold; 663 } 664 665 666 667 /** 668 * Sets the optional menu prompt. The prompt will be displayed above 669 * the menu. Menus do not have a prompt by default. 670 * 671 * @param prompt 672 * The menu prompt, or <code>null</code> if there is not 673 * prompt. 674 */ 675 public void setPrompt(Message prompt) { 676 this.prompt = prompt; 677 } 678 679 680 681 /** 682 * Sets the optional menu title. The title will be displayed above 683 * the menu prompt. Menus do not have a title by default. 684 * 685 * @param title 686 * The menu title, or <code>null</code> if there is not 687 * title. 688 */ 689 public void setTitle(Message title) { 690 this.title = title; 691 } 692 693 694 695 /** 696 * Creates a menu from this menu builder. 697 * 698 * @return Returns the new menu. 699 */ 700 public Menu<T> toMenu() { 701 TableBuilder nbuilder = new TableBuilder(); 702 Map<String, MenuCallback<T>> callbacks = 703 new HashMap<String, MenuCallback<T>>(); 704 705 // Determine whether multiple columns should be used for numeric 706 // options. 707 boolean useMultipleColumns = false; 708 if (threshold >= 0 && numericCallbacks.size() >= threshold) { 709 useMultipleColumns = true; 710 } 711 712 // Create optional column headers. 713 if (!columnHeadings.isEmpty()) { 714 nbuilder.appendHeading(); 715 for (Message heading : columnHeadings) { 716 if (heading != null) { 717 nbuilder.appendHeading(heading); 718 } else { 719 nbuilder.appendHeading(); 720 } 721 } 722 723 if (useMultipleColumns) { 724 nbuilder.appendHeading(); 725 for (Message heading : columnHeadings) { 726 if (heading != null) { 727 nbuilder.appendHeading(heading); 728 } else { 729 nbuilder.appendHeading(); 730 } 731 } 732 } 733 } 734 735 // Add the numeric options first. 736 int sz = numericCallbacks.size(); 737 int rows = sz; 738 739 if (useMultipleColumns) { 740 // Display in two columns the first column should contain half 741 // the options. If there are an odd number of columns then the 742 // first column should contain an additional option (e.g. if 743 // there are 23 options, the first column should contain 12 744 // options and the second column 11 options). 745 rows /= 2; 746 rows += sz % 2; 747 } 748 749 for (int i = 0, j = rows; i < rows; i++, j++) { 750 nbuilder.startRow(); 751 nbuilder.appendCell(INFO_MENU_NUMERIC_OPTION.get(i + 1)); 752 753 for (Message field : numericFields.get(i)) { 754 if (field != null) { 755 nbuilder.appendCell(field); 756 } else { 757 nbuilder.appendCell(); 758 } 759 } 760 761 callbacks.put(String.valueOf(i + 1), numericCallbacks.get(i)); 762 763 // Second column. 764 if (useMultipleColumns && (j < sz)) { 765 nbuilder.appendCell(INFO_MENU_NUMERIC_OPTION.get(j + 1)); 766 767 for (Message field : numericFields.get(j)) { 768 if (field != null) { 769 nbuilder.appendCell(field); 770 } else { 771 nbuilder.appendCell(); 772 } 773 } 774 775 callbacks.put(String.valueOf(j + 1), numericCallbacks.get(j)); 776 } 777 } 778 779 // Add the char options last. 780 TableBuilder cbuilder = new TableBuilder(); 781 for (int i = 0; i < charCallbacks.size(); i++) { 782 char c = charKeys.get(i).charAt(0); 783 Message option = INFO_MENU_CHAR_OPTION.get(c); 784 785 cbuilder.startRow(); 786 cbuilder.appendCell(option); 787 cbuilder.appendCell(charSynopsis.get(i)); 788 789 callbacks.put(String.valueOf(c), charCallbacks.get(i)); 790 } 791 792 // Configure the table printer. 793 TextTablePrinter printer = new TextTablePrinter(app.getErrorStream()); 794 795 if (columnHeadings.isEmpty()) { 796 printer.setDisplayHeadings(false); 797 } else { 798 printer.setDisplayHeadings(true); 799 printer.setHeadingSeparatorStartColumn(1); 800 } 801 802 printer.setIndentWidth(4); 803 if (columnWidths.isEmpty()) { 804 printer.setColumnWidth(1, 0); 805 if (useMultipleColumns) { 806 printer.setColumnWidth(3, 0); 807 } 808 } else { 809 for (int i = 0; i < columnWidths.size(); i++) { 810 Integer j = columnWidths.get(i); 811 if (j != null) { 812 // Skip the option key column. 813 printer.setColumnWidth(i + 1, j); 814 815 if (useMultipleColumns) { 816 printer.setColumnWidth(i + 2 + columnWidths.size(), j); 817 } 818 } 819 } 820 } 821 822 return new MenuImpl<T>(app, title, prompt, nbuilder, cbuilder, printer, 823 callbacks, allowMultiSelect, defaultCallback, defaultDescription, 824 nMaxTries); 825 } 826 827 /** 828 * Sets the maximum number of tries that the user can provide an invalid 829 * value in the menu. -1 for unlimited tries (the default). If this limit is 830 * reached a CLIException will be thrown. 831 * @param nTries the maximum number of tries. 832 */ 833 public void setMaxTries(int nTries) 834 { 835 nMaxTries = nTries; 836 } 837 }