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