diff --git a/client/src/main/java/envoy/client/data/commands/SystemCommandMap.java b/client/src/main/java/envoy/client/data/commands/SystemCommandMap.java index ddb2d5e..831de0d 100644 --- a/client/src/main/java/envoy/client/data/commands/SystemCommandMap.java +++ b/client/src/main/java/envoy/client/data/commands/SystemCommandMap.java @@ -1,27 +1,53 @@ package envoy.client.data.commands; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.logging.*; import java.util.regex.Pattern; import java.util.stream.Collectors; +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; + 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 * @since Envoy Client v0.2-beta */ public final class SystemCommandMap { - private final Map systemCommands = new HashMap<>(); - - private final Pattern commandPattern = Pattern.compile("^[a-zA-Z0-9_:!\\(\\)\\?\\.\\,\\;\\-]+$"); + private final Character activator; + private final Map systemCommands = new HashMap<>(); + private final Pattern commandPattern = Pattern.compile("^[a-zA-Z0-9_:!/\\(\\)\\?\\.\\,\\;\\-]+$"); 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. * @@ -39,19 +65,16 @@ public final class SystemCommandMap { /** * This method checks if the input String is a key in the map and returns the * 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). *

* Usage example:
- * {@code SystemCommandMap systemCommands = new SystemCommandMap();}
- * {@code Button button = new Button();} - * {@code systemCommands.add("example", text -> button.setText(text.get(0), 1);}
+ * {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}
+ * {@code systemCommands.add("example", new SystemCommand(text -> {}, 1, null, + * ""));}
* {@code ....}
- * user input: {@code "/example xyz ..."}
+ * user input: {@code "*example xyz ..."}
* {@code systemCommands.get("example xyz ...")} or - * {@code systemCommands.get("/example xyz ...")} - * result: {@code Optional} + * {@code systemCommands.get("*example xyz ...")} + * result: {@code Optional.get() != null} * * @param input the input string given by the user * @return the wrapped system command, if present @@ -60,33 +83,36 @@ public final class SystemCommandMap { public Optional get(String input) { return Optional.ofNullable(systemCommands.get(getCommand(input.toLowerCase()))); } /** - * This method ensures that the "/" of a {@link SystemCommand} is stripped.
+ * This method ensures that the activator of a {@link SystemCommand} is + * stripped.
+ * 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 * first word of the text.
- * It should only be called on strings that contain a "/" at position 0/-1. + * Activators in the middle of the word will be disregarded. * * @param raw the input * @return the command as entered in the map * @since Envoy Client v0.2-beta * @apiNote this method will (most likely) not return anything useful if - * whatever is entered after the slash is not a system command. Only - * exception: for recommendation purposes. + * whatever is entered after the activator is not a system command. + * Only exception: for recommendation purposes. */ public String getCommand(String raw) { final var trimmed = raw.stripLeading(); - // Entering only a slash should not throw an error - if (trimmed.length() == 1 && trimmed.charAt(0) == '/') return ""; + // Entering only the activator should not throw an error + if (trimmed.length() == 1 && activator != null && activator.equals(trimmed.charAt(0))) return ""; else { 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 * {@code Level.WARNING} if that key violates API constrictions.
- * (allowed chars are a-zA-Z0-9_:!()?.,;-) + * (allowed chars are a-zA-Z0-9_:!/()?.,;-) *

* 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 @@ -100,59 +126,86 @@ public final class SystemCommandMap { final var valid = commandPattern.matcher(command).matches(); if (!valid) logger.log(Level.WARNING, "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"); return valid; } /** - * Takes a 'raw' string (the whole input) and checks if "/" is the first visible - * character and then checks if a command is present after that "/". If that is - * the case, it will be executed. - *

+ * Takes a 'raw' string (the whole input) and checks if the activator is the + * first visible character and then checks if a command is present after that + * activator. If that is the case, it will be executed. * * @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 */ - public boolean executeIfAnyPresent(String raw) { + public boolean executeIfPresent(String raw) { + // possibly a command was detected and could be executed 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 if (commandFound) logger.log(Level.FINE, "executed system command " + getCommand(raw2)); return commandFound; } + /** + * Retrieves the recommendations based on the current input entered.
+ * The first word is used for the recommendations and + * it does not matter if the activator is at its beginning or not.
+ * If recommendations are present, the given function will be executed on the + * recommendations.
+ * Otherwise nothing will be done.
+ * + * @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> 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 * 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). *

* Usage example:
- * {@code SystemCommandMap systemCommands = new SystemCommandMap();}
+ * {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}
* {@code Button button = new Button();}
- * {@code systemCommands.add("example", (words)-> button.setText(words.get(0), 1);}
+ * {@code systemCommands.add("example", new SystemCommand(text -> + * {button.setText(text.get(0))}, 1, null, + * ""));}
* {@code ....}
- * user input: {@code "/example xyz ..."}
- * {@code systemCommands.executeIfPresent("example xyz ...")} + * user input: {@code "*example xyz ..."}
+ * {@code systemCommands.executeIfPresent("example xyz ...")} or + * {@code systemCommands.executeIfPresent("*example xyz ...")} * result: {@code button.getText()=="xyz"} * * @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 */ - public boolean executeIfPresent(String input) { - final var command = getCommand(input); - final var value = get(command); + private boolean executeAvailableCommand(String input) { + final var command = getCommand(input); + final var value = get(command); + final var commandExecuted = new AtomicBoolean(value.isPresent()); value.ifPresent(systemCommand -> { + // 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 // persist final var arguments = extractArguments(input, systemCommand); + // Executing the function try { systemCommand.call(arguments); @@ -161,11 +214,23 @@ public final class SystemCommandMap { String.format( "System command %s could not be performed correctly because the user is a dumbass and could not write a parseable number.", 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) { 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 */ private List extractArguments(String input, SystemCommand systemCommand) { + // no more arguments follow after the command (e.g. text = "/DABR") final var indexOfSpace = input.indexOf(" "); if (indexOfSpace < 0) return supplementDefaults(new String[] {}, systemCommand); + // the arguments behind a system command final var remainingString = input.substring(indexOfSpace + 1); final var numberOfArguments = systemCommand.getNumberOfArguments(); + // splitting those arguments and supplying default values final var textArguments = remainingString.split(" ", -1); final var originalArguments = numberOfArguments >= 0 ? Arrays.copyOfRange(textArguments, 0, numberOfArguments) : textArguments; @@ -190,37 +258,17 @@ public final class SystemCommandMap { return arguments; } - /** - * Retrieves the recommendations based on the current input entered.
- * The first word is used for the recommendations and - * it does not matter if the "/" is at its beginning or not.
- * If none are present, nothing will be done.
- * Otherwise the given function will be executed on the recommendations.
- * - * @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> 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.
- * In the current implementation, all we check is whether a key contains this - * input. This might be updated later on. + * In the current implementation, all that gets checked is whether a key + * contains this input. This might be updated later on. * * @param partialCommand the partially entered command * @return a set of all commands that match this input * @since Envoy Client v0.2-beta */ private Set recommendCommands(String partialCommand) { + // current implementation only looks if input is contained within a command, // might be updated 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 * any argument.
- * 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 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++) { String textArg = null; if (index < textArguments.length) textArg = textArguments[index]; + // 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. // In the worst case, an empty String will be used. @@ -266,4 +312,10 @@ public final class SystemCommandMap { * @since Envoy Client v0.2-beta */ public Map 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; } } diff --git a/client/src/main/java/envoy/client/ui/chatscene/ChatSceneCommands.java b/client/src/main/java/envoy/client/ui/chatscene/ChatSceneCommands.java index 850d8bb..e2fd619 100644 --- a/client/src/main/java/envoy/client/ui/chatscene/ChatSceneCommands.java +++ b/client/src/main/java/envoy/client/ui/chatscene/ChatSceneCommands.java @@ -40,6 +40,9 @@ public final class ChatSceneCommands { public ChatSceneCommands(ListView messageList, ChatScene chatScene) { this.messageList = messageList; + // Error message initialization + builder.setAction(text -> { throw new RuntimeException(); }).setDescription("Shows an error message.").buildNoArg("error"); + // Do A Barrel roll initialization final var random = new Random(); builder.setAction(text -> chatScene.doABarrelRoll(Integer.parseInt(text.get(0)), Double.parseDouble(text.get(1)))) diff --git a/client/src/main/java/envoy/client/ui/controller/ChatScene.java b/client/src/main/java/envoy/client/ui/controller/ChatScene.java index a8efa6c..5a55646 100644 --- a/client/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/client/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -625,7 +625,7 @@ public final class ChatScene implements EventListener, Restorable { return; } final var text = messageTextArea.getText().strip(); - if (!commands.getChatSceneCommands().executeIfAnyPresent(text)) { + if (!commands.getChatSceneCommands().executeIfPresent(text)) { // Creating the message and its metadata final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) .setText(text);