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