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