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 = System.getProperty(PROPERTY_DEFAULT_PROPERTIES_FILE_PATH);
2501    if (path == null)
2502    {
2503      path = System.getenv(ENV_DEFAULT_PROPERTIES_FILE_PATH);
2504    }
2505
2506    if (path != null)
2507    {
2508      final File propertiesFile = new File(path);
2509      if (propertiesFile.exists() && propertiesFile.isFile())
2510      {
2511        handlePropertiesFile(propertiesFile);
2512      }
2513    }
2514
2515    return true;
2516  }
2517
2518
2519
2520  /**
2521   * Write an empty properties file for this argument parser to the specified
2522   * path.
2523   *
2524   * @param  path  The path to the properties file to be written.
2525   *
2526   * @throws  ArgumentException  If a problem is encountered while writing the
2527   *                             properties file.
2528   */
2529  private void generatePropertiesFile(final String path)
2530          throws ArgumentException
2531  {
2532    final PrintWriter w;
2533    try
2534    {
2535      // The java.util.Properties specification states that properties files
2536      // should be read using the ISO 8859-1 character set.
2537      w = new PrintWriter(new OutputStreamWriter(new FileOutputStream(path),
2538           StandardCharsets.ISO_8859_1));
2539    }
2540    catch (final Exception e)
2541    {
2542      Debug.debugException(e);
2543      throw new ArgumentException(
2544           ERR_PARSER_GEN_PROPS_CANNOT_OPEN_FILE.get(path,
2545                StaticUtils.getExceptionMessage(e)),
2546           e);
2547    }
2548
2549    try
2550    {
2551      wrapComment(w, INFO_PARSER_GEN_PROPS_HEADER_1.get(commandName));
2552      w.println('#');
2553      wrapComment(w,
2554           INFO_PARSER_GEN_PROPS_HEADER_2.get(commandName,
2555                ARG_NAME_PROPERTIES_FILE_PATH,
2556                PROPERTY_DEFAULT_PROPERTIES_FILE_PATH,
2557                ENV_DEFAULT_PROPERTIES_FILE_PATH, ARG_NAME_NO_PROPERTIES_FILE));
2558      w.println('#');
2559      wrapComment(w, INFO_PARSER_GEN_PROPS_HEADER_3.get());
2560      w.println('#');
2561
2562      wrapComment(w, INFO_PARSER_GEN_PROPS_HEADER_4.get());
2563      w.println('#');
2564      wrapComment(w, INFO_PARSER_GEN_PROPS_HEADER_5.get(commandName));
2565
2566      for (final Argument a : getNamedArguments())
2567      {
2568        writeArgumentProperties(w, null, a);
2569      }
2570
2571      for (final SubCommand sc : getSubCommands())
2572      {
2573        for (final Argument a : sc.getArgumentParser().getNamedArguments())
2574        {
2575          writeArgumentProperties(w, sc, a);
2576        }
2577      }
2578    }
2579    finally
2580    {
2581      w.close();
2582    }
2583  }
2584
2585
2586
2587  /**
2588   * Writes information about the provided argument to the given writer.
2589   *
2590   * @param  w   The writer to which the properties should be written.  It must
2591   *             not be {@code null}.
2592   * @param  sc  The subcommand with which the argument is associated.  It may
2593   *             be {@code null} if the provided argument is a global argument.
2594   * @param  a   The argument for which to write the properties.  It must not be
2595   *             {@code null}.
2596   */
2597  private void writeArgumentProperties(final PrintWriter w,
2598                                       final SubCommand sc,
2599                                       final Argument a)
2600  {
2601    if (a.isUsageArgument() || a.isHidden())
2602    {
2603      return;
2604    }
2605
2606    w.println();
2607    w.println();
2608    wrapComment(w, a.getDescription());
2609    w.println('#');
2610
2611    final String constraints = a.getValueConstraints();
2612    if ((constraints != null) && (! constraints.isEmpty()) &&
2613        (! (a instanceof BooleanArgument)))
2614    {
2615      wrapComment(w, constraints);
2616      w.println('#');
2617    }
2618
2619    final String identifier;
2620    if (a.getLongIdentifier() != null)
2621    {
2622      identifier = a.getLongIdentifier();
2623    }
2624    else
2625    {
2626      identifier = a.getIdentifierString();
2627    }
2628
2629    String placeholder = a.getValuePlaceholder();
2630    if (placeholder == null)
2631    {
2632      if (a instanceof BooleanArgument)
2633      {
2634        placeholder = "{true|false}";
2635      }
2636      else
2637      {
2638        placeholder = "";
2639      }
2640    }
2641
2642    final String propertyName;
2643    if (sc == null)
2644    {
2645      propertyName = commandName + '.' + identifier;
2646    }
2647    else
2648    {
2649      propertyName = commandName + '.' + sc.getPrimaryName() + '.' + identifier;
2650    }
2651
2652    w.println("# " + propertyName + '=' + placeholder);
2653
2654    if (a.isPresent())
2655    {
2656      for (final String s : a.getValueStringRepresentations(false))
2657      {
2658        w.println(propertyName + '=' + s);
2659      }
2660    }
2661  }
2662
2663
2664
2665  /**
2666   * Wraps the given string and writes it as a comment to the provided writer.
2667   *
2668   * @param  w  The writer to use to write the wrapped and commented string.
2669   * @param  s  The string to be wrapped and written.
2670   */
2671  private static void wrapComment(final PrintWriter w, final String s)
2672  {
2673    for (final String line : StaticUtils.wrapLine(s, 77))
2674    {
2675      w.println("# " + line);
2676    }
2677  }
2678
2679
2680
2681  /**
2682   * Reads the contents of the specified properties file and updates the
2683   * configured arguments as appropriate.
2684   *
2685   * @param  propertiesFile  The properties file to process.
2686   *
2687   * @throws  ArgumentException  If a problem is encountered while examining the
2688   *                             properties file, or while trying to assign a
2689   *                             property value to a corresponding argument.
2690   */
2691  private void handlePropertiesFile(final File propertiesFile)
2692          throws ArgumentException
2693  {
2694    final String propertiesFilePath = propertiesFile.getAbsolutePath();
2695
2696    InputStream inputStream = null;
2697    final BufferedReader reader;
2698    try
2699    {
2700      inputStream = new FileInputStream(propertiesFile);
2701
2702
2703      // Handle the case in which the properties file may be encrypted.
2704      final List<char[]> cachedPasswords;
2705      final PrintStream err;
2706      final PrintStream out;
2707      final CommandLineTool tool = commandLineTool;
2708      if (tool == null)
2709      {
2710        cachedPasswords = Collections.emptyList();
2711        out = System.out;
2712        err = System.err;
2713      }
2714      else
2715      {
2716        cachedPasswords =
2717             tool.getPasswordFileReader().getCachedEncryptionPasswords();
2718        out = tool.getOut();
2719        err = tool.getErr();
2720      }
2721
2722      final ObjectPair<InputStream,char[]> encryptionData =
2723           ToolUtils.getPossiblyPassphraseEncryptedInputStream(inputStream,
2724                cachedPasswords, true,
2725                INFO_PARSER_PROMPT_FOR_PROP_FILE_ENC_PW.get(
2726                     propertiesFile.getAbsolutePath()),
2727                ERR_PARSER_WRONG_PROP_FILE_ENC_PW.get(
2728                     propertiesFile.getAbsolutePath()),
2729                out, err);
2730
2731      inputStream = encryptionData.getFirst();
2732      if ((tool != null) && (encryptionData.getSecond() != null))
2733      {
2734        tool.getPasswordFileReader().addToEncryptionPasswordCache(
2735             encryptionData.getSecond());
2736      }
2737
2738
2739      // Handle the case in which the properties file may be compressed.
2740      inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream);
2741
2742
2743      // The java.util.Properties specification states that properties files
2744      // should be read using the ISO 8859-1 character set, and that characters
2745      // that cannot be encoded in that format should be represented using
2746      // Unicode escapes that start with a backslash, a lowercase letter "u",
2747      // and four hexadecimal digits.  To provide compatibility with the Java
2748      // Properties file format (except we also support the same property
2749      // appearing multiple times), we will also use that encoding and will
2750      // support Unicode escape sequences.
2751      reader = new BufferedReader(new InputStreamReader(inputStream,
2752           StandardCharsets.ISO_8859_1));
2753    }
2754    catch (final Exception e)
2755    {
2756      if (inputStream != null)
2757      {
2758        try
2759        {
2760          inputStream.close();
2761        }
2762        catch (final Exception e2)
2763        {
2764          Debug.debugException(e2);
2765        }
2766      }
2767
2768      Debug.debugException(e);
2769      throw new ArgumentException(
2770           ERR_PARSER_CANNOT_OPEN_PROP_FILE.get(propertiesFilePath,
2771                StaticUtils.getExceptionMessage(e)),
2772           e);
2773    }
2774
2775    try
2776    {
2777      // Read all of the lines of the file, ignoring comments and unwrapping
2778      // properties that span multiple lines.
2779      boolean lineIsContinued = false;
2780      int lineNumber = 0;
2781      final ArrayList<ObjectPair<Integer,StringBuilder>> propertyLines =
2782           new ArrayList<>(10);
2783      while (true)
2784      {
2785        String line;
2786        try
2787        {
2788          line = reader.readLine();
2789          lineNumber++;
2790        }
2791        catch (final Exception e)
2792        {
2793          Debug.debugException(e);
2794          throw new ArgumentException(
2795               ERR_PARSER_ERROR_READING_PROP_FILE.get(propertiesFilePath,
2796                    StaticUtils.getExceptionMessage(e)),
2797               e);
2798        }
2799
2800
2801        // If the line is null, then we've reached the end of the file.  If we
2802        // expect a previous line to have been continued, then this is an error.
2803        if (line == null)
2804        {
2805          if (lineIsContinued)
2806          {
2807            throw new ArgumentException(
2808                 ERR_PARSER_PROP_FILE_MISSING_CONTINUATION.get(
2809                      (lineNumber-1), propertiesFilePath));
2810          }
2811          break;
2812        }
2813
2814
2815        // See if the line has any leading whitespace, and if so then trim it
2816        // off.  If there is leading whitespace, then make sure that we expect
2817        // the previous line to be continued.
2818        final int initialLength = line.length();
2819        line = StaticUtils.trimLeading(line);
2820        final boolean hasLeadingWhitespace = (line.length() < initialLength);
2821        if (hasLeadingWhitespace && (! lineIsContinued))
2822        {
2823          throw new ArgumentException(
2824               ERR_PARSER_PROP_FILE_UNEXPECTED_LEADING_SPACE.get(
2825                    propertiesFilePath, lineNumber));
2826        }
2827
2828
2829        // If the line is empty or starts with "#", then skip it.  But make sure
2830        // we didn't expect the previous line to be continued.
2831        if ((line.isEmpty()) || line.startsWith("#"))
2832        {
2833          if (lineIsContinued)
2834          {
2835            throw new ArgumentException(
2836                 ERR_PARSER_PROP_FILE_MISSING_CONTINUATION.get(
2837                      (lineNumber-1), propertiesFilePath));
2838          }
2839          continue;
2840        }
2841
2842
2843        // See if the line ends with a backslash and if so then trim it off.
2844        final boolean hasTrailingBackslash = line.endsWith("\\");
2845        if (line.endsWith("\\"))
2846        {
2847          line = line.substring(0, (line.length() - 1));
2848        }
2849
2850
2851        // If the previous line needs to be continued, then append the new line
2852        // to it.  Otherwise, add it as a new line.
2853        if (lineIsContinued)
2854        {
2855          propertyLines.get(propertyLines.size() - 1).getSecond().append(line);
2856        }
2857        else
2858        {
2859          propertyLines.add(
2860               new ObjectPair<>(lineNumber, new StringBuilder(line)));
2861        }
2862
2863        lineIsContinued = hasTrailingBackslash;
2864      }
2865
2866
2867      // Parse all of the lines into a map of identifiers and their
2868      // corresponding values.
2869      propertiesFileUsed = propertiesFile;
2870      if (propertyLines.isEmpty())
2871      {
2872        return;
2873      }
2874
2875      final HashMap<String,ArrayList<String>> propertyMap =
2876           new HashMap<>(StaticUtils.computeMapCapacity(propertyLines.size()));
2877      for (final ObjectPair<Integer,StringBuilder> p : propertyLines)
2878      {
2879        lineNumber = p.getFirst();
2880        final String line = handleUnicodeEscapes(propertiesFilePath, lineNumber,
2881             p.getSecond());
2882        final int equalPos = line.indexOf('=');
2883        if (equalPos <= 0)
2884        {
2885          throw new ArgumentException(ERR_PARSER_MALFORMED_PROP_LINE.get(
2886               propertiesFilePath, lineNumber, line));
2887        }
2888
2889        final String propertyName = line.substring(0, equalPos).trim();
2890        final String propertyValue = line.substring(equalPos+1).trim();
2891        if (propertyValue.isEmpty())
2892        {
2893          // The property doesn't have a value, so we can ignore it.
2894          continue;
2895        }
2896
2897
2898        // An argument can have multiple identifiers, and we will allow any of
2899        // them to be used to reference it.  To deal with this, we'll map the
2900        // argument identifier to its corresponding argument and then use the
2901        // preferred identifier for that argument in the map.  The same applies
2902        // to subcommand names.
2903        boolean prefixedWithToolName = false;
2904        boolean prefixedWithSubCommandName = false;
2905        Argument a = getNamedArgument(propertyName);
2906        if (a == null)
2907        {
2908          // It could be that the argument name was prefixed with the tool name.
2909          // Check to see if that was the case.
2910          if (propertyName.startsWith(commandName + '.'))
2911          {
2912            prefixedWithToolName = true;
2913
2914            String basePropertyName =
2915                 propertyName.substring(commandName.length()+1);
2916            a = getNamedArgument(basePropertyName);
2917
2918            if (a == null)
2919            {
2920              final int periodPos = basePropertyName.indexOf('.');
2921              if (periodPos > 0)
2922              {
2923                final String subCommandName =
2924                     basePropertyName.substring(0, periodPos);
2925                if ((selectedSubCommand != null) &&
2926                    selectedSubCommand.hasName(subCommandName))
2927                {
2928                  prefixedWithSubCommandName = true;
2929                  basePropertyName = basePropertyName.substring(periodPos+1);
2930                  a = selectedSubCommand.getArgumentParser().getNamedArgument(
2931                       basePropertyName);
2932                }
2933              }
2934              else if (selectedSubCommand != null)
2935              {
2936                a = selectedSubCommand.getArgumentParser().getNamedArgument(
2937                     basePropertyName);
2938              }
2939            }
2940          }
2941          else if (selectedSubCommand != null)
2942          {
2943            a = selectedSubCommand.getArgumentParser().getNamedArgument(
2944                 propertyName);
2945          }
2946        }
2947
2948        if (a == null)
2949        {
2950          // This could mean that there's a typo in the property name, but it's
2951          // more likely the case that the property is for a different tool.  In
2952          // either case, we'll ignore it.
2953          continue;
2954        }
2955
2956        final String canonicalPropertyName;
2957        if (prefixedWithToolName)
2958        {
2959          if (prefixedWithSubCommandName)
2960          {
2961            canonicalPropertyName = commandName + '.' +
2962                 selectedSubCommand.getPrimaryName() + '.' +
2963                 a.getIdentifierString();
2964          }
2965          else
2966          {
2967            canonicalPropertyName = commandName + '.' + a.getIdentifierString();
2968          }
2969        }
2970        else
2971        {
2972          canonicalPropertyName = a.getIdentifierString();
2973        }
2974
2975        ArrayList<String> valueList = propertyMap.get(canonicalPropertyName);
2976        if (valueList == null)
2977        {
2978          valueList = new ArrayList<>(5);
2979          propertyMap.put(canonicalPropertyName, valueList);
2980        }
2981        valueList.add(propertyValue);
2982      }
2983
2984
2985      // Iterate through all of the named arguments for the argument parser and
2986      // see if we should use the properties to assign values to any of the
2987      // arguments that weren't provided on the command line.
2988      setArgsFromPropertiesFile(propertyMap, false);
2989
2990
2991      // If there is a selected subcommand, then iterate through all of its
2992      // arguments.
2993      if (selectedSubCommand != null)
2994      {
2995        setArgsFromPropertiesFile(propertyMap, true);
2996      }
2997    }
2998    finally
2999    {
3000      try
3001      {
3002        reader.close();
3003      }
3004      catch (final Exception e)
3005      {
3006        Debug.debugException(e);
3007      }
3008    }
3009  }
3010
3011
3012
3013  /**
3014   * Retrieves a string that contains the contents of the provided buffer, but
3015   * with any Unicode escape sequences converted to the appropriate character
3016   * representation.
3017   *
3018   * @param  propertiesFilePath  The path to the properties file
3019   * @param  lineNumber  The line number on which the property definition
3020   *                     starts.
3021   * @param  buffer      The buffer containing the data to be processed.  It
3022   *                     must not be {@code null} but may be empty.
3023   *
3024   * @return  A string that contains the contents of the provided buffer, but
3025   *          with any Unicode escape sequences converted to the appropriate
3026   *          character representation.
3027   *
3028   * @throws  ArgumentException  If a malformed Unicode escape sequence is
3029   *                             encountered.
3030   */
3031  static String handleUnicodeEscapes(final String propertiesFilePath,
3032                                     final int lineNumber,
3033                                     final StringBuilder buffer)
3034         throws ArgumentException
3035  {
3036    int pos = 0;
3037    while (pos < buffer.length())
3038    {
3039      final char c = buffer.charAt(pos);
3040      if (c == '\\')
3041      {
3042        if (pos <= (buffer.length() - 5))
3043        {
3044          final char nextChar = buffer.charAt(pos+1);
3045          if ((nextChar == 'u') || (nextChar == 'U'))
3046          {
3047            try
3048            {
3049              final String hexDigits = buffer.substring(pos+2, pos+6);
3050              final byte[] bytes = StaticUtils.fromHex(hexDigits);
3051              final int i = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF);
3052              buffer.setCharAt(pos, (char) i);
3053              for (int j=0; j < 5; j++)
3054              {
3055                buffer.deleteCharAt(pos+1);
3056              }
3057            }
3058            catch (final Exception e)
3059            {
3060              Debug.debugException(e);
3061              throw new ArgumentException(
3062                   ERR_PARSER_MALFORMED_UNICODE_ESCAPE.get(propertiesFilePath,
3063                        lineNumber),
3064                   e);
3065            }
3066          }
3067          else
3068          {
3069            pos++;
3070          }
3071        }
3072      }
3073
3074      pos++;
3075    }
3076
3077    return buffer.toString();
3078  }
3079
3080
3081
3082  /**
3083   * Sets the values of any arguments not provided on the command line but
3084   * defined in the properties file.
3085   *
3086   * @param  propertyMap    A map of properties read from the properties file.
3087   * @param  useSubCommand  Indicates whether to use the argument parser
3088   *                        associated with the selected subcommand rather than
3089   *                        the global argument parser.
3090   *
3091   * @throws  ArgumentException  If a problem is encountered while examining the
3092   *                             properties file, or while trying to assign a
3093   *                             property value to a corresponding argument.
3094   */
3095  private void setArgsFromPropertiesFile(
3096                    final Map<String,ArrayList<String>> propertyMap,
3097                    final boolean useSubCommand)
3098          throws ArgumentException
3099  {
3100    final ArgumentParser p;
3101    if (useSubCommand)
3102    {
3103      p = selectedSubCommand.getArgumentParser();
3104    }
3105    else
3106    {
3107      p = this;
3108    }
3109
3110
3111    for (final Argument a : p.namedArgs)
3112    {
3113      // If the argument was provided on the command line, then that will always
3114      // override anything that might be in the properties file.
3115      if (a.getNumOccurrences() > 0)
3116      {
3117        continue;
3118      }
3119
3120
3121      // If the argument is part of an exclusive argument set, and if one of
3122      // the other arguments in that set was provided on the command line, then
3123      // don't look in the properties file for a value for the argument.
3124      boolean exclusiveArgumentHasValue = false;
3125exclusiveArgumentLoop:
3126      for (final Set<Argument> exclusiveArgumentSet : exclusiveArgumentSets)
3127      {
3128        if (exclusiveArgumentSet.contains(a))
3129        {
3130          for (final Argument exclusiveArg : exclusiveArgumentSet)
3131          {
3132            if (exclusiveArg.getNumOccurrences() > 0)
3133            {
3134              exclusiveArgumentHasValue = true;
3135              break exclusiveArgumentLoop;
3136            }
3137          }
3138        }
3139      }
3140
3141      if (exclusiveArgumentHasValue)
3142      {
3143        continue;
3144      }
3145
3146
3147      // If we should use a subcommand, then see if the properties file has a
3148      // property that is specific to the selected subcommand.  Then fall back
3149      // to a property that is specific to the tool, and finally fall back to
3150      // checking for a set of values that are generic to any tool that has an
3151      // argument with that name.
3152      List<String> values = null;
3153      if (useSubCommand)
3154      {
3155        values = propertyMap.get(commandName + '.' +
3156             selectedSubCommand.getPrimaryName()  + '.' +
3157             a.getIdentifierString());
3158      }
3159
3160      if (values == null)
3161      {
3162        values = propertyMap.get(commandName + '.' + a.getIdentifierString());
3163      }
3164
3165      if (values == null)
3166      {
3167        values = propertyMap.get(a.getIdentifierString());
3168      }
3169
3170      if (values != null)
3171      {
3172        for (final String value : values)
3173        {
3174          if (a instanceof BooleanArgument)
3175          {
3176            // We'll treat this as a BooleanValueArgument.
3177            final BooleanValueArgument bva = new BooleanValueArgument(
3178                 a.getShortIdentifier(), a.getLongIdentifier(), false, null,
3179                 a.getDescription());
3180            bva.addValue(value);
3181            if (bva.getValue())
3182            {
3183              a.incrementOccurrences();
3184              argumentsSetFromPropertiesFile.add(a.getIdentifierString());
3185            }
3186          }
3187          else
3188          {
3189            a.addValue(value);
3190            a.incrementOccurrences();
3191
3192            argumentsSetFromPropertiesFile.add(a.getIdentifierString());
3193            if (a.isSensitive())
3194            {
3195              argumentsSetFromPropertiesFile.add("***REDACTED***");
3196            }
3197            else
3198            {
3199              argumentsSetFromPropertiesFile.add(value);
3200            }
3201          }
3202        }
3203      }
3204    }
3205  }
3206
3207
3208
3209  /**
3210   * Retrieves lines that make up the usage information for this program,
3211   * optionally wrapping long lines.
3212   *
3213   * @param  maxWidth  The maximum line width to use for the output.  If this is
3214   *                   less than or equal to zero, then no wrapping will be
3215   *                   performed.
3216   *
3217   * @return  The lines that make up the usage information for this program.
3218   */
3219  public List<String> getUsage(final int maxWidth)
3220  {
3221    // If a subcommand was selected, then provide usage specific to that
3222    // subcommand.
3223    if (selectedSubCommand != null)
3224    {
3225      return getSubCommandUsage(maxWidth);
3226    }
3227
3228    // First is a description of the command.
3229    final ArrayList<String> lines = new ArrayList<>(100);
3230    lines.addAll(StaticUtils.wrapLine(commandDescription, maxWidth));
3231    lines.add("");
3232
3233
3234    for (final String additionalDescriptionParagraph :
3235         additionalCommandDescriptionParagraphs)
3236    {
3237      lines.addAll(StaticUtils.wrapLine(additionalDescriptionParagraph,
3238           maxWidth));
3239      lines.add("");
3240    }
3241
3242    // If the tool supports subcommands, and if there are fewer than 10
3243    // subcommands, then display them inline.
3244    if ((! subCommands.isEmpty()) && (subCommands.size() < 10))
3245    {
3246      lines.add(INFO_USAGE_SUBCOMMANDS_HEADER.get());
3247      lines.add("");
3248
3249      for (final SubCommand sc : subCommands)
3250      {
3251        final StringBuilder nameBuffer = new StringBuilder();
3252        nameBuffer.append("  ");
3253
3254        final Iterator<String> nameIterator = sc.getNames(false).iterator();
3255        while (nameIterator.hasNext())
3256        {
3257          nameBuffer.append(nameIterator.next());
3258          if (nameIterator.hasNext())
3259          {
3260            nameBuffer.append(", ");
3261          }
3262        }
3263        lines.add(nameBuffer.toString());
3264
3265        for (final String descriptionLine :
3266             StaticUtils.wrapLine(sc.getDescription(), (maxWidth - 4)))
3267        {
3268          lines.add("    " + descriptionLine);
3269        }
3270        lines.add("");
3271      }
3272    }
3273
3274
3275    // Next comes the usage.  It may include neither, either, or both of the
3276    // set of options and trailing arguments.
3277    if (! subCommands.isEmpty())
3278    {
3279      lines.addAll(StaticUtils.wrapLine(
3280           INFO_USAGE_SUBCOMMAND_USAGE.get(commandName), maxWidth));
3281    }
3282    else if (namedArgs.isEmpty())
3283    {
3284      if (maxTrailingArgs == 0)
3285      {
3286        lines.addAll(StaticUtils.wrapLine(
3287             INFO_USAGE_NOOPTIONS_NOTRAILING.get(commandName), maxWidth));
3288      }
3289      else
3290      {
3291        lines.addAll(StaticUtils.wrapLine(INFO_USAGE_NOOPTIONS_TRAILING.get(
3292             commandName, trailingArgsPlaceholder), maxWidth));
3293      }
3294    }
3295    else
3296    {
3297      if (maxTrailingArgs == 0)
3298      {
3299        lines.addAll(StaticUtils.wrapLine(
3300             INFO_USAGE_OPTIONS_NOTRAILING.get(commandName), maxWidth));
3301      }
3302      else
3303      {
3304        lines.addAll(StaticUtils.wrapLine(INFO_USAGE_OPTIONS_TRAILING.get(
3305             commandName, trailingArgsPlaceholder), maxWidth));
3306      }
3307    }
3308
3309    if (! namedArgs.isEmpty())
3310    {
3311      lines.add("");
3312      lines.add(INFO_USAGE_OPTIONS_INCLUDE.get());
3313
3314
3315      // If there are any argument groups, then collect the arguments in those
3316      // groups.
3317      boolean hasRequired = false;
3318      final LinkedHashMap<String,List<Argument>> argumentsByGroup =
3319           new LinkedHashMap<>(StaticUtils.computeMapCapacity(10));
3320      final ArrayList<Argument> argumentsWithoutGroup =
3321           new ArrayList<>(namedArgs.size());
3322      final ArrayList<Argument> usageArguments =
3323           new ArrayList<>(namedArgs.size());
3324      for (final Argument a : namedArgs)
3325      {
3326        if (a.isHidden())
3327        {
3328          // This argument shouldn't be included in the usage output.
3329          continue;
3330        }
3331
3332        if (a.isRequired() && (! a.hasDefaultValue()))
3333        {
3334          hasRequired = true;
3335        }
3336
3337        final String argumentGroup = a.getArgumentGroupName();
3338        if (argumentGroup == null)
3339        {
3340          if (a.isUsageArgument())
3341          {
3342            usageArguments.add(a);
3343          }
3344          else
3345          {
3346            argumentsWithoutGroup.add(a);
3347          }
3348        }
3349        else
3350        {
3351          List<Argument> groupArgs = argumentsByGroup.get(argumentGroup);
3352          if (groupArgs == null)
3353          {
3354            groupArgs = new ArrayList<>(10);
3355            argumentsByGroup.put(argumentGroup, groupArgs);
3356          }
3357
3358          groupArgs.add(a);
3359        }
3360      }
3361
3362
3363      // Iterate through the defined argument groups and display usage
3364      // information for each of them.
3365      for (final Map.Entry<String,List<Argument>> e :
3366           argumentsByGroup.entrySet())
3367      {
3368        lines.add("");
3369        lines.add("  " + e.getKey());
3370        lines.add("");
3371        for (final Argument a : e.getValue())
3372        {
3373          getArgUsage(a, lines, true, maxWidth);
3374        }
3375      }
3376
3377      if (! argumentsWithoutGroup.isEmpty())
3378      {
3379        if (argumentsByGroup.isEmpty())
3380        {
3381          for (final Argument a : argumentsWithoutGroup)
3382          {
3383            getArgUsage(a, lines, false, maxWidth);
3384          }
3385        }
3386        else
3387        {
3388          lines.add("");
3389          lines.add("  " + INFO_USAGE_UNGROUPED_ARGS.get());
3390          lines.add("");
3391          for (final Argument a : argumentsWithoutGroup)
3392          {
3393            getArgUsage(a, lines, true, maxWidth);
3394          }
3395        }
3396      }
3397
3398      if (! usageArguments.isEmpty())
3399      {
3400        if (argumentsByGroup.isEmpty())
3401        {
3402          for (final Argument a : usageArguments)
3403          {
3404            getArgUsage(a, lines, false, maxWidth);
3405          }
3406        }
3407        else
3408        {
3409          lines.add("");
3410          lines.add("  " + INFO_USAGE_USAGE_ARGS.get());
3411          lines.add("");
3412          for (final Argument a : usageArguments)
3413          {
3414            getArgUsage(a, lines, true, maxWidth);
3415          }
3416        }
3417      }
3418
3419      if (hasRequired)
3420      {
3421        lines.add("");
3422        if (argumentsByGroup.isEmpty())
3423        {
3424          lines.add("* " + INFO_USAGE_ARG_IS_REQUIRED.get());
3425        }
3426        else
3427        {
3428          lines.add("  * " + INFO_USAGE_ARG_IS_REQUIRED.get());
3429        }
3430      }
3431    }
3432
3433    return lines;
3434  }
3435
3436
3437
3438  /**
3439   * Retrieves lines that make up the usage information for the selected
3440   * subcommand.
3441   *
3442   * @param  maxWidth  The maximum line width to use for the output.  If this is
3443   *                   less than or equal to zero, then no wrapping will be
3444   *                   performed.
3445   *
3446   * @return  The lines that make up the usage information for the selected
3447   *          subcommand.
3448   */
3449  private List<String> getSubCommandUsage(final int maxWidth)
3450  {
3451    // First is a description of the subcommand.
3452    final ArrayList<String> lines = new ArrayList<>(100);
3453    lines.addAll(
3454         StaticUtils.wrapLine(selectedSubCommand.getDescription(), maxWidth));
3455    lines.add("");
3456
3457    // Next comes the usage.
3458    lines.addAll(StaticUtils.wrapLine(INFO_SUBCOMMAND_USAGE_OPTIONS.get(
3459         commandName, selectedSubCommand.getPrimaryName()), maxWidth));
3460
3461
3462    final ArgumentParser parser = selectedSubCommand.getArgumentParser();
3463    if (! parser.namedArgs.isEmpty())
3464    {
3465      lines.add("");
3466      lines.add(INFO_USAGE_OPTIONS_INCLUDE.get());
3467
3468
3469      // If there are any argument groups, then collect the arguments in those
3470      // groups.
3471      boolean hasRequired = false;
3472      final LinkedHashMap<String,List<Argument>> argumentsByGroup =
3473           new LinkedHashMap<>(StaticUtils.computeMapCapacity(10));
3474      final ArrayList<Argument> argumentsWithoutGroup =
3475           new ArrayList<>(parser.namedArgs.size());
3476      final ArrayList<Argument> usageArguments =
3477           new ArrayList<>(parser.namedArgs.size());
3478      for (final Argument a : parser.namedArgs)
3479      {
3480        if (a.isHidden())
3481        {
3482          // This argument shouldn't be included in the usage output.
3483          continue;
3484        }
3485
3486        if (a.isRequired() && (! a.hasDefaultValue()))
3487        {
3488          hasRequired = true;
3489        }
3490
3491        final String argumentGroup = a.getArgumentGroupName();
3492        if (argumentGroup == null)
3493        {
3494          if (a.isUsageArgument())
3495          {
3496            usageArguments.add(a);
3497          }
3498          else
3499          {
3500            argumentsWithoutGroup.add(a);
3501          }
3502        }
3503        else
3504        {
3505          List<Argument> groupArgs = argumentsByGroup.get(argumentGroup);
3506          if (groupArgs == null)
3507          {
3508            groupArgs = new ArrayList<>(10);
3509            argumentsByGroup.put(argumentGroup, groupArgs);
3510          }
3511
3512          groupArgs.add(a);
3513        }
3514      }
3515
3516
3517      // Iterate through the defined argument groups and display usage
3518      // information for each of them.
3519      for (final Map.Entry<String,List<Argument>> e :
3520           argumentsByGroup.entrySet())
3521      {
3522        lines.add("");
3523        lines.add("  " + e.getKey());
3524        lines.add("");
3525        for (final Argument a : e.getValue())
3526        {
3527          getArgUsage(a, lines, true, maxWidth);
3528        }
3529      }
3530
3531      if (! argumentsWithoutGroup.isEmpty())
3532      {
3533        if (argumentsByGroup.isEmpty())
3534        {
3535          for (final Argument a : argumentsWithoutGroup)
3536          {
3537            getArgUsage(a, lines, false, maxWidth);
3538          }
3539        }
3540        else
3541        {
3542          lines.add("");
3543          lines.add("  " + INFO_USAGE_UNGROUPED_ARGS.get());
3544          lines.add("");
3545          for (final Argument a : argumentsWithoutGroup)
3546          {
3547            getArgUsage(a, lines, true, maxWidth);
3548          }
3549        }
3550      }
3551
3552      if (! usageArguments.isEmpty())
3553      {
3554        if (argumentsByGroup.isEmpty())
3555        {
3556          for (final Argument a : usageArguments)
3557          {
3558            getArgUsage(a, lines, false, maxWidth);
3559          }
3560        }
3561        else
3562        {
3563          lines.add("");
3564          lines.add("  " + INFO_USAGE_USAGE_ARGS.get());
3565          lines.add("");
3566          for (final Argument a : usageArguments)
3567          {
3568            getArgUsage(a, lines, true, maxWidth);
3569          }
3570        }
3571      }
3572
3573      if (hasRequired)
3574      {
3575        lines.add("");
3576        if (argumentsByGroup.isEmpty())
3577        {
3578          lines.add("* " + INFO_USAGE_ARG_IS_REQUIRED.get());
3579        }
3580        else
3581        {
3582          lines.add("  * " + INFO_USAGE_ARG_IS_REQUIRED.get());
3583        }
3584      }
3585    }
3586
3587    return lines;
3588  }
3589
3590
3591
3592  /**
3593   * Adds usage information for the provided argument to the given list.
3594   *
3595   * @param  a         The argument for which to get the usage information.
3596   * @param  lines     The list to which the resulting lines should be added.
3597   * @param  indent    Indicates whether to indent each line.
3598   * @param  maxWidth  The maximum width of each line, in characters.
3599   */
3600  private static void getArgUsage(final Argument a, final List<String> lines,
3601                                  final boolean indent, final int maxWidth)
3602  {
3603    final StringBuilder argLine = new StringBuilder();
3604    if (indent && (maxWidth > 10))
3605    {
3606      if (a.isRequired() && (! a.hasDefaultValue()))
3607      {
3608        argLine.append("  * ");
3609      }
3610      else
3611      {
3612        argLine.append("    ");
3613      }
3614    }
3615    else if (a.isRequired() && (! a.hasDefaultValue()))
3616    {
3617      argLine.append("* ");
3618    }
3619
3620    boolean first = true;
3621    for (final Character c : a.getShortIdentifiers(false))
3622    {
3623      if (first)
3624      {
3625        argLine.append('-');
3626        first = false;
3627      }
3628      else
3629      {
3630        argLine.append(", -");
3631      }
3632      argLine.append(c);
3633    }
3634
3635    for (final String s : a.getLongIdentifiers(false))
3636    {
3637      if (first)
3638      {
3639        argLine.append("--");
3640        first = false;
3641      }
3642      else
3643      {
3644        argLine.append(", --");
3645      }
3646      argLine.append(s);
3647    }
3648
3649    final String valuePlaceholder = a.getValuePlaceholder();
3650    if (valuePlaceholder != null)
3651    {
3652      argLine.append(' ');
3653      argLine.append(valuePlaceholder);
3654    }
3655
3656    // If we need to wrap the argument line, then align the dashes on the left
3657    // edge.
3658    int subsequentLineWidth = maxWidth - 4;
3659    if (subsequentLineWidth < 4)
3660    {
3661      subsequentLineWidth = maxWidth;
3662    }
3663    final List<String> identifierLines =
3664         StaticUtils.wrapLine(argLine.toString(), maxWidth,
3665              subsequentLineWidth);
3666    for (int i=0; i < identifierLines.size(); i++)
3667    {
3668      if (i == 0)
3669      {
3670        lines.add(identifierLines.get(0));
3671      }
3672      else
3673      {
3674        lines.add("    " + identifierLines.get(i));
3675      }
3676    }
3677
3678
3679    // The description should be wrapped, if necessary.  We'll also want to
3680    // indent it (unless someone chose an absurdly small wrap width) to make
3681    // it stand out from the argument lines.
3682    final String description = a.getDescription();
3683    if (maxWidth > 10)
3684    {
3685      final String indentString;
3686      if (indent)
3687      {
3688        indentString = "        ";
3689      }
3690      else
3691      {
3692        indentString = "    ";
3693      }
3694
3695      final List<String> descLines = StaticUtils.wrapLine(description,
3696           (maxWidth-indentString.length()));
3697      for (final String s : descLines)
3698      {
3699        lines.add(indentString + s);
3700      }
3701    }
3702    else
3703    {
3704      lines.addAll(StaticUtils.wrapLine(description, maxWidth));
3705    }
3706  }
3707
3708
3709
3710  /**
3711   * Writes usage information for this program to the provided output stream
3712   * using the UTF-8 encoding, optionally wrapping long lines.
3713   *
3714   * @param  outputStream  The output stream to which the usage information
3715   *                       should be written.  It must not be {@code null}.
3716   * @param  maxWidth      The maximum line width to use for the output.  If
3717   *                       this is less than or equal to zero, then no wrapping
3718   *                       will be performed.
3719   *
3720   * @throws  IOException  If an error occurs while attempting to write to the
3721   *                       provided output stream.
3722   */
3723  public void getUsage(final OutputStream outputStream, final int maxWidth)
3724         throws IOException
3725  {
3726    final List<String> usageLines = getUsage(maxWidth);
3727    for (final String s : usageLines)
3728    {
3729      outputStream.write(StaticUtils.getBytes(s));
3730      outputStream.write(StaticUtils.EOL_BYTES);
3731    }
3732  }
3733
3734
3735
3736  /**
3737   * Retrieves a string representation of the usage information.
3738   *
3739   * @param  maxWidth  The maximum line width to use for the output.  If this is
3740   *                   less than or equal to zero, then no wrapping will be
3741   *                   performed.
3742   *
3743   * @return  A string representation of the usage information
3744   */
3745  public String getUsageString(final int maxWidth)
3746  {
3747    final StringBuilder buffer = new StringBuilder();
3748    getUsageString(buffer, maxWidth);
3749    return buffer.toString();
3750  }
3751
3752
3753
3754  /**
3755   * Appends a string representation of the usage information to the provided
3756   * buffer.
3757   *
3758   * @param  buffer    The buffer to which the information should be appended.
3759   * @param  maxWidth  The maximum line width to use for the output.  If this is
3760   *                   less than or equal to zero, then no wrapping will be
3761   *                   performed.
3762   */
3763  public void getUsageString(final StringBuilder buffer, final int maxWidth)
3764  {
3765    for (final String line : getUsage(maxWidth))
3766    {
3767      buffer.append(line);
3768      buffer.append(StaticUtils.EOL);
3769    }
3770  }
3771
3772
3773
3774  /**
3775   * Retrieves a string representation of this argument parser.
3776   *
3777   * @return  A string representation of this argument parser.
3778   */
3779  @Override()
3780  public String toString()
3781  {
3782    final StringBuilder buffer = new StringBuilder();
3783    toString(buffer);
3784    return buffer.toString();
3785  }
3786
3787
3788
3789  /**
3790   * Appends a string representation of this argument parser to the provided
3791   * buffer.
3792   *
3793   * @param  buffer  The buffer to which the information should be appended.
3794   */
3795  public void toString(final StringBuilder buffer)
3796  {
3797    buffer.append("ArgumentParser(commandName='");
3798    buffer.append(commandName);
3799    buffer.append("', commandDescription={");
3800    buffer.append('\'');
3801    buffer.append(commandDescription);
3802    buffer.append('\'');
3803
3804    if (additionalCommandDescriptionParagraphs != null)
3805    {
3806      for (final String additionalParagraph :
3807           additionalCommandDescriptionParagraphs)
3808      {
3809        buffer.append(", '");
3810        buffer.append(additionalParagraph);
3811        buffer.append('\'');
3812      }
3813    }
3814
3815    buffer.append("}, minTrailingArgs=");
3816    buffer.append(minTrailingArgs);
3817    buffer.append(", maxTrailingArgs=");
3818    buffer.append(maxTrailingArgs);
3819
3820    if (trailingArgsPlaceholder != null)
3821    {
3822      buffer.append(", trailingArgsPlaceholder='");
3823      buffer.append(trailingArgsPlaceholder);
3824      buffer.append('\'');
3825    }
3826
3827    buffer.append(", namedArgs={");
3828
3829    final Iterator<Argument> iterator = namedArgs.iterator();
3830    while (iterator.hasNext())
3831    {
3832      iterator.next().toString(buffer);
3833      if (iterator.hasNext())
3834      {
3835        buffer.append(", ");
3836      }
3837    }
3838
3839    buffer.append('}');
3840
3841    if (! subCommands.isEmpty())
3842    {
3843      buffer.append(", subCommands={");
3844
3845      final Iterator<SubCommand> subCommandIterator = subCommands.iterator();
3846      while (subCommandIterator.hasNext())
3847      {
3848        subCommandIterator.next().toString(buffer);
3849        if (subCommandIterator.hasNext())
3850        {
3851          buffer.append(", ");
3852        }
3853      }
3854
3855      buffer.append('}');
3856    }
3857
3858    buffer.append(')');
3859  }
3860}