Offer customization of activator char in SystemCommandMap
Additionally cleaned up SystemCommandMap a lot. This commit will also be the foundation of Envoy CLI, as it enables no activator as well.
This commit is contained in:
		@@ -9,19 +9,57 @@ import java.util.stream.Collectors;
 | 
			
		||||
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 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);
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * The default char to be used as activator.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Value: '/'.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.3-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static final char defaultActivator = '/';
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * The Character to use if every String should be considered as a possible
 | 
			
		||||
	 * {@link SystemCommand}.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Value: null.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.3-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public static final Character noActivator = null;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Creates a new {@link SystemCommandMap} with the given activator.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @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 {@link SystemCommandMap} with default activator.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @since Envoy Client v0.3-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public SystemCommandMap() { activator = defaultActivator; }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Adds a new command to the map if the command name is valid.
 | 
			
		||||
	 *
 | 
			
		||||
@@ -39,19 +77,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).
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Usage example:<br>
 | 
			
		||||
	 * {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br>
 | 
			
		||||
	 * {@code Button button = new  Button();}
 | 
			
		||||
	 * {@code systemCommands.add("example", text -> button.setText(text.get(0), 1);}<br>
 | 
			
		||||
	 * {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}<br>
 | 
			
		||||
	 * {@code systemCommands.add("example", new SystemCommand(text -> {}, 1, null,
 | 
			
		||||
	 * ""));}<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 ...")}
 | 
			
		||||
	 * result: {@code Optional<SystemCommand>}
 | 
			
		||||
	 * {@code systemCommands.get("*example xyz ...")}
 | 
			
		||||
	 * result: {@code Optional<SystemCommand>.get() != null}
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param input the input string given by the user
 | 
			
		||||
	 * @return the wrapped system command, if present
 | 
			
		||||
@@ -60,33 +95,36 @@ public final class SystemCommandMap {
 | 
			
		||||
	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
 | 
			
		||||
	 * 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 wod 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.<br>
 | 
			
		||||
	 * (allowed chars are <b>a-zA-Z0-9_:!()?.,;-</b>)
 | 
			
		||||
	 * (allowed chars are <b>a-zA-Z0-9_:!/()?.,;-</b>)
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * 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 +138,85 @@ 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.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * 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
 | 
			
		||||
	 * @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.<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
 | 
			
		||||
	 * 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>
 | 
			
		||||
	 * {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br>
 | 
			
		||||
	 * {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}<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>
 | 
			
		||||
	 * user input: {@code "/example xyz ..."}<br>
 | 
			
		||||
	 * {@code systemCommands.executeIfPresent("example xyz ...")}
 | 
			
		||||
	 * user input: {@code "*example xyz ..."}<br>
 | 
			
		||||
	 * {@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
 | 
			
		||||
	 * @since Envoy Client v0.2-beta
 | 
			
		||||
	 */
 | 
			
		||||
	public boolean executeIfPresent(String input) {
 | 
			
		||||
	private boolean executeAvailableCommand(String input) {
 | 
			
		||||
		final var	command	= getCommand(input);
 | 
			
		||||
		final var	value	= get(command);
 | 
			
		||||
		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);
 | 
			
		||||
@@ -177,12 +241,15 @@ public final class SystemCommandMap {
 | 
			
		||||
	 * @since Envoy Client v0.2-beta
 | 
			
		||||
	 */
 | 
			
		||||
	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;
 | 
			
		||||
@@ -190,37 +257,17 @@ public final class SystemCommandMap {
 | 
			
		||||
		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>
 | 
			
		||||
	 * 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<String> recommendCommands(String partialCommand) {
 | 
			
		||||
 | 
			
		||||
		// current implementation only looks if input is contained within a command,
 | 
			
		||||
		// might be updated
 | 
			
		||||
		return systemCommands.keySet()
 | 
			
		||||
@@ -231,11 +278,8 @@ public final class SystemCommandMap {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 *
 | 
			
		||||
	 * 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
 | 
			
		||||
@@ -253,6 +297,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 +311,10 @@ public final class SystemCommandMap {
 | 
			
		||||
	 * @since Envoy Client v0.2-beta
 | 
			
		||||
	 */
 | 
			
		||||
	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; }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user