001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.metaschema.cli.processor.command;
007
008import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
009import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
010import gov.nist.secauto.metaschema.core.util.CollectionUtil;
011
012import org.apache.commons.cli.CommandLine;
013import org.apache.commons.cli.Option;
014
015import java.util.Collection;
016import java.util.List;
017import java.util.stream.Collectors;
018
019import edu.umd.cs.findbugs.annotations.NonNull;
020import edu.umd.cs.findbugs.annotations.Nullable;
021
022/**
023 * A command line interface command.
024 */
025public interface ICommand {
026  /**
027   * Get the name of the command.
028   * <p>
029   * This name is used to call the command as a command line argument.
030   *
031   * @return the command's name
032   */
033  @NonNull
034  String getName();
035
036  /**
037   * Get a description of what the command does.
038   * <p>
039   * This description is displayed in help output.
040   *
041   * @return the description
042   */
043  @NonNull
044  String getDescription();
045
046  /**
047   * Get the non-option arguments.
048   *
049   * @return the arguments, or an empty list if there are no arguments
050   */
051  @NonNull
052  default List<ExtraArgument> getExtraArguments() {
053    return CollectionUtil.emptyList();
054  }
055
056  /**
057   * Used to gather options directly associated with this command.
058   *
059   * @return the options
060   */
061  @NonNull
062  default Collection<? extends Option> gatherOptions() {
063    // by default there are no options to handle
064    return CollectionUtil.emptyList();
065  }
066
067  /**
068   * Get any sub-commands associated with this command.
069   *
070   * @return the sub-commands
071   */
072  @NonNull
073  default Collection<ICommand> getSubCommands() {
074    // no sub-commands by default
075    return CollectionUtil.emptyList();
076  }
077
078  /**
079   * Get a sub-command by it's command name.
080   *
081   * @param name
082   *          the requested sub-command name
083   * @return the command or {@code null} if no sub-command exists with that name
084   */
085  @Nullable
086  default ICommand getSubCommandByName(@NonNull String name) {
087    // no sub-commands by default
088    return null;
089  }
090
091  /**
092   * Determine if this command requires the use of a sub-command.
093   *
094   * @return {@code true} if a sub-command is required or {@code false} otherwise
095   */
096  default boolean isSubCommandRequired() {
097    // no sub-commands by default
098    return false;
099  }
100
101  /**
102   * Validate the options provided on the command line based on what is required
103   * for this command.
104   *
105   * @param callingContext
106   *          the context of the command execution
107   * @param commandLine
108   *          the parsed command line details
109   * @throws InvalidArgumentException
110   *           if a problem was found while validating the options
111   */
112  default void validateOptions(
113      @NonNull CallingContext callingContext,
114      @NonNull CommandLine commandLine) throws InvalidArgumentException {
115    // by default there are no options to handle
116  }
117
118  /**
119   * Create a new executor for this command.
120   *
121   * @param callingContext
122   *          the context of the command execution
123   * @param commandLine
124   *          the parsed command line details
125   * @return the executor
126   */
127  @NonNull
128  ICommandExecutor newExecutor(
129      @NonNull CallingContext callingContext,
130      @NonNull CommandLine commandLine);
131
132  /**
133   * Validates that the provided extra arguments meet expectations.
134   *
135   * @param callingContext
136   *          the context of the command execution
137   * @param commandLine
138   *          the parsed command line details
139   * @throws InvalidArgumentException
140   *           if a problem was found while validating the extra arguments
141   */
142  default void validateExtraArguments(
143      @NonNull CallingContext callingContext,
144      @NonNull CommandLine commandLine)
145      throws InvalidArgumentException {
146
147    validateSubCommandRequirement();
148    validateArgumentCount(commandLine);
149    validateRequiredArguments(commandLine);
150  }
151
152  private void validateSubCommandRequirement() throws InvalidArgumentException {
153    if (isSubCommandRequired()) {
154      throw new InvalidArgumentException("Please choose a valid sub-command.");
155    }
156  }
157
158  private void validateArgumentCount(@NonNull CommandLine commandLine) throws InvalidArgumentException {
159    List<ExtraArgument> extraArguments = getExtraArguments();
160    int maxArguments = extraArguments.size();
161    List<String> actualArgs = commandLine.getArgList();
162
163    if (actualArgs.size() > maxArguments) {
164      throw new InvalidArgumentException(
165          String.format("Too many extra arguments provided. Expected at most %d, but got %d.",
166              maxArguments, actualArgs.size()));
167    }
168
169  }
170
171  private void validateRequiredArguments(@NonNull CommandLine commandLine) throws InvalidArgumentException {
172    List<String> actualArgs = commandLine.getArgList();
173    List<ExtraArgument> requiredExtraArguments = getExtraArguments().stream()
174        .filter(ExtraArgument::isRequired)
175        .collect(Collectors.toUnmodifiableList());
176
177    if (actualArgs.size() < requiredExtraArguments.size()) {
178      throw new InvalidArgumentException(
179          String.format("Missing required arguments: %s. Expected %d required arguments, but got %d.",
180              requiredExtraArguments.stream()
181                  .map(arg -> "<" + arg.getName() + ">")
182                  .collect(Collectors.joining(" ")),
183              requiredExtraArguments.size(),
184              actualArgs.size()));
185    }
186  }
187}