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