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