🧹 Clean up CommandManager

This commit is contained in:
Alexander Söderberg 2020-10-09 20:02:28 +02:00
parent 4368305bc9
commit 16623969ad
No known key found for this signature in database
GPG key ID: FACEA5B0F4C1BF80
6 changed files with 403 additions and 56 deletions

View file

@ -24,7 +24,9 @@
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;
@ -44,6 +46,7 @@ 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;
@ -63,7 +66,6 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.StringTokenizer;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Function;
@ -75,8 +77,6 @@ import java.util.function.Function;
*/
public abstract class CommandManager<C> {
private static final List<String> SINGLE_EMPTY_SUGGESTION = Collections.unmodifiableList(Collections.singletonList(""));
private final Map<Class<? extends Exception>, BiConsumer<C, ? extends Exception>> exceptionHandlers = new HashMap<>();
private final EnumSet<ManagerSettings> managerSettings = EnumSet.of(
ManagerSettings.ENFORCE_INTERMEDIARY_PERMISSIONS);
@ -87,6 +87,7 @@ public abstract class CommandManager<C> {
private final Collection<Command<C>> commands = new LinkedList<>();
private final CommandExecutionCoordinator<C> commandExecutionCoordinator;
private final CommandTree<C> commandTree;
private final CommandSuggestionEngine<C> commandSuggestionEngine;
private CommandSyntaxFormatter<C> commandSyntaxFormatter = new StandardCommandSyntaxFormatter<>();
private CommandSuggestionProcessor<C> commandSuggestionProcessor = new FilteringCommandSuggestionProcessor<>();
@ -105,30 +106,13 @@ public abstract class CommandManager<C> {
this.commandTree = CommandTree.newTree(this);
this.commandExecutionCoordinator = commandExecutionCoordinator.apply(commandTree);
this.commandRegistrationHandler = commandRegistrationHandler;
this.commandSuggestionEngine = new DelegatingCommandSuggestionEngineFactory<>(this).create();
this.servicePipeline.registerServiceType(new TypeToken<CommandPreprocessor<C>>() {
}, new AcceptingCommandPreprocessor<>());
this.servicePipeline.registerServiceType(new TypeToken<CommandPostprocessor<C>>() {
}, new AcceptingCommandPostprocessor<>());
}
/**
* Tokenize an input string
*
* @param input Input string
* @return List of tokens
*/
public static @NonNull LinkedList<@NonNull String> tokenize(final @NonNull String input) {
final StringTokenizer stringTokenizer = new StringTokenizer(input, " ");
final LinkedList<String> tokens = new LinkedList<>();
while (stringTokenizer.hasMoreElements()) {
tokens.add(stringTokenizer.nextToken());
}
if (input.endsWith(" ")) {
tokens.add("");
}
return tokens;
}
/**
* Execute a command and get a future that completes with the result
*
@ -141,7 +125,7 @@ public abstract class CommandManager<C> {
final @NonNull String input
) {
final CommandContext<C> context = this.commandContextFactory.create(false, commandSender);
final LinkedList<String> inputQueue = tokenize(input);
final LinkedList<String> inputQueue = new CommandInputTokenizer(input).tokenize();
try {
if (this.preprocessContext(context, inputQueue) == State.ACCEPTED) {
return this.commandExecutionCoordinator.coordinateExecution(context, inputQueue);
@ -167,25 +151,11 @@ public abstract class CommandManager<C> {
final @NonNull C commandSender,
final @NonNull String input
) {
final CommandContext<C> context = this.commandContextFactory.create(true, commandSender);
final LinkedList<String> inputQueue = tokenize(input);
final List<String> suggestions;
if (this.preprocessContext(context, inputQueue) == State.ACCEPTED) {
suggestions = this.commandSuggestionProcessor.apply(
new CommandPreprocessingContext<>(context, inputQueue),
this.commandTree.getSuggestions(
context, inputQueue)
);
} else {
suggestions = Collections.emptyList();
}
if (this.getSetting(ManagerSettings.FORCE_SUGGESTION) && suggestions.isEmpty()) {
return SINGLE_EMPTY_SUGGESTION;
}
return suggestions;
final CommandContext<C> context = this.commandContextFactory.create(
true,
commandSender
);
return this.commandSuggestionEngine.getSuggestions(context, input);
}
/**
@ -281,7 +251,14 @@ public abstract class CommandManager<C> {
public abstract boolean hasPermission(@NonNull C sender, @NonNull String permission);
/**
* Create a new command builder
* 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}
* <p>
* 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
@ -295,11 +272,25 @@ public abstract class CommandManager<C> {
final @NonNull Description description,
final @NonNull CommandMeta meta
) {
return Command.<C>newBuilder(name, meta, description, aliases.toArray(new String[0])).manager(this);
return Command.<C>newBuilder(
name,
meta,
description,
aliases.toArray(new String[0])
).manager(this);
}
/**
* Create a new command builder
* Create a new command builder with an empty description.
* <p>
* 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}
* <p>
* 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
@ -311,11 +302,23 @@ public abstract class CommandManager<C> {
final @NonNull Collection<String> aliases,
final @NonNull CommandMeta meta
) {
return Command.<C>newBuilder(name, meta, Description.empty(), aliases.toArray(new String[0])).manager(this);
return Command.<C>newBuilder(
name,
meta,
Description.empty(),
aliases.toArray(new String[0])
).manager(this);
}
/**
* Create a new command builder
* 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}
* <p>
* 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
@ -329,11 +332,25 @@ public abstract class CommandManager<C> {
final @NonNull Description description,
final @NonNull String... aliases
) {
return Command.<C>newBuilder(name, meta, description, aliases).manager(this);
return Command.<C>newBuilder(
name,
meta,
description,
aliases
).manager(this);
}
/**
* Create a new command builder
* Create a new command builder with an empty description.
* <p>
* 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}
* <p>
* 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
@ -345,11 +362,25 @@ public abstract class CommandManager<C> {
final @NonNull CommandMeta meta,
final @NonNull String... aliases
) {
return Command.<C>newBuilder(name, meta, Description.empty(), aliases).manager(this);
return Command.<C>newBuilder(
name,
meta,
Description.empty(),
aliases
).manager(this);
}
/**
* Create a new command builder using a default command meta instance.
* Create a new command builder using default command meta created by {@link #createDefaultCommandMeta()}.
* <p>
* 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}
* <p>
* 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
@ -363,11 +394,26 @@ public abstract class CommandManager<C> {
final @NonNull Description description,
final @NonNull String... aliases
) {
return Command.<C>newBuilder(name, this.createDefaultCommandMeta(), description, aliases).manager(this);
return Command.<C>newBuilder(
name,
this.createDefaultCommandMeta(),
description,
aliases
).manager(this);
}
/**
* Create a new command builder using a default command meta instance.
* Create a new command builder using default command meta created by {@link #createDefaultCommandMeta()}, and
* an empty description.
* <p>
* 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}
* <p>
* 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
@ -379,11 +425,20 @@ public abstract class CommandManager<C> {
final @NonNull String name,
final @NonNull String... aliases
) {
return Command.<C>newBuilder(name, this.createDefaultCommandMeta(), Description.empty(), aliases).manager(this);
return Command.<C>newBuilder(
name,
this.createDefaultCommandMeta(),
Description.empty(),
aliases
).manager(this);
}
/**
* Create a new command argument builder
* Create a new command argument builder.
* <p>
* 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
@ -433,8 +488,10 @@ public abstract class CommandManager<C> {
* @see #preprocessContext(CommandContext, LinkedList) Preprocess a context
*/
public void registerCommandPreProcessor(final @NonNull CommandPreprocessor<C> processor) {
this.servicePipeline.registerServiceImplementation(new TypeToken<CommandPreprocessor<C>>() {
}, processor,
this.servicePipeline.registerServiceImplementation(
new TypeToken<CommandPreprocessor<C>>() {
},
processor,
Collections.emptyList()
);
}
@ -612,6 +669,7 @@ public abstract class CommandManager<C> {
*
* @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);
@ -622,6 +680,7 @@ public abstract class CommandManager<C> {
*
* @param setting Setting to set
* @param value Value
* @see #getSetting(ManagerSettings) Get a manager setting
*/
@SuppressWarnings("unused")
public void setSetting(
@ -638,6 +697,9 @@ public abstract class CommandManager<C> {
/**
* Configurable command related settings
*
* @see CommandManager#setSetting(ManagerSettings, boolean) Set a manager setting
* @see CommandManager#getSetting(ManagerSettings) Get a manager setting
*/
public enum ManagerSettings {
/**

View file

@ -0,0 +1,51 @@
//
// 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.arguments;
import cloud.commandframework.context.CommandContext;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.List;
/**
* Handler that produces command suggestions depending on input
*
* @param <C> Command sender type
*/
public interface CommandSuggestionEngine<C> {
/**
* Get command suggestions for the "next" argument that would yield a correctly
* parsing command input
*
* @param context Request context
* @param input Input provided by the sender
* @return List of suggestions
*/
@NonNull List<@NonNull String> getSuggestions(
@NonNull CommandContext<C> context,
@NonNull String input
);
}

View file

@ -0,0 +1,88 @@
//
// 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.arguments;
import cloud.commandframework.CommandManager;
import cloud.commandframework.CommandTree;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.execution.preprocessor.CommandPreprocessingContext;
import cloud.commandframework.internal.CommandInputTokenizer;
import cloud.commandframework.services.State;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* Command suggestion engine that delegates to a {@link cloud.commandframework.CommandTree}
*
* @param <C> Command sender type
*/
public final class DelegatingCommandSuggestionEngine<C> implements CommandSuggestionEngine<C> {
private static final List<String> SINGLE_EMPTY_SUGGESTION = Collections.unmodifiableList(Collections.singletonList(""));
private final CommandManager<C> commandManager;
private final CommandTree<C> commandTree;
/**
* Create a new delegating command suggestion engine
*
* @param commandManager Command manager
* @param commandTree Command tree
*/
DelegatingCommandSuggestionEngine(
final @NonNull CommandManager<C> commandManager,
final @NonNull CommandTree<C> commandTree
) {
this.commandManager = commandManager;
this.commandTree = commandTree;
}
@Override
public @NonNull List<@NonNull String> getSuggestions(
@NonNull final CommandContext<C> context,
@NonNull final String input
) {
final @NonNull LinkedList<@NonNull String> inputQueue = new CommandInputTokenizer(input).tokenize();
final List<String> suggestions;
if (this.commandManager.preprocessContext(context, inputQueue) == State.ACCEPTED) {
suggestions = this.commandManager.getCommandSuggestionProcessor().apply(
new CommandPreprocessingContext<>(context, inputQueue),
this.commandTree.getSuggestions(
context,
inputQueue
)
);
} else {
suggestions = Collections.emptyList();
}
if (this.commandManager.getSetting(CommandManager.ManagerSettings.FORCE_SUGGESTION) && suggestions.isEmpty()) {
return SINGLE_EMPTY_SUGGESTION;
}
return suggestions;
}
}

View file

@ -0,0 +1,62 @@
//
// 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.arguments;
import cloud.commandframework.CommandManager;
import cloud.commandframework.CommandTree;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* Factory that produces {@link DelegatingCommandSuggestionEngine} instances
*
* @param <C> Command sender type
*/
public final class DelegatingCommandSuggestionEngineFactory<C> {
private final CommandManager<C> commandManager;
private final CommandTree<C> commandTree;
/**
* Create a new delegating command suggestion engine factory
*
* @param commandManager Command manager
*/
public DelegatingCommandSuggestionEngineFactory(final @NonNull CommandManager<C> commandManager) {
this.commandManager = commandManager;
this.commandTree = commandManager.getCommandTree();
}
/**
* Create the engine instance
*
* @return New engine instance
*/
public @NonNull DelegatingCommandSuggestionEngine<C> create() {
return new DelegatingCommandSuggestionEngine<>(
this.commandManager,
this.commandTree
);
}
}

View file

@ -0,0 +1,82 @@
//
// 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.internal;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.LinkedList;
import java.util.StringTokenizer;
/**
* Tokenizer that splits command inputs into tokens. This will split the string
* at every blank space. If the input string ends with a blank space, a trailing
* empty string will be added to the token list
*/
public final class CommandInputTokenizer {
private static final String DELIMITER = " ";
private static final String EMPTY = "";
private final StringTokenizerFactory stringTokenizerFactory = new StringTokenizerFactory();
private final String input;
/**
* Create a new input tokenizer
*
* @param input Input that is to be turned into tokens
*/
public CommandInputTokenizer(final @NonNull String input) {
this.input = input;
}
/**
* Turn the input into tokens
*
* @return Linked list containing the tokenized input
*/
public @NonNull LinkedList<@NonNull String> tokenize() {
final StringTokenizer stringTokenizer = stringTokenizerFactory.createStringTokenizer();
final LinkedList<String> tokens = new LinkedList<>();
while (stringTokenizer.hasMoreElements()) {
tokens.add(stringTokenizer.nextToken());
}
if (input.endsWith(DELIMITER)) {
tokens.add(EMPTY);
}
return tokens;
}
/**
* Factory class that creates {@link StringTokenizer} instances
*/
private final class StringTokenizerFactory {
private @NonNull StringTokenizer createStringTokenizer() {
return new StringTokenizer(input, DELIMITER);
}
}
}

View file

@ -14,6 +14,7 @@ include(':cloud-javacord')
include(':cloud-jda')
include(':example-bukkit')
include(':cloud-tasks')
include(':cloud-sponge')
project(':cloud-bukkit').projectDir = file('cloud-minecraft/cloud-bukkit')
project(':cloud-paper').projectDir = file('cloud-minecraft/cloud-paper')
project(':cloud-brigadier').projectDir = file('cloud-minecraft/cloud-brigadier')
@ -24,3 +25,4 @@ project(':cloud-cloudburst').projectDir = file('cloud-minecraft/cloud-cloudburst
project(':cloud-javacord').projectDir = file('cloud-discord/cloud-javacord')
project(':cloud-jda').projectDir = file('cloud-discord/cloud-jda')
project(':example-bukkit').projectDir = file('examples/example-bukkit')
project(':cloud-sponge').projectDir = file('cloud-minecraft/cloud-sponge')