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    }