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 }