001/*
002 * Copyright (C) 2012 eXo Platform SAS.
003 *
004 * This is free software; you can redistribute it and/or modify it
005 * under the terms of the GNU Lesser General Public License as
006 * published by the Free Software Foundation; either version 2.1 of
007 * the License, or (at your option) any later version.
008 *
009 * This software is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * You should have received a copy of the GNU Lesser General Public
015 * License along with this software; if not, write to the Free
016 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
017 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
018 */
019
020package org.crsh.cli.descriptor;
021
022import org.crsh.cli.impl.descriptor.IntrospectionException;
023import org.crsh.cli.impl.Multiplicity;
024import org.crsh.cli.impl.lang.Util;
025
026import java.io.IOException;
027import java.util.ArrayList;
028import java.util.Collection;
029import java.util.Collections;
030import java.util.Formatter;
031import java.util.HashSet;
032import java.util.LinkedHashMap;
033import java.util.List;
034import java.util.ListIterator;
035import java.util.Map;
036import java.util.Set;
037
038import static org.crsh.cli.impl.lang.Util.tuples;
039
040public abstract class CommandDescriptor<T> {
041
042  /** . */
043  private static final Set<String> MAIN_SINGLETON = Collections.singleton("main");
044
045  /** . */
046  private final String name;
047
048  /** . */
049  private final Description description;
050
051  /** . */
052  private final Map<String, OptionDescriptor> optionMap;
053
054  /** . */
055  private final Set<String> shortOptionNames;
056
057  /** . */
058  private final Set<String> longOptionNames;
059
060  /** . */
061  private boolean listArgument;
062
063  /** . */
064  private final List<OptionDescriptor> options;
065
066  /** . */
067  private final List<ArgumentDescriptor> arguments;
068
069  /** . */
070  private final List<ParameterDescriptor> parameters;
071
072  /** . */
073  private final Map<String, OptionDescriptor> uOptionMap;
074
075  /** . */
076  private final Set<String> uShortOptionNames;
077
078  /** . */
079  private final Set<String> uLongOptionNames;
080
081  /** . */
082  private final List<OptionDescriptor> uOptions;
083
084  /** . */
085  private final List<ArgumentDescriptor> uArguments;
086
087  /** . */
088  private final List<ParameterDescriptor> uParameters;
089
090  protected CommandDescriptor(String name, Description description) throws IntrospectionException {
091
092    //
093    this.description = description;
094    this.optionMap = new LinkedHashMap<String, OptionDescriptor>();
095    this.arguments = new ArrayList<ArgumentDescriptor>();
096    this.options = new ArrayList<OptionDescriptor>();
097    this.name = name;
098    this.parameters = new ArrayList<ParameterDescriptor>();
099    this.listArgument = false;
100    this.shortOptionNames = new HashSet<String>();
101    this.longOptionNames = new HashSet<String>();
102
103    //
104    this.uOptionMap = Collections.unmodifiableMap(optionMap);
105    this.uParameters = Collections.unmodifiableList(parameters);
106    this.uOptions = Collections.unmodifiableList(options);
107    this.uArguments = Collections.unmodifiableList(arguments);
108    this.uShortOptionNames = shortOptionNames;
109    this.uLongOptionNames = longOptionNames;
110  }
111
112  /**
113   * Add a parameter to the command.
114   *
115   * @param parameter the parameter to add
116   * @throws IntrospectionException any introspection exception that would prevent the parameter to be added
117   * @throws NullPointerException if the parameter is null
118   * @throws IllegalArgumentException if the parameter is already associated with another command
119   */
120  protected void addParameter(ParameterDescriptor parameter) throws IntrospectionException, NullPointerException, IllegalArgumentException {
121
122    //
123    if (parameter == null) {
124      throw new NullPointerException("No null parameter accepted");
125    }
126
127    //
128    if (parameter instanceof OptionDescriptor) {
129      OptionDescriptor option = (OptionDescriptor)parameter;
130      for (String optionName : option.getNames()) {
131        String name;
132        if (optionName.length() == 1) {
133          name = "-" + optionName;
134          shortOptionNames.add(name);
135        } else {
136          name = "--" + optionName;
137          longOptionNames.add(name);
138        }
139        optionMap.put(name, option);
140      }
141      options.add(option);
142      ListIterator<ParameterDescriptor> i = parameters.listIterator();
143      while (i.hasNext()) {
144        ParameterDescriptor next = i.next();
145        if (next instanceof ArgumentDescriptor) {
146          i.previous();
147          break;
148        }
149      }
150      i.add(parameter);
151    } else if (parameter instanceof ArgumentDescriptor) {
152      ArgumentDescriptor argument = (ArgumentDescriptor)parameter;
153      if (argument.getMultiplicity() == Multiplicity.MULTI) {
154        if (listArgument) {
155          throw new IntrospectionException();
156        }
157        listArgument = true;
158      }
159      arguments.add(argument);
160      parameters.add(argument);
161    }
162  }
163
164  public abstract Class<T> getType();
165
166  public abstract CommandDescriptor<T> getOwner();
167
168  public final int getDepth() {
169    CommandDescriptor<T> owner = getOwner();
170    return owner == null ? 0 : 1 + owner.getDepth();
171  }
172
173  public final void printUsage(Appendable writer) throws IOException {
174    int depth = getDepth();
175    switch (depth) {
176      case 0: {
177        Map<String, ? extends CommandDescriptor<T>> methods = getSubordinates();
178        if (methods.size() == 1) {
179          methods.values().iterator().next().printUsage(writer);
180        } else {
181          writer.append("usage: ").append(getName());
182          for (OptionDescriptor option : getOptions()) {
183            option.printUsage(writer);
184          }
185          writer.append(" COMMAND [ARGS]\n\n");
186          writer.append("The most commonly used ").append(getName()).append(" commands are:\n");
187          String format = "   %1$-16s %2$s\n";
188          for (CommandDescriptor<T> method : methods.values()) {
189            Formatter formatter = new Formatter(writer);
190            formatter.format(format, method.getName(), method.getUsage());
191          }
192        }
193        break;
194      }
195      case 1: {
196
197        CommandDescriptor<T> owner = getOwner();
198        int length = 0;
199        List<String> parameterUsages = new ArrayList<String>();
200        List<String> parameterBilto = new ArrayList<String>();
201        boolean printName = !owner.getSubordinates().keySet().equals(MAIN_SINGLETON);
202
203        //
204        writer.append("usage: ").append(owner.getName());
205
206        //
207        for (OptionDescriptor option : owner.getOptions()) {
208          writer.append(" ");
209          StringBuilder sb = new StringBuilder();
210          option.printUsage(sb);
211          String usage = sb.toString();
212          writer.append(usage);
213
214          length = Math.max(length, usage.length());
215          parameterUsages.add(usage);
216          parameterBilto.add(option.getUsage());
217        }
218
219        //
220        writer.append(printName ? (" " + getName()) : "");
221
222        //
223        for (ParameterDescriptor parameter : getParameters()) {
224          writer.append(" ");
225          StringBuilder sb = new StringBuilder();
226          parameter.printUsage(sb);
227          String usage = sb.toString();
228          writer.append(usage);
229
230          length = Math.max(length, usage.length());
231          parameterBilto.add(parameter.getUsage());
232          parameterUsages.add(usage);
233        }
234        writer.append("\n\n");
235
236        //
237        String format = "   %1$-" + length + "s %2$s\n";
238        for (String[] tuple : tuples(String.class, parameterUsages, parameterBilto)) {
239          Formatter formatter = new Formatter(writer);
240          formatter.format(format, tuple[0], tuple[1]);
241        }
242
243        //
244        writer.append("\n\n");
245        break;
246      }
247      default:
248        throw new UnsupportedOperationException("Does not make sense");
249    }
250
251
252  }
253
254  public final void printMan(Appendable writer) throws IOException {
255    int depth = getDepth();
256    switch (depth) {
257      case 0: {
258        Map<String, ? extends CommandDescriptor<T>> methods = getSubordinates();
259        if (methods.size() == 1) {
260          methods.values().iterator().next().printMan(writer);
261        } else {
262
263          // Name
264          writer.append("NAME\n");
265          writer.append(Util.MAN_TAB).append(getName());
266          if (getUsage().length() > 0) {
267            writer.append(" - ").append(getUsage());
268          }
269          writer.append("\n\n");
270
271          // Synopsis
272          writer.append("SYNOPSIS\n");
273          writer.append(Util.MAN_TAB).append(getName());
274          for (OptionDescriptor option : getOptions()) {
275            writer.append(" ");
276            option.printUsage(writer);
277          }
278          writer.append(" COMMAND [ARGS]\n\n");
279
280          //
281          String man = getDescription().getMan();
282          if (man.length() > 0) {
283            writer.append("DESCRIPTION\n");
284            Util.indent(Util.MAN_TAB, man, writer);
285            writer.append("\n\n");
286          }
287
288          // Common options
289          if (getOptions().size() > 0) {
290            writer.append("PARAMETERS\n");
291            for (OptionDescriptor option : getOptions()) {
292              writer.append(Util.MAN_TAB);
293              option.printUsage(writer);
294              String optionText = option.getDescription().getBestEffortMan();
295              if (optionText.length() > 0) {
296                writer.append("\n");
297                Util.indent(Util.MAN_TAB_EXTRA, optionText, writer);
298              }
299              writer.append("\n\n");
300            }
301          }
302
303          //
304          writer.append("COMMANDS\n");
305          for (CommandDescriptor<T> method : methods.values()) {
306            writer.append(Util.MAN_TAB).append(method.getName());
307            String methodText = method.getDescription().getBestEffortMan();
308            if (methodText.length() > 0) {
309              writer.append("\n");
310              Util.indent(Util.MAN_TAB_EXTRA, methodText, writer);
311            }
312            writer.append("\n\n");
313          }
314        }
315        break;
316      }
317      case 1: {
318
319        CommandDescriptor<T> owner = getOwner();
320
321        //
322        boolean printName = !owner.getSubordinates().keySet().equals(MAIN_SINGLETON);
323
324        // Name
325        writer.append("NAME\n");
326        writer.append(Util.MAN_TAB).append(owner.getName());
327        if (printName) {
328          writer.append(" ").append(getName());
329        }
330        if (getUsage().length() > 0) {
331          writer.append(" - ").append(getUsage());
332        }
333        writer.append("\n\n");
334
335        // Synopsis
336        writer.append("SYNOPSIS\n");
337        writer.append(Util.MAN_TAB).append(owner.getName());
338        for (OptionDescriptor option : owner.getOptions()) {
339          writer.append(" ");
340          option.printUsage(writer);
341        }
342        if (printName) {
343          writer.append(" ").append(getName());
344        }
345        for (OptionDescriptor option : getOptions()) {
346          writer.append(" ");
347          option.printUsage(writer);
348        }
349        for (ArgumentDescriptor argument : getArguments()) {
350          writer.append(" ");
351          argument.printUsage(writer);
352        }
353        writer.append("\n\n");
354
355        // Description
356        String man = getDescription().getMan();
357        if (man.length() > 0) {
358          writer.append("DESCRIPTION\n");
359          Util.indent(Util.MAN_TAB, man, writer);
360          writer.append("\n\n");
361        }
362
363        // Parameters
364        List<OptionDescriptor> options = new ArrayList<OptionDescriptor>();
365        options.addAll(owner.getOptions());
366        options.addAll(getOptions());
367        if (options.size() > 0) {
368          writer.append("\nPARAMETERS\n");
369          for (ParameterDescriptor parameter : Util.join(owner.getOptions(), getParameters())) {
370            writer.append(Util.MAN_TAB);
371            parameter.printUsage(writer);
372            String parameterText = parameter.getDescription().getBestEffortMan();
373            if (parameterText.length() > 0) {
374              writer.append("\n");
375              Util.indent(Util.MAN_TAB_EXTRA, parameterText, writer);
376            }
377            writer.append("\n\n");
378          }
379        }
380
381        //
382        break;
383      }
384      default:
385        throw new UnsupportedOperationException("Does not make sense");
386    }
387  }
388
389
390  /**
391   * Returns the command subordinates as a map.
392   *
393   * @return the subordinates
394   */
395  public abstract Map<String, ? extends CommandDescriptor<T>> getSubordinates();
396
397  public abstract CommandDescriptor<T> getSubordinate(String name);
398
399  /**
400   * Returns the command parameters, the returned collection contains the command options and
401   * the command arguments.
402   *
403   * @return the command parameters
404   */
405  public final List<ParameterDescriptor> getParameters() {
406    return uParameters;
407  }
408
409  /**
410   * Returns the command option names.
411   *
412   * @return the command option names
413   */
414  public final Set<String> getOptionNames() {
415    return uOptionMap.keySet();
416  }
417
418  /**
419   * Returns the command short option names.
420   *
421   * @return the command long option names
422   */
423  public final Set<String> getShortOptionNames() {
424    return uShortOptionNames;
425  }
426
427  /**
428   * Returns the command long option names.
429   *
430   * @return the command long option names
431   */
432  public final Set<String> getLongOptionNames() {
433    return uLongOptionNames;
434  }
435
436  /**
437   * Returns the command options.
438   *
439   * @return the command options
440   */
441  public final Collection<OptionDescriptor> getOptions() {
442    return uOptions;
443  }
444
445  /**
446   * Returns a command option by its name.
447   *
448   * @param name the option name
449   * @return the option
450   */
451  public final OptionDescriptor getOption(String name) {
452    return optionMap.get(name);
453  }
454
455  /**
456   * Find an command option by its name, this will look through the command hierarchy.
457   *
458   * @param name the option name
459   * @return the option or null
460   */
461  public final OptionDescriptor findOption(String name) {
462    OptionDescriptor option = getOption(name);
463    if (option == null) {
464      CommandDescriptor<T> owner = getOwner();
465      if (owner != null) {
466        option = owner.findOption(name);
467      }
468    }
469    return option;
470  }
471
472  /**
473   * Returns a list of the command arguments.
474   *
475   * @return the command arguments
476   */
477  public final List<ArgumentDescriptor> getArguments() {
478    return uArguments;
479  }
480
481  /**
482   * Returns a a specified argument by its index.
483   *
484   * @param index the argument index
485   * @return the command argument
486   * @throws IllegalArgumentException if the index is not within the bounds
487   */
488  public final ArgumentDescriptor getArgument(int index) throws IllegalArgumentException {
489    if (index < 0) {
490      throw new IllegalArgumentException();
491    }
492    if (index >= arguments.size()) {
493      throw new IllegalArgumentException();
494    }
495    return arguments.get(index);
496  }
497
498  /**
499   * Returns the command name.
500   *
501   * @return the command name
502   */
503  public final String getName() {
504    return name;
505  }
506
507  /**
508   * Returns the command description.
509   *
510   * @return the command description
511   */
512  public final Description getDescription() {
513    return description;
514  }
515
516  /**
517   * Returns the command usage, shortcut for invoking <code>getDescription().getUsage()</code> on this
518   * object.
519   *
520   * @return the command usage
521   */
522  public final String getUsage() {
523    return description != null ? description.getUsage() : "";
524  }
525}