Add Customizability to SystemCommandMap #84

Merged
delvh merged 2 commits from f/enhanced-system-commands into develop 2020-10-07 22:12:58 +02:00
3 changed files with 124 additions and 69 deletions

View File

@ -1,27 +1,53 @@
package envoy.client.data.commands; package envoy.client.data.commands;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.*; import java.util.logging.*;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
/** /**
* This class stores all {@link SystemCommand}s used. * Stores all {@link SystemCommand}s used.
* SystemCommands can be called using an activator char and the text that needs
* to be present behind the activator.
* Additionally offers the option to request recommendations for a partial input
* String.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public final class SystemCommandMap { public final class SystemCommandMap {
private final Map<String, SystemCommand> systemCommands = new HashMap<>(); private final Character activator;
private final Map<String, SystemCommand> systemCommands = new HashMap<>();
private final Pattern commandPattern = Pattern.compile("^[a-zA-Z0-9_:!\\(\\)\\?\\.\\,\\;\\-]+$"); private final Pattern commandPattern = Pattern.compile("^[a-zA-Z0-9_:!/\\(\\)\\?\\.\\,\\;\\-]+$");
private static final Logger logger = EnvoyLog.getLogger(SystemCommandMap.class); private static final Logger logger = EnvoyLog.getLogger(SystemCommandMap.class);
/**
* Creates a new {@code SystemCommandMap} with the given char as activator.
* If this Character is null, any text used as input will be treated as a system
* command.
*
* @param activator the char to use as activator for commands
* @since Envoy Client v0.3-beta
*/
public SystemCommandMap(Character activator) { this.activator = activator; }
/**
* Creates a new {@code SystemCommandMap} with '/' as activator.
*
* @since Envoy Client v0.3-beta
*/
public SystemCommandMap() { activator = '/'; }
/** /**
* Adds a new command to the map if the command name is valid. * Adds a new command to the map if the command name is valid.
* *
@ -39,19 +65,16 @@ public final class SystemCommandMap {
/** /**
* This method checks if the input String is a key in the map and returns the * This method checks if the input String is a key in the map and returns the
* wrapped System command if present. * wrapped System command if present.
* It will return an empty optional if the value after the slash is not a key in
* the map, which is a valid case (i.e. input="3/4" and "4" is not a key in the
* map).
* <p> * <p>
* Usage example:<br> * Usage example:<br>
* {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br> * {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}<br>
* {@code Button button = new Button();} * {@code systemCommands.add("example", new SystemCommand(text -> {}, 1, null,
* {@code systemCommands.add("example", text -> button.setText(text.get(0), 1);}<br> * ""));}<br>
* {@code ....}<br> * {@code ....}<br>
* user input: {@code "/example xyz ..."}<br> * user input: {@code "*example xyz ..."}<br>
* {@code systemCommands.get("example xyz ...")} or * {@code systemCommands.get("example xyz ...")} or
* {@code systemCommands.get("/example xyz ...")} * {@code systemCommands.get("*example xyz ...")}
* result: {@code Optional<SystemCommand>} * result: {@code Optional<SystemCommand>.get() != null}
* *
* @param input the input string given by the user * @param input the input string given by the user
* @return the wrapped system command, if present * @return the wrapped system command, if present
@ -60,33 +83,36 @@ public final class SystemCommandMap {
public Optional<SystemCommand> get(String input) { return Optional.ofNullable(systemCommands.get(getCommand(input.toLowerCase()))); } public Optional<SystemCommand> get(String input) { return Optional.ofNullable(systemCommands.get(getCommand(input.toLowerCase()))); }
/** /**
* This method ensures that the "/" of a {@link SystemCommand} is stripped.<br> * This method ensures that the activator of a {@link SystemCommand} is
* stripped.<br>
* It only checks the word beginning from the first non-blank position in the
* input.
* It returns the command as (most likely) entered as key in the map for the * It returns the command as (most likely) entered as key in the map for the
* first word of the text.<br> * first word of the text.<br>
* It should only be called on strings that contain a "/" at position 0/-1. * Activators in the middle of the word will be disregarded.
Outdated
Review

there is a typo

there is a typo
* *
* @param raw the input * @param raw the input
* @return the command as entered in the map * @return the command as entered in the map
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
* @apiNote this method will (most likely) not return anything useful if * @apiNote this method will (most likely) not return anything useful if
* whatever is entered after the slash is not a system command. Only * whatever is entered after the activator is not a system command.
* exception: for recommendation purposes. * Only exception: for recommendation purposes.
*/ */
public String getCommand(String raw) { public String getCommand(String raw) {
final var trimmed = raw.stripLeading(); final var trimmed = raw.stripLeading();
// Entering only a slash should not throw an error // Entering only the activator should not throw an error
if (trimmed.length() == 1 && trimmed.charAt(0) == '/') return ""; if (trimmed.length() == 1 && activator != null && activator.equals(trimmed.charAt(0))) return "";
else { else {
final var index = trimmed.indexOf(' '); final var index = trimmed.indexOf(' ');
return trimmed.substring(trimmed.charAt(0) == '/' ? 1 : 0, index < 1 ? trimmed.length() : index); return trimmed.substring(activator != null && activator.equals(trimmed.charAt(0)) ? 1 : 0, index < 1 ? trimmed.length() : index);
} }
} }
/** /**
* Examines whether a key can be put in the map and logs it with * Examines whether a key can be put in the map and logs it with
* {@code Level.WARNING} if that key violates API constrictions.<br> * {@code Level.WARNING} if that key violates API constrictions.<br>
* (allowed chars are <b>a-zA-Z0-9_:!()?.,;-</b>) * (allowed chars are <b>a-zA-Z0-9_:!/()?.,;-</b>)
* <p> * <p>
* The approach to not throw an exception was taken so that an ugly try-catch * The approach to not throw an exception was taken so that an ugly try-catch
* block for every addition to the system commands map could be avoided, an * block for every addition to the system commands map could be avoided, an
@ -100,59 +126,86 @@ public final class SystemCommandMap {
final var valid = commandPattern.matcher(command).matches(); final var valid = commandPattern.matcher(command).matches();
if (!valid) logger.log(Level.WARNING, if (!valid) logger.log(Level.WARNING,
"The command \"" + command "The command \"" + command
+ "\" is not valid. As it will cause problems in execution, it will not be entered into the map. Only the characters " + "\" is not valid. As it might cause problems when executed, it will not be entered into the map. Only the characters "
+ commandPattern + "are allowed"); + commandPattern + "are allowed");
return valid; return valid;
} }
/** /**
* Takes a 'raw' string (the whole input) and checks if "/" is the first visible * Takes a 'raw' string (the whole input) and checks if the activator is the
* character and then checks if a command is present after that "/". If that is * first visible character and then checks if a command is present after that
* the case, it will be executed. * activator. If that is the case, it will be executed.
* <p>
* *
* @param raw the raw input string * @param raw the raw input string
* @return whether a command could be found * @return whether a command could be found and successfully executed
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public boolean executeIfAnyPresent(String raw) { public boolean executeIfPresent(String raw) {
// possibly a command was detected and could be executed // possibly a command was detected and could be executed
final var raw2 = raw.stripLeading(); final var raw2 = raw.stripLeading();
final var commandFound = raw2.startsWith("/") ? executeIfPresent(raw2) : false; final var commandFound = activator == null || raw2.startsWith(activator.toString()) ? executeAvailableCommand(raw2) : false;
// the command was executed successfully - no further checking needed // the command was executed successfully - no further checking needed
if (commandFound) logger.log(Level.FINE, "executed system command " + getCommand(raw2)); if (commandFound) logger.log(Level.FINE, "executed system command " + getCommand(raw2));
return commandFound; return commandFound;
} }
/**
* Retrieves the recommendations based on the current input entered.<br>
* The first word is used for the recommendations and
* it does not matter if the activator is at its beginning or not.<br>
* If recommendations are present, the given function will be executed on the
* recommendations.<br>
* Otherwise nothing will be done.<br>
*
* @param input the input string
* @param action the action that should be taken for the recommendations, if any
* are present
* @since Envoy Client v0.2-beta
*/
public void requestRecommendations(String input, Consumer<Set<String>> action) {
final var partialCommand = getCommand(input);
// Get the expected commands
final var recommendations = recommendCommands(partialCommand);
if (recommendations.isEmpty()) return;
// Execute the given action
else action.accept(recommendations);
}
/** /**
* This method checks if the input String is a key in the map and executes the * This method checks if the input String is a key in the map and executes the
* wrapped System command if present. * wrapped System command if present.
* Its intended usage is after a "/" has been detected in the input String.
* It will do nothing if the value after the slash is not a key in
* the map, which is a valid case (i.e. input="3/4" and "4" is not a key in the
* map).
* <p> * <p>
* Usage example:<br> * Usage example:<br>
* {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br> * {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}<br>
* {@code Button button = new Button();}<br> * {@code Button button = new Button();}<br>
* {@code systemCommands.add("example", (words)-> button.setText(words.get(0), 1);}<br> * {@code systemCommands.add("example", new SystemCommand(text ->
* {button.setText(text.get(0))}, 1, null,
* ""));}<br>
* {@code ....}<br> * {@code ....}<br>
* user input: {@code "/example xyz ..."}<br> * user input: {@code "*example xyz ..."}<br>
* {@code systemCommands.executeIfPresent("example xyz ...")} * {@code systemCommands.executeIfPresent("example xyz ...")} or
* {@code systemCommands.executeIfPresent("*example xyz ...")}
* result: {@code button.getText()=="xyz"} * result: {@code button.getText()=="xyz"}
* *
* @param input the input string given by the user * @param input the input string given by the user
* @return whether a command could be found * @return whether a command could be found and successfully executed
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public boolean executeIfPresent(String input) { private boolean executeAvailableCommand(String input) {
final var command = getCommand(input); final var command = getCommand(input);
final var value = get(command); final var value = get(command);
final var commandExecuted = new AtomicBoolean(value.isPresent());
value.ifPresent(systemCommand -> { value.ifPresent(systemCommand -> {
// Splitting the String so that the leading command including the first " " is // Splitting the String so that the leading command including the first " " is
// removed and only as many following words as allowed by the system command // removed and only as many following words as allowed by the system command
// persist // persist
final var arguments = extractArguments(input, systemCommand); final var arguments = extractArguments(input, systemCommand);
// Executing the function // Executing the function
try { try {
systemCommand.call(arguments); systemCommand.call(arguments);
@ -161,11 +214,23 @@ public final class SystemCommandMap {
String.format( String.format(
"System command %s could not be performed correctly because the user is a dumbass and could not write a parseable number.", "System command %s could not be performed correctly because the user is a dumbass and could not write a parseable number.",
command)); command));
Platform.runLater(() -> {
final var alert = new Alert(AlertType.ERROR);
alert.setContentText("Please enter a readable number as argument.");
alert.showAndWait();
});
commandExecuted.set(false);
} catch (final Exception e) { } catch (final Exception e) {
logger.log(Level.WARNING, "System command " + command + " threw an exception: ", e); logger.log(Level.WARNING, "System command " + command + " threw an exception: ", e);
Platform.runLater(() -> {
final var alert = new Alert(AlertType.ERROR);
alert.setContentText("Could not execute system command: Internal error. Please insult the responsible programmer.");
alert.showAndWait();
});
commandExecuted.set(false);
} }
}); });
return value.isPresent(); return commandExecuted.get();
} }
/** /**
@ -177,12 +242,15 @@ public final class SystemCommandMap {
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
private List<String> extractArguments(String input, SystemCommand systemCommand) { private List<String> extractArguments(String input, SystemCommand systemCommand) {
// no more arguments follow after the command (e.g. text = "/DABR") // no more arguments follow after the command (e.g. text = "/DABR")
final var indexOfSpace = input.indexOf(" "); final var indexOfSpace = input.indexOf(" ");
if (indexOfSpace < 0) return supplementDefaults(new String[] {}, systemCommand); if (indexOfSpace < 0) return supplementDefaults(new String[] {}, systemCommand);
// the arguments behind a system command // the arguments behind a system command
final var remainingString = input.substring(indexOfSpace + 1); final var remainingString = input.substring(indexOfSpace + 1);
final var numberOfArguments = systemCommand.getNumberOfArguments(); final var numberOfArguments = systemCommand.getNumberOfArguments();
// splitting those arguments and supplying default values // splitting those arguments and supplying default values
final var textArguments = remainingString.split(" ", -1); final var textArguments = remainingString.split(" ", -1);
final var originalArguments = numberOfArguments >= 0 ? Arrays.copyOfRange(textArguments, 0, numberOfArguments) : textArguments; final var originalArguments = numberOfArguments >= 0 ? Arrays.copyOfRange(textArguments, 0, numberOfArguments) : textArguments;
@ -190,37 +258,17 @@ public final class SystemCommandMap {
return arguments; return arguments;
} }
/**
* Retrieves the recommendations based on the current input entered.<br>
* The first word is used for the recommendations and
* it does not matter if the "/" is at its beginning or not.<br>
* If none are present, nothing will be done.<br>
* Otherwise the given function will be executed on the recommendations.<br>
*
* @param input the input string
* @param action the action that should be taken for the recommendations, if any
* are present
* @since Envoy Client v0.2-beta
*/
public void requestRecommendations(String input, Consumer<Set<String>> action) {
final var partialCommand = getCommand(input);
// Get the expected commands
final var recommendations = recommendCommands(partialCommand);
if (recommendations.isEmpty()) return;
// Execute the given action
else action.accept(recommendations);
}
/** /**
* Recommends commands based upon the currently entered input.<br> * Recommends commands based upon the currently entered input.<br>
* In the current implementation, all we check is whether a key contains this * In the current implementation, all that gets checked is whether a key
* input. This might be updated later on. * contains this input. This might be updated later on.
* *
* @param partialCommand the partially entered command * @param partialCommand the partially entered command
* @return a set of all commands that match this input * @return a set of all commands that match this input
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
private Set<String> recommendCommands(String partialCommand) { private Set<String> recommendCommands(String partialCommand) {
// current implementation only looks if input is contained within a command, // current implementation only looks if input is contained within a command,
// might be updated // might be updated
return systemCommands.keySet() return systemCommands.keySet()
@ -231,11 +279,8 @@ public final class SystemCommandMap {
} }
/** /**
*
* Supplies the default values for arguments if none are present in the text for * Supplies the default values for arguments if none are present in the text for
* any argument. <br> * any argument. <br>
* Will only work for {@code SystemCommand}s whose argument counter is bigger
* than 1.
* *
* @param textArguments the arguments that were parsed from the text * @param textArguments the arguments that were parsed from the text
* @param toEvaluate the system command whose default values should be used * @param toEvaluate the system command whose default values should be used
@ -253,6 +298,7 @@ public final class SystemCommandMap {
if (toEvaluate.getNumberOfArguments() > 0) for (var index = 0; index < numberOfArguments; index++) { if (toEvaluate.getNumberOfArguments() > 0) for (var index = 0; index < numberOfArguments; index++) {
String textArg = null; String textArg = null;
if (index < textArguments.length) textArg = textArguments[index]; if (index < textArguments.length) textArg = textArguments[index];
// Set the argument at position index to the current argument of the text, if it // Set the argument at position index to the current argument of the text, if it
// is present. Otherwise the default for that argument will be taken if present. // is present. Otherwise the default for that argument will be taken if present.
// In the worst case, an empty String will be used. // In the worst case, an empty String will be used.
@ -266,4 +312,10 @@ public final class SystemCommandMap {
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public Map<String, SystemCommand> getSystemCommands() { return systemCommands; } public Map<String, SystemCommand> getSystemCommands() { return systemCommands; }
/**
* @return the activator of any command in this map. Can be null.
* @since Envoy Client v0.3-beta
*/
public Character getActivator() { return activator; }
} }

View File

@ -40,6 +40,9 @@ public final class ChatSceneCommands {
public ChatSceneCommands(ListView<Message> messageList, ChatScene chatScene) { public ChatSceneCommands(ListView<Message> messageList, ChatScene chatScene) {
this.messageList = messageList; this.messageList = messageList;
// Error message initialization
builder.setAction(text -> { throw new RuntimeException(); }).setDescription("Shows an error message.").buildNoArg("error");
// Do A Barrel roll initialization // Do A Barrel roll initialization
final var random = new Random(); final var random = new Random();
builder.setAction(text -> chatScene.doABarrelRoll(Integer.parseInt(text.get(0)), Double.parseDouble(text.get(1)))) builder.setAction(text -> chatScene.doABarrelRoll(Integer.parseInt(text.get(0)), Double.parseDouble(text.get(1))))

View File

@ -625,7 +625,7 @@ public final class ChatScene implements EventListener, Restorable {
return; return;
} }
final var text = messageTextArea.getText().strip(); final var text = messageTextArea.getText().strip();
if (!commands.getChatSceneCommands().executeIfAnyPresent(text)) { if (!commands.getChatSceneCommands().executeIfPresent(text)) {
// Creating the message and its metadata // Creating the message and its metadata
final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
.setText(text); .setText(text);