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.Collections;
020    import java.util.Comparator;
021    import java.util.List;
022    import java.util.ListIterator;
023    import java.util.Set;
024    import java.util.StringTokenizer;
025    
026    import org.apache.commons.cli2.Argument;
027    import org.apache.commons.cli2.DisplaySetting;
028    import org.apache.commons.cli2.HelpLine;
029    import org.apache.commons.cli2.Option;
030    import org.apache.commons.cli2.OptionException;
031    import org.apache.commons.cli2.WriteableCommandLine;
032    import org.apache.commons.cli2.resource.ResourceConstants;
033    import org.apache.commons.cli2.resource.ResourceHelper;
034    import org.apache.commons.cli2.validation.InvalidArgumentException;
035    import org.apache.commons.cli2.validation.Validator;
036    
037    /**
038     * An implementation of an Argument.
039     */
040    public class ArgumentImpl
041        extends OptionImpl implements Argument {
042        private static final char NUL = '\0';
043    
044        /**
045         * The default value for the initial separator char.
046         */
047        public static final char DEFAULT_INITIAL_SEPARATOR = NUL;
048    
049        /**
050         * The default value for the subsequent separator char.
051         */
052        public static final char DEFAULT_SUBSEQUENT_SEPARATOR = NUL;
053    
054        /**
055         * The default token to indicate that remaining arguments should be consumed
056         * as values.
057         */
058        public static final String DEFAULT_CONSUME_REMAINING = "--";
059        private final String name;
060        private final String description;
061        private final int minimum;
062        private final int maximum;
063        private final char initialSeparator;
064        private final char subsequentSeparator;
065        private final boolean subsequentSplit;
066        private final Validator validator;
067        private final String consumeRemaining;
068        private final List defaultValues;
069        private final ResourceHelper resources = ResourceHelper.getResourceHelper();
070    
071        /**
072         * Creates a new Argument instance.
073         *
074         * @param name
075         *            The name of the argument
076         * @param description
077         *            A description of the argument
078         * @param minimum
079         *            The minimum number of values needed to be valid
080         * @param maximum
081         *            The maximum number of values allowed to be valid
082         * @param initialSeparator
083         *            The char separating option from value
084         * @param subsequentSeparator
085         *            The char separating values from each other
086         * @param validator
087         *            The object responsible for validating the values
088         * @param consumeRemaining
089         *            The String used for the "consuming option" group
090         * @param valueDefaults
091         *            The values to be used if none are specified.
092         * @param id
093         *            The id of the option, 0 implies automatic assignment.
094         *
095         * @see OptionImpl#OptionImpl(int,boolean)
096         */
097        public ArgumentImpl(final String name,
098                            final String description,
099                            final int minimum,
100                            final int maximum,
101                            final char initialSeparator,
102                            final char subsequentSeparator,
103                            final Validator validator,
104                            final String consumeRemaining,
105                            final List valueDefaults,
106                            final int id) {
107            super(id, false);
108    
109            this.name = (name == null) ? "arg" : name;
110            this.description = description;
111            this.minimum = minimum;
112            this.maximum = maximum;
113            this.initialSeparator = initialSeparator;
114            this.subsequentSeparator = subsequentSeparator;
115            this.subsequentSplit = subsequentSeparator != NUL;
116            this.validator = validator;
117            this.consumeRemaining = consumeRemaining;
118            this.defaultValues = valueDefaults;
119    
120            if (minimum > maximum) {
121                throw new IllegalArgumentException(resources.getMessage(ResourceConstants.ARGUMENT_MIN_EXCEEDS_MAX));
122            }
123    
124            if ((valueDefaults != null) && (valueDefaults.size() > 0)) {
125                if (valueDefaults.size() < minimum) {
126                    throw new IllegalArgumentException(resources.getMessage(ResourceConstants.ARGUMENT_TOO_FEW_DEFAULTS));
127                }
128    
129                if (valueDefaults.size() > maximum) {
130                    throw new IllegalArgumentException(resources.getMessage(ResourceConstants.ARGUMENT_TOO_MANY_DEFAULTS));
131                }
132            }
133        }
134    
135        public String getPreferredName() {
136            return name;
137        }
138    
139        public void processValues(final WriteableCommandLine commandLine,
140                                  final ListIterator arguments,
141                                  final Option option)
142            throws OptionException {
143            // count of arguments processed for this option.
144            int argumentCount = commandLine.getUndefaultedValues(option).size();
145    
146            while (arguments.hasNext() && (argumentCount < maximum)) {
147                final String allValuesQuoted = (String) arguments.next();
148                final String allValues = stripBoundaryQuotes(allValuesQuoted);
149    
150                // should we ignore things that look like options?
151                if (allValuesQuoted.equals(consumeRemaining)) {
152                    while (arguments.hasNext() && (argumentCount < maximum)) {
153                        ++argumentCount;
154                        commandLine.addValue(option, arguments.next());
155                    }
156                }
157                // does it look like an option?
158                else if (commandLine.looksLikeOption(allValuesQuoted)) {
159                    arguments.previous();
160    
161                    break;
162                }
163                // should we split the string up?
164                else if (subsequentSplit) {
165                    final StringTokenizer values =
166                        new StringTokenizer(allValues, String.valueOf(subsequentSeparator));
167    
168                    arguments.remove();
169    
170                    while (values.hasMoreTokens() && (argumentCount < maximum)) {
171                        ++argumentCount;
172    
173                        final String token = values.nextToken();
174                        commandLine.addValue(option, token);
175                        arguments.add(token);
176                    }
177    
178                    if (values.hasMoreTokens()) {
179                        throw new OptionException(option, ResourceConstants.ARGUMENT_UNEXPECTED_VALUE,
180                                                  values.nextToken());
181                    }
182                }
183                // it must be a value as it is
184                else {
185                    ++argumentCount;
186                    commandLine.addValue(option, allValues);
187                }
188            }
189        }
190    
191        public boolean canProcess(final WriteableCommandLine commandLine,
192                                  final String arg) {
193            return true;
194        }
195    
196        public Set getPrefixes() {
197            return Collections.EMPTY_SET;
198        }
199    
200        public void process(WriteableCommandLine commandLine,
201                            ListIterator args)
202            throws OptionException {
203            processValues(commandLine, args, this);
204        }
205    
206        public char getInitialSeparator() {
207            return this.initialSeparator;
208        }
209    
210        public char getSubsequentSeparator() {
211            return this.subsequentSeparator;
212        }
213    
214        public Set getTriggers() {
215            return Collections.EMPTY_SET;
216        }
217    
218        public String getConsumeRemaining() {
219            return this.consumeRemaining;
220        }
221    
222        public List getDefaultValues() {
223            return this.defaultValues;
224        }
225    
226        public Validator getValidator() {
227            return this.validator;
228        }
229    
230        public void validate(final WriteableCommandLine commandLine)
231            throws OptionException {
232            validate(commandLine, this);
233        }
234    
235        public void validate(final WriteableCommandLine commandLine,
236                             final Option option)
237            throws OptionException {
238            final List values = commandLine.getValues(option);
239    
240            if (values.size() < minimum) {
241                throw new OptionException(option, ResourceConstants.ARGUMENT_MISSING_VALUES);
242            }
243    
244            if (values.size() > maximum) {
245                throw new OptionException(option, ResourceConstants.ARGUMENT_UNEXPECTED_VALUE,
246                                          (String) values.get(maximum));
247            }
248    
249            if (validator != null) {
250                try {
251                    validator.validate(values);
252                } catch (InvalidArgumentException ive) {
253                    throw new OptionException(option, ResourceConstants.ARGUMENT_UNEXPECTED_VALUE,
254                                              ive.getMessage());
255                }
256            }
257        }
258    
259        public void appendUsage(final StringBuffer buffer,
260                                final Set helpSettings,
261                                final Comparator comp) {
262            // do we display the outer optionality
263            final boolean optional = helpSettings.contains(DisplaySetting.DISPLAY_OPTIONAL);
264    
265            // allow numbering if multiple args
266            final boolean numbered =
267                (maximum > 1) && helpSettings.contains(DisplaySetting.DISPLAY_ARGUMENT_NUMBERED);
268    
269            final boolean bracketed = helpSettings.contains(DisplaySetting.DISPLAY_ARGUMENT_BRACKETED);
270    
271            // if infinite args are allowed then crop the list
272            final int max = (maximum == Integer.MAX_VALUE) ? 2 : maximum;
273    
274            int i = 0;
275    
276            // for each argument
277            while (i < max) {
278                // if we're past the first add a space
279                if (i > 0) {
280                    buffer.append(' ');
281                }
282    
283                // if the next arg is optional
284                if ((i >= minimum) && (optional || (i > 0))) {
285                    buffer.append('[');
286                }
287    
288                if (bracketed) {
289                    buffer.append('<');
290                }
291    
292                // add name
293                buffer.append(name);
294                ++i;
295    
296                // if numbering
297                if (numbered) {
298                    buffer.append(i);
299                }
300    
301                if (bracketed) {
302                    buffer.append('>');
303                }
304            }
305    
306            // if infinite args are allowed
307            if (maximum == Integer.MAX_VALUE) {
308                // append elipsis
309                buffer.append(" ...");
310            }
311    
312            // for each argument
313            while (i > 0) {
314                --i;
315    
316                // if the next arg is optional
317                if ((i >= minimum) && (optional || (i > 0))) {
318                    buffer.append(']');
319                }
320            }
321        }
322    
323        public String getDescription() {
324            return description;
325        }
326    
327        public List helpLines(final int depth,
328                              final Set helpSettings,
329                              final Comparator comp) {
330            final HelpLine helpLine = new HelpLineImpl(this, depth);
331    
332            return Collections.singletonList(helpLine);
333        }
334    
335        public int getMaximum() {
336            return maximum;
337        }
338    
339        public int getMinimum() {
340            return minimum;
341        }
342    
343        /**
344         * If there are any leading or trailing quotes remove them from the
345         * specified token.
346         *
347         * @param token
348         *            the token to strip leading and trailing quotes
349         *
350         * @return String the possibly modified token
351         */
352        public String stripBoundaryQuotes(String token) {
353            if (!token.startsWith("\"") || !token.endsWith("\"")) {
354                return token;
355            }
356    
357            token = token.substring(1, token.length() - 1);
358    
359            return token;
360        }
361    
362        public boolean isRequired() {
363            return getMinimum() > 0;
364        }
365    
366        public void defaults(final WriteableCommandLine commandLine) {
367            super.defaults(commandLine);
368            defaultValues(commandLine, this);
369        }
370    
371        public void defaultValues(final WriteableCommandLine commandLine,
372                                  final Option option) {
373            commandLine.setDefaultValues(option, defaultValues);
374        }
375    }