001    /*
002     * Copyright 2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2016 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     */
021    package com.unboundid.util.args;
022    
023    
024    
025    import java.util.ArrayList;
026    import java.util.Collections;
027    import java.util.Iterator;
028    import java.util.LinkedHashMap;
029    import java.util.List;
030    import java.util.Map;
031    
032    import com.unboundid.util.Mutable;
033    import com.unboundid.util.StaticUtils;
034    import com.unboundid.util.ThreadSafety;
035    import com.unboundid.util.ThreadSafetyLevel;
036    
037    import 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)
078    public 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    }