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 + '=' + s);
2839      }
2840    }
2841  }
2842
2843
2844
2845  /**
2846   * Wraps the given string and writes it as a comment to the provided writer.
2847   *
2848   * @param  w  The writer to use to write the wrapped and commented string.
2849   * @param  s  The string to be wrapped and written.
2850   */
2851  private static void wrapComment(@NotNull final PrintWriter w,
2852                                  @NotNull final String s)
2853  {
2854    for (final String line : StaticUtils.wrapLine(s, 77))
2855    {
2856      w.println("# " + line);
2857    }
2858  }
2859
2860
2861
2862  /**
2863   * Reads the contents of the specified properties file and updates the
2864   * configured arguments as appropriate.
2865   *
2866   * @param  propertiesFile       The properties file to process.
2867   * @param  skipFinalValidation  A flag that indicates whether to skip final
2868   *                              validation because a qualifying usage argument
2869   *                              was provided.
2870   *
2871   * @throws  ArgumentException  If a problem is encountered while examining the
2872   *                             properties file, or while trying to assign a
2873   *                             property value to a corresponding argument.
2874   */
2875  private void handlePropertiesFile(@NotNull final File propertiesFile,
2876                    @NotNull final AtomicBoolean skipFinalValidation)
2877          throws ArgumentException
2878  {
2879    final String propertiesFilePath = propertiesFile.getAbsolutePath();
2880
2881    InputStream inputStream = null;
2882    final BufferedReader reader;
2883    try
2884    {
2885      inputStream = new FileInputStream(propertiesFile);
2886
2887
2888      // Handle the case in which the properties file may be encrypted.
2889      final List<char[]> cachedPasswords;
2890      final PrintStream err;
2891      final PrintStream out;
2892      final CommandLineTool tool = commandLineTool;
2893      if (tool == null)
2894      {
2895        cachedPasswords = Collections.emptyList();
2896        out = System.out;
2897        err = System.err;
2898      }
2899      else
2900      {
2901        cachedPasswords =
2902             tool.getPasswordFileReader().getCachedEncryptionPasswords();
2903        out = tool.getOut();
2904        err = tool.getErr();
2905      }
2906
2907      final ObjectPair<InputStream,char[]> encryptionData =
2908           ToolUtils.getPossiblyPassphraseEncryptedInputStream(inputStream,
2909                cachedPasswords, true,
2910                INFO_PARSER_PROMPT_FOR_PROP_FILE_ENC_PW.get(
2911                     propertiesFile.getAbsolutePath()),
2912                ERR_PARSER_WRONG_PROP_FILE_ENC_PW.get(
2913                     propertiesFile.getAbsolutePath()),
2914                out, err);
2915
2916      inputStream = encryptionData.getFirst();
2917      if ((tool != null) && (encryptionData.getSecond() != null))
2918      {
2919        tool.getPasswordFileReader().addToEncryptionPasswordCache(
2920             encryptionData.getSecond());
2921      }
2922
2923
2924      // Handle the case in which the properties file may be compressed.
2925      inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream);
2926
2927
2928      // The java.util.Properties specification states that properties files
2929      // should be read using the ISO 8859-1 character set, and that characters
2930      // that cannot be encoded in that format should be represented using
2931      // Unicode escapes that start with a backslash, a lowercase letter "u",
2932      // and four hexadecimal digits.  To provide compatibility with the Java
2933      // Properties file format (except we also support the same property
2934      // appearing multiple times), we will also use that encoding and will
2935      // support Unicode escape sequences.
2936      reader = new BufferedReader(new InputStreamReader(inputStream,
2937           StandardCharsets.ISO_8859_1));
2938    }
2939    catch (final Exception e)
2940    {
2941      if (inputStream != null)
2942      {
2943        try
2944        {
2945          inputStream.close();
2946        }
2947        catch (final Exception e2)
2948        {
2949          Debug.debugException(e2);
2950        }
2951      }
2952
2953      Debug.debugException(e);
2954      throw new ArgumentException(
2955           ERR_PARSER_CANNOT_OPEN_PROP_FILE.get(propertiesFilePath,
2956                StaticUtils.getExceptionMessage(e)),
2957           e);
2958    }
2959
2960    try
2961    {
2962      // Read all of the lines of the file, ignoring comments and unwrapping
2963      // properties that span multiple lines.
2964      boolean lineIsContinued = false;
2965      int lineNumber = 0;
2966      final ArrayList<ObjectPair<Integer,StringBuilder>> propertyLines =
2967           new ArrayList<>(10);
2968      while (true)
2969      {
2970        String line;
2971        try
2972        {
2973          line = reader.readLine();
2974          lineNumber++;
2975        }
2976        catch (final Exception e)
2977        {
2978          Debug.debugException(e);
2979          throw new ArgumentException(
2980               ERR_PARSER_ERROR_READING_PROP_FILE.get(propertiesFilePath,
2981                    StaticUtils.getExceptionMessage(e)),
2982               e);
2983        }
2984
2985
2986        // If the line is null, then we've reached the end of the file.  If we
2987        // expect a previous line to have been continued, then this is an error.
2988        if (line == null)
2989        {
2990          if (lineIsContinued)
2991          {
2992            throw new ArgumentException(
2993                 ERR_PARSER_PROP_FILE_MISSING_CONTINUATION.get(
2994                      (lineNumber-1), propertiesFilePath));
2995          }
2996          break;
2997        }
2998
2999
3000        // See if the line has any leading whitespace, and if so then trim it
3001        // off.  If there is leading whitespace, then make sure that we expect
3002        // the previous line to be continued.
3003        final int initialLength = line.length();
3004        line = StaticUtils.trimLeading(line);
3005        final boolean hasLeadingWhitespace = (line.length() < initialLength);
3006        if (hasLeadingWhitespace && (! lineIsContinued))
3007        {
3008          throw new ArgumentException(
3009               ERR_PARSER_PROP_FILE_UNEXPECTED_LEADING_SPACE.get(
3010                    propertiesFilePath, lineNumber));
3011        }
3012
3013
3014        // If the line is empty or starts with "#", then skip it.  But make sure
3015        // we didn't expect the previous line to be continued.
3016        if ((line.isEmpty()) || line.startsWith("#"))
3017        {
3018          if (lineIsContinued)
3019          {
3020            throw new ArgumentException(
3021                 ERR_PARSER_PROP_FILE_MISSING_CONTINUATION.get(
3022                      (lineNumber-1), propertiesFilePath));
3023          }
3024          continue;
3025        }
3026
3027
3028        // See if the line ends with a backslash and if so then trim it off.
3029        final boolean hasTrailingBackslash = line.endsWith("\\");
3030        if (line.endsWith("\\"))
3031        {
3032          line = line.substring(0, (line.length() - 1));
3033        }
3034
3035
3036        // If the previous line needs to be continued, then append the new line
3037        // to it.  Otherwise, add it as a new line.
3038        if (lineIsContinued)
3039        {
3040          propertyLines.get(propertyLines.size() - 1).getSecond().append(line);
3041        }
3042        else
3043        {
3044          propertyLines.add(
3045               new ObjectPair<>(lineNumber, new StringBuilder(line)));
3046        }
3047
3048        lineIsContinued = hasTrailingBackslash;
3049      }
3050
3051
3052      // Parse all of the lines into a map of identifiers and their
3053      // corresponding values.
3054      propertiesFileUsed = propertiesFile;
3055      if (propertyLines.isEmpty())
3056      {
3057        return;
3058      }
3059
3060      final HashMap<String,ArrayList<String>> propertyMap =
3061           new HashMap<>(StaticUtils.computeMapCapacity(propertyLines.size()));
3062      for (final ObjectPair<Integer,StringBuilder> p : propertyLines)
3063      {
3064        lineNumber = p.getFirst();
3065        final String line = handleUnicodeEscapes(propertiesFilePath, lineNumber,
3066             p.getSecond());
3067        final int equalPos = line.indexOf('=');
3068        if (equalPos <= 0)
3069        {
3070          throw new ArgumentException(ERR_PARSER_MALFORMED_PROP_LINE.get(
3071               propertiesFilePath, lineNumber, line));
3072        }
3073
3074        final String propertyName = line.substring(0, equalPos).trim();
3075        final String propertyValue = line.substring(equalPos+1).trim();
3076        if (propertyValue.isEmpty())
3077        {
3078          // The property doesn't have a value, so we can ignore it.
3079          continue;
3080        }
3081
3082
3083        // An argument can have multiple identifiers, and we will allow any of
3084        // them to be used to reference it.  To deal with this, we'll map the
3085        // argument identifier to its corresponding argument and then use the
3086        // preferred identifier for that argument in the map.  The same applies
3087        // to subcommand names.
3088        boolean prefixedWithToolName = false;
3089        boolean prefixedWithSubCommandName = false;
3090        Argument a = getNamedArgument(propertyName);
3091        if (a == null)
3092        {
3093          // It could be that the argument name was prefixed with the tool name.
3094          // Check to see if that was the case.
3095          if (propertyName.startsWith(commandName + '.'))
3096          {
3097            prefixedWithToolName = true;
3098
3099            String basePropertyName =
3100                 propertyName.substring(commandName.length()+1);
3101            a = getNamedArgument(basePropertyName);
3102
3103            if (a == null)
3104            {
3105              final int periodPos = basePropertyName.indexOf('.');
3106              if (periodPos > 0)
3107              {
3108                final String subCommandName =
3109                     basePropertyName.substring(0, periodPos);
3110                if ((selectedSubCommand != null) &&
3111                    selectedSubCommand.hasName(subCommandName))
3112                {
3113                  prefixedWithSubCommandName = true;
3114                  basePropertyName = basePropertyName.substring(periodPos+1);
3115                  a = selectedSubCommand.getArgumentParser().getNamedArgument(
3116                       basePropertyName);
3117                }
3118              }
3119              else if (selectedSubCommand != null)
3120              {
3121                a = selectedSubCommand.getArgumentParser().getNamedArgument(
3122                     basePropertyName);
3123              }
3124            }
3125          }
3126          else if (selectedSubCommand != null)
3127          {
3128            a = selectedSubCommand.getArgumentParser().getNamedArgument(
3129                 propertyName);
3130          }
3131        }
3132
3133        if (a == null)
3134        {
3135          // This could mean that there's a typo in the property name, but it's
3136          // more likely the case that the property is for a different tool.  In
3137          // either case, we'll ignore it.
3138          continue;
3139        }
3140
3141        final String canonicalPropertyName;
3142        if (prefixedWithToolName)
3143        {
3144          if (prefixedWithSubCommandName)
3145          {
3146            canonicalPropertyName = commandName + '.' +
3147                 selectedSubCommand.getPrimaryName() + '.' +
3148                 a.getIdentifierString();
3149          }
3150          else
3151          {
3152            canonicalPropertyName = commandName + '.' + a.getIdentifierString();
3153          }
3154        }
3155        else
3156        {
3157          canonicalPropertyName = a.getIdentifierString();
3158        }
3159
3160        ArrayList<String> valueList = propertyMap.get(canonicalPropertyName);
3161        if (valueList == null)
3162        {
3163          valueList = new ArrayList<>(5);
3164          propertyMap.put(canonicalPropertyName, valueList);
3165        }
3166        valueList.add(propertyValue);
3167      }
3168
3169
3170      // Iterate through all of the named arguments for the argument parser and
3171      // see if we should use the properties to assign values to any of the
3172      // arguments that weren't provided on the command line.
3173      setArgsFromPropertiesFile(propertyMap, false, skipFinalValidation);
3174
3175
3176      // If there is a selected subcommand, then iterate through all of its
3177      // arguments.
3178      if (selectedSubCommand != null)
3179      {
3180        setArgsFromPropertiesFile(propertyMap, true, skipFinalValidation);
3181      }
3182    }
3183    finally
3184    {
3185      try
3186      {
3187        reader.close();
3188      }
3189      catch (final Exception e)
3190      {
3191        Debug.debugException(e);
3192      }
3193    }
3194  }
3195
3196
3197
3198  /**
3199   * Retrieves a string that contains the contents of the provided buffer, but
3200   * with any Unicode escape sequences converted to the appropriate character
3201   * representation, and any other escapes having the initial backslash
3202   * removed.
3203   *
3204   * @param  propertiesFilePath  The path to the properties file being written.
3205   *                             It must not be {@code null}.
3206   * @param  lineNumber          The line number on which the property
3207   *                             definition starts.
3208   * @param  buffer              The buffer containing the data to be processed.
3209   *                             It must not be {@code null} but may be empty.
3210   *
3211   * @return  A string that contains the contents of the provided buffer, but
3212   *          with any Unicode escape sequences converted to the appropriate
3213   *          character representation.
3214   *
3215   * @throws  ArgumentException  If a malformed Unicode escape sequence is
3216   *                             encountered.
3217   */
3218  @NotNull()
3219  static String handleUnicodeEscapes(@NotNull final String propertiesFilePath,
3220                                     final int lineNumber,
3221                                     @NotNull final StringBuilder buffer)
3222         throws ArgumentException
3223  {
3224    int pos = 0;
3225    while (pos < buffer.length())
3226    {
3227      final char c = buffer.charAt(pos);
3228      if (c == '\\')
3229      {
3230        if (pos <= (buffer.length() - 5))
3231        {
3232          final char nextChar = buffer.charAt(pos+1);
3233          if ((nextChar == 'u') || (nextChar == 'U'))
3234          {
3235            try
3236            {
3237              final String hexDigits = buffer.substring(pos+2, pos+6);
3238              final byte[] bytes = StaticUtils.fromHex(hexDigits);
3239              final int i = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF);
3240              buffer.setCharAt(pos, (char) i);
3241              for (int j=0; j < 5; j++)
3242              {
3243                buffer.deleteCharAt(pos+1);
3244              }
3245            }
3246            catch (final Exception e)
3247            {
3248              Debug.debugException(e);
3249              throw new ArgumentException(
3250                   ERR_PARSER_MALFORMED_UNICODE_ESCAPE.get(propertiesFilePath,
3251                        lineNumber),
3252                   e);
3253            }
3254          }
3255          else
3256          {
3257            buffer.deleteCharAt(pos);
3258          }
3259        }
3260      }
3261
3262      pos++;
3263    }
3264
3265    return buffer.toString();
3266  }
3267
3268
3269
3270  /**
3271   * Sets the values of any arguments not provided on the command line but
3272   * defined in the properties file.
3273   *
3274   * @param  propertyMap          A map of properties read from the properties
3275   *                              file.
3276   * @param  useSubCommand        Indicates whether to use the argument parser
3277   *                              associated with the selected subcommand rather
3278   *                              than the global argument parser.
3279   * @param  skipFinalValidation  A flag that indicates whether to skip final
3280   *                              validation because a qualifying usage argument
3281   *                              was provided.
3282   *
3283   * @throws  ArgumentException  If a problem is encountered while examining the
3284   *                             properties file, or while trying to assign a
3285   *                             property value to a corresponding argument.
3286   */
3287  private void setArgsFromPropertiesFile(
3288                    @NotNull final Map<String,ArrayList<String>> propertyMap,
3289                    final boolean useSubCommand,
3290                    @NotNull final AtomicBoolean skipFinalValidation)
3291          throws ArgumentException
3292  {
3293    final ArgumentParser p;
3294    if (useSubCommand)
3295    {
3296      p = selectedSubCommand.getArgumentParser();
3297    }
3298    else
3299    {
3300      p = this;
3301    }
3302
3303
3304    for (final Argument a : p.namedArgs)
3305    {
3306      // If the argument was provided on the command line, then that will always
3307      // override anything that might be in the properties file.
3308      if (a.getNumOccurrences() > 0)
3309      {
3310        continue;
3311      }
3312
3313
3314      // If the argument is part of an exclusive argument set, and if one of
3315      // the other arguments in that set was provided on the command line, then
3316      // don't look in the properties file for a value for the argument.
3317      boolean exclusiveArgumentHasValue = false;
3318exclusiveArgumentLoop:
3319      for (final Set<Argument> exclusiveArgumentSet : exclusiveArgumentSets)
3320      {
3321        if (exclusiveArgumentSet.contains(a))
3322        {
3323          for (final Argument exclusiveArg : exclusiveArgumentSet)
3324          {
3325            if (exclusiveArg.getNumOccurrences() > 0)
3326            {
3327              exclusiveArgumentHasValue = true;
3328              break exclusiveArgumentLoop;
3329            }
3330          }
3331        }
3332      }
3333
3334      if (exclusiveArgumentHasValue)
3335      {
3336        continue;
3337      }
3338
3339
3340      // If we should use a subcommand, then see if the properties file has a
3341      // property that is specific to the selected subcommand.  Then fall back
3342      // to a property that is specific to the tool, and finally fall back to
3343      // checking for a set of values that are generic to any tool that has an
3344      // argument with that name.
3345      List<String> values = null;
3346      if (useSubCommand)
3347      {
3348        values = propertyMap.get(commandName + '.' +
3349             selectedSubCommand.getPrimaryName()  + '.' +
3350             a.getIdentifierString());
3351      }
3352
3353      if (values == null)
3354      {
3355        values = propertyMap.get(commandName + '.' + a.getIdentifierString());
3356      }
3357
3358      if (values == null)
3359      {
3360        values = propertyMap.get(a.getIdentifierString());
3361      }
3362
3363      if (values != null)
3364      {
3365        for (final String value : values)
3366        {
3367          if (a instanceof BooleanArgument)
3368          {
3369            // We'll treat this as a BooleanValueArgument.
3370            final BooleanValueArgument bva = new BooleanValueArgument(
3371                 a.getShortIdentifier(), a.getLongIdentifier(), false, null,
3372                 a.getDescription());
3373            bva.addValue(value);
3374            if (bva.getValue())
3375            {
3376              a.incrementOccurrences();
3377              argumentsSetFromPropertiesFile.add(a.getIdentifierString());
3378            }
3379          }
3380          else
3381          {
3382            a.addValue(value);
3383            a.incrementOccurrences();
3384
3385            argumentsSetFromPropertiesFile.add(a.getIdentifierString());
3386            if (a.isSensitive())
3387            {
3388              argumentsSetFromPropertiesFile.add("***REDACTED***");
3389            }
3390            else
3391            {
3392              argumentsSetFromPropertiesFile.add(value);
3393            }
3394          }
3395        }
3396
3397        if (a.isUsageArgument() && skipFinalValidationBecauseOfArgument(a))
3398        {
3399          skipFinalValidation.set(true);
3400        }
3401      }
3402    }
3403  }
3404
3405
3406
3407  /**
3408   * Retrieves lines that make up the usage information for this program,
3409   * optionally wrapping long lines.
3410   *
3411   * @param  maxWidth  The maximum line width to use for the output.  If this is
3412   *                   less than or equal to zero, then no wrapping will be
3413   *                   performed.
3414   *
3415   * @return  The lines that make up the usage information for this program.
3416   */
3417  @NotNull()
3418  public List<String> getUsage(final int maxWidth)
3419  {
3420    // If a subcommand was selected, then provide usage specific to that
3421    // subcommand.
3422    if (selectedSubCommand != null)
3423    {
3424      return getSubCommandUsage(maxWidth);
3425    }
3426
3427    // First is a description of the command.
3428    final ArrayList<String> lines = new ArrayList<>(100);
3429    lines.addAll(StaticUtils.wrapLine(commandDescription, maxWidth));
3430    lines.add("");
3431
3432
3433    for (final String additionalDescriptionParagraph :
3434         additionalCommandDescriptionParagraphs)
3435    {
3436      lines.addAll(StaticUtils.wrapLine(additionalDescriptionParagraph,
3437           maxWidth));
3438      lines.add("");
3439    }
3440
3441    // If the tool supports subcommands, and if there are fewer than 10
3442    // subcommands, then display them inline.
3443    if ((! subCommands.isEmpty()) && (subCommands.size() < 10))
3444    {
3445      lines.add(INFO_USAGE_SUBCOMMANDS_HEADER.get());
3446      lines.add("");
3447
3448      for (final SubCommand sc : subCommands)
3449      {
3450        final StringBuilder nameBuffer = new StringBuilder();
3451        nameBuffer.append("  ");
3452
3453        final Iterator<String> nameIterator = sc.getNames(false).iterator();
3454        while (nameIterator.hasNext())
3455        {
3456          nameBuffer.append(nameIterator.next());
3457          if (nameIterator.hasNext())
3458          {
3459            nameBuffer.append(", ");
3460          }
3461        }
3462        lines.add(nameBuffer.toString());
3463
3464        for (final String descriptionLine :
3465             StaticUtils.wrapLine(sc.getDescription(), (maxWidth - 4)))
3466        {
3467          lines.add("    " + descriptionLine);
3468        }
3469        lines.add("");
3470      }
3471    }
3472
3473
3474    // Next comes the usage.  It may include neither, either, or both of the
3475    // set of options and trailing arguments.
3476    if (! subCommands.isEmpty())
3477    {
3478      lines.addAll(StaticUtils.wrapLine(
3479           INFO_USAGE_SUBCOMMAND_USAGE.get(commandName), maxWidth));
3480    }
3481    else if (namedArgs.isEmpty())
3482    {
3483      if (maxTrailingArgs == 0)
3484      {
3485        lines.addAll(StaticUtils.wrapLine(
3486             INFO_USAGE_NOOPTIONS_NOTRAILING.get(commandName), maxWidth));
3487      }
3488      else
3489      {
3490        lines.addAll(StaticUtils.wrapLine(INFO_USAGE_NOOPTIONS_TRAILING.get(
3491             commandName, trailingArgsPlaceholder), maxWidth));
3492      }
3493    }
3494    else
3495    {
3496      if (maxTrailingArgs == 0)
3497      {
3498        lines.addAll(StaticUtils.wrapLine(
3499             INFO_USAGE_OPTIONS_NOTRAILING.get(commandName), maxWidth));
3500      }
3501      else
3502      {
3503        lines.addAll(StaticUtils.wrapLine(INFO_USAGE_OPTIONS_TRAILING.get(
3504             commandName, trailingArgsPlaceholder), maxWidth));
3505      }
3506    }
3507
3508    if (! namedArgs.isEmpty())
3509    {
3510      lines.add("");
3511      lines.add(INFO_USAGE_OPTIONS_INCLUDE.get());
3512
3513
3514      // If there are any argument groups, then collect the arguments in those
3515      // groups.
3516      boolean hasRequired = false;
3517      final LinkedHashMap<String,List<Argument>> argumentsByGroup =
3518           new LinkedHashMap<>(StaticUtils.computeMapCapacity(10));
3519      final ArrayList<Argument> argumentsWithoutGroup =
3520           new ArrayList<>(namedArgs.size());
3521      final ArrayList<Argument> usageArguments =
3522           new ArrayList<>(namedArgs.size());
3523      for (final Argument a : namedArgs)
3524      {
3525        if (a.isHidden())
3526        {
3527          // This argument shouldn't be included in the usage output.
3528          continue;
3529        }
3530
3531        if (a.isRequired() && (! a.hasDefaultValue()))
3532        {
3533          hasRequired = true;
3534        }
3535
3536        final String argumentGroup = a.getArgumentGroupName();
3537        if (argumentGroup == null)
3538        {
3539          if (a.isUsageArgument())
3540          {
3541            usageArguments.add(a);
3542          }
3543          else
3544          {
3545            argumentsWithoutGroup.add(a);
3546          }
3547        }
3548        else
3549        {
3550          List<Argument> groupArgs = argumentsByGroup.get(argumentGroup);
3551          if (groupArgs == null)
3552          {
3553            groupArgs = new ArrayList<>(10);
3554            argumentsByGroup.put(argumentGroup, groupArgs);
3555          }
3556
3557          groupArgs.add(a);
3558        }
3559      }
3560
3561
3562      // Iterate through the defined argument groups and display usage
3563      // information for each of them.
3564      for (final Map.Entry<String,List<Argument>> e :
3565           argumentsByGroup.entrySet())
3566      {
3567        lines.add("");
3568        lines.add("  " + e.getKey());
3569        lines.add("");
3570        for (final Argument a : e.getValue())
3571        {
3572          getArgUsage(a, lines, true, maxWidth);
3573        }
3574      }
3575
3576      if (! argumentsWithoutGroup.isEmpty())
3577      {
3578        if (argumentsByGroup.isEmpty())
3579        {
3580          for (final Argument a : argumentsWithoutGroup)
3581          {
3582            getArgUsage(a, lines, false, maxWidth);
3583          }
3584        }
3585        else
3586        {
3587          lines.add("");
3588          lines.add("  " + INFO_USAGE_UNGROUPED_ARGS.get());
3589          lines.add("");
3590          for (final Argument a : argumentsWithoutGroup)
3591          {
3592            getArgUsage(a, lines, true, maxWidth);
3593          }
3594        }
3595      }
3596
3597      if (! usageArguments.isEmpty())
3598      {
3599        if (argumentsByGroup.isEmpty())
3600        {
3601          for (final Argument a : usageArguments)
3602          {
3603            getArgUsage(a, lines, false, maxWidth);
3604          }
3605        }
3606        else
3607        {
3608          lines.add("");
3609          lines.add("  " + INFO_USAGE_USAGE_ARGS.get());
3610          lines.add("");
3611          for (final Argument a : usageArguments)
3612          {
3613            getArgUsage(a, lines, true, maxWidth);
3614          }
3615        }
3616      }
3617
3618      if (hasRequired)
3619      {
3620        lines.add("");
3621        if (argumentsByGroup.isEmpty())
3622        {
3623          lines.add("* " + INFO_USAGE_ARG_IS_REQUIRED.get());
3624        }
3625        else
3626        {
3627          lines.add("  * " + INFO_USAGE_ARG_IS_REQUIRED.get());
3628        }
3629      }
3630    }
3631
3632    return lines;
3633  }
3634
3635
3636
3637  /**
3638   * Retrieves lines that make up the usage information for the selected
3639   * subcommand.
3640   *
3641   * @param  maxWidth  The maximum line width to use for the output.  If this is
3642   *                   less than or equal to zero, then no wrapping will be
3643   *                   performed.
3644   *
3645   * @return  The lines that make up the usage information for the selected
3646   *          subcommand.
3647   */
3648  @NotNull()
3649  private List<String> getSubCommandUsage(final int maxWidth)
3650  {
3651    // First is a description of the subcommand.
3652    final ArrayList<String> lines = new ArrayList<>(100);
3653    lines.addAll(
3654         StaticUtils.wrapLine(selectedSubCommand.getDescription(), maxWidth));
3655    lines.add("");
3656
3657    // Next comes the usage.
3658    lines.addAll(StaticUtils.wrapLine(INFO_SUBCOMMAND_USAGE_OPTIONS.get(
3659         commandName, selectedSubCommand.getPrimaryName()), maxWidth));
3660
3661
3662    final ArgumentParser parser = selectedSubCommand.getArgumentParser();
3663    if (! parser.namedArgs.isEmpty())
3664    {
3665      lines.add("");
3666      lines.add(INFO_USAGE_OPTIONS_INCLUDE.get());
3667
3668
3669      // If there are any argument groups, then collect the arguments in those
3670      // groups.
3671      boolean hasRequired = false;
3672      final LinkedHashMap<String,List<Argument>> argumentsByGroup =
3673           new LinkedHashMap<>(StaticUtils.computeMapCapacity(10));
3674      final ArrayList<Argument> argumentsWithoutGroup =
3675           new ArrayList<>(parser.namedArgs.size());
3676      final ArrayList<Argument> usageArguments =
3677           new ArrayList<>(parser.namedArgs.size());
3678      for (final Argument a : parser.namedArgs)
3679      {
3680        if (a.isHidden())
3681        {
3682          // This argument shouldn't be included in the usage output.
3683          continue;
3684        }
3685
3686        if (a.isRequired() && (! a.hasDefaultValue()))
3687        {
3688          hasRequired = true;
3689        }
3690
3691        final String argumentGroup = a.getArgumentGroupName();
3692        if (argumentGroup == null)
3693        {
3694          if (a.isUsageArgument())
3695          {
3696            usageArguments.add(a);
3697          }
3698          else
3699          {
3700            argumentsWithoutGroup.add(a);
3701          }
3702        }
3703        else
3704        {
3705          List<Argument> groupArgs = argumentsByGroup.get(argumentGroup);
3706          if (groupArgs == null)
3707          {
3708            groupArgs = new ArrayList<>(10);
3709            argumentsByGroup.put(argumentGroup, groupArgs);
3710          }
3711
3712          groupArgs.add(a);
3713        }
3714      }
3715
3716
3717      // Iterate through the defined argument groups and display usage
3718      // information for each of them.
3719      for (final Map.Entry<String,List<Argument>> e :
3720           argumentsByGroup.entrySet())
3721      {
3722        lines.add("");
3723        lines.add("  " + e.getKey());
3724        lines.add("");
3725        for (final Argument a : e.getValue())
3726        {
3727          getArgUsage(a, lines, true, maxWidth);
3728        }
3729      }
3730
3731      if (! argumentsWithoutGroup.isEmpty())
3732      {
3733        if (argumentsByGroup.isEmpty())
3734        {
3735          for (final Argument a : argumentsWithoutGroup)
3736          {
3737            getArgUsage(a, lines, false, maxWidth);
3738          }
3739        }
3740        else
3741        {
3742          lines.add("");
3743          lines.add("  " + INFO_USAGE_UNGROUPED_ARGS.get());
3744          lines.add("");
3745          for (final Argument a : argumentsWithoutGroup)
3746          {
3747            getArgUsage(a, lines, true, maxWidth);
3748          }
3749        }
3750      }
3751
3752      if (! usageArguments.isEmpty())
3753      {
3754        if (argumentsByGroup.isEmpty())
3755        {
3756          for (final Argument a : usageArguments)
3757          {
3758            getArgUsage(a, lines, false, maxWidth);
3759          }
3760        }
3761        else
3762        {
3763          lines.add("");
3764          lines.add("  " + INFO_USAGE_USAGE_ARGS.get());
3765          lines.add("");
3766          for (final Argument a : usageArguments)
3767          {
3768            getArgUsage(a, lines, true, maxWidth);
3769          }
3770        }
3771      }
3772
3773      if (hasRequired)
3774      {
3775        lines.add("");
3776        if (argumentsByGroup.isEmpty())
3777        {
3778          lines.add("* " + INFO_USAGE_ARG_IS_REQUIRED.get());
3779        }
3780        else
3781        {
3782          lines.add("  * " + INFO_USAGE_ARG_IS_REQUIRED.get());
3783        }
3784      }
3785    }
3786
3787    return lines;
3788  }
3789
3790
3791
3792  /**
3793   * Adds usage information for the provided argument to the given list.
3794   *
3795   * @param  a         The argument for which to get the usage information.
3796   * @param  lines     The list to which the resulting lines should be added.
3797   * @param  indent    Indicates whether to indent each line.
3798   * @param  maxWidth  The maximum width of each line, in characters.
3799   */
3800  private static void getArgUsage(@NotNull final Argument a,
3801                                  @NotNull final List<String> lines,
3802                                  final boolean indent, final int maxWidth)
3803  {
3804    final StringBuilder argLine = new StringBuilder();
3805    if (indent && (maxWidth > 10))
3806    {
3807      if (a.isRequired() && (! a.hasDefaultValue()))
3808      {
3809        argLine.append("  * ");
3810      }
3811      else
3812      {
3813        argLine.append("    ");
3814      }
3815    }
3816    else if (a.isRequired() && (! a.hasDefaultValue()))
3817    {
3818      argLine.append("* ");
3819    }
3820
3821    boolean first = true;
3822    for (final Character c : a.getShortIdentifiers(false))
3823    {
3824      if (first)
3825      {
3826        argLine.append('-');
3827        first = false;
3828      }
3829      else
3830      {
3831        argLine.append(", -");
3832      }
3833      argLine.append(c);
3834    }
3835
3836    for (final String s : a.getLongIdentifiers(false))
3837    {
3838      if (first)
3839      {
3840        argLine.append("--");
3841        first = false;
3842      }
3843      else
3844      {
3845        argLine.append(", --");
3846      }
3847      argLine.append(s);
3848    }
3849
3850    final String valuePlaceholder = a.getValuePlaceholder();
3851    if (valuePlaceholder != null)
3852    {
3853      argLine.append(' ');
3854      argLine.append(valuePlaceholder);
3855    }
3856
3857    // If we need to wrap the argument line, then align the dashes on the left
3858    // edge.
3859    int subsequentLineWidth = maxWidth - 4;
3860    if (subsequentLineWidth < 4)
3861    {
3862      subsequentLineWidth = maxWidth;
3863    }
3864    final List<String> identifierLines =
3865         StaticUtils.wrapLine(argLine.toString(), maxWidth,
3866              subsequentLineWidth);
3867    for (int i=0; i < identifierLines.size(); i++)
3868    {
3869      if (i == 0)
3870      {
3871        lines.add(identifierLines.get(0));
3872      }
3873      else
3874      {
3875        lines.add("    " + identifierLines.get(i));
3876      }
3877    }
3878
3879
3880    // The description should be wrapped, if necessary.  We'll also want to
3881    // indent it (unless someone chose an absurdly small wrap width) to make
3882    // it stand out from the argument lines.
3883    final String description = a.getDescription();
3884    if (maxWidth > 10)
3885    {
3886      final String indentString;
3887      if (indent)
3888      {
3889        indentString = "        ";
3890      }
3891      else
3892      {
3893        indentString = "    ";
3894      }
3895
3896      final List<String> descLines = StaticUtils.wrapLine(description,
3897           (maxWidth-indentString.length()));
3898      for (final String s : descLines)
3899      {
3900        lines.add(indentString + s);
3901      }
3902    }
3903    else
3904    {
3905      lines.addAll(StaticUtils.wrapLine(description, maxWidth));
3906    }
3907  }
3908
3909
3910
3911  /**
3912   * Writes usage information for this program to the provided output stream
3913   * using the UTF-8 encoding, optionally wrapping long lines.
3914   *
3915   * @param  outputStream  The output stream to which the usage information
3916   *                       should be written.  It must not be {@code null}.
3917   * @param  maxWidth      The maximum line width to use for the output.  If
3918   *                       this is less than or equal to zero, then no wrapping
3919   *                       will be performed.
3920   *
3921   * @throws  IOException  If an error occurs while attempting to write to the
3922   *                       provided output stream.
3923   */
3924  public void getUsage(@NotNull final OutputStream outputStream,
3925                       final int maxWidth)
3926         throws IOException
3927  {
3928    final List<String> usageLines = getUsage(maxWidth);
3929    for (final String s : usageLines)
3930    {
3931      outputStream.write(StaticUtils.getBytes(s));
3932      outputStream.write(StaticUtils.EOL_BYTES);
3933    }
3934  }
3935
3936
3937
3938  /**
3939   * Retrieves a string representation of the usage information.
3940   *
3941   * @param  maxWidth  The maximum line width to use for the output.  If this is
3942   *                   less than or equal to zero, then no wrapping will be
3943   *                   performed.
3944   *
3945   * @return  A string representation of the usage information
3946   */
3947  @NotNull()
3948  public String getUsageString(final int maxWidth)
3949  {
3950    final StringBuilder buffer = new StringBuilder();
3951    getUsageString(buffer, maxWidth);
3952    return buffer.toString();
3953  }
3954
3955
3956
3957  /**
3958   * Appends a string representation of the usage information to the provided
3959   * buffer.
3960   *
3961   * @param  buffer    The buffer to which the information should be appended.
3962   * @param  maxWidth  The maximum line width to use for the output.  If this is
3963   *                   less than or equal to zero, then no wrapping will be
3964   *                   performed.
3965   */
3966  public void getUsageString(@NotNull final StringBuilder buffer,
3967                             final int maxWidth)
3968  {
3969    for (final String line : getUsage(maxWidth))
3970    {
3971      buffer.append(line);
3972      buffer.append(StaticUtils.EOL);
3973    }
3974  }
3975
3976
3977
3978  /**
3979   * Retrieves a string representation of this argument parser.
3980   *
3981   * @return  A string representation of this argument parser.
3982   */
3983  @Override()
3984  @NotNull()
3985  public String toString()
3986  {
3987    final StringBuilder buffer = new StringBuilder();
3988    toString(buffer);
3989    return buffer.toString();
3990  }
3991
3992
3993
3994  /**
3995   * Appends a string representation of this argument parser to the provided
3996   * buffer.
3997   *
3998   * @param  buffer  The buffer to which the information should be appended.
3999   */
4000  public void toString(@NotNull final StringBuilder buffer)
4001  {
4002    buffer.append("ArgumentParser(commandName='");
4003    buffer.append(commandName);
4004    buffer.append("', commandDescription={");
4005    buffer.append('\'');
4006    buffer.append(commandDescription);
4007    buffer.append('\'');
4008
4009    if (additionalCommandDescriptionParagraphs != null)
4010    {
4011      for (final String additionalParagraph :
4012           additionalCommandDescriptionParagraphs)
4013      {
4014        buffer.append(", '");
4015        buffer.append(additionalParagraph);
4016        buffer.append('\'');
4017      }
4018    }
4019
4020    buffer.append("}, minTrailingArgs=");
4021    buffer.append(minTrailingArgs);
4022    buffer.append(", maxTrailingArgs=");
4023    buffer.append(maxTrailingArgs);
4024
4025    if (trailingArgsPlaceholder != null)
4026    {
4027      buffer.append(", trailingArgsPlaceholder='");
4028      buffer.append(trailingArgsPlaceholder);
4029      buffer.append('\'');
4030    }
4031
4032    buffer.append(", namedArgs={");
4033
4034    final Iterator<Argument> iterator = namedArgs.iterator();
4035    while (iterator.hasNext())
4036    {
4037      iterator.next().toString(buffer);
4038      if (iterator.hasNext())
4039      {
4040        buffer.append(", ");
4041      }
4042    }
4043
4044    buffer.append('}');
4045
4046    if (! subCommands.isEmpty())
4047    {
4048      buffer.append(", subCommands={");
4049
4050      final Iterator<SubCommand> subCommandIterator = subCommands.iterator();
4051      while (subCommandIterator.hasNext())
4052      {
4053        subCommandIterator.next().toString(buffer);
4054        if (subCommandIterator.hasNext())
4055        {
4056          buffer.append(", ");
4057        }
4058      }
4059
4060      buffer.append('}');
4061    }
4062
4063    buffer.append(')');
4064  }
4065}