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