001/*
002 * Copyright 2016-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2016-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2016-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.util.args;
037
038
039
040import java.util.ArrayList;
041import java.util.Collections;
042import java.util.Iterator;
043import java.util.LinkedHashMap;
044import java.util.List;
045import java.util.Map;
046
047import com.unboundid.util.Mutable;
048import com.unboundid.util.NotNull;
049import com.unboundid.util.Nullable;
050import com.unboundid.util.ObjectPair;
051import com.unboundid.util.StaticUtils;
052import com.unboundid.util.ThreadSafety;
053import com.unboundid.util.ThreadSafetyLevel;
054
055import static com.unboundid.util.args.ArgsMessages.*;
056
057
058
059/**
060 * This class provides a data structure that represents a subcommand that can be
061 * used in conjunction with the argument parser.  A subcommand can be used to
062 * allow a single command to do multiple different things.  A subcommand is
063 * represented in the argument list as a string that is not prefixed by any
064 * dashes, and there can be at most one subcommand in the argument list.  Each
065 * subcommand has its own argument parser that defines the arguments available
066 * for use with that subcommand, and the tool still provides support for global
067 * arguments that are not associated with any of the subcommands.
068 * <BR><BR>
069 * The use of subcommands imposes the following constraints on an argument
070 * parser:
071 * <UL>
072 *   <LI>
073 *     Each subcommand must be registered with the argument parser that defines
074 *     the global arguments for the tool.  Subcommands cannot be registered with
075 *     a subcommand's argument parser (i.e., you cannot have a subcommand with
076 *     its own subcommands).
077 *   </LI>
078 *   <LI>
079 *     There must not be any conflicts between the global arguments and the
080 *     subcommand-specific arguments.  However, there can be conflicts between
081 *     the arguments used across separate subcommands.
082 *   </LI>
083 *   <LI>
084 *     If the global argument parser cannot support both unnamed subcommands and
085 *     unnamed trailing arguments.
086 *   </LI>
087 *   <LI>
088 *     Global arguments can exist anywhere in the argument list, whether before
089 *     or after the subcommand.  Subcommand-specific arguments must only appear
090 *     after the subcommand in the argument list.
091 *   </LI>
092 * </UL>
093 */
094@Mutable()
095@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE)
096public final class SubCommand
097{
098  // The global argument parser with which this subcommand is associated.
099  @Nullable private volatile ArgumentParser globalArgumentParser;
100
101  // The argument parser for the arguments specific to this subcommand.
102  @NotNull private final ArgumentParser subcommandArgumentParser;
103
104  // Indicates whether this subcommand was provided in the set of command-line
105  // arguments.
106  private volatile boolean isPresent;
107
108  // The set of example usages for this subcommand.
109  @NotNull private final LinkedHashMap<String[],String> exampleUsages;
110
111  // The names for this subcommand, mapped from an all-lowercase representation
112  // to an object pair that has the name in the desired case and an indicate
113  // as to whether the name is hidden.
114  @NotNull private final Map<String,ObjectPair<String,Boolean>> names;
115
116  // The description for this subcommand.
117  @NotNull private final String description;
118
119
120
121  /**
122   * Creates a new subcommand with the provided information.
123   *
124   * @param  name           A name that may be used to reference this subcommand
125   *                        in the argument list.  It must not be {@code null}
126   *                        or empty, and it will be treated in a
127   *                        case-insensitive manner.
128   * @param  description    The description for this subcommand.  It must not be
129   *                        {@code null}.
130   * @param  parser         The argument parser that will be used to validate
131   *                        the subcommand-specific arguments.  It must not be
132   *                        {@code null}, it must not be configured with any
133   *                        subcommands of its own, and it must not be
134   *                        configured to allow unnamed trailing arguments.
135   * @param  exampleUsages  An optional map correlating a complete set of
136   *                        arguments that may be used when running the tool
137   *                        with this subcommand (including the subcommand and
138   *                        any appropriate global and/or subcommand-specific
139   *                        arguments) and a description of the behavior with
140   *                        that subcommand.
141   *
142   * @throws  ArgumentException  If there is a problem with the provided name,
143   *                             description, or argument parser.
144   */
145  public SubCommand(@NotNull final String name,
146                    @NotNull final String description,
147                    @NotNull final ArgumentParser parser,
148                    @NotNull final LinkedHashMap<String[],String> exampleUsages)
149         throws ArgumentException
150  {
151    names = new LinkedHashMap<>(StaticUtils.computeMapCapacity(5));
152    addName(name);
153
154    this.description = description;
155    if ((description == null) || description.isEmpty())
156    {
157      throw new ArgumentException(
158           ERR_SUBCOMMAND_DESCRIPTION_NULL_OR_EMPTY.get());
159    }
160
161    subcommandArgumentParser = parser;
162    if (parser == null)
163    {
164      throw new ArgumentException(ERR_SUBCOMMAND_PARSER_NULL.get());
165    }
166    else if (parser.allowsTrailingArguments())
167    {
168      throw new ArgumentException(
169           ERR_SUBCOMMAND_PARSER_ALLOWS_TRAILING_ARGS.get());
170    }
171     else if (parser.hasSubCommands())
172    {
173      throw new ArgumentException(ERR_SUBCOMMAND_PARSER_HAS_SUBCOMMANDS.get());
174    }
175
176    if (exampleUsages == null)
177    {
178      this.exampleUsages =
179           new LinkedHashMap<>(StaticUtils.computeMapCapacity(10));
180    }
181    else
182    {
183      this.exampleUsages = new LinkedHashMap<>(exampleUsages);
184    }
185
186    isPresent = false;
187    globalArgumentParser = null;
188  }
189
190
191
192  /**
193   * Creates a new subcommand that is a "clean" copy of the provided source
194   * subcommand.
195   *
196   * @param  source  The source subcommand to use for this subcommand.
197   */
198  private SubCommand(@NotNull final SubCommand source)
199  {
200    names = new LinkedHashMap<>(source.names);
201    description = source.description;
202    subcommandArgumentParser =
203         new ArgumentParser(source.subcommandArgumentParser, this);
204    exampleUsages = new LinkedHashMap<>(source.exampleUsages);
205    isPresent = false;
206    globalArgumentParser = null;
207  }
208
209
210
211  /**
212   * Retrieves the primary name for this subcommand, which is the first name
213   * that was assigned to it.
214   *
215   * @return  The primary name for this subcommand.
216   */
217  @NotNull()
218  public String getPrimaryName()
219  {
220    return names.values().iterator().next().getFirst();
221  }
222
223
224
225  /**
226   * Retrieves the list of all names, including hidden names, for this
227   * subcommand.
228   *
229   * @return  The list of all names for this subcommand.
230   */
231  @NotNull()
232  public List<String> getNames()
233  {
234    return getNames(true);
235  }
236
237
238
239  /**
240   * Retrieves a list of the non-hidden names for this subcommand.
241   *
242   *
243   * @param  includeHidden  Indicates whether to include hidden names in the
244   *                        list that is returned.
245   *
246   * @return  A list of the non-hidden names for this subcommand.
247   */
248  @NotNull()
249  public List<String> getNames(final boolean includeHidden)
250  {
251    final ArrayList<String> nameList = new ArrayList<>(names.size());
252    for (final ObjectPair<String,Boolean> p : names.values())
253    {
254      if (includeHidden || (! p.getSecond()))
255      {
256        nameList.add(p.getFirst());
257      }
258    }
259
260    return Collections.unmodifiableList(nameList);
261  }
262
263
264
265  /**
266   * Indicates whether the provided name is assigned to this subcommand.
267   *
268   * @param  name  The name for which to make the determination.  It must not be
269   *               {@code null}.
270   *
271   * @return  {@code true} if the provided name is assigned to this subcommand,
272   *          or {@code false} if not.
273   */
274  public boolean hasName(@NotNull final String name)
275  {
276    return names.containsKey(StaticUtils.toLowerCase(name));
277  }
278
279
280
281  /**
282   * Adds the provided name that may be used to reference this subcommand.  It
283   * will not be hidden.
284   *
285   * @param  name  A name that may be used to reference this subcommand in the
286   *               argument list.  It must not be {@code null} or empty, and it
287   *               will be treated in a case-insensitive manner.
288   *
289   * @throws  ArgumentException  If the provided name is already registered with
290   *                             this subcommand, or with another subcommand
291   *                             also registered with the global argument
292   *                             parser.
293   */
294  public void addName(@NotNull final String name)
295         throws ArgumentException
296  {
297    addName(name, false);
298  }
299
300
301
302  /**
303   * Adds the provided name that may be used to reference this subcommand.
304   *
305   * @param  name      A name that may be used to reference this subcommand in
306   *                   the argument list.  It must not be {@code null} or empty,
307   *                   and it will be treated in a case-insensitive manner.
308   * @param  isHidden  Indicates whether the provided name should be hidden.  A
309   *                   hidden name may be used to invoke this subcommand but
310   *                   will not be displayed in usage information.
311   *
312   * @throws  ArgumentException  If the provided name is already registered with
313   *                             this subcommand, or with another subcommand
314   *                             also registered with the global argument
315   *                             parser.
316   */
317  public void addName(@NotNull final String name, final boolean isHidden)
318         throws ArgumentException
319  {
320    if ((name == null) || name.isEmpty())
321    {
322      throw new ArgumentException(ERR_SUBCOMMAND_NAME_NULL_OR_EMPTY.get());
323    }
324
325    final String lowerName = StaticUtils.toLowerCase(name);
326    if (names.containsKey(lowerName))
327    {
328      throw new ArgumentException(ERR_SUBCOMMAND_NAME_ALREADY_IN_USE.get(name));
329    }
330
331    if (globalArgumentParser != null)
332    {
333      globalArgumentParser.addSubCommand(name, this);
334    }
335
336    names.put(lowerName, new ObjectPair<>(name, isHidden));
337  }
338
339
340
341  /**
342   * Retrieves the description for this subcommand.
343   *
344   * @return  The description for this subcommand.
345   */
346  @NotNull()
347  public String getDescription()
348  {
349    return description;
350  }
351
352
353
354  /**
355   * Retrieves the argument parser that will be used to process arguments
356   * specific to this subcommand.
357   *
358   * @return  The argument parser that will be used to process arguments
359   *          specific to this subcommand.
360   */
361  @NotNull()
362  public ArgumentParser getArgumentParser()
363  {
364    return subcommandArgumentParser;
365  }
366
367
368
369  /**
370   * Indicates whether this subcommand was provided in the set of command-line
371   * arguments.
372   *
373   * @return  {@code true} if this subcommand was provided in the set of
374   *          command-line arguments, or {@code false} if not.
375   */
376  public boolean isPresent()
377  {
378    return isPresent;
379  }
380
381
382
383  /**
384   * Indicates that this subcommand was provided in the set of command-line
385   * arguments.
386   */
387  void setPresent()
388  {
389    isPresent = true;
390  }
391
392
393
394  /**
395   * Retrieves the global argument parser with which this subcommand is
396   * registered.
397   *
398   * @return  The global argument parser with which this subcommand is
399   *          registered.
400   */
401  @Nullable()
402  ArgumentParser getGlobalArgumentParser()
403  {
404    return globalArgumentParser;
405  }
406
407
408
409  /**
410   * Sets the global argument parser for this subcommand.
411   *
412   * @param  globalArgumentParser  The global argument parser for this
413   *                               subcommand.
414   */
415  void setGlobalArgumentParser(
416            @NotNull final ArgumentParser globalArgumentParser)
417  {
418    this.globalArgumentParser = globalArgumentParser;
419  }
420
421
422
423  /**
424   * Retrieves a set of information that may be used to generate example usage
425   * information when the tool is run with this subcommand.  Each element in the
426   * returned map should consist of a map between an example set of arguments
427   * (including the subcommand name) and a string that describes the behavior of
428   * the tool when invoked with that set of arguments.
429   *
430   * @return  A set of information that may be used to generate example usage
431   *          information, or an empty map if no example usages are available.
432   */
433  @NotNull()
434  public LinkedHashMap<String[],String> getExampleUsages()
435  {
436    return exampleUsages;
437  }
438
439
440
441  /**
442   * Creates a copy of this subcommand that is "clean" and appears as if it has
443   * not been used to parse an argument set.  The new subcommand will have all
444   * of the same names and argument constraints as this subcommand.
445   *
446   * @return  The "clean" copy of this subcommand.
447   */
448  @NotNull()
449  public SubCommand getCleanCopy()
450  {
451    return new SubCommand(this);
452  }
453
454
455
456  /**
457   * Retrieves a string representation of this subcommand.
458   *
459   * @return  A string representation of this subcommand.
460   */
461  @Override()
462  @NotNull()
463  public String toString()
464  {
465    final StringBuilder buffer = new StringBuilder();
466    toString(buffer);
467    return buffer.toString();
468  }
469
470
471
472  /**
473   * Appends a string representation of this subcommand to the provided buffer.
474   *
475   * @param  buffer  The buffer to which the information should be appended.
476   */
477  public void toString(@NotNull final StringBuilder buffer)
478  {
479    buffer.append("SubCommand(");
480
481    if (names.size() == 1)
482    {
483      buffer.append("name='");
484      buffer.append(names.values().iterator().next());
485      buffer.append('\'');
486    }
487    else
488    {
489      buffer.append("names={");
490
491      final Iterator<ObjectPair<String,Boolean>> iterator =
492           names.values().iterator();
493      while (iterator.hasNext())
494      {
495        buffer.append('\'');
496        buffer.append(iterator.next().getFirst());
497        buffer.append('\'');
498
499        if (iterator.hasNext())
500        {
501          buffer.append(", ");
502        }
503      }
504
505      buffer.append('}');
506    }
507
508    buffer.append(", description='");
509    buffer.append(description);
510    buffer.append("', parser=");
511    subcommandArgumentParser.toString(buffer);
512    buffer.append(')');
513  }
514}