package envoy.client.data.commands; import java.util.*; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.stream.Collectors; import envoy.util.EnvoyLog; /** * This class stores all {@link SystemCommand}s used. *

* Project: envoy-client
* File: SystemCommandsMap.java
* Created: 17.07.2020
* * @author Leon Hofmeister * @since Envoy Client v0.2-beta */ public class SystemCommandsMap { private final HashMap systemCommands = new HashMap<>(); private final Pattern commandBounds = Pattern.compile("^[a-zA-Z0-9_:!\\(\\)\\?\\.\\,\\;\\-]+$"); private static final Logger logger = EnvoyLog.getLogger(SystemCommandsMap.class); private boolean commandExecuted = false; /** * Adds a command with according action and the number of arguments that should * be parsed if the command does not violate API constrictions to the map. * * @param command the string that must be inputted to execute the * given action * @param action the action that should be performed * @param numberOfArguments the amount of arguments that need to be parsed for * the underlying function * @see SystemCommandsMap#isValidKey(String) * @since Envoy Client v0.2-beta */ public void addCommand(String command, Function action, int numberOfArguments) { if (isValidKey(command)) systemCommands.put(command, new SystemCommand(action, numberOfArguments, "")); } /** * Adds a command with according action, the number of arguments that should be * parsed and a description of the system command, if the command does not * violate API constrictions, to the map. * * @param command the string that must be inputted to execute the * given action * @param action the action that should be performed * @param numberOfArguments the amount of arguments that need to be parsed for * the underlying function * @param description the description of this {@link SystemCommand} * @see SystemCommandsMap#isValidKey(String) * @since Envoy Client v0.2-beta */ public void addCommand(String command, Function action, int numberOfArguments, String description) { if (isValidKey(command)) systemCommands.put(command, new SystemCommand(action, numberOfArguments, description)); } /** * Adds a command with according action that does not depend on arguments, if * the command does not violate API constrictions, to the map. * * @param command the string that must be inputted to execute the given action * @param action the action that should be performed. To see why this Function * takes a {@code String[]}, see {@link SystemCommand} * @see SystemCommandsMap#isValidKey(String) * @since Envoy Client v0.2-beta */ public void addNoArgCommand(String command, Function action) { addCommand(command, action, 0); } /** * Adds a command with according action that does not depend on arguments and a * description of that action, if the command does not violate API * constrictions, to the map. * * @param command the string that must be inputted to execute the given * action * @param action the action that should be performed. To see why this * Function takes a {@code String[]}, see * {@link SystemCommand} * @param description the description of this {@link SystemCommand} * @see SystemCommandsMap#isValidKey(String) * @since Envoy Client v0.2-beta */ public void addNoArgCommand(String command, Function action, String description) { addCommand(command, action, 0); } /** * Convenience method that does the same as * {@link SystemCommandsMap#addCommand(String, Function, int)}. *

* Adds a command with according action and the number of arguments that should * be parsed if the command does not violate API constrictions to the map. * * @param command the string that must be inputted to execute the * given action * @param action the action that should be performed. To see why this * Function takes a {@code String[]}, see * {@link SystemCommand} * @param numberOfArguments the amount of arguments that need to be parsed for * the underlying function * @see SystemCommandsMap#isValidKey(String) * @since Envoy Client v0.2-beta */ public void add(String command, Function action, int numberOfArguments) { addCommand(command, action, numberOfArguments); } /** * 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 final boolean isValidKey(String command) { final boolean valid = commandBounds.matcher(command).matches(); if (!valid) EnvoyLog.getLogger(SystemCommandsMap.class) .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 " + commandBounds + "are allowed"); return valid; } /** * This method checks if the input String is a key in the map and returns the * wrapped System command if present. * Its intended usage is after a "/" has been detected in the input String. * 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 SystemCommandsMap systemCommands = new SystemCommandsMap();}
* {@code systemCommands.add("example", Function.identity, 1);}
* {@code ....}
* user input: {@code "/example xyz ..."}
* {@code systemCommands.checkPresent("example xyz ...")} * result: {@code SystemCommand[action=Function.identity, numberOfArguments=1]} * * @param input the input string given by the user, excluding the "/" * @return the wrapped system command, if present * @since Envoy Client v0.2-beta */ public Optional checkPresent(String input) { return Optional.ofNullable(systemCommands.get(getCommand(input))); } /** * Takes a 'raw' string (the whole input) and checks if a command is present * after a "/". If that is the case, it will be executed. *

* Only one system command can be present, afterwards checking will not be * continued. * * @param raw the raw input string * @since Envoy Client v0.2-beta */ public void checkForCommands(String raw) { checkForCommands(raw, 0); } /** * Takes a 'raw' string (the whole input) and checks from {@code fromIndex} on * if a command is present * after a "/". If that is the case, it will be executed. *

* Only one system command can be present, afterwards checking will not be * continued. * * @param raw the raw input string * @param fromIndex the index to start checking on * @since Envoy Client v0.2-beta */ public void checkForCommands(String raw, int fromIndex) { // The minimum length of a command is "/" + a letter, hence raw.length()-2 is // the highest index needed for (int i = fromIndex; i < raw.length() - 2; i++) // possibly a command was detected if (raw.charAt(i) == '/') { executeIfPresent(getCommand(raw, i)); // the command was executed successfully - no further checking needed if (commandExecuted) { commandExecuted = false; logger.log(Level.FINE, "executed system command " + getCommand(raw, i)); break; } } } /** * This method ensures that the "/" of a {@link SystemCommand} is stripped.
* It returns the command as (most likely) entered as key in the map starting * from {@code fromIndex}.
* It should only be called on strings that contain a "/" . * * @param raw the input * @param fromIndex the index from which to expect the system command - * regardless of whether the slash is still present at this * index * @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. */ public String getCommand(String raw, int fromIndex) { final var index = raw.indexOf(' '); return raw.substring(fromIndex + raw.charAt(0) == '/' ? 1 : 0, index < 1 ? raw.length() : index); } /** * This method ensures that the "/" of a {@link SystemCommand} is stripped.
* 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. * * @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. */ public String getCommand(String raw) { return getCommand(raw, 0); } /** * 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 SystemCommandsMap systemCommands = new SystemCommandsMap();}
* {@code Button button = new Button();}
* {@code systemCommands.add("example", (words)-> button.setText(words[0]), 1);}
* {@code ....}
* user input: {@code "/example xyz ..."}
* {@code systemCommands.executeIfPresent("example xyz ...")} * result: {@code button.getText()=="xyz"} * * @param input the input string given by the user, excluding the "/" * @since Envoy Client v0.2-beta */ public void executeIfPresent(String input) { checkPresent(input).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 remainingString = input.substring(input.indexOf(" ") + 1); // TODO: Current implementation will fail in certain cases, i.e. two spaces // behind each other (" "), not enough words, ... final var arguments = Arrays.copyOfRange(remainingString.split(" "), 0, systemCommand.getNumberOfArguments()); // Executing the function try { systemCommand.getAction().apply(arguments); commandExecuted = true; systemCommand.onCall(); } catch (final Exception e) { logger.log(Level.WARNING, "The system command " + getCommand(input) + " threw an exception: ", e); } }); } /** * 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 * @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, Function, Void> action) { requestRecommendations(input, 0, action); } /** * Retrieves the recommendations based on the current input entered.
* The word beginning at {@code fromIndex} 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 * @param fromIndex the index to start checking on * @param action the action that should be taken for the recommendations, if * any are present * @since Envoy Client v0.2-beta */ private void requestRecommendations(String input, int fromIndex, Function, Void> action) { final var partialCommand = getCommand(input, fromIndex); // Get the expected commands final var recommendations = recommendCommands(partialCommand); if (recommendations.isEmpty()) return; // Execute the given action else action.apply(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. * * @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)).collect(Collectors.toSet()); } /** * @return all {@link SystemCommand}s used with the underlying command as key * @since Envoy Client v0.2-beta */ public HashMap getSystemCommands() { return systemCommands; } }