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