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.Collections;
020import java.util.Comparator;
021import java.util.List;
022import java.util.ListIterator;
023import java.util.Set;
024import java.util.StringTokenizer;
025
026import org.apache.commons.cli2.Argument;
027import org.apache.commons.cli2.DisplaySetting;
028import org.apache.commons.cli2.HelpLine;
029import org.apache.commons.cli2.Option;
030import org.apache.commons.cli2.OptionException;
031import org.apache.commons.cli2.WriteableCommandLine;
032import org.apache.commons.cli2.resource.ResourceConstants;
033import org.apache.commons.cli2.resource.ResourceHelper;
034import org.apache.commons.cli2.validation.InvalidArgumentException;
035import org.apache.commons.cli2.validation.Validator;
036
037/**
038 * An implementation of an Argument.
039 */
040public 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}