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