View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.cli2.option;
18  
19  import java.util.ArrayList;
20  import java.util.Collection;
21  import java.util.Collections;
22  import java.util.Comparator;
23  import java.util.HashSet;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.ListIterator;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.SortedMap;
30  import java.util.TreeMap;
31  
32  import org.apache.commons.cli2.Argument;
33  import org.apache.commons.cli2.DisplaySetting;
34  import org.apache.commons.cli2.Group;
35  import org.apache.commons.cli2.HelpLine;
36  import org.apache.commons.cli2.Option;
37  import org.apache.commons.cli2.OptionException;
38  import org.apache.commons.cli2.WriteableCommandLine;
39  import org.apache.commons.cli2.resource.ResourceConstants;
40  
41  /**
42   * An implementation of Group
43   */
44  public class GroupImpl
45      extends OptionImpl implements Group {
46      private final String name;
47      private final String description;
48      private final List options;
49      private final int minimum;
50      private final int maximum;
51      private final List anonymous;
52      private final SortedMap optionMap;
53      private final Set prefixes;
54  
55      /**
56       * Creates a new GroupImpl using the specified parameters.
57       *
58       * @param options the Options and Arguments that make up the Group
59       * @param name the name of this Group, or null
60       * @param description a description of this Group
61       * @param minimum the minimum number of Options for a valid CommandLine
62       * @param maximum the maximum number of Options for a valid CommandLine
63       * @param required a flag whether this group is required
64       */
65      public GroupImpl(final List options,
66                       final String name,
67                       final String description,
68                       final int minimum,
69                       final int maximum,
70                       final boolean required) {
71          super(0, required);
72  
73          this.name = name;
74          this.description = description;
75          this.minimum = minimum;
76          this.maximum = maximum;
77  
78          // store a copy of the options to be used by the
79          // help methods
80          this.options = Collections.unmodifiableList(options);
81  
82          // anonymous Argument temporary storage
83          final List newAnonymous = new ArrayList();
84  
85          // map (key=trigger & value=Option) temporary storage
86          final SortedMap newOptionMap = new TreeMap(ReverseStringComparator.getInstance());
87  
88          // prefixes temporary storage
89          final Set newPrefixes = new HashSet();
90  
91          // process the options
92          for (final Iterator i = options.iterator(); i.hasNext();) {
93              final Option option = (Option) i.next();
94              option.setParent(this);
95  
96              if (option instanceof Argument) {
97                  i.remove();
98                  newAnonymous.add(option);
99              } 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 
524 class 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 }