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     */
017    package org.apache.commons.cli2.option;
018    
019    import java.util.ArrayList;
020    import java.util.Collection;
021    import java.util.Collections;
022    import java.util.Comparator;
023    import java.util.HashSet;
024    import java.util.Iterator;
025    import java.util.List;
026    import java.util.ListIterator;
027    import java.util.Map;
028    import java.util.Set;
029    import java.util.SortedMap;
030    import java.util.TreeMap;
031    
032    import org.apache.commons.cli2.Argument;
033    import org.apache.commons.cli2.DisplaySetting;
034    import org.apache.commons.cli2.Group;
035    import org.apache.commons.cli2.HelpLine;
036    import org.apache.commons.cli2.Option;
037    import org.apache.commons.cli2.OptionException;
038    import org.apache.commons.cli2.WriteableCommandLine;
039    import org.apache.commons.cli2.resource.ResourceConstants;
040    
041    /**
042     * An implementation of Group
043     */
044    public 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    
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    }