cloud.commandframework.CommandManager Maven / Gradle / Ivy
Show all versions of cloud-core Show documentation
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
package cloud.commandframework;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.CommandSuggestionEngine;
import cloud.commandframework.arguments.CommandSyntaxFormatter;
import cloud.commandframework.arguments.DelegatingCommandSuggestionEngineFactory;
import cloud.commandframework.arguments.StandardCommandSyntaxFormatter;
import cloud.commandframework.arguments.flags.CommandFlag;
import cloud.commandframework.arguments.parser.ArgumentParser;
import cloud.commandframework.arguments.parser.ParserParameter;
import cloud.commandframework.arguments.parser.ParserRegistry;
import cloud.commandframework.arguments.parser.StandardParserRegistry;
import cloud.commandframework.captions.CaptionRegistry;
import cloud.commandframework.captions.SimpleCaptionRegistryFactory;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.context.CommandContextFactory;
import cloud.commandframework.context.StandardCommandContextFactory;
import cloud.commandframework.execution.CommandExecutionCoordinator;
import cloud.commandframework.execution.CommandResult;
import cloud.commandframework.execution.CommandSuggestionProcessor;
import cloud.commandframework.execution.FilteringCommandSuggestionProcessor;
import cloud.commandframework.execution.postprocessor.AcceptingCommandPostprocessor;
import cloud.commandframework.execution.postprocessor.CommandPostprocessingContext;
import cloud.commandframework.execution.postprocessor.CommandPostprocessor;
import cloud.commandframework.execution.preprocessor.AcceptingCommandPreprocessor;
import cloud.commandframework.execution.preprocessor.CommandPreprocessingContext;
import cloud.commandframework.execution.preprocessor.CommandPreprocessor;
import cloud.commandframework.internal.CommandInputTokenizer;
import cloud.commandframework.internal.CommandRegistrationHandler;
import cloud.commandframework.meta.CommandMeta;
import cloud.commandframework.permission.CommandPermission;
import cloud.commandframework.permission.OrPermission;
import cloud.commandframework.permission.Permission;
import cloud.commandframework.services.ServicePipeline;
import cloud.commandframework.services.State;
import io.leangen.geantyref.TypeToken;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* The manager is responsible for command registration, parsing delegation, etc.
*
* @param Command sender type
*/
public abstract class CommandManager {
private final Map, BiConsumer> exceptionHandlers = new HashMap<>();
private final EnumSet managerSettings = EnumSet.of(
ManagerSettings.ENFORCE_INTERMEDIARY_PERMISSIONS);
private final CommandContextFactory commandContextFactory = new StandardCommandContextFactory<>();
private final ServicePipeline servicePipeline = ServicePipeline.builder().build();
private final ParserRegistry parserRegistry = new StandardParserRegistry<>();
private final Collection> commands = new LinkedList<>();
private final CommandExecutionCoordinator commandExecutionCoordinator;
private final CommandTree commandTree;
private final CommandSuggestionEngine commandSuggestionEngine;
private CommandSyntaxFormatter commandSyntaxFormatter = new StandardCommandSyntaxFormatter<>();
private CommandSuggestionProcessor commandSuggestionProcessor = new FilteringCommandSuggestionProcessor<>();
private CommandRegistrationHandler commandRegistrationHandler;
private CaptionRegistry captionRegistry;
/**
* Create a new command manager instance
*
* @param commandExecutionCoordinator Execution coordinator instance
* @param commandRegistrationHandler Command registration handler
*/
public CommandManager(
final @NonNull Function<@NonNull CommandTree, @NonNull CommandExecutionCoordinator> commandExecutionCoordinator,
final @NonNull CommandRegistrationHandler commandRegistrationHandler
) {
this.commandTree = CommandTree.newTree(this);
this.commandExecutionCoordinator = commandExecutionCoordinator.apply(commandTree);
this.commandRegistrationHandler = commandRegistrationHandler;
this.commandSuggestionEngine = new DelegatingCommandSuggestionEngineFactory<>(this).create();
this.servicePipeline.registerServiceType(new TypeToken>() {
}, new AcceptingCommandPreprocessor<>());
this.servicePipeline.registerServiceType(new TypeToken>() {
}, new AcceptingCommandPostprocessor<>());
this.captionRegistry = new SimpleCaptionRegistryFactory().create();
}
/**
* Execute a command and get a future that completes with the result
*
* @param commandSender Sender of the command
* @param input Input provided by the sender
* @return Command result
*/
public @NonNull CompletableFuture> executeCommand(
final @NonNull C commandSender,
final @NonNull String input
) {
final CommandContext context = this.commandContextFactory.create(
false,
commandSender,
this.captionRegistry
);
final LinkedList inputQueue = new CommandInputTokenizer(input).tokenize();
try {
if (this.preprocessContext(context, inputQueue) == State.ACCEPTED) {
return this.commandExecutionCoordinator.coordinateExecution(context, inputQueue);
}
} catch (final Exception e) {
final CompletableFuture> future = new CompletableFuture<>();
future.completeExceptionally(e);
return future;
}
/* Wasn't allowed to execute the command */
return CompletableFuture.completedFuture(null);
}
/**
* Get command suggestions for the "next" argument that would yield a correctly
* parsing command input
*
* @param commandSender Sender of the command
* @param input Input provided by the sender
* @return List of suggestions
*/
public @NonNull List<@NonNull String> suggest(
final @NonNull C commandSender,
final @NonNull String input
) {
final CommandContext context = this.commandContextFactory.create(
true,
commandSender,
this.captionRegistry
);
return this.commandSuggestionEngine.getSuggestions(context, input);
}
/**
* Register a new command
*
* @param command Command to register
* @return The command manager instance
*/
public @NonNull CommandManager command(final @NonNull Command command) {
this.commandTree.insertCommand(command);
this.commands.add(command);
return this;
}
/**
* Register a new command
*
* @param command Command to register. {@link Command.Builder#build()}} will be invoked.
* @return The command manager instance
*/
public @NonNull CommandManager command(final Command.@NonNull Builder command) {
return this.command(command.manager(this).build());
}
/**
* Get the command syntax formatter
*
* @return Command syntax formatter
*/
public @NonNull CommandSyntaxFormatter getCommandSyntaxFormatter() {
return this.commandSyntaxFormatter;
}
/**
* Set the command syntax formatter
*
* @param commandSyntaxFormatter New formatter
*/
public void setCommandSyntaxFormatter(final @NonNull CommandSyntaxFormatter commandSyntaxFormatter) {
this.commandSyntaxFormatter = commandSyntaxFormatter;
}
/**
* Get the command registration handler
*
* @return Command registration handler
*/
public @NonNull CommandRegistrationHandler getCommandRegistrationHandler() {
return this.commandRegistrationHandler;
}
protected final void setCommandRegistrationHandler(final @NonNull CommandRegistrationHandler commandRegistrationHandler) {
this.commandRegistrationHandler = commandRegistrationHandler;
}
/**
* Check if the command sender has the required permission. If the permission node is
* empty, this should return {@code true}
*
* @param sender Command sender
* @param permission Permission node
* @return {@code true} if the sender has the permission, else {@code false}
*/
public boolean hasPermission(
final @NonNull C sender,
final @NonNull CommandPermission permission
) {
if (permission.toString().isEmpty()) {
return true;
}
if (permission instanceof Permission) {
return hasPermission(sender, permission.toString());
}
for (final CommandPermission innerPermission : permission.getPermissions()) {
final boolean hasPermission = this.hasPermission(sender, innerPermission);
if (permission instanceof OrPermission) {
if (hasPermission) {
return true;
}
}
}
return false;
}
/**
* Get the caption registry
*
* @return Caption registry
*/
public final @NonNull CaptionRegistry getCaptionRegistry() {
return this.captionRegistry;
}
/**
* Replace the caption registry
*
* @param captionRegistry New caption registry
*/
public final void setCaptionRegistry(final @NonNull CaptionRegistry captionRegistry) {
this.captionRegistry = captionRegistry;
}
/**
* Replace the default caption registry
*
* @param captionRegistry Caption registry to use
*/
public final void registerDefaultCaptions(final @NonNull CaptionRegistry captionRegistry) {
this.captionRegistry = captionRegistry;
}
/**
* Check if the command sender has the required permission. If the permission node is
* empty, this should return {@code true}
*
* @param sender Command sender
* @param permission Permission node
* @return {@code true} if the sender has the permission, else {@code false}
*/
public abstract boolean hasPermission(@NonNull C sender, @NonNull String permission);
/**
* Create a new command builder. This will also register the creating manager in the command
* builder using {@link Command.Builder#manager(CommandManager)}, so that the command
* builder is associated with the creating manager. This allows for parser inference based on
* the type, with the help of the {@link ParserRegistry parser registry}
*
* This method will not register the command in the manager. To do that, {@link #command(Command.Builder)}
* or {@link #command(Command)} has to be invoked with either the {@link Command.Builder} instance, or the constructed
* {@link Command command} instance
*
* @param name Command name
* @param aliases Command aliases
* @param description Command description
* @param meta Command meta
* @return Builder instance
*/
public Command.@NonNull Builder commandBuilder(
final @NonNull String name,
final @NonNull Collection aliases,
final @NonNull Description description,
final @NonNull CommandMeta meta
) {
return Command.newBuilder(
name,
meta,
description,
aliases.toArray(new String[0])
).manager(this);
}
/**
* Create a new command builder with an empty description.
*
* This will also register the creating manager in the command
* builder using {@link Command.Builder#manager(CommandManager)}, so that the command
* builder is associated with the creating manager. This allows for parser inference based on
* the type, with the help of the {@link ParserRegistry parser registry}
*
* This method will not register the command in the manager. To do that, {@link #command(Command.Builder)}
* or {@link #command(Command)} has to be invoked with either the {@link Command.Builder} instance, or the constructed
* {@link Command command} instance
*
* @param name Command name
* @param aliases Command aliases
* @param meta Command meta
* @return Builder instance
*/
public Command.@NonNull Builder commandBuilder(
final @NonNull String name,
final @NonNull Collection aliases,
final @NonNull CommandMeta meta
) {
return Command.newBuilder(
name,
meta,
Description.empty(),
aliases.toArray(new String[0])
).manager(this);
}
/**
* Create a new command builder. This will also register the creating manager in the command
* builder using {@link Command.Builder#manager(CommandManager)}, so that the command
* builder is associated with the creating manager. This allows for parser inference based on
* the type, with the help of the {@link ParserRegistry parser registry}
*
* This method will not register the command in the manager. To do that, {@link #command(Command.Builder)}
* or {@link #command(Command)} has to be invoked with either the {@link Command.Builder} instance, or the constructed
* {@link Command command} instance
*
* @param name Command name
* @param meta Command meta
* @param description Command description
* @param aliases Command aliases
* @return Builder instance
*/
public Command.@NonNull Builder commandBuilder(
final @NonNull String name,
final @NonNull CommandMeta meta,
final @NonNull Description description,
final @NonNull String... aliases
) {
return Command.newBuilder(
name,
meta,
description,
aliases
).manager(this);
}
/**
* Create a new command builder with an empty description.
*
* This will also register the creating manager in the command
* builder using {@link Command.Builder#manager(CommandManager)}, so that the command
* builder is associated with the creating manager. This allows for parser inference based on
* the type, with the help of the {@link ParserRegistry parser registry}
*
* This method will not register the command in the manager. To do that, {@link #command(Command.Builder)}
* or {@link #command(Command)} has to be invoked with either the {@link Command.Builder} instance, or the constructed
* {@link Command command} instance
*
* @param name Command name
* @param meta Command meta
* @param aliases Command aliases
* @return Builder instance
*/
public Command.@NonNull Builder commandBuilder(
final @NonNull String name,
final @NonNull CommandMeta meta,
final @NonNull String... aliases
) {
return Command.newBuilder(
name,
meta,
Description.empty(),
aliases
).manager(this);
}
/**
* Create a new command builder using default command meta created by {@link #createDefaultCommandMeta()}.
*
* This will also register the creating manager in the command
* builder using {@link Command.Builder#manager(CommandManager)}, so that the command
* builder is associated with the creating manager. This allows for parser inference based on
* the type, with the help of the {@link ParserRegistry parser registry}
*
* This method will not register the command in the manager. To do that, {@link #command(Command.Builder)}
* or {@link #command(Command)} has to be invoked with either the {@link Command.Builder} instance, or the constructed
* {@link Command command} instance
*
* @param name Command name
* @param description Command description
* @param aliases Command aliases
* @return Builder instance
* @throws UnsupportedOperationException If the command manager does not support default command meta creation
* @see #createDefaultCommandMeta() Default command meta creation
*/
public Command.@NonNull Builder commandBuilder(
final @NonNull String name,
final @NonNull Description description,
final @NonNull String... aliases
) {
return Command.newBuilder(
name,
this.createDefaultCommandMeta(),
description,
aliases
).manager(this);
}
/**
* Create a new command builder using default command meta created by {@link #createDefaultCommandMeta()}, and
* an empty description.
*
* This will also register the creating manager in the command
* builder using {@link Command.Builder#manager(CommandManager)}, so that the command
* builder is associated with the creating manager. This allows for parser inference based on
* the type, with the help of the {@link ParserRegistry parser registry}
*
* This method will not register the command in the manager. To do that, {@link #command(Command.Builder)}
* or {@link #command(Command)} has to be invoked with either the {@link Command.Builder} instance, or the constructed
* {@link Command command} instance
*
* @param name Command name
* @param aliases Command aliases
* @return Builder instance
* @throws UnsupportedOperationException If the command manager does not support default command meta creation
* @see #createDefaultCommandMeta() Default command meta creation
*/
public Command.@NonNull Builder commandBuilder(
final @NonNull String name,
final @NonNull String... aliases
) {
return Command.newBuilder(
name,
this.createDefaultCommandMeta(),
Description.empty(),
aliases
).manager(this);
}
/**
* Create a new command argument builder.
*
* This will also invoke {@link CommandArgument.Builder#manager(CommandManager)}
* so that the argument is associated with the calling command manager. This allows for parser inference based on
* the type, with the help of the {@link ParserRegistry parser registry}.
*
* @param type Argument type
* @param name Argument name
* @param Generic argument name
* @return Argument builder
*/
public CommandArgument.@NonNull Builder argumentBuilder(
final @NonNull Class type,
final @NonNull String name
) {
return CommandArgument.ofType(type, name).manager(this);
}
/**
* Create a new command flag builder
*
* @param name Flag name
* @return Flag builder
*/
public CommandFlag.@NonNull Builder flagBuilder(final @NonNull String name) {
return CommandFlag.newBuilder(name);
}
/**
* Get the internal command tree. This should not be accessed unless you know what you
* are doing
*
* @return Command tree
*/
public @NonNull CommandTree getCommandTree() {
return this.commandTree;
}
/**
* Construct a default command meta instance
*
* @return Default command meta
* @throws UnsupportedOperationException If the command manager does not support this operation
*/
public abstract @NonNull CommandMeta createDefaultCommandMeta();
/**
* Register a new command preprocessor. The order they are registered in is respected, and they
* are called in LIFO order
*
* @param processor Processor to register
* @see #preprocessContext(CommandContext, LinkedList) Preprocess a context
*/
public void registerCommandPreProcessor(final @NonNull CommandPreprocessor processor) {
this.servicePipeline.registerServiceImplementation(
new TypeToken>() {
},
processor,
Collections.emptyList()
);
}
/**
* Register a new command postprocessor. The order they are registered in is respected, and they
* are called in LIFO order
*
* @param processor Processor to register
* @see #preprocessContext(CommandContext, LinkedList) Preprocess a context
*/
public void registerCommandPostProcessor(final @NonNull CommandPostprocessor processor) {
this.servicePipeline.registerServiceImplementation(new TypeToken>() {
}, processor,
Collections.emptyList()
);
}
/**
* Preprocess a command context instance
*
* @param context Command context
* @param inputQueue Command input as supplied by sender
* @return {@link State#ACCEPTED} if the command should be parsed and executed, else {@link State#REJECTED}
* @see #registerCommandPreProcessor(CommandPreprocessor) Register a command preprocessor
*/
public State preprocessContext(
final @NonNull CommandContext context,
final @NonNull LinkedList<@NonNull String> inputQueue
) {
this.servicePipeline.pump(new CommandPreprocessingContext<>(context, inputQueue))
.through(new TypeToken>() {
})
.getResult();
return context.getOptional(AcceptingCommandPreprocessor.PROCESSED_INDICATOR_KEY).orElse("").isEmpty()
? State.REJECTED
: State.ACCEPTED;
}
/**
* Postprocess a command context instance
*
* @param context Command context
* @param command Command instance
* @return {@link State#ACCEPTED} if the command should be parsed and executed, else {@link State#REJECTED}
* @see #registerCommandPostProcessor(CommandPostprocessor) Register a command postprocessor
*/
public State postprocessContext(
final @NonNull CommandContext context,
final @NonNull Command command
) {
this.servicePipeline.pump(new CommandPostprocessingContext<>(context, command))
.through(new TypeToken>() {
})
.getResult();
return context.getOptional(AcceptingCommandPostprocessor.PROCESSED_INDICATOR_KEY).orElse("").isEmpty()
? State.REJECTED
: State.ACCEPTED;
}
/**
* Get the command suggestions processor instance currently used in this command manager
*
* @return Command suggestions processor
* @see #setCommandSuggestionProcessor(CommandSuggestionProcessor) Setting the suggestion processor
*/
public @NonNull CommandSuggestionProcessor getCommandSuggestionProcessor() {
return this.commandSuggestionProcessor;
}
/**
* Set the command suggestions processor for this command manager. This will be called every
* time {@link #suggest(Object, String)} is called, to process the list of suggestions
* before it's returned to the caller
*
* @param commandSuggestionProcessor New command suggestions processor
*/
public void setCommandSuggestionProcessor(final @NonNull CommandSuggestionProcessor commandSuggestionProcessor) {
this.commandSuggestionProcessor = commandSuggestionProcessor;
}
/**
* Get the parser registry instance. The parser registry contains default
* mappings to {@link ArgumentParser}
* and allows for the registration of custom mappings. The parser registry also
* contains mappings of annotations to {@link ParserParameter}
* which allows for annotations to be used to customize parser settings.
*
* When creating a new parser type, it is recommended to register it in the parser
* registry. In particular, default parser types (shipped with cloud implementations)
* should be registered in the constructor of the platform {@link CommandManager}
*
* @return Parser registry instance
*/
public ParserRegistry getParserRegistry() {
return this.parserRegistry;
}
/**
* Get the exception handler for an exception type, if one has been registered
*
* @param clazz Exception class
* @param Exception type
* @return Exception handler, or {@code null}
* @see #registerCommandPreProcessor(CommandPreprocessor) Registering an exception handler
*/
public final @Nullable BiConsumer<@NonNull C, @NonNull E>
getExceptionHandler(final @NonNull Class clazz) {
final BiConsumer consumer = this.exceptionHandlers.get(clazz);
if (consumer == null) {
return null;
}
//noinspection unchecked
return (BiConsumer) consumer;
}
/**
* Register an exception handler for an exception type. This will then be used
* when {@link #handleException(Object, Class, Exception, BiConsumer)} is called
* for the particular exception type
*
* @param clazz Exception class
* @param handler Exception handler
* @param Exception type
*/
public final void registerExceptionHandler(
final @NonNull Class clazz,
final @NonNull BiConsumer<@NonNull C, @NonNull E> handler
) {
this.exceptionHandlers.put(clazz, handler);
}
/**
* Handle an exception using the registered exception handler for the exception type, or using the
* provided default handler if no exception handler has been registered for the exception type
*
* @param sender Executing command sender
* @param clazz Exception class
* @param exception Exception instance
* @param defaultHandler Default exception handler. Will be called if there is no exception
* handler stored for the exception type
* @param Exception type
*/
public final void handleException(
final @NonNull C sender,
final @NonNull Class clazz,
final @NonNull E exception,
final @NonNull BiConsumer defaultHandler
) {
Optional.ofNullable(this.getExceptionHandler(clazz)).orElse(defaultHandler).accept(sender, exception);
}
/**
* Get a collection containing all registered commands.
*
* @return Unmodifiable view of all registered commands
*/
public final @NonNull Collection<@NonNull Command> getCommands() {
return Collections.unmodifiableCollection(this.commands);
}
/**
* Get a command help handler instance. This can be used to assist in the production
* of command help menus, etc.
*
* @return Command help handler. A new instance will be created
* each time this method is called.
*/
public final @NonNull CommandHelpHandler getCommandHelpHandler() {
return new CommandHelpHandler<>(this);
}
/**
* Get a command manager setting
*
* @param setting Setting
* @return {@code true} if the setting is activated or {@code false} if it's not
* @see #setSetting(ManagerSettings, boolean) Update a manager setting
*/
public boolean getSetting(final @NonNull ManagerSettings setting) {
return this.managerSettings.contains(setting);
}
/**
* Set the setting
*
* @param setting Setting to set
* @param value Value
* @see #getSetting(ManagerSettings) Get a manager setting
*/
@SuppressWarnings("unused")
public void setSetting(
final @NonNull ManagerSettings setting,
final boolean value
) {
if (value) {
this.managerSettings.add(setting);
} else {
this.managerSettings.remove(setting);
}
}
/**
* Configurable command related settings
*
* @see CommandManager#setSetting(ManagerSettings, boolean) Set a manager setting
* @see CommandManager#getSetting(ManagerSettings) Get a manager setting
*/
public enum ManagerSettings {
/**
* Do not create a compound permission and do not look greedily
* for child permission values, if a preceding command in the tree path
* has a command handler attached
*/
ENFORCE_INTERMEDIARY_PERMISSIONS,
/**
* Force sending of an empty suggestion (i.e. a singleton list containing an empty string)
* when no suggestions are present
*/
FORCE_SUGGESTION
}
}