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 }