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