2020-07-17 23:27:54 +02:00
|
|
|
package envoy.client.data.commands;
|
|
|
|
|
2020-07-18 11:25:41 +02:00
|
|
|
import java.util.*;
|
2020-09-21 18:45:01 +02:00
|
|
|
import java.util.function.Consumer;
|
|
|
|
import java.util.logging.*;
|
2020-07-23 09:23:29 +02:00
|
|
|
import java.util.regex.Pattern;
|
2020-07-18 11:25:41 +02:00
|
|
|
import java.util.stream.Collectors;
|
2020-07-17 23:27:54 +02:00
|
|
|
|
|
|
|
import envoy.util.EnvoyLog;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This class stores all {@link SystemCommand}s used.
|
|
|
|
*
|
|
|
|
* @author Leon Hofmeister
|
2020-07-23 15:36:23 +02:00
|
|
|
* @since Envoy Client v0.2-beta
|
2020-07-17 23:27:54 +02:00
|
|
|
*/
|
2020-09-23 17:03:32 +02:00
|
|
|
public final class SystemCommandMap {
|
2020-07-17 23:27:54 +02:00
|
|
|
|
2020-07-24 11:09:05 +02:00
|
|
|
private final Map<String, SystemCommand> systemCommands = new HashMap<>();
|
2020-07-17 23:27:54 +02:00
|
|
|
|
2020-07-24 11:09:05 +02:00
|
|
|
private final Pattern commandPattern = Pattern.compile("^[a-zA-Z0-9_:!\\(\\)\\?\\.\\,\\;\\-]+$");
|
2020-07-23 09:23:29 +02:00
|
|
|
|
2020-09-23 17:03:32 +02:00
|
|
|
private static final Logger logger = EnvoyLog.getLogger(SystemCommandMap.class);
|
2020-07-23 15:36:23 +02:00
|
|
|
|
2020-07-17 23:27:54 +02:00
|
|
|
/**
|
2020-07-23 18:51:20 +02:00
|
|
|
* Adds a new command to the map if the command name is valid.
|
2020-07-23 09:23:29 +02:00
|
|
|
*
|
2020-07-24 11:09:05 +02:00
|
|
|
* @param command the input string to execute the
|
|
|
|
* given action
|
|
|
|
* @param systemCommand the command to add - can be built using
|
|
|
|
* {@link SystemCommandBuilder}
|
2020-09-23 17:03:32 +02:00
|
|
|
* @see SystemCommandMap#isValidKey(String)
|
2020-07-23 15:36:23 +02:00
|
|
|
* @since Envoy Client v0.2-beta
|
2020-07-18 11:25:41 +02:00
|
|
|
*/
|
2020-08-01 21:40:20 +02:00
|
|
|
public void add(String command, SystemCommand systemCommand) {
|
|
|
|
if (isValidKey(command)) systemCommands.put(command.toLowerCase(), systemCommand);
|
|
|
|
}
|
2020-07-17 23:27:54 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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).
|
|
|
|
* <p>
|
|
|
|
* Usage example:<br>
|
2020-09-23 17:03:32 +02:00
|
|
|
* {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br>
|
2020-07-24 11:09:05 +02:00
|
|
|
* {@code Button button = new Button();}
|
|
|
|
* {@code systemCommands.add("example", text -> button.setText(text.get(0), 1);}<br>
|
2020-07-17 23:27:54 +02:00
|
|
|
* {@code ....}<br>
|
|
|
|
* user input: {@code "/example xyz ..."}<br>
|
2020-07-24 11:09:05 +02:00
|
|
|
* {@code systemCommands.get("example xyz ...")} or
|
|
|
|
* {@code systemCommands.get("/example xyz ...")}
|
|
|
|
* result: {@code Optional<SystemCommand>}
|
2020-07-17 23:27:54 +02:00
|
|
|
*
|
2020-07-24 11:09:05 +02:00
|
|
|
* @param input the input string given by the user
|
2020-07-17 23:27:54 +02:00
|
|
|
* @return the wrapped system command, if present
|
2020-07-23 15:36:23 +02:00
|
|
|
* @since Envoy Client v0.2-beta
|
2020-07-17 23:27:54 +02:00
|
|
|
*/
|
2020-08-01 21:40:20 +02:00
|
|
|
public Optional<SystemCommand> get(String input) { return Optional.ofNullable(systemCommands.get(getCommand(input.toLowerCase()))); }
|
2020-07-17 23:27:54 +02:00
|
|
|
|
2020-07-23 11:07:54 +02:00
|
|
|
/**
|
2020-07-24 11:09:05 +02:00
|
|
|
* This method ensures that the "/" of a {@link SystemCommand} is stripped.<br>
|
|
|
|
* It returns the command as (most likely) entered as key in the map for the
|
|
|
|
* first word of the text.<br>
|
|
|
|
* It should only be called on strings that contain a "/" at position 0/-1.
|
2020-07-23 11:07:54 +02:00
|
|
|
*
|
2020-07-24 11:09:05 +02:00
|
|
|
* @param raw the input
|
|
|
|
* @return the command as entered in the map
|
2020-07-23 15:36:23 +02:00
|
|
|
* @since Envoy Client v0.2-beta
|
2020-07-24 11:09:05 +02:00
|
|
|
* @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.
|
2020-07-23 11:07:54 +02:00
|
|
|
*/
|
2020-07-24 11:09:05 +02:00
|
|
|
public String getCommand(String raw) {
|
|
|
|
final var trimmed = raw.stripLeading();
|
|
|
|
final var index = trimmed.indexOf(' ');
|
|
|
|
return trimmed.substring(trimmed.charAt(0) == '/' ? 1 : 0, index < 1 ? trimmed.length() : index);
|
|
|
|
}
|
2020-07-23 11:07:54 +02:00
|
|
|
|
|
|
|
/**
|
2020-07-24 11:09:05 +02:00
|
|
|
* Examines whether a key can be put in the map and logs it with
|
|
|
|
* {@code Level.WARNING} if that key violates API constrictions.<br>
|
|
|
|
* (allowed chars are <b>a-zA-Z0-9_:!()?.,;-</b>)
|
2020-07-23 11:07:54 +02:00
|
|
|
* <p>
|
2020-07-24 11:09:05 +02:00
|
|
|
* 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.
|
2020-07-23 11:07:54 +02:00
|
|
|
*
|
2020-07-24 11:09:05 +02:00
|
|
|
* @param command the key to examine
|
|
|
|
* @return whether this key can be used in the map
|
2020-07-23 15:36:23 +02:00
|
|
|
* @since Envoy Client v0.2-beta
|
2020-07-23 11:07:54 +02:00
|
|
|
*/
|
2020-07-24 11:09:05 +02:00
|
|
|
public boolean isValidKey(String command) {
|
|
|
|
final boolean 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 "
|
|
|
|
+ commandPattern + "are allowed");
|
|
|
|
return valid;
|
2020-07-23 11:07:54 +02:00
|
|
|
}
|
|
|
|
|
2020-07-23 15:50:45 +02:00
|
|
|
/**
|
2020-07-24 11:09:05 +02:00
|
|
|
* 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.
|
|
|
|
* <p>
|
2020-07-23 15:50:45 +02:00
|
|
|
*
|
2020-07-24 11:09:05 +02:00
|
|
|
* @param raw the raw input string
|
|
|
|
* @return whether a command could be found
|
2020-07-23 15:50:45 +02:00
|
|
|
* @since Envoy Client v0.2-beta
|
|
|
|
*/
|
2020-07-24 11:09:05 +02:00
|
|
|
public boolean executeIfAnyPresent(String raw) {
|
|
|
|
// possibly a command was detected and could be executed
|
|
|
|
final var raw2 = raw.stripLeading();
|
|
|
|
final var commandFound = raw2.startsWith("/") ? executeIfPresent(raw2) : false;
|
|
|
|
// the command was executed successfully - no further checking needed
|
|
|
|
if (commandFound) logger.log(Level.FINE, "executed system command " + getCommand(raw2));
|
|
|
|
return commandFound;
|
2020-07-23 15:50:45 +02:00
|
|
|
}
|
|
|
|
|
2020-07-17 23:27:54 +02:00
|
|
|
/**
|
|
|
|
* 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).
|
|
|
|
* <p>
|
|
|
|
* Usage example:<br>
|
2020-09-23 17:03:32 +02:00
|
|
|
* {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br>
|
2020-07-23 11:07:54 +02:00
|
|
|
* {@code Button button = new Button();}<br>
|
2020-07-24 11:09:05 +02:00
|
|
|
* {@code systemCommands.add("example", (words)-> button.setText(words.get(0), 1);}<br>
|
2020-07-17 23:27:54 +02:00
|
|
|
* {@code ....}<br>
|
|
|
|
* user input: {@code "/example xyz ..."}<br>
|
|
|
|
* {@code systemCommands.executeIfPresent("example xyz ...")}
|
2020-07-23 11:07:54 +02:00
|
|
|
* result: {@code button.getText()=="xyz"}
|
2020-07-17 23:27:54 +02:00
|
|
|
*
|
2020-07-24 11:09:05 +02:00
|
|
|
* @param input the input string given by the user
|
|
|
|
* @return whether a command could be found
|
2020-07-23 15:36:23 +02:00
|
|
|
* @since Envoy Client v0.2-beta
|
2020-07-17 23:27:54 +02:00
|
|
|
*/
|
2020-07-24 11:09:05 +02:00
|
|
|
public boolean executeIfPresent(String input) {
|
|
|
|
final var command = getCommand(input);
|
|
|
|
final var value = get(command);
|
|
|
|
value.ifPresent(systemCommand -> {
|
2020-07-17 23:27:54 +02:00
|
|
|
// 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
|
2020-07-24 11:09:05 +02:00
|
|
|
final var arguments = extractArguments(input, systemCommand);
|
2020-07-17 23:27:54 +02:00
|
|
|
// Executing the function
|
|
|
|
try {
|
2020-07-24 11:09:05 +02:00
|
|
|
systemCommand.getAction().accept(arguments);
|
2020-07-23 16:37:28 +02:00
|
|
|
systemCommand.onCall();
|
2020-07-17 23:27:54 +02:00
|
|
|
} catch (final Exception e) {
|
2020-07-24 11:09:05 +02:00
|
|
|
logger.log(Level.WARNING, "The system command " + command + " threw an exception: ", e);
|
2020-07-17 23:27:54 +02:00
|
|
|
}
|
|
|
|
});
|
2020-07-24 11:09:05 +02:00
|
|
|
return value.isPresent();
|
2020-07-17 23:27:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-07-24 11:09:05 +02:00
|
|
|
* Supplies missing values with default values.
|
2020-07-18 11:25:41 +02:00
|
|
|
*
|
2020-07-24 11:09:05 +02:00
|
|
|
* @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
|
2020-07-23 15:36:23 +02:00
|
|
|
* @since Envoy Client v0.2-beta
|
2020-07-17 23:27:54 +02:00
|
|
|
*/
|
2020-07-24 11:09:05 +02:00
|
|
|
private List<String> 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;
|
|
|
|
}
|
2020-07-23 11:07:54 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieves the recommendations based on the current input entered.<br>
|
2020-07-24 11:09:05 +02:00
|
|
|
* The first word is used for the recommendations and
|
2020-07-23 11:07:54 +02:00
|
|
|
* 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>
|
|
|
|
*
|
2020-07-24 11:09:05 +02:00
|
|
|
* @param input the input string
|
|
|
|
* @param action the action that should be taken for the recommendations, if any
|
|
|
|
* are present
|
2020-07-23 15:36:23 +02:00
|
|
|
* @since Envoy Client v0.2-beta
|
2020-07-23 11:07:54 +02:00
|
|
|
*/
|
2020-09-21 18:45:01 +02:00
|
|
|
public void requestRecommendations(String input, Consumer<Set<String>> action) {
|
2020-07-24 11:09:05 +02:00
|
|
|
final var partialCommand = getCommand(input);
|
2020-07-18 11:25:41 +02:00
|
|
|
// Get the expected commands
|
|
|
|
final var recommendations = recommendCommands(partialCommand);
|
|
|
|
if (recommendations.isEmpty()) return;
|
|
|
|
// Execute the given action
|
2020-09-21 18:45:01 +02:00
|
|
|
else action.accept(recommendations);
|
2020-07-18 11:25:41 +02:00
|
|
|
}
|
2020-07-17 23:27:54 +02:00
|
|
|
|
|
|
|
/**
|
2020-07-18 11:25:41 +02:00
|
|
|
* Recommends commands based upon the currently entered input.<br>
|
|
|
|
* 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
|
2020-07-23 15:36:23 +02:00
|
|
|
* @since Envoy Client v0.2-beta
|
2020-07-17 23:27:54 +02:00
|
|
|
*/
|
2020-07-18 11:25:41 +02:00
|
|
|
private Set<String> recommendCommands(String partialCommand) {
|
|
|
|
// current implementation only looks if input is contained within a command,
|
|
|
|
// might be updated
|
2020-07-24 11:09:05 +02:00
|
|
|
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. <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 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<String> supplementDefaults(String[] textArguments, SystemCommand toEvaluate) {
|
|
|
|
final var defaults = toEvaluate.getDefaults();
|
|
|
|
final var numberOfArguments = toEvaluate.getNumberOfArguments();
|
|
|
|
final List<String> result = new ArrayList<>();
|
|
|
|
|
|
|
|
if (toEvaluate.getNumberOfArguments() > 0) for (int 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;
|
2020-07-17 23:27:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-07-18 11:25:41 +02:00
|
|
|
* @return all {@link SystemCommand}s used with the underlying command as key
|
2020-07-23 15:36:23 +02:00
|
|
|
* @since Envoy Client v0.2-beta
|
2020-07-17 23:27:54 +02:00
|
|
|
*/
|
2020-07-24 11:09:05 +02:00
|
|
|
public Map<String, SystemCommand> getSystemCommands() { return systemCommands; }
|
2020-07-17 23:27:54 +02:00
|
|
|
}
|