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 }