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}