001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.cli2.option; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.Comparator; 023import java.util.HashSet; 024import java.util.Iterator; 025import java.util.List; 026import java.util.ListIterator; 027import java.util.Map; 028import java.util.Set; 029import java.util.SortedMap; 030import java.util.TreeMap; 031 032import org.apache.commons.cli2.Argument; 033import org.apache.commons.cli2.DisplaySetting; 034import org.apache.commons.cli2.Group; 035import org.apache.commons.cli2.HelpLine; 036import org.apache.commons.cli2.Option; 037import org.apache.commons.cli2.OptionException; 038import org.apache.commons.cli2.WriteableCommandLine; 039import org.apache.commons.cli2.resource.ResourceConstants; 040 041/** 042 * An implementation of Group 043 */ 044public class GroupImpl 045 extends OptionImpl implements Group { 046 private final String name; 047 private final String description; 048 private final List options; 049 private final int minimum; 050 private final int maximum; 051 private final List anonymous; 052 private final SortedMap optionMap; 053 private final Set prefixes; 054 055 /** 056 * Creates a new GroupImpl using the specified parameters. 057 * 058 * @param options the Options and Arguments that make up the Group 059 * @param name the name of this Group, or null 060 * @param description a description of this Group 061 * @param minimum the minimum number of Options for a valid CommandLine 062 * @param maximum the maximum number of Options for a valid CommandLine 063 * @param required a flag whether this group is required 064 */ 065 public GroupImpl(final List options, 066 final String name, 067 final String description, 068 final int minimum, 069 final int maximum, 070 final boolean required) { 071 super(0, required); 072 073 this.name = name; 074 this.description = description; 075 this.minimum = minimum; 076 this.maximum = maximum; 077 078 // store a copy of the options to be used by the 079 // help methods 080 this.options = Collections.unmodifiableList(options); 081 082 // anonymous Argument temporary storage 083 final List newAnonymous = new ArrayList(); 084 085 // map (key=trigger & value=Option) temporary storage 086 final SortedMap newOptionMap = new TreeMap(ReverseStringComparator.getInstance()); 087 088 // prefixes temporary storage 089 final Set newPrefixes = new HashSet(); 090 091 // process the options 092 for (final Iterator i = options.iterator(); i.hasNext();) { 093 final Option option = (Option) i.next(); 094 option.setParent(this); 095 096 if (option instanceof Argument) { 097 i.remove(); 098 newAnonymous.add(option); 099 } else { 100 final Set triggers = option.getTriggers(); 101 102 for (Iterator j = triggers.iterator(); j.hasNext();) { 103 newOptionMap.put(j.next(), option); 104 } 105 106 // store the prefixes 107 newPrefixes.addAll(option.getPrefixes()); 108 } 109 } 110 111 this.anonymous = Collections.unmodifiableList(newAnonymous); 112 this.optionMap = Collections.unmodifiableSortedMap(newOptionMap); 113 this.prefixes = Collections.unmodifiableSet(newPrefixes); 114 } 115 116 public boolean canProcess(final WriteableCommandLine commandLine, 117 final String arg) { 118 if (arg == null) { 119 return false; 120 } 121 122 // if arg does not require bursting 123 if (optionMap.containsKey(arg)) { 124 return true; 125 } 126 127 // filter 128 final Map tailMap = optionMap.tailMap(arg); 129 130 // check if bursting is required 131 for (final Iterator iter = tailMap.values().iterator(); iter.hasNext();) { 132 final Option option = (Option) iter.next(); 133 134 if (option.canProcess(commandLine, arg)) { 135 return true; 136 } 137 } 138 139 if (looksLikeOption(commandLine, arg)) { 140 return false; 141 } 142 143 // anonymous argument(s) means we can process it 144 if (anonymous.size() > 0) { 145 return true; 146 } 147 148 return false; 149 } 150 151 public Set getPrefixes() { 152 return prefixes; 153 } 154 155 public Set getTriggers() { 156 return optionMap.keySet(); 157 } 158 159 public void process(final WriteableCommandLine commandLine, 160 final ListIterator arguments) 161 throws OptionException { 162 String previous = null; 163 164 // [START process each command line token 165 while (arguments.hasNext()) { 166 // grab the next argument 167 final String arg = (String) arguments.next(); 168 169 // if we have just tried to process this instance 170 if (arg == previous) { 171 // rollback and abort 172 arguments.previous(); 173 174 break; 175 } 176 177 // remember last processed instance 178 previous = arg; 179 180 final Option opt = (Option) optionMap.get(arg); 181 182 // option found 183 if (opt != null) { 184 arguments.previous(); 185 opt.process(commandLine, arguments); 186 } 187 // [START option NOT found 188 else { 189 // it might be an anonymous argument continue search 190 // [START argument may be anonymous 191 if (looksLikeOption(commandLine, arg)) { 192 // narrow the search 193 final Collection values = optionMap.tailMap(arg).values(); 194 195 boolean foundMemberOption = false; 196 197 for (Iterator i = values.iterator(); i.hasNext() && !foundMemberOption;) { 198 final Option option = (Option) i.next(); 199 200 if (option.canProcess(commandLine, arg)) { 201 foundMemberOption = true; 202 arguments.previous(); 203 option.process(commandLine, arguments); 204 } 205 } 206 207 // back track and abort this group if necessary 208 if (!foundMemberOption) { 209 arguments.previous(); 210 211 return; 212 } 213 } // [END argument may be anonymous 214 215 // [START argument is NOT anonymous 216 else { 217 // move iterator back, current value not used 218 arguments.previous(); 219 220 // if there are no anonymous arguments then this group can't 221 // process the argument 222 if (anonymous.isEmpty()) { 223 break; 224 } 225 226 // TODO: why do we iterate over all anonymous arguments? 227 // canProcess will always return true? 228 for (final Iterator i = anonymous.iterator(); i.hasNext();) { 229 final Argument argument = (Argument) i.next(); 230 231 if (argument.canProcess(commandLine, arguments)) { 232 argument.process(commandLine, arguments); 233 } 234 } 235 } // [END argument is NOT anonymous 236 } // [END option NOT found 237 } // [END process each command line token 238 } 239 240 public void validate(final WriteableCommandLine commandLine) 241 throws OptionException { 242 // number of options found 243 int present = 0; 244 245 // reference to first unexpected option 246 Option unexpected = null; 247 248 for (final Iterator i = options.iterator(); i.hasNext();) { 249 final Option option = (Option) i.next(); 250 251 // needs validation? 252 boolean validate = option.isRequired(); 253 254 // if the child option is present then validate it 255 if (commandLine.hasOption(option)) { 256 if (++present > maximum) { 257 unexpected = option; 258 259 break; 260 } 261 validate = true; 262 } 263 264 if (validate) { 265 option.validate(commandLine); 266 } 267 } 268 269 // too many options 270 if (unexpected != null) { 271 throw new OptionException(this, ResourceConstants.UNEXPECTED_TOKEN, 272 unexpected.getPreferredName()); 273 } 274 275 // too few option 276 if (present < minimum) { 277 throw new OptionException(this, ResourceConstants.MISSING_OPTION); 278 } 279 280 // validate each anonymous argument 281 for (final Iterator i = anonymous.iterator(); i.hasNext();) { 282 final Option option = (Option) i.next(); 283 option.validate(commandLine); 284 } 285 } 286 287 public String getPreferredName() { 288 return name; 289 } 290 291 public String getDescription() { 292 return description; 293 } 294 295 public void appendUsage(final StringBuffer buffer, 296 final Set helpSettings, 297 final Comparator comp) { 298 appendUsage(buffer, helpSettings, comp, "|"); 299 } 300 301 public void appendUsage(final StringBuffer buffer, 302 final Set helpSettings, 303 final Comparator comp, 304 final String separator) { 305 final Set helpSettingsCopy = new HashSet(helpSettings); 306 307 final boolean optional = !isRequired() 308 && (helpSettingsCopy.contains(DisplaySetting.DISPLAY_OPTIONAL) || 309 helpSettingsCopy.contains(DisplaySetting.DISPLAY_OPTIONAL_CHILD_GROUP)); 310 311 final boolean expanded = 312 (name == null) || helpSettingsCopy.contains(DisplaySetting.DISPLAY_GROUP_EXPANDED); 313 314 final boolean named = 315 !expanded || 316 ((name != null) && helpSettingsCopy.contains(DisplaySetting.DISPLAY_GROUP_NAME)); 317 318 final boolean arguments = helpSettingsCopy.contains(DisplaySetting.DISPLAY_GROUP_ARGUMENT); 319 320 final boolean outer = helpSettingsCopy.contains(DisplaySetting.DISPLAY_GROUP_OUTER); 321 322 helpSettingsCopy.remove(DisplaySetting.DISPLAY_GROUP_OUTER); 323 324 final boolean both = named && expanded; 325 326 if (optional) { 327 buffer.append('['); 328 } 329 330 if (named) { 331 buffer.append(name); 332 } 333 334 if (both) { 335 buffer.append(" ("); 336 } 337 338 if (expanded) { 339 final Set childSettings; 340 341 if (!helpSettingsCopy.contains(DisplaySetting.DISPLAY_GROUP_EXPANDED)) { 342 childSettings = DisplaySetting.NONE; 343 } else { 344 childSettings = new HashSet(helpSettingsCopy); 345 childSettings.remove(DisplaySetting.DISPLAY_OPTIONAL); 346 } 347 348 // grab a list of the group's options. 349 final List list; 350 351 if (comp == null) { 352 // default to using the initial order 353 list = options; 354 } else { 355 // sort options if comparator is supplied 356 list = new ArrayList(options); 357 Collections.sort(list, comp); 358 } 359 360 // for each option. 361 for (final Iterator i = list.iterator(); i.hasNext();) { 362 final Option option = (Option) i.next(); 363 364 // append usage information 365 option.appendUsage(buffer, childSettings, comp); 366 367 // add separators as needed 368 if (i.hasNext()) { 369 buffer.append(separator); 370 } 371 } 372 } 373 374 if (both) { 375 buffer.append(')'); 376 } 377 378 if (optional && outer) { 379 buffer.append(']'); 380 } 381 382 if (arguments) { 383 for (final Iterator i = anonymous.iterator(); i.hasNext();) { 384 buffer.append(' '); 385 386 final Option option = (Option) i.next(); 387 option.appendUsage(buffer, helpSettingsCopy, comp); 388 } 389 } 390 391 if (optional && !outer) { 392 buffer.append(']'); 393 } 394 } 395 396 public List helpLines(final int depth, 397 final Set helpSettings, 398 final Comparator comp) { 399 final List helpLines = new ArrayList(); 400 401 if (helpSettings.contains(DisplaySetting.DISPLAY_GROUP_NAME)) { 402 final HelpLine helpLine = new HelpLineImpl(this, depth); 403 helpLines.add(helpLine); 404 } 405 406 if (helpSettings.contains(DisplaySetting.DISPLAY_GROUP_EXPANDED)) { 407 // grab a list of the group's options. 408 final List list; 409 410 if (comp == null) { 411 // default to using the initial order 412 list = options; 413 } else { 414 // sort options if comparator is supplied 415 list = new ArrayList(options); 416 Collections.sort(list, comp); 417 } 418 419 // for each option 420 for (final Iterator i = list.iterator(); i.hasNext();) { 421 final Option option = (Option) i.next(); 422 helpLines.addAll(option.helpLines(depth + 1, helpSettings, comp)); 423 } 424 } 425 426 if (helpSettings.contains(DisplaySetting.DISPLAY_GROUP_ARGUMENT)) { 427 for (final Iterator i = anonymous.iterator(); i.hasNext();) { 428 final Option option = (Option) i.next(); 429 helpLines.addAll(option.helpLines(depth + 1, helpSettings, comp)); 430 } 431 } 432 433 return helpLines; 434 } 435 436 /** 437 * Gets the member Options of thie Group. 438 * Note this does not include any Arguments 439 * @return only the non Argument Options of the Group 440 */ 441 public List getOptions() { 442 return options; 443 } 444 445 /** 446 * Gets the anonymous Arguments of this Group. 447 * @return the Argument options of this Group 448 */ 449 public List getAnonymous() { 450 return anonymous; 451 } 452 453 public Option findOption(final String trigger) { 454 final Iterator i = getOptions().iterator(); 455 456 while (i.hasNext()) { 457 final Option option = (Option) i.next(); 458 final Option found = option.findOption(trigger); 459 460 if (found != null) { 461 return found; 462 } 463 } 464 465 return null; 466 } 467 468 public int getMinimum() { 469 return minimum; 470 } 471 472 public int getMaximum() { 473 return maximum; 474 } 475 476 /** 477 * Tests whether this option is required. For groups we evaluate the 478 * <code>required</code> flag common to all options, but also take the 479 * minimum constraints into account. 480 * 481 * @return a flag whether this option is required 482 */ 483 public boolean isRequired() 484 { 485 return (getParent() == null || super.isRequired()) && getMinimum() > 0; 486 } 487 488 public void defaults(final WriteableCommandLine commandLine) { 489 super.defaults(commandLine); 490 491 for (final Iterator i = options.iterator(); i.hasNext();) { 492 final Option option = (Option) i.next(); 493 option.defaults(commandLine); 494 } 495 496 for (final Iterator i = anonymous.iterator(); i.hasNext();) { 497 final Option option = (Option) i.next(); 498 option.defaults(commandLine); 499 } 500 } 501 502 /** 503 * Helper method for testing whether an element of the command line looks 504 * like an option. This method queries the command line, but sets the 505 * current option first. 506 * 507 * @param commandLine the command line 508 * @param trigger the trigger to be checked 509 * @return a flag whether this element looks like an option 510 */ 511 private boolean looksLikeOption(final WriteableCommandLine commandLine, 512 final String trigger) { 513 Option oldOption = commandLine.getCurrentOption(); 514 try { 515 commandLine.setCurrentOption(this); 516 return commandLine.looksLikeOption(trigger); 517 } finally { 518 commandLine.setCurrentOption(oldOption); 519 } 520 } 521} 522 523 524class ReverseStringComparator implements Comparator { 525 private static final Comparator instance = new ReverseStringComparator(); 526 527 private ReverseStringComparator() { 528 // just making sure nobody else creates one 529 } 530 531 /** 532 * Gets a singleton instance of a ReverseStringComparator 533 * @return the singleton instance 534 */ 535 public static final Comparator getInstance() { 536 return instance; 537 } 538 539 public int compare(final Object o1, 540 final Object o2) { 541 final String s1 = (String) o1; 542 final String s2 = (String) o2; 543 544 return -s1.compareTo(s2); 545 } 546}