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; /** * 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 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. * * @param command the input string to execute the given action * @param systemCommand the command to add - can be built using {@link SystemCommandBuilder} * @see SystemCommandMap#isValidKey(String) * @since Envoy Client v0.2-beta */ public void add(String command, SystemCommand systemCommand) { if (isValidKey(command)) systemCommands.put(command.toLowerCase(), systemCommand); } /** * This method checks if the input String is a key in the map and returns the wrapped System * command if present. *

* Usage example:
* {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}
* {@code systemCommands.add("example", new SystemCommand(text -> {}, 1, null, ""));}
* {@code ....}
* user input: {@code "*example xyz ..."}
* {@code systemCommands.get("example xyz ...")} or * {@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 * @since Envoy Client v0.2-beta */ public Optional get(String input) { return Optional.ofNullable(systemCommands.get(getCommand(input.toLowerCase()))); } /** * 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.
* 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 activator is not a system command. Only exception: for recommendation * purposes. */ public String getCommand(String raw) { final var trimmed = raw.stripLeading(); // 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( 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_:!/()?.,;-) *

* 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 error that should only occur during * implementation and not in production. * * @param command the key to examine * @return whether this key can be used in the map * @since Envoy Client v0.2-beta */ public boolean isValidKey(String command) { final var valid = commandPattern.matcher(command).matches(); if (!valid) logger.log(Level.WARNING, "The command \"" + command + "\" 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 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 and successfully executed * @since Envoy Client v0.2-beta */ public boolean executeIfPresent(String raw) { // possibly a command was detected and could be executed final var raw2 = raw.stripLeading(); 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. *

* Usage example:
* {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}
* {@code Button button = new Button();}
* {@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 ...")} 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 and successfully executed * @since Envoy Client v0.2-beta */ 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); } catch (final NumberFormatException e) { logger.log(Level.INFO, 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 commandExecuted.get(); } /** * Supplies missing values with default values. * * @param input the input String * @param systemCommand the command that is expected * @return the list of arguments that can be used to parse the systemCommand * @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; final var arguments = supplementDefaults(originalArguments, systemCommand); return arguments; } /** * Recommends commands based upon the currently entered input.
* 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() .stream() .filter(command -> command.contains(partialCommand)) .sorted( (command1, command2) -> Integer.compare(systemCommands.get(command1).getRelevance(), systemCommands.get(command2).getRelevance())) .collect(Collectors.toSet()); } /** * Supplies the default values for arguments if none are present in the text for any argument. *
* * @param textArguments the arguments that were parsed from the text * @param toEvaluate the system command whose default values should be used * @return the final argument list * @since Envoy Client v0.2-beta * @apiNote this method will insert an empty String if the size of the list given to the * {@code SystemCommand} is smaller than its argument counter and no more text * arguments could be found. */ private List supplementDefaults(String[] textArguments, SystemCommand toEvaluate) { final var defaults = toEvaluate.getDefaults(); final var numberOfArguments = toEvaluate.getNumberOfArguments(); final List result = new ArrayList<>(); 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. result.add(!(textArg == null) && !textArg.isBlank() ? textArg : index < defaults.size() ? defaults.get(index) : ""); } return result; } /** * @return all {@link SystemCommand}s used with the underlying command as key * @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; } }