001    /*
002     * Copyright 2008-2017 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2017 UnboundID Corp.
007     *
008     * This program is free software; you can redistribute it and/or modify
009     * it under the terms of the GNU General Public License (GPLv2 only)
010     * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011     * as published by the Free Software Foundation.
012     *
013     * This program is distributed in the hope that it will be useful,
014     * but WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program; if not, see <http://www.gnu.org/licenses>.
020     */
021    package com.unboundid.util.args;
022    
023    
024    
025    import java.io.BufferedReader;
026    import java.io.File;
027    import java.io.FileReader;
028    import java.io.IOException;
029    import java.io.OutputStream;
030    import java.io.PrintWriter;
031    import java.io.Serializable;
032    import java.util.ArrayList;
033    import java.util.Arrays;
034    import java.util.Collection;
035    import java.util.Collections;
036    import java.util.HashMap;
037    import java.util.Iterator;
038    import java.util.LinkedHashSet;
039    import java.util.LinkedHashMap;
040    import java.util.List;
041    import java.util.Map;
042    import java.util.Set;
043    
044    import com.unboundid.util.Debug;
045    import com.unboundid.util.ObjectPair;
046    import com.unboundid.util.ThreadSafety;
047    import com.unboundid.util.ThreadSafetyLevel;
048    
049    import static com.unboundid.util.StaticUtils.*;
050    import static com.unboundid.util.Validator.*;
051    import static com.unboundid.util.args.ArgsMessages.*;
052    
053    
054    
055    /**
056     * This class provides an argument parser, which may be used to process command
057     * line arguments provided to Java applications.  See the package-level Javadoc
058     * documentation for details regarding the capabilities of the argument parser.
059     */
060    @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
061    public final class ArgumentParser
062           implements Serializable
063    {
064      /**
065       * The name of the system property that can be used to specify the default
066       * properties file that should be used to obtain the default values for
067       * arguments not specified via the command line.
068       */
069      public static final String PROPERTY_DEFAULT_PROPERTIES_FILE_PATH =
070           ArgumentParser.class.getName() + ".propertiesFilePath";
071    
072    
073    
074      /**
075       * The name of an environment variable that can be used to specify the default
076       * properties file that should be used to obtain the default values for
077       * arguments not specified via the command line.
078       */
079      public static final String ENV_DEFAULT_PROPERTIES_FILE_PATH =
080           "UNBOUNDID_TOOL_PROPERTIES_FILE_PATH";
081    
082    
083    
084      /**
085       * The name of the argument used to specify the path to a file to which all
086       * output should be written.
087       */
088      private static final String ARG_NAME_OUTPUT_FILE = "outputFile";
089    
090    
091    
092      /**
093       * The name of the argument used to indicate that output should be written to
094       * both the output file and the console.
095       */
096      private static final String ARG_NAME_TEE_OUTPUT = "teeOutput";
097    
098    
099    
100      /**
101       * The name of the argument used to specify the path to a properties file from
102       * which to obtain the default values for arguments not specified via the
103       * command line.
104       */
105      private static final String ARG_NAME_PROPERTIES_FILE_PATH =
106           "propertiesFilePath";
107    
108    
109    
110      /**
111       * The name of the argument used to specify the path to a file to be generated
112       * with information about the properties that the tool supports.
113       */
114      private static final String ARG_NAME_GENERATE_PROPERTIES_FILE =
115           "generatePropertiesFile";
116    
117    
118    
119      /**
120       * The name of the argument used to indicate that the tool should not use any
121       * properties file to obtain default values for arguments not specified via
122       * the command line.
123       */
124      private static final String ARG_NAME_NO_PROPERTIES_FILE = "noPropertiesFile";
125    
126    
127    
128      /**
129       * The serial version UID for this serializable class.
130       */
131      private static final long serialVersionUID = 3053102992180360269L;
132    
133    
134    
135      // The properties file used to obtain arguments for this tool.
136      private volatile File propertiesFileUsed;
137    
138      // The maximum number of trailing arguments allowed to be provided.
139      private final int maxTrailingArgs;
140    
141      // The minimum number of trailing arguments allowed to be provided.
142      private final int minTrailingArgs;
143    
144      // The set of named arguments associated with this parser, indexed by short
145      // identifier.
146      private final LinkedHashMap<Character,Argument> namedArgsByShortID;
147    
148      // The set of named arguments associated with this parser, indexed by long
149      // identifier.
150      private final LinkedHashMap<String,Argument> namedArgsByLongID;
151    
152      // The set of subcommands associated with this parser, indexed by name.
153      private final LinkedHashMap<String,SubCommand> subCommandsByName;
154    
155      // The full set of named arguments associated with this parser.
156      private final List<Argument> namedArgs;
157    
158      // Sets of arguments in which if the key argument is provided, then at least
159      // one of the value arguments must also be provided.
160      private final List<ObjectPair<Argument,Set<Argument>>> dependentArgumentSets;
161    
162      // Sets of arguments in which at most one argument in the list is allowed to
163      // be present.
164      private final List<Set<Argument>> exclusiveArgumentSets;
165    
166      // Sets of arguments in which at least one argument in the list is required to
167      // be present.
168      private final List<Set<Argument>> requiredArgumentSets;
169    
170      // A list of any arguments set from the properties file rather than explicitly
171      // provided on the command line.
172      private final List<String> argumentsSetFromPropertiesFile;
173    
174      // The list of trailing arguments provided on the command line.
175      private final List<String> trailingArgs;
176    
177      // The full list of subcommands associated with this argument parser.
178      private final List<SubCommand> subCommands;
179    
180      // The description for the associated command.
181      private final String commandDescription;
182    
183      // The name for the associated command.
184      private final String commandName;
185    
186      // The placeholder string for the trailing arguments.
187      private final String trailingArgsPlaceholder;
188    
189      // The subcommand with which this argument parser is associated.
190      private volatile SubCommand parentSubCommand;
191    
192      // The subcommand that was included in the set of command-line arguments.
193      private volatile SubCommand selectedSubCommand;
194    
195    
196    
197      /**
198       * Creates a new instance of this argument parser with the provided
199       * information.  It will not allow unnamed trailing arguments.
200       *
201       * @param  commandName         The name of the application or utility with
202       *                             which this argument parser is associated.  It
203       *                             must not be {@code null}.
204       * @param  commandDescription  A description of the application or utility
205       *                             with which this argument parser is associated.
206       *                             It will be included in generated usage
207       *                             information.  It must not be {@code null}.
208       *
209       * @throws  ArgumentException  If either the command name or command
210       *                             description is {@code null},
211       */
212      public ArgumentParser(final String commandName,
213                            final String commandDescription)
214             throws ArgumentException
215      {
216        this(commandName, commandDescription, 0, null);
217      }
218    
219    
220    
221      /**
222       * Creates a new instance of this argument parser with the provided
223       * information.
224       *
225       * @param  commandName              The name of the application or utility
226       *                                  with which this argument parser is
227       *                                  associated.  It must not be {@code null}.
228       * @param  commandDescription       A description of the application or
229       *                                  utility with which this argument parser is
230       *                                  associated.  It will be included in
231       *                                  generated usage information.  It must not
232       *                                  be {@code null}.
233       * @param  maxTrailingArgs          The maximum number of trailing arguments
234       *                                  that may be provided to this command.  A
235       *                                  value of zero indicates that no trailing
236       *                                  arguments will be allowed.  A value less
237       *                                  than zero will indicate that there is no
238       *                                  limit on the number of trailing arguments
239       *                                  allowed.
240       * @param  trailingArgsPlaceholder  A placeholder string that will be included
241       *                                  in usage output to indicate what trailing
242       *                                  arguments may be provided.  It must not be
243       *                                  {@code null} if {@code maxTrailingArgs} is
244       *                                  anything other than zero.
245       *
246       * @throws  ArgumentException  If either the command name or command
247       *                             description is {@code null}, or if the maximum
248       *                             number of trailing arguments is non-zero and
249       *                             the trailing arguments placeholder is
250       *                             {@code null}.
251       */
252      public ArgumentParser(final String commandName,
253                            final String commandDescription,
254                            final int maxTrailingArgs,
255                            final String trailingArgsPlaceholder)
256             throws ArgumentException
257      {
258        this(commandName, commandDescription, 0, maxTrailingArgs,
259             trailingArgsPlaceholder);
260      }
261    
262    
263    
264      /**
265       * Creates a new instance of this argument parser with the provided
266       * information.
267       *
268       * @param  commandName              The name of the application or utility
269       *                                  with which this argument parser is
270       *                                  associated.  It must not be {@code null}.
271       * @param  commandDescription       A description of the application or
272       *                                  utility with which this argument parser is
273       *                                  associated.  It will be included in
274       *                                  generated usage information.  It must not
275       *                                  be {@code null}.
276       * @param  minTrailingArgs          The minimum number of trailing arguments
277       *                                  that must be provided for this command.  A
278       *                                  value of zero indicates that the command
279       *                                  may be invoked without any trailing
280       *                                  arguments.
281       * @param  maxTrailingArgs          The maximum number of trailing arguments
282       *                                  that may be provided to this command.  A
283       *                                  value of zero indicates that no trailing
284       *                                  arguments will be allowed.  A value less
285       *                                  than zero will indicate that there is no
286       *                                  limit on the number of trailing arguments
287       *                                  allowed.
288       * @param  trailingArgsPlaceholder  A placeholder string that will be included
289       *                                  in usage output to indicate what trailing
290       *                                  arguments may be provided.  It must not be
291       *                                  {@code null} if {@code maxTrailingArgs} is
292       *                                  anything other than zero.
293       *
294       * @throws  ArgumentException  If either the command name or command
295       *                             description is {@code null}, or if the maximum
296       *                             number of trailing arguments is non-zero and
297       *                             the trailing arguments placeholder is
298       *                             {@code null}.
299       */
300      public ArgumentParser(final String commandName,
301                            final String commandDescription,
302                            final int minTrailingArgs,
303                            final int maxTrailingArgs,
304                            final String trailingArgsPlaceholder)
305             throws ArgumentException
306      {
307        if (commandName == null)
308        {
309          throw new ArgumentException(ERR_PARSER_COMMAND_NAME_NULL.get());
310        }
311    
312        if (commandDescription == null)
313        {
314          throw new ArgumentException(ERR_PARSER_COMMAND_DESCRIPTION_NULL.get());
315        }
316    
317        if ((maxTrailingArgs != 0) && (trailingArgsPlaceholder == null))
318        {
319          throw new ArgumentException(
320                         ERR_PARSER_TRAILING_ARGS_PLACEHOLDER_NULL.get());
321        }
322    
323        this.commandName             = commandName;
324        this.commandDescription      = commandDescription;
325        this.trailingArgsPlaceholder = trailingArgsPlaceholder;
326    
327        if (minTrailingArgs >= 0)
328        {
329          this.minTrailingArgs = minTrailingArgs;
330        }
331        else
332        {
333          this.minTrailingArgs = 0;
334        }
335    
336        if (maxTrailingArgs >= 0)
337        {
338          this.maxTrailingArgs = maxTrailingArgs;
339        }
340        else
341        {
342          this.maxTrailingArgs = Integer.MAX_VALUE;
343        }
344    
345        if (this.minTrailingArgs > this.maxTrailingArgs)
346        {
347          throw new ArgumentException(ERR_PARSER_TRAILING_ARGS_COUNT_MISMATCH.get(
348               this.minTrailingArgs, this.maxTrailingArgs));
349        }
350    
351        namedArgsByShortID    = new LinkedHashMap<Character,Argument>();
352        namedArgsByLongID     = new LinkedHashMap<String,Argument>();
353        namedArgs             = new ArrayList<Argument>();
354        trailingArgs          = new ArrayList<String>();
355        dependentArgumentSets = new ArrayList<ObjectPair<Argument,Set<Argument>>>();
356        exclusiveArgumentSets = new ArrayList<Set<Argument>>();
357        requiredArgumentSets  = new ArrayList<Set<Argument>>();
358        parentSubCommand      = null;
359        selectedSubCommand    = null;
360        subCommands           = new ArrayList<SubCommand>();
361        subCommandsByName     = new LinkedHashMap<String,SubCommand>(10);
362        propertiesFileUsed    = null;
363        argumentsSetFromPropertiesFile = new ArrayList<String>();
364      }
365    
366    
367    
368      /**
369       * Creates a new argument parser that is a "clean" copy of the provided source
370       * argument parser.
371       *
372       * @param  source      The source argument parser to use for this argument
373       *                     parser.
374       * @param  subCommand  The subcommand with which this argument parser is to be
375       *                     associated.
376       */
377      ArgumentParser(final ArgumentParser source, final SubCommand subCommand)
378      {
379        commandName             = source.commandName;
380        commandDescription      = source.commandDescription;
381        minTrailingArgs         = source.minTrailingArgs;
382        maxTrailingArgs         = source.maxTrailingArgs;
383        trailingArgsPlaceholder = source.trailingArgsPlaceholder;
384    
385        propertiesFileUsed = null;
386        argumentsSetFromPropertiesFile = new ArrayList<String>();
387        trailingArgs = new ArrayList<String>();
388    
389        namedArgs = new ArrayList<Argument>(source.namedArgs.size());
390        namedArgsByLongID =
391             new LinkedHashMap<String,Argument>(source.namedArgsByLongID.size());
392        namedArgsByShortID = new LinkedHashMap<Character,Argument>(
393             source.namedArgsByShortID.size());
394    
395        final LinkedHashMap<String,Argument> argsByID =
396             new LinkedHashMap<String,Argument>(source.namedArgs.size());
397        for (final Argument sourceArg : source.namedArgs)
398        {
399          final Argument a = sourceArg.getCleanCopy();
400    
401          try
402          {
403            a.setRegistered();
404          }
405          catch (final ArgumentException ae)
406          {
407            // This should never happen.
408            Debug.debugException(ae);
409          }
410    
411          namedArgs.add(a);
412          argsByID.put(a.getIdentifierString(), a);
413    
414          for (final Character c : a.getShortIdentifiers())
415          {
416            namedArgsByShortID.put(c, a);
417          }
418    
419          for (final String s : a.getLongIdentifiers())
420          {
421            namedArgsByLongID.put(toLowerCase(s), a);
422          }
423        }
424    
425        dependentArgumentSets = new ArrayList<ObjectPair<Argument,Set<Argument>>>(
426             source.dependentArgumentSets.size());
427        for (final ObjectPair<Argument,Set<Argument>> p :
428             source.dependentArgumentSets)
429        {
430          final Set<Argument> sourceSet = p.getSecond();
431          final LinkedHashSet<Argument> newSet =
432               new LinkedHashSet<Argument>(sourceSet.size());
433          for (final Argument a : sourceSet)
434          {
435            newSet.add(argsByID.get(a.getIdentifierString()));
436          }
437    
438          final Argument sourceFirst = p.getFirst();
439          final Argument newFirst = argsByID.get(sourceFirst.getIdentifierString());
440          dependentArgumentSets.add(
441               new ObjectPair<Argument, Set<Argument>>(newFirst, newSet));
442        }
443    
444        exclusiveArgumentSets =
445             new ArrayList<Set<Argument>>(source.exclusiveArgumentSets.size());
446        for (final Set<Argument> sourceSet : source.exclusiveArgumentSets)
447        {
448          final LinkedHashSet<Argument> newSet =
449               new LinkedHashSet<Argument>(sourceSet.size());
450          for (final Argument a : sourceSet)
451          {
452            newSet.add(argsByID.get(a.getIdentifierString()));
453          }
454    
455          exclusiveArgumentSets.add(newSet);
456        }
457    
458        requiredArgumentSets =
459             new ArrayList<Set<Argument>>(source.requiredArgumentSets.size());
460        for (final Set<Argument> sourceSet : source.requiredArgumentSets)
461        {
462          final LinkedHashSet<Argument> newSet =
463               new LinkedHashSet<Argument>(sourceSet.size());
464          for (final Argument a : sourceSet)
465          {
466            newSet.add(argsByID.get(a.getIdentifierString()));
467          }
468          requiredArgumentSets.add(newSet);
469        }
470    
471        parentSubCommand = subCommand;
472        selectedSubCommand = null;
473        subCommands = new ArrayList<SubCommand>(source.subCommands.size());
474        subCommandsByName =
475             new LinkedHashMap<String,SubCommand>(source.subCommandsByName.size());
476        for (final SubCommand sc : source.subCommands)
477        {
478          subCommands.add(sc.getCleanCopy());
479          for (final String name : sc.getNames())
480          {
481            subCommandsByName.put(toLowerCase(name), sc);
482          }
483        }
484      }
485    
486    
487    
488      /**
489       * Retrieves the name of the application or utility with which this command
490       * line argument parser is associated.
491       *
492       * @return  The name of the application or utility with which this command
493       *          line argument parser is associated.
494       */
495      public String getCommandName()
496      {
497        return commandName;
498      }
499    
500    
501    
502      /**
503       * Retrieves a description of the application or utility with which this
504       * command line argument parser is associated.
505       *
506       * @return  A description of the application or utility with which this
507       *          command line argument parser is associated.
508       */
509      public String getCommandDescription()
510      {
511        return commandDescription;
512      }
513    
514    
515    
516      /**
517       * Indicates whether this argument parser allows any unnamed trailing
518       * arguments to be provided.
519       *
520       * @return  {@code true} if at least one unnamed trailing argument may be
521       *          provided, or {@code false} if not.
522       */
523      public boolean allowsTrailingArguments()
524      {
525        return (maxTrailingArgs != 0);
526      }
527    
528    
529    
530      /**
531       * Indicates whether this argument parser requires at least unnamed trailing
532       * argument to be provided.
533       *
534       * @return  {@code true} if at least one unnamed trailing argument must be
535       *          provided, or {@code false} if the tool may be invoked without any
536       *          such arguments.
537       */
538      public boolean requiresTrailingArguments()
539      {
540        return (minTrailingArgs != 0);
541      }
542    
543    
544    
545      /**
546       * Retrieves the placeholder string that will be provided in usage information
547       * to indicate what may be included in the trailing arguments.
548       *
549       * @return  The placeholder string that will be provided in usage information
550       *          to indicate what may be included in the trailing arguments, or
551       *          {@code null} if unnamed trailing arguments are not allowed.
552       */
553      public String getTrailingArgumentsPlaceholder()
554      {
555        return trailingArgsPlaceholder;
556      }
557    
558    
559    
560      /**
561       * Retrieves the minimum number of unnamed trailing arguments that must be
562       * provided.
563       *
564       * @return  The minimum number of unnamed trailing arguments that must be
565       *          provided.
566       */
567      public int getMinTrailingArguments()
568      {
569        return minTrailingArgs;
570      }
571    
572    
573    
574      /**
575       * Retrieves the maximum number of unnamed trailing arguments that may be
576       * provided.
577       *
578       * @return  The maximum number of unnamed trailing arguments that may be
579       *          provided.
580       */
581      public int getMaxTrailingArguments()
582      {
583        return maxTrailingArgs;
584      }
585    
586    
587    
588      /**
589       * Updates this argument parser to enable support for a properties file that
590       * can be used to specify the default values for any properties that were not
591       * supplied via the command line.  This method should be invoked after the
592       * argument parser has been configured with all of the other arguments that it
593       * supports and before the {@link #parse} method is invoked.  In addition,
594       * after invoking the {@code parse} method, the caller must also invoke the
595       * {@link #getGeneratedPropertiesFile} method to determine if the only
596       * processing performed that should be performed is the generation of a
597       * properties file that will have already been performed.
598       * <BR><BR>
599       * This method will update the argument parser to add the following additional
600       * arguments:
601       * <UL>
602       *   <LI>
603       *     {@code propertiesFilePath} -- Specifies the path to the properties file
604       *     that should be used to obtain default values for any arguments not
605       *     provided on the command line.  If this is not specified and the
606       *     {@code noPropertiesFile} argument is not present, then the argument
607       *     parser may use a default properties file path specified using either
608       *     the {@code com.unboundid.util.args.ArgumentParser..propertiesFilePath}
609       *     system property or the {@code UNBOUNDID_TOOL_PROPERTIES_FILE_PATH}
610       *     environment variable.
611       *   </LI>
612       *   <LI>
613       *     {@code generatePropertiesFile} -- Indicates that the tool should
614       *     generate a properties file for this argument parser and write it to the
615       *     specified location.  The generated properties file will not have any
616       *     properties set, but will include comments that describe all of the
617       *     supported arguments, as well general information about the use of a
618       *     properties file.  If this argument is specified on the command line,
619       *     then no other arguments should be given.
620       *   </LI>
621       *   <LI>
622       *     {@code noPropertiesFile} -- Indicates that the tool should not use a
623       *     properties file to obtain default values for any arguments not provided
624       *     on the command line.
625       *   </LI>
626       * </UL>
627       *
628       * @throws  ArgumentException  If any of the arguments related to properties
629       *                             file processing conflicts with an argument that
630       *                             has already been added to the argument parser.
631       */
632      public void enablePropertiesFileSupport()
633             throws ArgumentException
634      {
635        final FileArgument propertiesFilePath = new FileArgument(null,
636             ARG_NAME_PROPERTIES_FILE_PATH, false, 1, null,
637             INFO_ARG_DESCRIPTION_PROP_FILE_PATH.get(), true, true, true, false);
638        propertiesFilePath.setUsageArgument(true);
639        propertiesFilePath.addLongIdentifier("properties-file-path");
640        addArgument(propertiesFilePath);
641    
642        final FileArgument generatePropertiesFile = new FileArgument(null,
643             ARG_NAME_GENERATE_PROPERTIES_FILE, false, 1, null,
644             INFO_ARG_DESCRIPTION_GEN_PROP_FILE.get(), false, true, true, false);
645        generatePropertiesFile.setUsageArgument(true);
646        generatePropertiesFile.addLongIdentifier("generate-properties-file");
647        addArgument(generatePropertiesFile);
648    
649        final BooleanArgument noPropertiesFile = new BooleanArgument(null,
650             ARG_NAME_NO_PROPERTIES_FILE, INFO_ARG_DESCRIPTION_NO_PROP_FILE.get());
651        noPropertiesFile.setUsageArgument(true);
652        noPropertiesFile.addLongIdentifier("no-properties-file");
653        addArgument(noPropertiesFile);
654    
655    
656        // The propertiesFilePath and noPropertiesFile arguments cannot be used
657        // together.
658        addExclusiveArgumentSet(propertiesFilePath, noPropertiesFile);
659      }
660    
661    
662    
663      /**
664       * Indicates whether this argument parser was used to generate a properties
665       * file.  If so, then the tool invoking the parser should return without
666       * performing any further processing.
667       *
668       * @return  A {@code File} object that represents the path to the properties
669       *          file that was generated, or {@code null} if no properties file was
670       *          generated.
671       */
672      public File getGeneratedPropertiesFile()
673      {
674        final Argument a = getNamedArgument(ARG_NAME_GENERATE_PROPERTIES_FILE);
675        if ((a == null) || (! a.isPresent()) || (! (a instanceof FileArgument)))
676        {
677          return null;
678        }
679    
680        return ((FileArgument) a).getValue();
681      }
682    
683    
684    
685      /**
686       * Retrieves the named argument with the specified short identifier.
687       *
688       * @param  shortIdentifier  The short identifier of the argument to retrieve.
689       *                          It must not be {@code null}.
690       *
691       * @return  The named argument with the specified short identifier, or
692       *          {@code null} if there is no such argument.
693       */
694      public Argument getNamedArgument(final Character shortIdentifier)
695      {
696        ensureNotNull(shortIdentifier);
697        return namedArgsByShortID.get(shortIdentifier);
698      }
699    
700    
701    
702      /**
703       * Retrieves the named argument with the specified identifier.
704       *
705       * @param  identifier  The identifier of the argument to retrieve.  It may be
706       *                     the long identifier without any dashes, the short
707       *                     identifier character preceded by a single dash, or the
708       *                     long identifier preceded by two dashes. It must not be
709       *                     {@code null}.
710       *
711       * @return  The named argument with the specified long identifier, or
712       *          {@code null} if there is no such argument.
713       */
714      public Argument getNamedArgument(final String identifier)
715      {
716        ensureNotNull(identifier);
717    
718        if (identifier.startsWith("--") && (identifier.length() > 2))
719        {
720          return namedArgsByLongID.get(toLowerCase(identifier.substring(2)));
721        }
722        else if (identifier.startsWith("-") && (identifier.length() == 2))
723        {
724          return namedArgsByShortID.get(identifier.charAt(1));
725        }
726        else
727        {
728          return namedArgsByLongID.get(toLowerCase(identifier));
729        }
730      }
731    
732    
733    
734      /**
735       * Retrieves the argument list argument with the specified identifier.
736       *
737       * @param  identifier  The identifier of the argument to retrieve.  It may be
738       *                     the long identifier without any dashes, the short
739       *                     identifier character preceded by a single dash, or the
740       *                     long identifier preceded by two dashes. It must not be
741       *                     {@code null}.
742       *
743       * @return  The argument list argument with the specified identifier, or
744       *          {@code null} if there is no such argument.
745       */
746      public ArgumentListArgument getArgumentListArgument(final String identifier)
747      {
748        final Argument a = getNamedArgument(identifier);
749        if (a == null)
750        {
751          return null;
752        }
753        else
754        {
755          return (ArgumentListArgument) a;
756        }
757      }
758    
759    
760    
761      /**
762       * Retrieves the Boolean argument with the specified identifier.
763       *
764       * @param  identifier  The identifier of the argument to retrieve.  It may be
765       *                     the long identifier without any dashes, the short
766       *                     identifier character preceded by a single dash, or the
767       *                     long identifier preceded by two dashes. It must not be
768       *                     {@code null}.
769       *
770       * @return  The Boolean argument with the specified identifier, or
771       *          {@code null} if there is no such argument.
772       */
773      public BooleanArgument getBooleanArgument(final String identifier)
774      {
775        final Argument a = getNamedArgument(identifier);
776        if (a == null)
777        {
778          return null;
779        }
780        else
781        {
782          return (BooleanArgument) a;
783        }
784      }
785    
786    
787    
788      /**
789       * Retrieves the Boolean value argument with the specified identifier.
790       *
791       * @param  identifier  The identifier of the argument to retrieve.  It may be
792       *                     the long identifier without any dashes, the short
793       *                     identifier character preceded by a single dash, or the
794       *                     long identifier preceded by two dashes. It must not be
795       *                     {@code null}.
796       *
797       * @return  The Boolean value argument with the specified identifier, or
798       *          {@code null} if there is no such argument.
799       */
800      public BooleanValueArgument getBooleanValueArgument(final String identifier)
801      {
802        final Argument a = getNamedArgument(identifier);
803        if (a == null)
804        {
805          return null;
806        }
807        else
808        {
809          return (BooleanValueArgument) a;
810        }
811      }
812    
813    
814    
815      /**
816       * Retrieves the control argument with the specified identifier.
817       *
818       * @param  identifier  The identifier of the argument to retrieve.  It may be
819       *                     the long identifier without any dashes, the short
820       *                     identifier character preceded by a single dash, or the
821       *                     long identifier preceded by two dashes. It must not be
822       *                     {@code null}.
823       *
824       * @return  The control argument with the specified identifier, or
825       *          {@code null} if there is no such argument.
826       */
827      public ControlArgument getControlArgument(final String identifier)
828      {
829        final Argument a = getNamedArgument(identifier);
830        if (a == null)
831        {
832          return null;
833        }
834        else
835        {
836          return (ControlArgument) a;
837        }
838      }
839    
840    
841    
842      /**
843       * Retrieves the DN argument with the specified identifier.
844       *
845       * @param  identifier  The identifier of the argument to retrieve.  It may be
846       *                     the long identifier without any dashes, the short
847       *                     identifier character preceded by a single dash, or the
848       *                     long identifier preceded by two dashes. It must not be
849       *                     {@code null}.
850       *
851       * @return  The DN argument with the specified identifier, or
852       *          {@code null} if there is no such argument.
853       */
854      public DNArgument getDNArgument(final String identifier)
855      {
856        final Argument a = getNamedArgument(identifier);
857        if (a == null)
858        {
859          return null;
860        }
861        else
862        {
863          return (DNArgument) a;
864        }
865      }
866    
867    
868    
869      /**
870       * Retrieves the duration argument with the specified identifier.
871       *
872       * @param  identifier  The identifier of the argument to retrieve.  It may be
873       *                     the long identifier without any dashes, the short
874       *                     identifier character preceded by a single dash, or the
875       *                     long identifier preceded by two dashes. It must not be
876       *                     {@code null}.
877       *
878       * @return  The duration argument with the specified identifier, or
879       *          {@code null} if there is no such argument.
880       */
881      public DurationArgument getDurationArgument(final String identifier)
882      {
883        final Argument a = getNamedArgument(identifier);
884        if (a == null)
885        {
886          return null;
887        }
888        else
889        {
890          return (DurationArgument) a;
891        }
892      }
893    
894    
895    
896      /**
897       * Retrieves the file argument with the specified identifier.
898       *
899       * @param  identifier  The identifier of the argument to retrieve.  It may be
900       *                     the long identifier without any dashes, the short
901       *                     identifier character preceded by a single dash, or the
902       *                     long identifier preceded by two dashes. It must not be
903       *                     {@code null}.
904       *
905       * @return  The file argument with the specified identifier, or
906       *          {@code null} if there is no such argument.
907       */
908      public FileArgument getFileArgument(final String identifier)
909      {
910        final Argument a = getNamedArgument(identifier);
911        if (a == null)
912        {
913          return null;
914        }
915        else
916        {
917          return (FileArgument) a;
918        }
919      }
920    
921    
922    
923      /**
924       * Retrieves the filter argument with the specified identifier.
925       *
926       * @param  identifier  The identifier of the argument to retrieve.  It may be
927       *                     the long identifier without any dashes, the short
928       *                     identifier character preceded by a single dash, or the
929       *                     long identifier preceded by two dashes. It must not be
930       *                     {@code null}.
931       *
932       * @return  The filter argument with the specified identifier, or
933       *          {@code null} if there is no such argument.
934       */
935      public FilterArgument getFilterArgument(final String identifier)
936      {
937        final Argument a = getNamedArgument(identifier);
938        if (a == null)
939        {
940          return null;
941        }
942        else
943        {
944          return (FilterArgument) a;
945        }
946      }
947    
948    
949    
950      /**
951       * Retrieves the integer argument with the specified identifier.
952       *
953       * @param  identifier  The identifier of the argument to retrieve.  It may be
954       *                     the long identifier without any dashes, the short
955       *                     identifier character preceded by a single dash, or the
956       *                     long identifier preceded by two dashes. It must not be
957       *                     {@code null}.
958       *
959       * @return  The integer argument with the specified identifier, or
960       *          {@code null} if there is no such argument.
961       */
962      public IntegerArgument getIntegerArgument(final String identifier)
963      {
964        final Argument a = getNamedArgument(identifier);
965        if (a == null)
966        {
967          return null;
968        }
969        else
970        {
971          return (IntegerArgument) a;
972        }
973      }
974    
975    
976    
977      /**
978       * Retrieves the scope argument with the specified identifier.
979       *
980       * @param  identifier  The identifier of the argument to retrieve.  It may be
981       *                     the long identifier without any dashes, the short
982       *                     identifier character preceded by a single dash, or the
983       *                     long identifier preceded by two dashes. It must not be
984       *                     {@code null}.
985       *
986       * @return  The scope argument with the specified identifier, or
987       *          {@code null} if there is no such argument.
988       */
989      public ScopeArgument getScopeArgument(final String identifier)
990      {
991        final Argument a = getNamedArgument(identifier);
992        if (a == null)
993        {
994          return null;
995        }
996        else
997        {
998          return (ScopeArgument) a;
999        }
1000      }
1001    
1002    
1003    
1004      /**
1005       * Retrieves the string argument with the specified identifier.
1006       *
1007       * @param  identifier  The identifier of the argument to retrieve.  It may be
1008       *                     the long identifier without any dashes, the short
1009       *                     identifier character preceded by a single dash, or the
1010       *                     long identifier preceded by two dashes. It must not be
1011       *                     {@code null}.
1012       *
1013       * @return  The string argument with the specified identifier, or
1014       *          {@code null} if there is no such argument.
1015       */
1016      public StringArgument getStringArgument(final String identifier)
1017      {
1018        final Argument a = getNamedArgument(identifier);
1019        if (a == null)
1020        {
1021          return null;
1022        }
1023        else
1024        {
1025          return (StringArgument) a;
1026        }
1027      }
1028    
1029    
1030    
1031      /**
1032       * Retrieves the timestamp argument with the specified identifier.
1033       *
1034       * @param  identifier  The identifier of the argument to retrieve.  It may be
1035       *                     the long identifier without any dashes, the short
1036       *                     identifier character preceded by a single dash, or the
1037       *                     long identifier preceded by two dashes. It must not be
1038       *                     {@code null}.
1039       *
1040       * @return  The timestamp argument with the specified identifier, or
1041       *          {@code null} if there is no such argument.
1042       */
1043      public TimestampArgument getTimestampArgument(final String identifier)
1044      {
1045        final Argument a = getNamedArgument(identifier);
1046        if (a == null)
1047        {
1048          return null;
1049        }
1050        else
1051        {
1052          return (TimestampArgument) a;
1053        }
1054      }
1055    
1056    
1057    
1058      /**
1059       * Retrieves the set of named arguments defined for use with this argument
1060       * parser.
1061       *
1062       * @return  The set of named arguments defined for use with this argument
1063       *          parser.
1064       */
1065      public List<Argument> getNamedArguments()
1066      {
1067        return Collections.unmodifiableList(namedArgs);
1068      }
1069    
1070    
1071    
1072      /**
1073       * Registers the provided argument with this argument parser.
1074       *
1075       * @param  argument  The argument to be registered.
1076       *
1077       * @throws  ArgumentException  If the provided argument conflicts with another
1078       *                             argument already registered with this parser.
1079       */
1080      public void addArgument(final Argument argument)
1081             throws ArgumentException
1082      {
1083        argument.setRegistered();
1084        for (final Character c : argument.getShortIdentifiers())
1085        {
1086          if (namedArgsByShortID.containsKey(c))
1087          {
1088            throw new ArgumentException(ERR_PARSER_SHORT_ID_CONFLICT.get(c));
1089          }
1090    
1091          if ((parentSubCommand != null) &&
1092              (parentSubCommand.getArgumentParser().namedArgsByShortID.containsKey(
1093                   c)))
1094          {
1095            throw new ArgumentException(ERR_PARSER_SHORT_ID_CONFLICT.get(c));
1096          }
1097        }
1098    
1099        for (final String s : argument.getLongIdentifiers())
1100        {
1101          if (namedArgsByLongID.containsKey(toLowerCase(s)))
1102          {
1103            throw new ArgumentException(ERR_PARSER_LONG_ID_CONFLICT.get(s));
1104          }
1105    
1106          if ((parentSubCommand != null) &&
1107              (parentSubCommand.getArgumentParser().namedArgsByLongID.containsKey(
1108                    toLowerCase(s))))
1109          {
1110            throw new ArgumentException(ERR_PARSER_LONG_ID_CONFLICT.get(s));
1111          }
1112        }
1113    
1114        for (final SubCommand sc : subCommands)
1115        {
1116          final ArgumentParser parser = sc.getArgumentParser();
1117          for (final Character c : argument.getShortIdentifiers())
1118          {
1119            if (parser.namedArgsByShortID.containsKey(c))
1120            {
1121              throw new ArgumentException(
1122                   ERR_PARSER_SHORT_ID_CONFLICT_WITH_SUBCOMMAND.get(c,
1123                        sc.getPrimaryName()));
1124            }
1125          }
1126    
1127          for (final String s : argument.getLongIdentifiers())
1128          {
1129            if (parser.namedArgsByLongID.containsKey(toLowerCase(s)))
1130            {
1131              throw new ArgumentException(
1132                   ERR_PARSER_LONG_ID_CONFLICT_WITH_SUBCOMMAND.get(s,
1133                        sc.getPrimaryName()));
1134            }
1135          }
1136        }
1137    
1138        for (final Character c : argument.getShortIdentifiers())
1139        {
1140          namedArgsByShortID.put(c, argument);
1141        }
1142    
1143        for (final String s : argument.getLongIdentifiers())
1144        {
1145          namedArgsByLongID.put(toLowerCase(s), argument);
1146        }
1147    
1148        namedArgs.add(argument);
1149      }
1150    
1151    
1152    
1153      /**
1154       * Retrieves the list of dependent argument sets for this argument parser.  If
1155       * an argument contained as the first object in the pair in a dependent
1156       * argument set is provided, then at least one of the arguments in the paired
1157       * set must also be provided.
1158       *
1159       * @return  The list of dependent argument sets for this argument parser.
1160       */
1161      public List<ObjectPair<Argument,Set<Argument>>> getDependentArgumentSets()
1162      {
1163        return Collections.unmodifiableList(dependentArgumentSets);
1164      }
1165    
1166    
1167    
1168      /**
1169       * Adds the provided collection of arguments as dependent upon the given
1170       * argument.  All of the arguments must have already been registered with this
1171       * argument parser using the {@link #addArgument} method.
1172       *
1173       * @param  targetArgument      The argument whose presence indicates that at
1174       *                             least one of the dependent arguments must also
1175       *                             be present.  It must not be {@code null}, and
1176       *                             it must have already been registered with this
1177       *                             argument parser.
1178       * @param  dependentArguments  The set of arguments from which at least one
1179       *                             argument must be present if the target argument
1180       *                             is present.  It must not be {@code null} or
1181       *                             empty, and all arguments must have already been
1182       *                             registered with this argument parser.
1183       */
1184      public void addDependentArgumentSet(final Argument targetArgument,
1185                       final Collection<Argument> dependentArguments)
1186      {
1187        ensureNotNull(targetArgument, dependentArguments);
1188    
1189        ensureFalse(dependentArguments.isEmpty(),
1190             "The ArgumentParser.addDependentArgumentSet method must not be " +
1191                  "called with an empty collection of dependentArguments");
1192    
1193        ensureTrue(namedArgs.contains(targetArgument),
1194             "The ArgumentParser.addDependentArgumentSet method may only be used " +
1195                  "if all of the provided arguments have already been registered " +
1196                  "with the argument parser via the ArgumentParser.addArgument " +
1197                  "method.  The " + targetArgument.getIdentifierString() +
1198                  " argument has not been registered with the argument parser.");
1199        for (final Argument a : dependentArguments)
1200        {
1201          ensureTrue(namedArgs.contains(a),
1202               "The ArgumentParser.addDependentArgumentSet method may only be " +
1203                    "used if all of the provided arguments have already been " +
1204                    "registered with the argument parser via the " +
1205                    "ArgumentParser.addArgument method.  The " +
1206                    a.getIdentifierString() + " argument has not been registered " +
1207                    "with the argument parser.");
1208        }
1209    
1210        final LinkedHashSet<Argument> argSet =
1211             new LinkedHashSet<Argument>(dependentArguments);
1212        dependentArgumentSets.add(
1213             new ObjectPair<Argument,Set<Argument>>(targetArgument, argSet));
1214      }
1215    
1216    
1217    
1218      /**
1219       * Adds the provided collection of arguments as dependent upon the given
1220       * argument.  All of the arguments must have already been registered with this
1221       * argument parser using the {@link #addArgument} method.
1222       *
1223       * @param  targetArgument  The argument whose presence indicates that at least
1224       *                         one of the dependent arguments must also be
1225       *                         present.  It must not be {@code null}, and it must
1226       *                         have already been registered with this argument
1227       *                         parser.
1228       * @param  dependentArg1   The first argument in the set of arguments in which
1229       *                         at least one argument must be present if the target
1230       *                         argument is present.  It must not be {@code null},
1231       *                         and it must have already been registered with this
1232       *                         argument parser.
1233       * @param  remaining       The remaining arguments in the set of arguments in
1234       *                         which at least one argument must be present if the
1235       *                         target argument is present.  It may be {@code null}
1236       *                         or empty if no additional dependent arguments are
1237       *                         needed, but if it is non-empty then all arguments
1238       *                         must have already been registered with this
1239       *                         argument parser.
1240       */
1241      public void addDependentArgumentSet(final Argument targetArgument,
1242                                          final Argument dependentArg1,
1243                                          final Argument... remaining)
1244      {
1245        ensureNotNull(targetArgument, dependentArg1);
1246    
1247        ensureTrue(namedArgs.contains(targetArgument),
1248             "The ArgumentParser.addDependentArgumentSet method may only be used " +
1249                  "if all of the provided arguments have already been registered " +
1250                  "with the argument parser via the ArgumentParser.addArgument " +
1251                  "method.  The " + targetArgument.getIdentifierString() +
1252                  " argument has not been registered with the argument parser.");
1253        ensureTrue(namedArgs.contains(dependentArg1),
1254             "The ArgumentParser.addDependentArgumentSet method may only be used " +
1255                  "if all of the provided arguments have already been registered " +
1256                  "with the argument parser via the ArgumentParser.addArgument " +
1257                  "method.  The " + dependentArg1.getIdentifierString() +
1258                  " argument has not been registered with the argument parser.");
1259        if (remaining != null)
1260        {
1261          for (final Argument a : remaining)
1262          {
1263            ensureTrue(namedArgs.contains(a),
1264                 "The ArgumentParser.addDependentArgumentSet method may only be " +
1265                      "used if all of the provided arguments have already been " +
1266                      "registered with the argument parser via the " +
1267                      "ArgumentParser.addArgument method.  The " +
1268                      a.getIdentifierString() + " argument has not been " +
1269                      "registered with the argument parser.");
1270          }
1271        }
1272    
1273        final LinkedHashSet<Argument> argSet = new LinkedHashSet<Argument>();
1274        argSet.add(dependentArg1);
1275        if (remaining != null)
1276        {
1277          argSet.addAll(Arrays.asList(remaining));
1278        }
1279    
1280        dependentArgumentSets.add(
1281             new ObjectPair<Argument,Set<Argument>>(targetArgument, argSet));
1282      }
1283    
1284    
1285    
1286      /**
1287       * Retrieves the list of exclusive argument sets for this argument parser.
1288       * If an argument contained in an exclusive argument set is provided, then
1289       * none of the other arguments in that set may be provided.  It is acceptable
1290       * for none of the arguments in the set to be provided, unless the same set
1291       * of arguments is also defined as a required argument set.
1292       *
1293       * @return  The list of exclusive argument sets for this argument parser.
1294       */
1295      public List<Set<Argument>> getExclusiveArgumentSets()
1296      {
1297        return Collections.unmodifiableList(exclusiveArgumentSets);
1298      }
1299    
1300    
1301    
1302      /**
1303       * Adds the provided collection of arguments as an exclusive argument set, in
1304       * which at most one of the arguments may be provided.  All of the arguments
1305       * must have already been registered with this argument parser using the
1306       * {@link #addArgument} method.
1307       *
1308       * @param  exclusiveArguments  The collection of arguments to form an
1309       *                             exclusive argument set.  It must not be
1310       *                             {@code null}, and all of the arguments must
1311       *                             have already been registered with this argument
1312       *                             parser.
1313       */
1314      public void addExclusiveArgumentSet(
1315                       final Collection<Argument> exclusiveArguments)
1316      {
1317        ensureNotNull(exclusiveArguments);
1318    
1319        for (final Argument a : exclusiveArguments)
1320        {
1321          ensureTrue(namedArgs.contains(a),
1322               "The ArgumentParser.addExclusiveArgumentSet method may only be " +
1323                    "used if all of the provided arguments have already been " +
1324                    "registered with the argument parser via the " +
1325                    "ArgumentParser.addArgument method.  The " +
1326                    a.getIdentifierString() + " argument has not been " +
1327                    "registered with the argument parser.");
1328        }
1329    
1330        final LinkedHashSet<Argument> argSet =
1331             new LinkedHashSet<Argument>(exclusiveArguments);
1332        exclusiveArgumentSets.add(Collections.unmodifiableSet(argSet));
1333      }
1334    
1335    
1336    
1337      /**
1338       * Adds the provided set of arguments as an exclusive argument set, in
1339       * which at most one of the arguments may be provided.  All of the arguments
1340       * must have already been registered with this argument parser using the
1341       * {@link #addArgument} method.
1342       *
1343       * @param  arg1       The first argument to include in the exclusive argument
1344       *                    set.  It must not be {@code null}, and it must have
1345       *                    already been registered with this argument parser.
1346       * @param  arg2       The second argument to include in the exclusive argument
1347       *                    set.  It must not be {@code null}, and it must have
1348       *                    already been registered with this argument parser.
1349       * @param  remaining  Any additional arguments to include in the exclusive
1350       *                    argument set.  It may be {@code null} or empty if no
1351       *                    additional exclusive arguments are needed, but if it is
1352       *                    non-empty then all arguments must have already been
1353       *                    registered with this argument parser.
1354       */
1355      public void addExclusiveArgumentSet(final Argument arg1, final Argument arg2,
1356                                          final Argument... remaining)
1357      {
1358        ensureNotNull(arg1, arg2);
1359    
1360        ensureTrue(namedArgs.contains(arg1),
1361             "The ArgumentParser.addExclusiveArgumentSet method may only be " +
1362                  "used if all of the provided arguments have already been " +
1363                  "registered with the argument parser via the " +
1364                  "ArgumentParser.addArgument method.  The " +
1365                  arg1.getIdentifierString() + " argument has not been " +
1366                  "registered with the argument parser.");
1367        ensureTrue(namedArgs.contains(arg2),
1368             "The ArgumentParser.addExclusiveArgumentSet method may only be " +
1369                  "used if all of the provided arguments have already been " +
1370                  "registered with the argument parser via the " +
1371                  "ArgumentParser.addArgument method.  The " +
1372                  arg2.getIdentifierString() + " argument has not been " +
1373                  "registered with the argument parser.");
1374    
1375        if (remaining != null)
1376        {
1377          for (final Argument a : remaining)
1378          {
1379            ensureTrue(namedArgs.contains(a),
1380                 "The ArgumentParser.addExclusiveArgumentSet method may only be " +
1381                      "used if all of the provided arguments have already been " +
1382                      "registered with the argument parser via the " +
1383                      "ArgumentParser.addArgument method.  The " +
1384                      a.getIdentifierString() + " argument has not been " +
1385                      "registered with the argument parser.");
1386          }
1387        }
1388    
1389        final LinkedHashSet<Argument> argSet = new LinkedHashSet<Argument>();
1390        argSet.add(arg1);
1391        argSet.add(arg2);
1392        argSet.addAll(Arrays.asList(remaining));
1393    
1394        exclusiveArgumentSets.add(Collections.unmodifiableSet(argSet));
1395      }
1396    
1397    
1398    
1399      /**
1400       * Retrieves the list of required argument sets for this argument parser.  At
1401       * least one of the arguments contained in this set must be provided.  If this
1402       * same set is also defined as an exclusive argument set, then exactly one
1403       * of those arguments must be provided.
1404       *
1405       * @return  The list of required argument sets for this argument parser.
1406       */
1407      public List<Set<Argument>> getRequiredArgumentSets()
1408      {
1409        return Collections.unmodifiableList(requiredArgumentSets);
1410      }
1411    
1412    
1413    
1414      /**
1415       * Adds the provided collection of arguments as a required argument set, in
1416       * which at least one of the arguments must be provided.  All of the arguments
1417       * must have already been registered with this argument parser using the
1418       * {@link #addArgument} method.
1419       *
1420       * @param  requiredArguments  The collection of arguments to form an
1421       *                            required argument set.  It must not be
1422       *                            {@code null}, and all of the arguments must have
1423       *                            already been registered with this argument
1424       *                            parser.
1425       */
1426      public void addRequiredArgumentSet(
1427                       final Collection<Argument> requiredArguments)
1428      {
1429        ensureNotNull(requiredArguments);
1430    
1431        for (final Argument a : requiredArguments)
1432        {
1433          ensureTrue(namedArgs.contains(a),
1434               "The ArgumentParser.addRequiredArgumentSet method may only be " +
1435                    "used if all of the provided arguments have already been " +
1436                    "registered with the argument parser via the " +
1437                    "ArgumentParser.addArgument method.  The " +
1438                    a.getIdentifierString() + " argument has not been " +
1439                    "registered with the argument parser.");
1440        }
1441    
1442        final LinkedHashSet<Argument> argSet =
1443             new LinkedHashSet<Argument>(requiredArguments);
1444        requiredArgumentSets.add(Collections.unmodifiableSet(argSet));
1445      }
1446    
1447    
1448    
1449      /**
1450       * Adds the provided set of arguments as a required argument set, in which
1451       * at least one of the arguments must be provided.  All of the arguments must
1452       * have already been registered with this argument parser using the
1453       * {@link #addArgument} method.
1454       *
1455       * @param  arg1       The first argument to include in the required argument
1456       *                    set.  It must not be {@code null}, and it must have
1457       *                    already been registered with this argument parser.
1458       * @param  arg2       The second argument to include in the required argument
1459       *                    set.  It must not be {@code null}, and it must have
1460       *                    already been registered with this argument parser.
1461       * @param  remaining  Any additional arguments to include in the required
1462       *                    argument set.  It may be {@code null} or empty if no
1463       *                    additional required arguments are needed, but if it is
1464       *                    non-empty then all arguments must have already been
1465       *                    registered with this argument parser.
1466       */
1467      public void addRequiredArgumentSet(final Argument arg1, final Argument arg2,
1468                                         final Argument... remaining)
1469      {
1470        ensureNotNull(arg1, arg2);
1471    
1472        ensureTrue(namedArgs.contains(arg1),
1473             "The ArgumentParser.addRequiredArgumentSet method may only be " +
1474                  "used if all of the provided arguments have already been " +
1475                  "registered with the argument parser via the " +
1476                  "ArgumentParser.addArgument method.  The " +
1477                  arg1.getIdentifierString() + " argument has not been " +
1478                  "registered with the argument parser.");
1479        ensureTrue(namedArgs.contains(arg2),
1480             "The ArgumentParser.addRequiredArgumentSet method may only be " +
1481                  "used if all of the provided arguments have already been " +
1482                  "registered with the argument parser via the " +
1483                  "ArgumentParser.addArgument method.  The " +
1484                  arg2.getIdentifierString() + " argument has not been " +
1485                  "registered with the argument parser.");
1486    
1487        if (remaining != null)
1488        {
1489          for (final Argument a : remaining)
1490          {
1491            ensureTrue(namedArgs.contains(a),
1492                 "The ArgumentParser.addRequiredArgumentSet method may only be " +
1493                      "used if all of the provided arguments have already been " +
1494                      "registered with the argument parser via the " +
1495                      "ArgumentParser.addArgument method.  The " +
1496                      a.getIdentifierString() + " argument has not been " +
1497                      "registered with the argument parser.");
1498          }
1499        }
1500    
1501        final LinkedHashSet<Argument> argSet = new LinkedHashSet<Argument>();
1502        argSet.add(arg1);
1503        argSet.add(arg2);
1504        argSet.addAll(Arrays.asList(remaining));
1505    
1506        requiredArgumentSets.add(Collections.unmodifiableSet(argSet));
1507      }
1508    
1509    
1510    
1511      /**
1512       * Indicates whether any subcommands have been registered with this argument
1513       * parser.
1514       *
1515       * @return  {@code true} if one or more subcommands have been registered with
1516       *          this argument parser, or {@code false} if not.
1517       */
1518      public boolean hasSubCommands()
1519      {
1520        return (! subCommands.isEmpty());
1521      }
1522    
1523    
1524    
1525      /**
1526       * Retrieves the subcommand that was provided in the set of command-line
1527       * arguments, if any.
1528       *
1529       * @return  The subcommand that was provided in the set of command-line
1530       *          arguments, or {@code null} if there is none.
1531       */
1532      public SubCommand getSelectedSubCommand()
1533      {
1534        return selectedSubCommand;
1535      }
1536    
1537    
1538    
1539      /**
1540       * Specifies the subcommand that was provided in the set of command-line
1541       * arguments.
1542       *
1543       * @param  subcommand  The subcommand that was provided in the set of
1544       *                     command-line arguments.  It may be {@code null} if no
1545       *                     subcommand should be used.
1546       */
1547      void setSelectedSubCommand(final SubCommand subcommand)
1548      {
1549        selectedSubCommand = subcommand;
1550        if (subcommand != null)
1551        {
1552          subcommand.setPresent();
1553        }
1554      }
1555    
1556    
1557    
1558      /**
1559       * Retrieves a list of all subcommands associated with this argument parser.
1560       *
1561       * @return  A list of all subcommands associated with this argument parser, or
1562       *          an empty list if there are no associated subcommands.
1563       */
1564      public List<SubCommand> getSubCommands()
1565      {
1566        return Collections.unmodifiableList(subCommands);
1567      }
1568    
1569    
1570    
1571      /**
1572       * Retrieves the subcommand for the provided name.
1573       *
1574       * @param  name  The name of the subcommand to retrieve.
1575       *
1576       * @return  The subcommand with the provided name, or {@code null} if there is
1577       *          no such subcommand.
1578       */
1579      public SubCommand getSubCommand(final String name)
1580      {
1581        if (name == null)
1582        {
1583          return null;
1584        }
1585    
1586        return subCommandsByName.get(toLowerCase(name));
1587      }
1588    
1589    
1590    
1591      /**
1592       * Registers the provided subcommand with this argument parser.
1593       *
1594       * @param  subCommand  The subcommand to register with this argument parser.
1595       *                     It must not be {@code null}.
1596       *
1597       * @throws  ArgumentException  If this argument parser does not allow
1598       *                             subcommands, if there is a conflict between any
1599       *                             of the names of the provided subcommand and an
1600       *                             already-registered subcommand, or if there is a
1601       *                             conflict between any of the subcommand-specific
1602       *                             arguments and global arguments.
1603       */
1604      public void addSubCommand(final SubCommand subCommand)
1605             throws ArgumentException
1606      {
1607        // Ensure that the subcommand isn't already registered with an argument
1608        // parser.
1609        if (subCommand.getGlobalArgumentParser() != null)
1610        {
1611          throw new ArgumentException(
1612               ERR_PARSER_SUBCOMMAND_ALREADY_REGISTERED_WITH_PARSER.get());
1613        }
1614    
1615        // Ensure that the caller isn't trying to create a nested subcommand.
1616        if (this.parentSubCommand != null)
1617        {
1618          throw new ArgumentException(
1619               ERR_PARSER_CANNOT_CREATE_NESTED_SUBCOMMAND.get(
1620                    this.parentSubCommand.getPrimaryName()));
1621        }
1622    
1623        // Ensure that this argument parser doesn't allow trailing arguments.
1624        if (allowsTrailingArguments())
1625        {
1626          throw new ArgumentException(
1627               ERR_PARSER_WITH_TRAILING_ARGS_CANNOT_HAVE_SUBCOMMANDS.get());
1628        }
1629    
1630        // Ensure that the subcommand doesn't have any names that conflict with an
1631        // existing subcommand.
1632        for (final String name : subCommand.getNames())
1633        {
1634          if (subCommandsByName.containsKey(toLowerCase(name)))
1635          {
1636            throw new ArgumentException(
1637                 ERR_SUBCOMMAND_NAME_ALREADY_IN_USE.get(name));
1638          }
1639        }
1640    
1641        // Register the subcommand.
1642        for (final String name : subCommand.getNames())
1643        {
1644          subCommandsByName.put(toLowerCase(name), subCommand);
1645        }
1646        subCommands.add(subCommand);
1647        subCommand.setGlobalArgumentParser(this);
1648      }
1649    
1650    
1651    
1652      /**
1653       * Registers the provided additional name for this subcommand.
1654       *
1655       * @param  name        The name to be registered.  It must not be
1656       *                     {@code null} or empty.
1657       * @param  subCommand  The subcommand with which the name is associated.  It
1658       *                     must not be {@code null}.
1659       *
1660       * @throws  ArgumentException  If the provided name is already in use.
1661       */
1662      void addSubCommand(final String name, final SubCommand subCommand)
1663           throws ArgumentException
1664      {
1665        final String lowerName = toLowerCase(name);
1666        if (subCommandsByName.containsKey(lowerName))
1667        {
1668          throw new ArgumentException(
1669               ERR_SUBCOMMAND_NAME_ALREADY_IN_USE.get(name));
1670        }
1671    
1672        subCommandsByName.put(lowerName, subCommand);
1673      }
1674    
1675    
1676    
1677      /**
1678       * Retrieves the set of unnamed trailing arguments in the provided command
1679       * line arguments.
1680       *
1681       * @return  The set of unnamed trailing arguments in the provided command line
1682       *          arguments, or an empty list if there were none.
1683       */
1684      public List<String> getTrailingArguments()
1685      {
1686        return Collections.unmodifiableList(trailingArgs);
1687      }
1688    
1689    
1690    
1691      /**
1692       * Clears the set of trailing arguments for this argument parser.
1693       */
1694      void resetTrailingArguments()
1695      {
1696        trailingArgs.clear();
1697      }
1698    
1699    
1700    
1701      /**
1702       * Adds the provided value to the set of trailing arguments.
1703       *
1704       * @param  value  The value to add to the set of trailing arguments.
1705       *
1706       * @throws  ArgumentException  If the parser already has the maximum allowed
1707       *                             number of trailing arguments.
1708       */
1709      void addTrailingArgument(final String value)
1710           throws ArgumentException
1711      {
1712        if ((maxTrailingArgs > 0) && (trailingArgs.size() >= maxTrailingArgs))
1713        {
1714          throw new ArgumentException(ERR_PARSER_TOO_MANY_TRAILING_ARGS.get(value,
1715               commandName, maxTrailingArgs));
1716        }
1717    
1718        trailingArgs.add(value);
1719      }
1720    
1721    
1722    
1723      /**
1724       * Retrieves the properties file that was used to obtain values for arguments
1725       * not set on the command line.
1726       *
1727       * @return  The properties file that was used to obtain values for arguments
1728       *          not set on the command line, or {@code null} if no properties file
1729       *          was used.
1730       */
1731      public File getPropertiesFileUsed()
1732      {
1733        return propertiesFileUsed;
1734      }
1735    
1736    
1737    
1738      /**
1739       * Retrieves a list of the string representations of any arguments used for
1740       * the associated tool that were set from a properties file rather than
1741       * provided on the command line.  The values of any arguments marked as
1742       * sensitive will be obscured.
1743       *
1744       * @return  A list of the string representations any arguments used for the
1745       *          associated tool that were set from a properties file rather than
1746       *          provided on the command line, or an empty list if no arguments
1747       *          were set from a properties file.
1748       */
1749      public List<String> getArgumentsSetFromPropertiesFile()
1750      {
1751        return Collections.unmodifiableList(argumentsSetFromPropertiesFile);
1752      }
1753    
1754    
1755    
1756      /**
1757       * Creates a copy of this argument parser that is "clean" and appears as if it
1758       * has not been used to parse an argument set.  The new parser will have all
1759       * of the same arguments and constraints as this parser.
1760       *
1761       * @return  The "clean" copy of this argument parser.
1762       */
1763      public ArgumentParser getCleanCopy()
1764      {
1765        return new ArgumentParser(this, null);
1766      }
1767    
1768    
1769    
1770      /**
1771       * Parses the provided set of arguments.
1772       *
1773       * @param  args  An array containing the argument information to parse.  It
1774       *               must not be {@code null}.
1775       *
1776       * @throws  ArgumentException  If a problem occurs while attempting to parse
1777       *                             the argument information.
1778       */
1779      public void parse(final String[] args)
1780             throws ArgumentException
1781      {
1782        // Iterate through the provided args strings and process them.
1783        ArgumentParser subCommandParser    = null;
1784        boolean        inTrailingArgs      = false;
1785        boolean        skipFinalValidation = false;
1786        String         subCommandName      = null;
1787        for (int i=0; i < args.length; i++)
1788        {
1789          final String s = args[i];
1790    
1791          if (inTrailingArgs)
1792          {
1793            if (maxTrailingArgs == 0)
1794            {
1795              throw new ArgumentException(ERR_PARSER_TRAILING_ARGS_NOT_ALLOWED.get(
1796                                               s, commandName));
1797            }
1798            else if (trailingArgs.size() >= maxTrailingArgs)
1799            {
1800              throw new ArgumentException(ERR_PARSER_TOO_MANY_TRAILING_ARGS.get(s,
1801                                               commandName, maxTrailingArgs));
1802            }
1803            else
1804            {
1805              trailingArgs.add(s);
1806            }
1807          }
1808          else if (s.equals("--"))
1809          {
1810            // This signifies the end of the named arguments and the beginning of
1811            // the trailing arguments.
1812            inTrailingArgs = true;
1813          }
1814          else if (s.startsWith("--"))
1815          {
1816            // There may be an equal sign to separate the name from the value.
1817            final String argName;
1818            final int equalPos = s.indexOf('=');
1819            if (equalPos > 0)
1820            {
1821              argName = s.substring(2, equalPos);
1822            }
1823            else
1824            {
1825              argName = s.substring(2);
1826            }
1827    
1828            final String lowerName = toLowerCase(argName);
1829            Argument a = namedArgsByLongID.get(lowerName);
1830            if ((a == null) && (subCommandParser != null))
1831            {
1832              a = subCommandParser.namedArgsByLongID.get(lowerName);
1833            }
1834    
1835            if (a == null)
1836            {
1837              throw new ArgumentException(ERR_PARSER_NO_SUCH_LONG_ID.get(argName));
1838            }
1839            else if (a.isUsageArgument())
1840            {
1841              skipFinalValidation |= skipFinalValidationBecauseOfArgument(a);
1842            }
1843    
1844            a.incrementOccurrences();
1845            if (a.takesValue())
1846            {
1847              if (equalPos > 0)
1848              {
1849                a.addValue(s.substring(equalPos+1));
1850              }
1851              else
1852              {
1853                i++;
1854                if (i >= args.length)
1855                {
1856                  throw new ArgumentException(ERR_PARSER_LONG_ARG_MISSING_VALUE.get(
1857                                                   argName));
1858                }
1859                else
1860                {
1861                  a.addValue(args[i]);
1862                }
1863              }
1864            }
1865            else
1866            {
1867              if (equalPos > 0)
1868              {
1869                throw new ArgumentException(
1870                               ERR_PARSER_LONG_ARG_DOESNT_TAKE_VALUE.get(argName));
1871              }
1872            }
1873          }
1874          else if (s.startsWith("-"))
1875          {
1876            if (s.length() == 1)
1877            {
1878              throw new ArgumentException(ERR_PARSER_UNEXPECTED_DASH.get());
1879            }
1880            else if (s.length() == 2)
1881            {
1882              final char c = s.charAt(1);
1883    
1884              Argument a = namedArgsByShortID.get(c);
1885              if ((a == null) && (subCommandParser != null))
1886              {
1887                a = subCommandParser.namedArgsByShortID.get(c);
1888              }
1889    
1890              if (a == null)
1891              {
1892                throw new ArgumentException(ERR_PARSER_NO_SUCH_SHORT_ID.get(c));
1893              }
1894              else if (a.isUsageArgument())
1895              {
1896                skipFinalValidation |= skipFinalValidationBecauseOfArgument(a);
1897              }
1898    
1899              a.incrementOccurrences();
1900              if (a.takesValue())
1901              {
1902                i++;
1903                if (i >= args.length)
1904                {
1905                  throw new ArgumentException(
1906                                 ERR_PARSER_SHORT_ARG_MISSING_VALUE.get(c));
1907                }
1908                else
1909                {
1910                  a.addValue(args[i]);
1911                }
1912              }
1913            }
1914            else
1915            {
1916              char c = s.charAt(1);
1917              Argument a = namedArgsByShortID.get(c);
1918              if ((a == null) && (subCommandParser != null))
1919              {
1920                a = subCommandParser.namedArgsByShortID.get(c);
1921              }
1922    
1923              if (a == null)
1924              {
1925                throw new ArgumentException(ERR_PARSER_NO_SUCH_SHORT_ID.get(c));
1926              }
1927              else if (a.isUsageArgument())
1928              {
1929                skipFinalValidation |= skipFinalValidationBecauseOfArgument(a);
1930              }
1931    
1932              a.incrementOccurrences();
1933              if (a.takesValue())
1934              {
1935                a.addValue(s.substring(2));
1936              }
1937              else
1938              {
1939                // The rest of the characters in the string must also resolve to
1940                // arguments that don't take values.
1941                for (int j=2; j < s.length(); j++)
1942                {
1943                  c = s.charAt(j);
1944                  a = namedArgsByShortID.get(c);
1945                  if ((a == null) && (subCommandParser != null))
1946                  {
1947                    a = subCommandParser.namedArgsByShortID.get(c);
1948                  }
1949    
1950                  if (a == null)
1951                  {
1952                    throw new ArgumentException(
1953                                   ERR_PARSER_NO_SUBSEQUENT_SHORT_ARG.get(c, s));
1954                  }
1955                  else if (a.isUsageArgument())
1956                  {
1957                    skipFinalValidation |= skipFinalValidationBecauseOfArgument(a);
1958                  }
1959    
1960                  a.incrementOccurrences();
1961                  if (a.takesValue())
1962                  {
1963                    throw new ArgumentException(
1964                                   ERR_PARSER_SUBSEQUENT_SHORT_ARG_TAKES_VALUE.get(
1965                                        c, s));
1966                  }
1967                }
1968              }
1969            }
1970          }
1971          else if (subCommands.isEmpty())
1972          {
1973            inTrailingArgs = true;
1974            if (maxTrailingArgs == 0)
1975            {
1976              throw new ArgumentException(ERR_PARSER_TRAILING_ARGS_NOT_ALLOWED.get(
1977                   s, commandName));
1978            }
1979            else
1980            {
1981              trailingArgs.add(s);
1982            }
1983          }
1984          else
1985          {
1986            if (selectedSubCommand == null)
1987            {
1988              subCommandName = s;
1989              selectedSubCommand = subCommandsByName.get(toLowerCase(s));
1990              if (selectedSubCommand == null)
1991              {
1992                throw new ArgumentException(ERR_PARSER_NO_SUCH_SUBCOMMAND.get(s,
1993                     commandName));
1994              }
1995              else
1996              {
1997                selectedSubCommand.setPresent();
1998                subCommandParser = selectedSubCommand.getArgumentParser();
1999              }
2000            }
2001            else
2002            {
2003              throw new ArgumentException(ERR_PARSER_CONFLICTING_SUBCOMMANDS.get(
2004                   subCommandName, s));
2005            }
2006          }
2007        }
2008    
2009    
2010        // Perform any appropriate processing related to the use of a properties
2011        // file.
2012        if (! handlePropertiesFile())
2013        {
2014          return;
2015        }
2016    
2017    
2018        // If a usage argument was provided, then no further validation should be
2019        // performed.
2020        if (skipFinalValidation)
2021        {
2022          return;
2023        }
2024    
2025    
2026        // If any subcommands are defined, then one must have been provided.
2027        if ((! subCommands.isEmpty()) && (selectedSubCommand == null))
2028        {
2029          throw new ArgumentException(
2030               ERR_PARSER_MISSING_SUBCOMMAND.get(commandName));
2031        }
2032    
2033    
2034        doFinalValidation(this);
2035        if (selectedSubCommand != null)
2036        {
2037          doFinalValidation(selectedSubCommand.getArgumentParser());
2038        }
2039      }
2040    
2041    
2042    
2043      /**
2044       * Performs the final validation for the provided argument parser.
2045       *
2046       * @param  parser  The argument parser for which to perform the final
2047       *                 validation.
2048       *
2049       * @throws  ArgumentException  If a validation problem is encountered.
2050       */
2051      private static void doFinalValidation(final ArgumentParser parser)
2052              throws ArgumentException
2053      {
2054        // Make sure that all required arguments have values.
2055        for (final Argument a : parser.namedArgs)
2056        {
2057          if (a.isRequired() && (! a.isPresent()))
2058          {
2059            throw new ArgumentException(ERR_PARSER_MISSING_REQUIRED_ARG.get(
2060                                             a.getIdentifierString()));
2061          }
2062        }
2063    
2064    
2065        // Make sure that at least the minimum number of trailing arguments were
2066        // provided.
2067        if (parser.trailingArgs.size() < parser.minTrailingArgs)
2068        {
2069          throw new ArgumentException(ERR_PARSER_NOT_ENOUGH_TRAILING_ARGS.get(
2070               parser.commandName, parser.minTrailingArgs,
2071               parser.trailingArgsPlaceholder));
2072        }
2073    
2074    
2075        // Make sure that there are no dependent argument set conflicts.
2076        for (final ObjectPair<Argument,Set<Argument>> p :
2077             parser.dependentArgumentSets)
2078        {
2079          final Argument targetArg = p.getFirst();
2080          if (targetArg.getNumOccurrences() > 0)
2081          {
2082            final Set<Argument> argSet = p.getSecond();
2083            boolean found = false;
2084            for (final Argument a : argSet)
2085            {
2086              if (a.getNumOccurrences() > 0)
2087              {
2088                found = true;
2089                break;
2090              }
2091            }
2092    
2093            if (! found)
2094            {
2095              if (argSet.size() == 1)
2096              {
2097                throw new ArgumentException(
2098                     ERR_PARSER_DEPENDENT_CONFLICT_SINGLE.get(
2099                          targetArg.getIdentifierString(),
2100                          argSet.iterator().next().getIdentifierString()));
2101              }
2102              else
2103              {
2104                boolean first = true;
2105                final StringBuilder buffer = new StringBuilder();
2106                for (final Argument a : argSet)
2107                {
2108                  if (first)
2109                  {
2110                    first = false;
2111                  }
2112                  else
2113                  {
2114                    buffer.append(", ");
2115                  }
2116                  buffer.append(a.getIdentifierString());
2117                }
2118                throw new ArgumentException(
2119                     ERR_PARSER_DEPENDENT_CONFLICT_MULTIPLE.get(
2120                          targetArg.getIdentifierString(), buffer.toString()));
2121              }
2122            }
2123          }
2124        }
2125    
2126    
2127        // Make sure that there are no exclusive argument set conflicts.
2128        for (final Set<Argument> argSet : parser.exclusiveArgumentSets)
2129        {
2130          Argument setArg = null;
2131          for (final Argument a : argSet)
2132          {
2133            if (a.getNumOccurrences() > 0)
2134            {
2135              if (setArg == null)
2136              {
2137                setArg = a;
2138              }
2139              else
2140              {
2141                throw new ArgumentException(ERR_PARSER_EXCLUSIVE_CONFLICT.get(
2142                                                 setArg.getIdentifierString(),
2143                                                 a.getIdentifierString()));
2144              }
2145            }
2146          }
2147        }
2148    
2149        // Make sure that there are no required argument set conflicts.
2150        for (final Set<Argument> argSet : parser.requiredArgumentSets)
2151        {
2152          boolean found = false;
2153          for (final Argument a : argSet)
2154          {
2155            if (a.getNumOccurrences() > 0)
2156            {
2157              found = true;
2158              break;
2159            }
2160          }
2161    
2162          if (! found)
2163          {
2164            boolean first = true;
2165            final StringBuilder buffer = new StringBuilder();
2166            for (final Argument a : argSet)
2167            {
2168              if (first)
2169              {
2170                first = false;
2171              }
2172              else
2173              {
2174                buffer.append(", ");
2175              }
2176              buffer.append(a.getIdentifierString());
2177            }
2178            throw new ArgumentException(ERR_PARSER_REQUIRED_CONFLICT.get(
2179                                             buffer.toString()));
2180          }
2181        }
2182      }
2183    
2184    
2185    
2186      /**
2187       * Indicates whether the provided argument is one that indicates that the
2188       * parser should skip all validation except that performed when assigning
2189       * values from command-line arguments.  Validation that will be skipped
2190       * includes ensuring that all required arguments have values, ensuring that
2191       * the minimum number of trailing arguments were provided, and ensuring that
2192       * there were no dependent/exclusive/required argument set conflicts.
2193       *
2194       * @param  a  The argument for which to make the determination.
2195       *
2196       * @return  {@code true} if the provided argument is one that indicates that
2197       *          final validation should be skipped, or {@code false} if not.
2198       */
2199      private static boolean skipFinalValidationBecauseOfArgument(final Argument a)
2200      {
2201        // We will skip final validation for all usage arguments except the
2202        // propertiesFilePath and noPropertiesFile arguments.
2203        if (ARG_NAME_PROPERTIES_FILE_PATH.equals(a.getLongIdentifier()) ||
2204            ARG_NAME_NO_PROPERTIES_FILE.equals(a.getLongIdentifier()) ||
2205            ARG_NAME_OUTPUT_FILE.equals(a.getLongIdentifier()) ||
2206            ARG_NAME_TEE_OUTPUT.equals(a.getLongIdentifier()))
2207        {
2208          return false;
2209        }
2210    
2211        return a.isUsageArgument();
2212      }
2213    
2214    
2215    
2216      /**
2217       * Performs any appropriate properties file processing for this argument
2218       * parser.
2219       *
2220       * @return  {@code true} if the tool should continue processing, or
2221       *          {@code false} if it should return immediately.
2222       *
2223       * @throws  ArgumentException  If a problem is encountered while attempting
2224       *                             to parse a properties file or update arguments
2225       *                             with the values contained in it.
2226       */
2227      private boolean handlePropertiesFile()
2228              throws ArgumentException
2229      {
2230        final BooleanArgument noPropertiesFile;
2231        final FileArgument generatePropertiesFile;
2232        final FileArgument propertiesFilePath;
2233        try
2234        {
2235          propertiesFilePath = getFileArgument(ARG_NAME_PROPERTIES_FILE_PATH);
2236          generatePropertiesFile =
2237               getFileArgument(ARG_NAME_GENERATE_PROPERTIES_FILE);
2238          noPropertiesFile = getBooleanArgument(ARG_NAME_NO_PROPERTIES_FILE);
2239        }
2240        catch (final Exception e)
2241        {
2242          Debug.debugException(e);
2243    
2244          // This should only ever happen if the argument parser has an argument
2245          // with a name that conflicts with one of the properties file arguments
2246          // but isn't of the right type.  In this case, we'll assume that no
2247          // properties file will be used.
2248          return true;
2249        }
2250    
2251    
2252        // If any of the properties file arguments isn't defined, then we'll assume
2253        // that no properties file will be used.
2254        if ((propertiesFilePath == null) || (generatePropertiesFile == null) ||
2255            (noPropertiesFile == null))
2256        {
2257          return true;
2258        }
2259    
2260    
2261        // If the noPropertiesFile argument is present, then don't do anything but
2262        // make sure that neither of the other arguments was specified.
2263        if (noPropertiesFile.isPresent())
2264        {
2265          if (propertiesFilePath.isPresent())
2266          {
2267            throw new ArgumentException(ERR_PARSER_EXCLUSIVE_CONFLICT.get(
2268                 noPropertiesFile.getIdentifierString(),
2269                 propertiesFilePath.getIdentifierString()));
2270          }
2271          else if (generatePropertiesFile.isPresent())
2272          {
2273            throw new ArgumentException(ERR_PARSER_EXCLUSIVE_CONFLICT.get(
2274                 noPropertiesFile.getIdentifierString(),
2275                 generatePropertiesFile.getIdentifierString()));
2276          }
2277          else
2278          {
2279            return true;
2280          }
2281        }
2282    
2283    
2284        // If the generatePropertiesFile argument is present, then make sure the
2285        // propertiesFilePath argument is not set and generate the output.
2286        if (generatePropertiesFile.isPresent())
2287        {
2288          if (propertiesFilePath.isPresent())
2289          {
2290            throw new ArgumentException(ERR_PARSER_EXCLUSIVE_CONFLICT.get(
2291                 generatePropertiesFile.getIdentifierString(),
2292                 propertiesFilePath.getIdentifierString()));
2293          }
2294          else
2295          {
2296            generatePropertiesFile(
2297                 generatePropertiesFile.getValue().getAbsolutePath());
2298            return false;
2299          }
2300        }
2301    
2302    
2303        // If the propertiesFilePath argument is present, then try to make use of
2304        // the specified file.
2305        if (propertiesFilePath.isPresent())
2306        {
2307          final File propertiesFile = propertiesFilePath.getValue();
2308          if (propertiesFile.exists() && propertiesFile.isFile())
2309          {
2310            handlePropertiesFile(propertiesFilePath.getValue());
2311          }
2312          else
2313          {
2314            throw new ArgumentException(
2315                 ERR_PARSER_NO_SUCH_PROPERTIES_FILE.get(
2316                      propertiesFilePath.getIdentifierString(),
2317                      propertiesFile.getAbsolutePath()));
2318          }
2319          return true;
2320        }
2321    
2322    
2323        // We may still use a properties file if the path was specified in either a
2324        // JVM property or an environment variable.  If both are defined, the JVM
2325        // property will take precedence.  If a property or environment variable
2326        // specifies an invalid value, then we'll just ignore it.
2327        String path = System.getProperty(PROPERTY_DEFAULT_PROPERTIES_FILE_PATH);
2328        if (path == null)
2329        {
2330          path = System.getenv(ENV_DEFAULT_PROPERTIES_FILE_PATH);
2331        }
2332    
2333        if (path != null)
2334        {
2335          final File propertiesFile = new File(path);
2336          if (propertiesFile.exists() && propertiesFile.isFile())
2337          {
2338            handlePropertiesFile(propertiesFile);
2339          }
2340        }
2341    
2342        return true;
2343      }
2344    
2345    
2346    
2347      /**
2348       * Write an empty properties file for this argument parser to the specified
2349       * path.
2350       *
2351       * @param  path  The path to the properties file to be written.
2352       *
2353       * @throws  ArgumentException  If a problem is encountered while writing the
2354       *                             properties file.
2355       */
2356      private void generatePropertiesFile(final String path)
2357              throws ArgumentException
2358      {
2359        final PrintWriter w;
2360        try
2361        {
2362          w = new PrintWriter(path);
2363        }
2364        catch (final Exception e)
2365        {
2366          Debug.debugException(e);
2367          throw new ArgumentException(
2368               ERR_PARSER_GEN_PROPS_CANNOT_OPEN_FILE.get(path,
2369                    getExceptionMessage(e)),
2370               e);
2371        }
2372    
2373        try
2374        {
2375          wrapComment(w, INFO_PARSER_GEN_PROPS_HEADER_1.get(commandName));
2376          w.println('#');
2377          wrapComment(w,
2378               INFO_PARSER_GEN_PROPS_HEADER_2.get(commandName,
2379                    ARG_NAME_PROPERTIES_FILE_PATH,
2380                    PROPERTY_DEFAULT_PROPERTIES_FILE_PATH,
2381                    ENV_DEFAULT_PROPERTIES_FILE_PATH, ARG_NAME_NO_PROPERTIES_FILE));
2382          w.println('#');
2383          wrapComment(w, INFO_PARSER_GEN_PROPS_HEADER_3.get());
2384          w.println('#');
2385    
2386          wrapComment(w, INFO_PARSER_GEN_PROPS_HEADER_4.get());
2387          w.println('#');
2388          wrapComment(w, INFO_PARSER_GEN_PROPS_HEADER_5.get(commandName));
2389    
2390          for (final Argument a : getNamedArguments())
2391          {
2392            writeArgumentProperties(w, null, a);
2393          }
2394    
2395          for (final SubCommand sc : getSubCommands())
2396          {
2397            for (final Argument a : sc.getArgumentParser().getNamedArguments())
2398            {
2399              writeArgumentProperties(w, sc, a);
2400            }
2401          }
2402        }
2403        finally
2404        {
2405          w.close();
2406        }
2407      }
2408    
2409    
2410    
2411      /**
2412       * Writes information about the provided argument to the given writer.
2413       *
2414       * @param  w   The writer to which the properties should be written.  It must
2415       *             not be {@code null}.
2416       * @param  sc  The subcommand with which the argument is associated.  It may
2417       *             be {@code null} if the provided argument is a global argument.
2418       * @param  a   The argument for which to write the properties.  It must not be
2419       *             {@code null}.
2420       */
2421      private void writeArgumentProperties(final PrintWriter w,
2422                                           final SubCommand sc,
2423                                           final Argument a)
2424      {
2425        if (a.isUsageArgument() || a.isHidden())
2426        {
2427          return;
2428        }
2429    
2430        w.println();
2431        w.println();
2432        wrapComment(w, a.getDescription());
2433        w.println('#');
2434    
2435        final String constraints = a.getValueConstraints();
2436        if ((constraints != null) && (constraints.length() > 0) &&
2437            (! (a instanceof BooleanArgument)))
2438        {
2439          wrapComment(w, constraints);
2440          w.println('#');
2441        }
2442    
2443        final String identifier;
2444        if (a.getLongIdentifier() != null)
2445        {
2446          identifier = a.getLongIdentifier();
2447        }
2448        else
2449        {
2450          identifier = a.getIdentifierString();
2451        }
2452    
2453        String placeholder = a.getValuePlaceholder();
2454        if (placeholder == null)
2455        {
2456          if (a instanceof BooleanArgument)
2457          {
2458            placeholder = "{true|false}";
2459          }
2460          else
2461          {
2462            placeholder = "";
2463          }
2464        }
2465    
2466        final String propertyName;
2467        if (sc == null)
2468        {
2469          propertyName = commandName + '.' + identifier;
2470        }
2471        else
2472        {
2473          propertyName = commandName + '.' + sc.getPrimaryName() + '.' + identifier;
2474        }
2475    
2476        w.println("# " + propertyName + '=' + placeholder);
2477    
2478        if (a.isPresent())
2479        {
2480          for (final String s : a.getValueStringRepresentations(false))
2481          {
2482            w.println(propertyName + '=' + s);
2483          }
2484        }
2485      }
2486    
2487    
2488    
2489      /**
2490       * Wraps the given string and writes it as a comment to the provided writer.
2491       *
2492       * @param  w  The writer to use to write the wrapped and commented string.
2493       * @param  s  The string to be wrapped and written.
2494       */
2495      private static void wrapComment(final PrintWriter w, final String s)
2496      {
2497        for (final String line : wrapLine(s, 77))
2498        {
2499          w.println("# " + line);
2500        }
2501      }
2502    
2503    
2504    
2505      /**
2506       * Reads the contents of the specified properties file and updates the
2507       * configured arguments as appropriate.
2508       *
2509       * @param  propertiesFile  The properties file to process.
2510       *
2511       * @throws  ArgumentException  If a problem is encountered while examining the
2512       *                             properties file, or while trying to assign a
2513       *                             property value to a corresponding argument.
2514       */
2515      private void handlePropertiesFile(final File propertiesFile)
2516              throws ArgumentException
2517      {
2518        final BufferedReader reader;
2519        try
2520        {
2521          reader = new BufferedReader(new FileReader(propertiesFile));
2522        }
2523        catch (final Exception e)
2524        {
2525          Debug.debugException(e);
2526          throw new ArgumentException(
2527               ERR_PARSER_CANNOT_OPEN_PROP_FILE.get(
2528                    propertiesFile.getAbsolutePath(), getExceptionMessage(e)),
2529               e);
2530        }
2531    
2532        try
2533        {
2534          // Read all of the lines of the file, ignoring comments and unwrapping
2535          // properties that span multiple lines.
2536          boolean lineIsContinued = false;
2537          int lineNumber = 0;
2538          final ArrayList<ObjectPair<Integer,StringBuilder>> propertyLines =
2539               new ArrayList<ObjectPair<Integer,StringBuilder>>(10);
2540          while (true)
2541          {
2542            String line;
2543            try
2544            {
2545              line = reader.readLine();
2546              lineNumber++;
2547            }
2548            catch (final Exception e)
2549            {
2550              Debug.debugException(e);
2551              throw new ArgumentException(
2552                   ERR_PARSER_ERROR_READING_PROP_FILE.get(
2553                        propertiesFile.getAbsolutePath(), getExceptionMessage(e)),
2554                   e);
2555            }
2556    
2557    
2558            // If the line is null, then we've reached the end of the file.  If we
2559            // expect a previous line to have been continued, then this is an error.
2560            if (line == null)
2561            {
2562              if (lineIsContinued)
2563              {
2564                throw new ArgumentException(
2565                     ERR_PARSER_PROP_FILE_MISSING_CONTINUATION.get(
2566                          (lineNumber-1), propertiesFile.getAbsolutePath()));
2567              }
2568              break;
2569            }
2570    
2571    
2572            // See if the line has any leading whitespace, and if so then trim it
2573            // off.  If there is leading whitespace, then make sure that we expect
2574            // the previous line to be continued.
2575            final int initialLength = line.length();
2576            line = trimLeading(line);
2577            final boolean hasLeadingWhitespace = (line.length() < initialLength);
2578            if (hasLeadingWhitespace && (! lineIsContinued))
2579            {
2580              throw new ArgumentException(
2581                   ERR_PARSER_PROP_FILE_UNEXPECTED_LEADING_SPACE.get(
2582                        propertiesFile.getAbsolutePath(), lineNumber));
2583            }
2584    
2585    
2586            // If the line is empty or starts with "#", then skip it.  But make sure
2587            // we didn't expect the previous line to be continued.
2588            if ((line.length() == 0) || line.startsWith("#"))
2589            {
2590              if (lineIsContinued)
2591              {
2592                throw new ArgumentException(
2593                     ERR_PARSER_PROP_FILE_MISSING_CONTINUATION.get(
2594                          (lineNumber-1), propertiesFile.getAbsolutePath()));
2595              }
2596              continue;
2597            }
2598    
2599    
2600            // See if the line ends with a backslash and if so then trim it off.
2601            final boolean hasTrailingBackslash = line.endsWith("\\");
2602            if (line.endsWith("\\"))
2603            {
2604              line = line.substring(0, (line.length() - 1));
2605            }
2606    
2607    
2608            // If the previous line needs to be continued, then append the new line
2609            // to it.  Otherwise, add it as a new line.
2610            if (lineIsContinued)
2611            {
2612              propertyLines.get(propertyLines.size() - 1).getSecond().append(line);
2613            }
2614            else
2615            {
2616              propertyLines.add(new ObjectPair<Integer,StringBuilder>(lineNumber,
2617                   new StringBuilder(line)));
2618            }
2619    
2620            lineIsContinued = hasTrailingBackslash;
2621          }
2622    
2623    
2624          // Parse all of the lines into a map of identifiers and their
2625          // corresponding values.
2626          propertiesFileUsed = propertiesFile;
2627          if (propertyLines.isEmpty())
2628          {
2629            return;
2630          }
2631    
2632          final HashMap<String,ArrayList<String>> propertyMap =
2633               new HashMap<String,ArrayList<String>>(propertyLines.size());
2634          for (final ObjectPair<Integer,StringBuilder> p : propertyLines)
2635          {
2636            final String line = p.getSecond().toString();
2637            final int equalPos = line.indexOf('=');
2638            if (equalPos <= 0)
2639            {
2640              throw new ArgumentException(ERR_PARSER_MALFORMED_PROP_LINE.get(
2641                   propertiesFile.getAbsolutePath(), p.getFirst(), line));
2642            }
2643    
2644            final String propertyName = line.substring(0, equalPos).trim();
2645            final String propertyValue = line.substring(equalPos+1).trim();
2646            if (propertyValue.length() == 0)
2647            {
2648              // The property doesn't have a value, so we can ignore it.
2649              continue;
2650            }
2651    
2652    
2653            // An argument can have multiple identifiers, and we will allow any of
2654            // them to be used to reference it.  To deal with this, we'll map the
2655            // argument identifier to its corresponding argument and then use the
2656            // preferred identifier for that argument in the map.  The same applies
2657            // to subcommand names.
2658            boolean prefixedWithToolName = false;
2659            boolean prefixedWithSubCommandName = false;
2660            Argument a = getNamedArgument(propertyName);
2661            if (a == null)
2662            {
2663              // It could be that the argument name was prefixed with the tool name.
2664              // Check to see if that was the case.
2665              if (propertyName.startsWith(commandName + '.'))
2666              {
2667                prefixedWithToolName = true;
2668    
2669                String basePropertyName =
2670                     propertyName.substring(commandName.length()+1);
2671                a = getNamedArgument(basePropertyName);
2672    
2673                if (a == null)
2674                {
2675                  final int periodPos = basePropertyName.indexOf('.');
2676                  if (periodPos > 0)
2677                  {
2678                    final String subCommandName =
2679                         basePropertyName.substring(0, periodPos);
2680                    if ((selectedSubCommand != null) &&
2681                        selectedSubCommand.hasName(subCommandName))
2682                    {
2683                      prefixedWithSubCommandName = true;
2684                      basePropertyName = basePropertyName.substring(periodPos+1);
2685                      a = selectedSubCommand.getArgumentParser().getNamedArgument(
2686                           basePropertyName);
2687                    }
2688                  }
2689                  else if (selectedSubCommand != null)
2690                  {
2691                    a = selectedSubCommand.getArgumentParser().getNamedArgument(
2692                         basePropertyName);
2693                  }
2694                }
2695              }
2696              else if (selectedSubCommand != null)
2697              {
2698                a = selectedSubCommand.getArgumentParser().getNamedArgument(
2699                     propertyName);
2700              }
2701            }
2702    
2703            if (a == null)
2704            {
2705              // This could mean that there's a typo in the property name, but it's
2706              // more likely the case that the property is for a different tool.  In
2707              // either case, we'll ignore it.
2708              continue;
2709            }
2710    
2711            final String canonicalPropertyName;
2712            if (prefixedWithToolName)
2713            {
2714              if (prefixedWithSubCommandName)
2715              {
2716                canonicalPropertyName = commandName + '.' +
2717                     selectedSubCommand.getPrimaryName() + '.' +
2718                     a.getIdentifierString();
2719              }
2720              else
2721              {
2722                canonicalPropertyName = commandName + '.' + a.getIdentifierString();
2723              }
2724            }
2725            else
2726            {
2727              canonicalPropertyName = a.getIdentifierString();
2728            }
2729    
2730            ArrayList<String> valueList = propertyMap.get(canonicalPropertyName);
2731            if (valueList == null)
2732            {
2733              valueList = new ArrayList<String>(5);
2734              propertyMap.put(canonicalPropertyName, valueList);
2735            }
2736            valueList.add(propertyValue);
2737          }
2738    
2739    
2740          // Iterate through all of the named arguments for the argument parser and
2741          // see if we should use the properties to assign values to any of the
2742          // arguments that weren't provided on the command line.
2743          setArgsFromPropertiesFile(propertyMap, false);
2744    
2745    
2746          // If there is a selected subcommand, then iterate through all of its
2747          // arguments.
2748          if (selectedSubCommand != null)
2749          {
2750            setArgsFromPropertiesFile(propertyMap, true);
2751          }
2752        }
2753        finally
2754        {
2755          try
2756          {
2757            reader.close();
2758          }
2759          catch (final Exception e)
2760          {
2761            Debug.debugException(e);
2762          }
2763        }
2764      }
2765    
2766    
2767    
2768      /**
2769       * Sets the values of any arguments not provided on the command line but
2770       * defined in the properties file.
2771       *
2772       * @param  propertyMap    A map of properties read from the properties file.
2773       * @param  useSubCommand  Indicates whether to use the argument parser
2774       *                        associated with the selected subcommand rather than
2775       *                        the global argument parser.
2776       *
2777       * @throws  ArgumentException  If a problem is encountered while examining the
2778       *                             properties file, or while trying to assign a
2779       *                             property value to a corresponding argument.
2780       */
2781      private void setArgsFromPropertiesFile(
2782                        final Map<String,ArrayList<String>> propertyMap,
2783                        final boolean useSubCommand)
2784              throws ArgumentException
2785      {
2786        final ArgumentParser p;
2787        if (useSubCommand)
2788        {
2789          p = selectedSubCommand.getArgumentParser();
2790        }
2791        else
2792        {
2793          p = this;
2794        }
2795    
2796    
2797        for (final Argument a : p.namedArgs)
2798        {
2799          if (a.getNumOccurrences() > 0)
2800          {
2801            // The argument was provided on the command line, and that will always
2802            // override anything that might be in the properties file.
2803            continue;
2804          }
2805    
2806    
2807          // If we should use a subcommand, then see if the properties file has a
2808          // property that is specific to the selected subcommand.  Then fall back
2809          // to a property that is specific to the tool, and finally fall back to
2810          // checking for a set of values that are generic to any tool that has an
2811          // argument with that name.
2812          List<String> values = null;
2813          if (useSubCommand)
2814          {
2815            values = propertyMap.get(commandName + '.' +
2816                 selectedSubCommand.getPrimaryName()  + '.' +
2817                 a.getIdentifierString());
2818          }
2819    
2820          if (values == null)
2821          {
2822            values = propertyMap.get(commandName + '.' + a.getIdentifierString());
2823          }
2824    
2825          if (values == null)
2826          {
2827            values = propertyMap.get(a.getIdentifierString());
2828          }
2829    
2830          if (values != null)
2831          {
2832            for (final String value : values)
2833            {
2834              if (a instanceof BooleanArgument)
2835              {
2836                // We'll treat this as a BooleanValueArgument.
2837                final BooleanValueArgument bva = new BooleanValueArgument(
2838                     a.getShortIdentifier(), a.getLongIdentifier(), false, null,
2839                     a.getDescription());
2840                bva.addValue(value);
2841                if (bva.getValue())
2842                {
2843                  a.incrementOccurrences();
2844                }
2845    
2846                argumentsSetFromPropertiesFile.add(a.getIdentifierString());
2847              }
2848              else
2849              {
2850                a.addValue(value);
2851                a.incrementOccurrences();
2852    
2853                argumentsSetFromPropertiesFile.add(a.getIdentifierString());
2854                if (a.isSensitive())
2855                {
2856                  argumentsSetFromPropertiesFile.add("***REDACTED***");
2857                }
2858                else
2859                {
2860                  argumentsSetFromPropertiesFile.add(value);
2861                }
2862              }
2863            }
2864          }
2865        }
2866      }
2867    
2868    
2869    
2870      /**
2871       * Retrieves lines that make up the usage information for this program,
2872       * optionally wrapping long lines.
2873       *
2874       * @param  maxWidth  The maximum line width to use for the output.  If this is
2875       *                   less than or equal to zero, then no wrapping will be
2876       *                   performed.
2877       *
2878       * @return  The lines that make up the usage information for this program.
2879       */
2880      public List<String> getUsage(final int maxWidth)
2881      {
2882        // If a subcommand was selected, then provide usage specific to that
2883        // subcommand.
2884        if (selectedSubCommand != null)
2885        {
2886          return getSubCommandUsage(maxWidth);
2887        }
2888    
2889        // First is a description of the command.
2890        final ArrayList<String> lines = new ArrayList<String>(100);
2891        lines.addAll(wrapLine(commandDescription, maxWidth));
2892        lines.add("");
2893    
2894    
2895        // If the tool supports subcommands, and if there are fewer than 10
2896        // subcommands, then display them inline.
2897        if ((! subCommands.isEmpty()) && (subCommands.size() < 10))
2898        {
2899          lines.add(INFO_USAGE_SUBCOMMANDS_HEADER.get());
2900          lines.add("");
2901    
2902          for (final SubCommand sc : subCommands)
2903          {
2904            final StringBuilder nameBuffer = new StringBuilder();
2905            nameBuffer.append("  ");
2906    
2907            final Iterator<String> nameIterator = sc.getNames().iterator();
2908            while (nameIterator.hasNext())
2909            {
2910              nameBuffer.append(nameIterator.next());
2911              if (nameIterator.hasNext())
2912              {
2913                nameBuffer.append(", ");
2914              }
2915            }
2916            lines.add(nameBuffer.toString());
2917    
2918            for (final String descriptionLine :
2919                 wrapLine(sc.getDescription(), (maxWidth - 4)))
2920            {
2921              lines.add("    " + descriptionLine);
2922            }
2923            lines.add("");
2924          }
2925        }
2926    
2927    
2928        // Next comes the usage.  It may include neither, either, or both of the
2929        // set of options and trailing arguments.
2930        if (! subCommands.isEmpty())
2931        {
2932          lines.addAll(wrapLine(INFO_USAGE_SUBCOMMAND_USAGE.get(commandName),
2933                                maxWidth));
2934        }
2935        else if (namedArgs.isEmpty())
2936        {
2937          if (maxTrailingArgs == 0)
2938          {
2939            lines.addAll(wrapLine(INFO_USAGE_NOOPTIONS_NOTRAILING.get(commandName),
2940                                  maxWidth));
2941          }
2942          else
2943          {
2944            lines.addAll(wrapLine(INFO_USAGE_NOOPTIONS_TRAILING.get(
2945                                       commandName, trailingArgsPlaceholder),
2946                                  maxWidth));
2947          }
2948        }
2949        else
2950        {
2951          if (maxTrailingArgs == 0)
2952          {
2953            lines.addAll(wrapLine(INFO_USAGE_OPTIONS_NOTRAILING.get(commandName),
2954                 maxWidth));
2955          }
2956          else
2957          {
2958            lines.addAll(wrapLine(INFO_USAGE_OPTIONS_TRAILING.get(
2959                 commandName, trailingArgsPlaceholder),
2960                 maxWidth));
2961          }
2962        }
2963    
2964        if (! namedArgs.isEmpty())
2965        {
2966          lines.add("");
2967          lines.add(INFO_USAGE_OPTIONS_INCLUDE.get());
2968    
2969    
2970          // If there are any argument groups, then collect the arguments in those
2971          // groups.
2972          boolean hasRequired = false;
2973          final LinkedHashMap<String,List<Argument>> argumentsByGroup =
2974               new LinkedHashMap<String,List<Argument>>(10);
2975          final ArrayList<Argument> argumentsWithoutGroup =
2976               new ArrayList<Argument>(namedArgs.size());
2977          final ArrayList<Argument> usageArguments =
2978               new ArrayList<Argument>(namedArgs.size());
2979          for (final Argument a : namedArgs)
2980          {
2981            if (a.isHidden())
2982            {
2983              // This argument shouldn't be included in the usage output.
2984              continue;
2985            }
2986    
2987            if (a.isRequired() && (! a.hasDefaultValue()))
2988            {
2989              hasRequired = true;
2990            }
2991    
2992            final String argumentGroup = a.getArgumentGroupName();
2993            if (argumentGroup == null)
2994            {
2995              if (a.isUsageArgument())
2996              {
2997                usageArguments.add(a);
2998              }
2999              else
3000              {
3001                argumentsWithoutGroup.add(a);
3002              }
3003            }
3004            else
3005            {
3006              List<Argument> groupArgs = argumentsByGroup.get(argumentGroup);
3007              if (groupArgs == null)
3008              {
3009                groupArgs = new ArrayList<Argument>(10);
3010                argumentsByGroup.put(argumentGroup, groupArgs);
3011              }
3012    
3013              groupArgs.add(a);
3014            }
3015          }
3016    
3017    
3018          // Iterate through the defined argument groups and display usage
3019          // information for each of them.
3020          for (final Map.Entry<String,List<Argument>> e :
3021               argumentsByGroup.entrySet())
3022          {
3023            lines.add("");
3024            lines.add("  " + e.getKey());
3025            lines.add("");
3026            for (final Argument a : e.getValue())
3027            {
3028              getArgUsage(a, lines, true, maxWidth);
3029            }
3030          }
3031    
3032          if (! argumentsWithoutGroup.isEmpty())
3033          {
3034            if (argumentsByGroup.isEmpty())
3035            {
3036              for (final Argument a : argumentsWithoutGroup)
3037              {
3038                getArgUsage(a, lines, false, maxWidth);
3039              }
3040            }
3041            else
3042            {
3043              lines.add("");
3044              lines.add("  " + INFO_USAGE_UNGROUPED_ARGS.get());
3045              lines.add("");
3046              for (final Argument a : argumentsWithoutGroup)
3047              {
3048                getArgUsage(a, lines, true, maxWidth);
3049              }
3050            }
3051          }
3052    
3053          if (! usageArguments.isEmpty())
3054          {
3055            if (argumentsByGroup.isEmpty())
3056            {
3057              for (final Argument a : usageArguments)
3058              {
3059                getArgUsage(a, lines, false, maxWidth);
3060              }
3061            }
3062            else
3063            {
3064              lines.add("");
3065              lines.add("  " + INFO_USAGE_USAGE_ARGS.get());
3066              lines.add("");
3067              for (final Argument a : usageArguments)
3068              {
3069                getArgUsage(a, lines, true, maxWidth);
3070              }
3071            }
3072          }
3073    
3074          if (hasRequired)
3075          {
3076            lines.add("");
3077            if (argumentsByGroup.isEmpty())
3078            {
3079              lines.add("* " + INFO_USAGE_ARG_IS_REQUIRED.get());
3080            }
3081            else
3082            {
3083              lines.add("  * " + INFO_USAGE_ARG_IS_REQUIRED.get());
3084            }
3085          }
3086        }
3087    
3088        return lines;
3089      }
3090    
3091    
3092    
3093      /**
3094       * Retrieves lines that make up the usage information for the selected
3095       * subcommand.
3096       *
3097       * @param  maxWidth  The maximum line width to use for the output.  If this is
3098       *                   less than or equal to zero, then no wrapping will be
3099       *                   performed.
3100       *
3101       * @return  The lines that make up the usage information for the selected
3102       *          subcommand.
3103       */
3104      private List<String> getSubCommandUsage(final int maxWidth)
3105      {
3106        // First is a description of the subcommand.
3107        final ArrayList<String> lines = new ArrayList<String>(100);
3108        lines.addAll(wrapLine(selectedSubCommand.getDescription(), maxWidth));
3109        lines.add("");
3110    
3111        // Next comes the usage.
3112        lines.addAll(wrapLine(
3113             INFO_SUBCOMMAND_USAGE_OPTIONS.get(commandName,
3114                  selectedSubCommand.getPrimaryName()),
3115             maxWidth));
3116    
3117    
3118        final ArgumentParser parser = selectedSubCommand.getArgumentParser();
3119        if (! parser.namedArgs.isEmpty())
3120        {
3121          lines.add("");
3122          lines.add(INFO_USAGE_OPTIONS_INCLUDE.get());
3123    
3124    
3125          // If there are any argument groups, then collect the arguments in those
3126          // groups.
3127          boolean hasRequired = false;
3128          final LinkedHashMap<String,List<Argument>> argumentsByGroup =
3129               new LinkedHashMap<String,List<Argument>>(10);
3130          final ArrayList<Argument> argumentsWithoutGroup =
3131               new ArrayList<Argument>(parser.namedArgs.size());
3132          final ArrayList<Argument> usageArguments =
3133               new ArrayList<Argument>(parser.namedArgs.size());
3134          for (final Argument a : parser.namedArgs)
3135          {
3136            if (a.isHidden())
3137            {
3138              // This argument shouldn't be included in the usage output.
3139              continue;
3140            }
3141    
3142            if (a.isRequired() && (! a.hasDefaultValue()))
3143            {
3144              hasRequired = true;
3145            }
3146    
3147            final String argumentGroup = a.getArgumentGroupName();
3148            if (argumentGroup == null)
3149            {
3150              if (a.isUsageArgument())
3151              {
3152                usageArguments.add(a);
3153              }
3154              else
3155              {
3156                argumentsWithoutGroup.add(a);
3157              }
3158            }
3159            else
3160            {
3161              List<Argument> groupArgs = argumentsByGroup.get(argumentGroup);
3162              if (groupArgs == null)
3163              {
3164                groupArgs = new ArrayList<Argument>(10);
3165                argumentsByGroup.put(argumentGroup, groupArgs);
3166              }
3167    
3168              groupArgs.add(a);
3169            }
3170          }
3171    
3172    
3173          // Iterate through the defined argument groups and display usage
3174          // information for each of them.
3175          for (final Map.Entry<String,List<Argument>> e :
3176               argumentsByGroup.entrySet())
3177          {
3178            lines.add("");
3179            lines.add("  " + e.getKey());
3180            lines.add("");
3181            for (final Argument a : e.getValue())
3182            {
3183              getArgUsage(a, lines, true, maxWidth);
3184            }
3185          }
3186    
3187          if (! argumentsWithoutGroup.isEmpty())
3188          {
3189            if (argumentsByGroup.isEmpty())
3190            {
3191              for (final Argument a : argumentsWithoutGroup)
3192              {
3193                getArgUsage(a, lines, false, maxWidth);
3194              }
3195            }
3196            else
3197            {
3198              lines.add("");
3199              lines.add("  " + INFO_USAGE_UNGROUPED_ARGS.get());
3200              lines.add("");
3201              for (final Argument a : argumentsWithoutGroup)
3202              {
3203                getArgUsage(a, lines, true, maxWidth);
3204              }
3205            }
3206          }
3207    
3208          if (! usageArguments.isEmpty())
3209          {
3210            if (argumentsByGroup.isEmpty())
3211            {
3212              for (final Argument a : usageArguments)
3213              {
3214                getArgUsage(a, lines, false, maxWidth);
3215              }
3216            }
3217            else
3218            {
3219              lines.add("");
3220              lines.add("  " + INFO_USAGE_USAGE_ARGS.get());
3221              lines.add("");
3222              for (final Argument a : usageArguments)
3223              {
3224                getArgUsage(a, lines, true, maxWidth);
3225              }
3226            }
3227          }
3228    
3229          if (hasRequired)
3230          {
3231            lines.add("");
3232            if (argumentsByGroup.isEmpty())
3233            {
3234              lines.add("* " + INFO_USAGE_ARG_IS_REQUIRED.get());
3235            }
3236            else
3237            {
3238              lines.add("  * " + INFO_USAGE_ARG_IS_REQUIRED.get());
3239            }
3240          }
3241        }
3242    
3243        return lines;
3244      }
3245    
3246    
3247    
3248      /**
3249       * Adds usage information for the provided argument to the given list.
3250       *
3251       * @param  a         The argument for which to get the usage information.
3252       * @param  lines     The list to which the resulting lines should be added.
3253       * @param  indent    Indicates whether to indent each line.
3254       * @param  maxWidth  The maximum width of each line, in characters.
3255       */
3256      private static void getArgUsage(final Argument a, final List<String> lines,
3257                                      final boolean indent, final int maxWidth)
3258      {
3259        final StringBuilder argLine = new StringBuilder();
3260        if (indent && (maxWidth > 10))
3261        {
3262          if (a.isRequired() && (! a.hasDefaultValue()))
3263          {
3264            argLine.append("  * ");
3265          }
3266          else
3267          {
3268            argLine.append("    ");
3269          }
3270        }
3271        else if (a.isRequired() && (! a.hasDefaultValue()))
3272        {
3273          argLine.append("* ");
3274        }
3275    
3276        boolean first = true;
3277        for (final Character c : a.getShortIdentifiers())
3278        {
3279          if (first)
3280          {
3281            argLine.append('-');
3282            first = false;
3283          }
3284          else
3285          {
3286            argLine.append(", -");
3287          }
3288          argLine.append(c);
3289        }
3290    
3291        for (final String s : a.getLongIdentifiers())
3292        {
3293          if (first)
3294          {
3295            argLine.append("--");
3296            first = false;
3297          }
3298          else
3299          {
3300            argLine.append(", --");
3301          }
3302          argLine.append(s);
3303        }
3304    
3305        final String valuePlaceholder = a.getValuePlaceholder();
3306        if (valuePlaceholder != null)
3307        {
3308          argLine.append(' ');
3309          argLine.append(valuePlaceholder);
3310        }
3311    
3312        // If we need to wrap the argument line, then align the dashes on the left
3313        // edge.
3314        int subsequentLineWidth = maxWidth - 4;
3315        if (subsequentLineWidth < 4)
3316        {
3317          subsequentLineWidth = maxWidth;
3318        }
3319        final List<String> identifierLines =
3320             wrapLine(argLine.toString(), maxWidth, subsequentLineWidth);
3321        for (int i=0; i < identifierLines.size(); i++)
3322        {
3323          if (i == 0)
3324          {
3325            lines.add(identifierLines.get(0));
3326          }
3327          else
3328          {
3329            lines.add("    " + identifierLines.get(i));
3330          }
3331        }
3332    
3333    
3334        // The description should be wrapped, if necessary.  We'll also want to
3335        // indent it (unless someone chose an absurdly small wrap width) to make
3336        // it stand out from the argument lines.
3337        final String description = a.getDescription();
3338        if (maxWidth > 10)
3339        {
3340          final String indentString;
3341          if (indent)
3342          {
3343            indentString = "        ";
3344          }
3345          else
3346          {
3347            indentString = "    ";
3348          }
3349    
3350          final List<String> descLines = wrapLine(description,
3351               (maxWidth-indentString.length()));
3352          for (final String s : descLines)
3353          {
3354            lines.add(indentString + s);
3355          }
3356        }
3357        else
3358        {
3359          lines.addAll(wrapLine(description, maxWidth));
3360        }
3361      }
3362    
3363    
3364    
3365      /**
3366       * Writes usage information for this program to the provided output stream
3367       * using the UTF-8 encoding, optionally wrapping long lines.
3368       *
3369       * @param  outputStream  The output stream to which the usage information
3370       *                       should be written.  It must not be {@code null}.
3371       * @param  maxWidth      The maximum line width to use for the output.  If
3372       *                       this is less than or equal to zero, then no wrapping
3373       *                       will be performed.
3374       *
3375       * @throws  IOException  If an error occurs while attempting to write to the
3376       *                       provided output stream.
3377       */
3378      public void getUsage(final OutputStream outputStream, final int maxWidth)
3379             throws IOException
3380      {
3381        final List<String> usageLines = getUsage(maxWidth);
3382        for (final String s : usageLines)
3383        {
3384          outputStream.write(getBytes(s));
3385          outputStream.write(EOL_BYTES);
3386        }
3387      }
3388    
3389    
3390    
3391      /**
3392       * Retrieves a string representation of the usage information.
3393       *
3394       * @param  maxWidth  The maximum line width to use for the output.  If this is
3395       *                   less than or equal to zero, then no wrapping will be
3396       *                   performed.
3397       *
3398       * @return  A string representation of the usage information
3399       */
3400      public String getUsageString(final int maxWidth)
3401      {
3402        final StringBuilder buffer = new StringBuilder();
3403        getUsageString(buffer, maxWidth);
3404        return buffer.toString();
3405      }
3406    
3407    
3408    
3409      /**
3410       * Appends a string representation of the usage information to the provided
3411       * buffer.
3412       *
3413       * @param  buffer    The buffer to which the information should be appended.
3414       * @param  maxWidth  The maximum line width to use for the output.  If this is
3415       *                   less than or equal to zero, then no wrapping will be
3416       *                   performed.
3417       */
3418      public void getUsageString(final StringBuilder buffer, final int maxWidth)
3419      {
3420        for (final String line : getUsage(maxWidth))
3421        {
3422          buffer.append(line);
3423          buffer.append(EOL);
3424        }
3425      }
3426    
3427    
3428    
3429      /**
3430       * Retrieves a string representation of this argument parser.
3431       *
3432       * @return  A string representation of this argument parser.
3433       */
3434      @Override()
3435      public String toString()
3436      {
3437        final StringBuilder buffer = new StringBuilder();
3438        toString(buffer);
3439        return buffer.toString();
3440      }
3441    
3442    
3443    
3444      /**
3445       * Appends a string representation of this argument parser to the provided
3446       * buffer.
3447       *
3448       * @param  buffer  The buffer to which the information should be appended.
3449       */
3450      public void toString(final StringBuilder buffer)
3451      {
3452        buffer.append("ArgumentParser(commandName='");
3453        buffer.append(commandName);
3454        buffer.append("', commandDescription='");
3455        buffer.append(commandDescription);
3456        buffer.append("', minTrailingArgs=");
3457        buffer.append(minTrailingArgs);
3458        buffer.append("', maxTrailingArgs=");
3459        buffer.append(maxTrailingArgs);
3460    
3461        if (trailingArgsPlaceholder != null)
3462        {
3463          buffer.append(", trailingArgsPlaceholder='");
3464          buffer.append(trailingArgsPlaceholder);
3465          buffer.append('\'');
3466        }
3467    
3468        buffer.append("namedArgs={");
3469    
3470        final Iterator<Argument> iterator = namedArgs.iterator();
3471        while (iterator.hasNext())
3472        {
3473          iterator.next().toString(buffer);
3474          if (iterator.hasNext())
3475          {
3476            buffer.append(", ");
3477          }
3478        }
3479    
3480        buffer.append('}');
3481    
3482        if (! subCommands.isEmpty())
3483        {
3484          buffer.append(", subCommands={");
3485    
3486          final Iterator<SubCommand> subCommandIterator = subCommands.iterator();
3487          while (subCommandIterator.hasNext())
3488          {
3489            subCommandIterator.next().toString(buffer);
3490            if (subCommandIterator.hasNext())
3491            {
3492              buffer.append(", ");
3493            }
3494          }
3495    
3496          buffer.append('}');
3497        }
3498    
3499        buffer.append(')');
3500      }
3501    }