1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.cli.processor.command;
7   
8   import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
9   import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
10  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
11  
12  import org.apache.commons.cli.CommandLine;
13  import org.apache.commons.cli.Option;
14  
15  import java.util.Collection;
16  import java.util.List;
17  import java.util.stream.Collectors;
18  
19  import edu.umd.cs.findbugs.annotations.NonNull;
20  import edu.umd.cs.findbugs.annotations.Nullable;
21  
22  /**
23   * A command line interface command.
24   */
25  public interface ICommand {
26    /**
27     * Get the name of the command.
28     * <p>
29     * This name is used to call the command as a command line argument.
30     *
31     * @return the command's name
32     */
33    @NonNull
34    String getName();
35  
36    /**
37     * Get a description of what the command does.
38     * <p>
39     * This description is displayed in help output.
40     *
41     * @return the description
42     */
43    @NonNull
44    String getDescription();
45  
46    /**
47     * Get the non-option arguments.
48     *
49     * @return the arguments, or an empty list if there are no arguments
50     */
51    @NonNull
52    default List<ExtraArgument> getExtraArguments() {
53      return CollectionUtil.emptyList();
54    }
55  
56    /**
57     * Used to gather options directly associated with this command.
58     *
59     * @return the options
60     */
61    @NonNull
62    default Collection<? extends Option> gatherOptions() {
63      // by default there are no options to handle
64      return CollectionUtil.emptyList();
65    }
66  
67    /**
68     * Get any sub-commands associated with this command.
69     *
70     * @return the sub-commands
71     */
72    @NonNull
73    default Collection<ICommand> getSubCommands() {
74      // no sub-commands by default
75      return CollectionUtil.emptyList();
76    }
77  
78    /**
79     * Get a sub-command by it's command name.
80     *
81     * @param name
82     *          the requested sub-command name
83     * @return the command or {@code null} if no sub-command exists with that name
84     */
85    @Nullable
86    default ICommand getSubCommandByName(@NonNull String name) {
87      // no sub-commands by default
88      return null;
89    }
90  
91    /**
92     * Determine if this command requires the use of a sub-command.
93     *
94     * @return {@code true} if a sub-command is required or {@code false} otherwise
95     */
96    default boolean isSubCommandRequired() {
97      // no sub-commands by default
98      return false;
99    }
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 }