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 }