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    
020    package org.crsh.cli.descriptor;
021    
022    import org.crsh.cli.impl.descriptor.IntrospectionException;
023    import org.crsh.cli.impl.Multiplicity;
024    import org.crsh.cli.impl.lang.Util;
025    
026    import java.io.IOException;
027    import java.util.ArrayList;
028    import java.util.Collection;
029    import java.util.Collections;
030    import java.util.Formatter;
031    import java.util.HashSet;
032    import java.util.LinkedHashMap;
033    import java.util.List;
034    import java.util.ListIterator;
035    import java.util.Map;
036    import java.util.Set;
037    
038    import static org.crsh.cli.impl.lang.Util.tuples;
039    
040    public 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    }