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