LoginSystem/src/main/java/fr/xephi/authme/command/CommandHandler.java
ljacqu ec9009d776 Separate command handler into smaller parts (wip – doesn't compile)
- Move logic from CommandMapper to CommandHandler
- Create "ResultStatus" on FoundCommandResult to precisely return the error case
Naming and structure not final
2015-12-11 23:16:13 +01:00

231 lines
9.6 KiB
Java

package fr.xephi.authme.command;
import fr.xephi.authme.AuthMe;
import fr.xephi.authme.command.help.HelpProvider;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.util.CollectionUtils;
import fr.xephi.authme.util.StringUtils;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* The AuthMe command handler, responsible for mapping incoming commands to the correct {@link CommandDescription}
* or to display help messages for unknown invocations.
*/
public class CommandHandler {
/**
* The threshold for suggesting a similar command. If the difference is below this value, we will
* ask the player whether he meant the similar command.
*/
private static final double SUGGEST_COMMAND_THRESHOLD = 0.75;
private final Set<CommandDescription> baseCommands;
private final PermissionsManager permissionsManager;
/**
* Create a command handler.
*
* @param baseCommands The collection of available AuthMe base commands
*/
public CommandHandler(Set<CommandDescription> baseCommands, PermissionsManager permissionsManager) {
this.baseCommands = baseCommands;
this.permissionsManager = permissionsManager;
}
/**
* Map a command that was invoked to the proper {@link CommandDescription} or return a useful error
* message upon failure.
*
* @param sender The command sender (Bukkit).
* @param bukkitCommandLabel The command label (Bukkit).
* @param bukkitArgs The command arguments (Bukkit).
*
* @return True if the command was executed, false otherwise.
*/
public boolean processCommand(CommandSender sender, String bukkitCommandLabel, String[] bukkitArgs) {
// Add the Bukkit command label to the front so we get a list like [authme, register, bobby, mysecret]
List<String> parts = skipEmptyArguments(bukkitArgs);
parts.add(0, bukkitCommandLabel);
// Get the base command of the result, e.g. authme for [authme, register, bobby, mysecret]
FoundCommandResult result = mapPartsToCommand(parts);
switch (result.getResultStatus()) {
case SUCCESS:
// Check perms + process
break;
case MISSING_BASE_COMMAND:
sender.sendMessage(ChatColor.DARK_RED + "Failed to parse " + AuthMe.getPluginName() + " command!");
return false;
case INCORRECT_ARGUMENTS:
// sendImproperArgumentsMessage(sender, result);
break;
case UNKNOWN_LABEL:
// sendUnknownCommandMessage(sender);
break;
default:
throw new RuntimeException("Unknown result '" + result.getResultStatus() + "'");
}
return true;
}
/**
* Skip all entries of the given array that are simply whitespace.
*
* @param args The array to process
* @return List of the items that are not empty
*/
private static List<String> skipEmptyArguments(String[] args) {
List<String> cleanArguments = new ArrayList<>(args.length);
for (String argument : args) {
if (!StringUtils.isEmpty(argument)) {
cleanArguments.add(argument);
}
}
return cleanArguments;
}
/**
* Show an "unknown command" to the user and suggests an existing command if its similarity is within
* the defined threshold.
*
* @param sender The command sender
* @param result The command that was found during the mapping process
* @param baseCommand The base command
*/
private static void sendUnknownCommandMessage(CommandSender sender, FoundCommandResult result, String baseCommand) {
sender.sendMessage(ChatColor.DARK_RED + "Unknown command!");
// Show a command suggestion if available and the difference isn't too big
if (result.getDifference() < SUGGEST_COMMAND_THRESHOLD && result.getCommandDescription() != null) {
sender.sendMessage(ChatColor.YELLOW + "Did you mean " + ChatColor.GOLD + "/"
+ result.getCommandDescription() + ChatColor.YELLOW + "?");
// TODO: Define a proper string representation of command description
}
sender.sendMessage(ChatColor.YELLOW + "Use the command " + ChatColor.GOLD + "/" + baseCommand + " help"
+ ChatColor.YELLOW + " to view help.");
}
private static void sendImproperArgumentsMessage(CommandSender sender, FoundCommandResult result,
CommandParts commandReference, String baseCommand) {
// Get the command and the suggested command reference
// FIXME List<String> suggestedCommandReference =
// result.getCommandDescription().getCommandReference(commandReference).getList();
// List<String> helpCommandReference = CollectionUtils.getRange(suggestedCommandReference, 1);
// Show the invalid arguments warning
sender.sendMessage(ChatColor.DARK_RED + "Incorrect command arguments!");
// Show the command argument help
// HelpProvider.showHelp(sender, commandReference, new CommandParts(suggestedCommandReference),
// true, false, true, false, false, false);
// Show the command to use for detailed help
// sender.sendMessage(ChatColor.GOLD + "Detailed help: " + ChatColor.WHITE + "/" + baseCommand
// + " help " + CommandUtils.labelsToString(helpCommandReference));
}
public FoundCommandResult mapPartsToCommand(final List<String> parts) {
if (CollectionUtils.isEmpty(parts)) {
return new FoundCommandResult(null, parts, null, 0.0, FoundCommandResult.ResultStatus.MISSING_BASE_COMMAND);
}
CommandDescription base = getBaseCommand(parts.get(0));
if (base == null) {
return new FoundCommandResult(null, parts, null, 0.0, FoundCommandResult.ResultStatus.MISSING_BASE_COMMAND);
}
// Prefer labels: /register help goes to "Help command", not "Register command" with argument 'help'
List<String> remaining = parts.subList(1, parts.size());
CommandDescription childCommand = returnSuitableChild(base, remaining);
if (childCommand != null) {
return new FoundCommandResult(childCommand, parts.subList(2, parts.size()), parts.subList(0, 2));
} else if (isSuitableArgumentCount(base, remaining.size())) {
return new FoundCommandResult(base, parts.subList(1, parts.size()), parts.subList(0, 1));
}
// TODO: return getCommandWithSmallestDifference()
return null;
}
// TODO: Return FoundCommandDescription immediately
private CommandDescription getCommandWithSmallestDifference(CommandDescription base, List<String> parts) {
final String label = parts.get(0);
final int argumentCount = parts.size() - 1;
double minDifference = Double.POSITIVE_INFINITY;
CommandDescription closestCommand = null;
for (CommandDescription child : base.getChildren()) {
double argumentDifference = getArgumentCountDifference(child, argumentCount);
double labelDifference = getLabelDifference(child, label);
// Weigh argument difference less
double difference = labelDifference + argumentCount / 2;
if (difference < minDifference) {
minDifference = difference;
closestCommand = child;
}
}
return closestCommand;
}
private static boolean isSuitableArgumentCount(CommandDescription command, int argumentCount) {
int minArgs = CommandUtils.getMinNumberOfArguments(command);
int maxArgs = CommandUtils.getMaxNumberOfArguments(command);
return argumentCount >= minArgs && argumentCount <= maxArgs;
}
private static double getLabelDifference(CommandDescription command, String givenLabel) {
double minDifference = Double.POSITIVE_INFINITY;
for (String commandLabel : command.getLabels()) {
double difference = StringUtils.getDifference(commandLabel, givenLabel);
if (difference < minDifference) {
minDifference = difference;
}
}
return minDifference;
}
private static int getArgumentCountDifference(CommandDescription commandDescription, int givenArgumentsCount) {
return Math.min(
Math.abs(givenArgumentsCount - CommandUtils.getMinNumberOfArguments(commandDescription)),
Math.abs(givenArgumentsCount - CommandUtils.getMaxNumberOfArguments(commandDescription)));
}
// Is the given command a suitable match for the given parts? parts is for example [changepassword, newpw, newpw]
public CommandDescription returnSuitableChild(CommandDescription baseCommand, List<String> parts) {
if (CollectionUtils.isEmpty(parts)) {
return null;
}
final String label = parts.get(0).toLowerCase();
final int argumentCount = parts.size() - 1;
for (CommandDescription child : baseCommand.getChildren()) {
if (child.hasLabel(label) && isSuitableArgumentCount(child, argumentCount)) {
return child;
}
}
return null;
}
private CommandDescription getBaseCommand(String label) {
String baseLabel = label.toLowerCase();
for (CommandDescription command : baseCommands) {
if (command.hasLabel(baseLabel)) {
return command;
}
}
return null;
}
}