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