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}