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