001/*
002 * Copyright 2016-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2016-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.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<>(5);
134    addName(name);
135
136    this.description = description;
137    if ((description == null) || (description.length() == 0))
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 = new LinkedHashMap<>();
161    }
162    else
163    {
164      this.exampleUsages = new LinkedHashMap<>(exampleUsages);
165    }
166
167    isPresent = false;
168    globalArgumentParser = null;
169  }
170
171
172
173  /**
174   * Creates a new subcommand that is a "clean" copy of the provided source
175   * subcommand.
176   *
177   * @param  source  The source subcommand to use for this subcommand.
178   */
179  private SubCommand(final SubCommand source)
180  {
181    names = new LinkedHashMap<>(source.names);
182    description = source.description;
183    subcommandArgumentParser =
184         new ArgumentParser(source.subcommandArgumentParser, this);
185    exampleUsages = new LinkedHashMap<>(source.exampleUsages);
186    isPresent = false;
187    globalArgumentParser = null;
188  }
189
190
191
192  /**
193   * Retrieves the primary name for this subcommand, which is the first name
194   * that was assigned to it.
195   *
196   * @return  The primary name for this subcommand.
197   */
198  public String getPrimaryName()
199  {
200    return names.values().iterator().next().getFirst();
201  }
202
203
204
205  /**
206   * Retrieves the list of all names, including hidden names, for this
207   * subcommand.
208   *
209   * @return  The list of all names for this subcommand.
210   */
211  public List<String> getNames()
212  {
213    return getNames(true);
214  }
215
216
217
218  /**
219   * Retrieves a list of the non-hidden names for this subcommand.
220   *
221   *
222   * @param  includeHidden  Indicates whether to include hidden names in the
223   *                        list that is returned.
224   *
225   * @return  A list of the non-hidden names for this subcommand.
226   */
227  public List<String> getNames(final boolean includeHidden)
228  {
229    final ArrayList<String> nameList = new ArrayList<>(names.size());
230    for (final ObjectPair<String,Boolean> p : names.values())
231    {
232      if (includeHidden || (! p.getSecond()))
233      {
234        nameList.add(p.getFirst());
235      }
236    }
237
238    return Collections.unmodifiableList(nameList);
239  }
240
241
242
243  /**
244   * Indicates whether the provided name is assigned to this subcommand.
245   *
246   * @param  name  The name for which to make the determination.  It must not be
247   *               {@code null}.
248   *
249   * @return  {@code true} if the provided name is assigned to this subcommand,
250   *          or {@code false} if not.
251   */
252  public boolean hasName(final String name)
253  {
254    return names.containsKey(StaticUtils.toLowerCase(name));
255  }
256
257
258
259  /**
260   * Adds the provided name that may be used to reference this subcommand.  It
261   * will not be hidden.
262   *
263   * @param  name  A name that may be used to reference this subcommand in the
264   *               argument list.  It must not be {@code null} or empty, and it
265   *               will be treated in a case-insensitive manner.
266   *
267   * @throws  ArgumentException  If the provided name is already registered with
268   *                             this subcommand, or with another subcommand
269   *                             also registered with the global argument
270   *                             parser.
271   */
272  public void addName(final String name)
273         throws ArgumentException
274  {
275    addName(name, false);
276  }
277
278
279
280  /**
281   * Adds the provided name that may be used to reference this subcommand.
282   *
283   * @param  name      A name that may be used to reference this subcommand in
284   *                   the argument list.  It must not be {@code null} or empty,
285   *                   and it will be treated in a case-insensitive manner.
286   * @param  isHidden  Indicates whether the provided name should be hidden.  A
287   *                   hidden name may be used to invoke this subcommand but
288   *                   will not be displayed in usage information.
289   *
290   * @throws  ArgumentException  If the provided name is already registered with
291   *                             this subcommand, or with another subcommand
292   *                             also registered with the global argument
293   *                             parser.
294   */
295  public void addName(final String name, final boolean isHidden)
296         throws ArgumentException
297  {
298    if ((name == null) || (name.length() == 0))
299    {
300      throw new ArgumentException(ERR_SUBCOMMAND_NAME_NULL_OR_EMPTY.get());
301    }
302
303    final String lowerName = StaticUtils.toLowerCase(name);
304    if (names.containsKey(lowerName))
305    {
306      throw new ArgumentException(ERR_SUBCOMMAND_NAME_ALREADY_IN_USE.get(name));
307    }
308
309    if (globalArgumentParser != null)
310    {
311      globalArgumentParser.addSubCommand(name, this);
312    }
313
314    names.put(lowerName, new ObjectPair<>(name, isHidden));
315  }
316
317
318
319  /**
320   * Retrieves the description for this subcommand.
321   *
322   * @return  The description for this subcommand.
323   */
324  public String getDescription()
325  {
326    return description;
327  }
328
329
330
331  /**
332   * Retrieves the argument parser that will be used to process arguments
333   * specific to this subcommand.
334   *
335   * @return  The argument parser that will be used to process arguments
336   *          specific to this subcommand.
337   */
338  public ArgumentParser getArgumentParser()
339  {
340    return subcommandArgumentParser;
341  }
342
343
344
345  /**
346   * Indicates whether this subcommand was provided in the set of command-line
347   * arguments.
348   *
349   * @return  {@code true} if this subcommand was provided in the set of
350   *          command-line arguments, or {@code false} if not.
351   */
352  public boolean isPresent()
353  {
354    return isPresent;
355  }
356
357
358
359  /**
360   * Indicates that this subcommand was provided in the set of command-line
361   * arguments.
362   */
363  void setPresent()
364  {
365    isPresent = true;
366  }
367
368
369
370  /**
371   * Retrieves the global argument parser with which this subcommand is
372   * registered.
373   *
374   * @return  The global argument parser with which this subcommand is
375   *          registered.
376   */
377  ArgumentParser getGlobalArgumentParser()
378  {
379    return globalArgumentParser;
380  }
381
382
383
384  /**
385   * Sets the global argument parser for this subcommand.
386   *
387   * @param  globalArgumentParser  The global argument parser for this
388   *                               subcommand.
389   */
390  void setGlobalArgumentParser(final ArgumentParser globalArgumentParser)
391  {
392    this.globalArgumentParser = globalArgumentParser;
393  }
394
395
396
397  /**
398   * Retrieves a set of information that may be used to generate example usage
399   * information when the tool is run with this subcommand.  Each element in the
400   * returned map should consist of a map between an example set of arguments
401   * (including the subcommand name) and a string that describes the behavior of
402   * the tool when invoked with that set of arguments.
403   *
404   * @return  A set of information that may be used to generate example usage
405   *          information, or an empty map if no example usages are available.
406   */
407  public LinkedHashMap<String[],String> getExampleUsages()
408  {
409    return exampleUsages;
410  }
411
412
413
414  /**
415   * Creates a copy of this subcommand that is "clean" and appears as if it has
416   * not been used to parse an argument set.  The new subcommand will have all
417   * of the same names and argument constraints as this subcommand.
418   *
419   * @return  The "clean" copy of this subcommand.
420   */
421  public SubCommand getCleanCopy()
422  {
423    return new SubCommand(this);
424  }
425
426
427
428  /**
429   * Retrieves a string representation of this subcommand.
430   *
431   * @return  A string representation of this subcommand.
432   */
433  @Override()
434  public String toString()
435  {
436    final StringBuilder buffer = new StringBuilder();
437    toString(buffer);
438    return buffer.toString();
439  }
440
441
442
443  /**
444   * Appends a string representation of this subcommand to the provided buffer.
445   *
446   * @param  buffer  The buffer to which the information should be appended.
447   */
448  public void toString(final StringBuilder buffer)
449  {
450    buffer.append("SubCommand(");
451
452    if (names.size() == 1)
453    {
454      buffer.append("name='");
455      buffer.append(names.values().iterator().next());
456      buffer.append('\'');
457    }
458    else
459    {
460      buffer.append("names={");
461
462      final Iterator<ObjectPair<String,Boolean>> iterator =
463           names.values().iterator();
464      while (iterator.hasNext())
465      {
466        buffer.append('\'');
467        buffer.append(iterator.next().getFirst());
468        buffer.append('\'');
469
470        if (iterator.hasNext())
471        {
472          buffer.append(", ");
473        }
474      }
475
476      buffer.append('}');
477    }
478
479    buffer.append(", description='");
480    buffer.append(description);
481    buffer.append("', parser=");
482    subcommandArgumentParser.toString(buffer);
483    buffer.append(')');
484  }
485}