From 40eaa180c65625cde8abd1e208995500bf6a2302 Mon Sep 17 00:00:00 2001 From: yoyosource Date: Thu, 21 Apr 2022 22:43:14 +0200 Subject: [PATCH] Add CommandFramework (needs Message System for completion)? --- .../command/AbstractGuardChecker.java | 9 + .../steamwar/command/AbstractSWCommand.java | 304 ++++++++++++++++++ .../steamwar/command/AbstractTypeMapper.java | 12 + .../command/CommandFrameworkException.java | 76 +++++ .../command/CommandNoHelpException.java | 25 ++ .../command/CommandParseException.java | 26 ++ src/de/steamwar/command/CommandPart.java | 196 +++++++++++ src/de/steamwar/command/GuardCheckType.java | 26 ++ src/de/steamwar/command/GuardResult.java | 26 ++ src/de/steamwar/command/SWCommandUtils.java | 241 ++++++++++++++ src/de/steamwar/command/SubCommand.java | 99 ++++++ 11 files changed, 1040 insertions(+) create mode 100644 src/de/steamwar/command/AbstractGuardChecker.java create mode 100644 src/de/steamwar/command/AbstractSWCommand.java create mode 100644 src/de/steamwar/command/AbstractTypeMapper.java create mode 100644 src/de/steamwar/command/CommandFrameworkException.java create mode 100644 src/de/steamwar/command/CommandNoHelpException.java create mode 100644 src/de/steamwar/command/CommandParseException.java create mode 100644 src/de/steamwar/command/CommandPart.java create mode 100644 src/de/steamwar/command/GuardCheckType.java create mode 100644 src/de/steamwar/command/GuardResult.java create mode 100644 src/de/steamwar/command/SWCommandUtils.java create mode 100644 src/de/steamwar/command/SubCommand.java diff --git a/src/de/steamwar/command/AbstractGuardChecker.java b/src/de/steamwar/command/AbstractGuardChecker.java new file mode 100644 index 0000000..8d1a269 --- /dev/null +++ b/src/de/steamwar/command/AbstractGuardChecker.java @@ -0,0 +1,9 @@ +package de.steamwar.command; + +@FunctionalInterface +public interface AbstractGuardChecker { + /** + * While guarding the first parameter of the command the parameter s of this method is {@code null} + */ + GuardResult guard(T t, GuardCheckType guardCheckType, String[] previousArguments, String s); +} diff --git a/src/de/steamwar/command/AbstractSWCommand.java b/src/de/steamwar/command/AbstractSWCommand.java new file mode 100644 index 0000000..b13c745 --- /dev/null +++ b/src/de/steamwar/command/AbstractSWCommand.java @@ -0,0 +1,304 @@ +package de.steamwar.command; + +import java.lang.annotation.*; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.IntPredicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public abstract class AbstractSWCommand { + + private Class clazz; // This is used in createMappings() + + private boolean initialized = false; + protected final List> commandList = new ArrayList<>(); + protected final List> commandHelpList = new ArrayList<>(); + + private final Map> localTypeMapper = new HashMap<>(); + private final Map> localGuardChecker = new HashMap<>(); + + protected AbstractSWCommand(Class clazz, String command) { + this(clazz, command, new String[0]); + } + + protected AbstractSWCommand(Class clazz, String command, String[] aliases) { + this.clazz = clazz; + createAndSafeCommand(command, aliases); + unregister(); + register(); + } + + protected abstract void createAndSafeCommand(String command, String[] aliases); + + public abstract void unregister(); + + public abstract void register(); + + protected void commandSystemError(T sender, CommandFrameworkException e) { + e.printStackTrace(); + } + + protected void commandSystemWarning(Supplier message) { + System.out.println(message.get()); + } + + protected final void execute(T sender, String alias, String[] args) { + initialize(); + try { + if (!commandList.stream().anyMatch(s -> s.invoke(sender, alias, args))) { + commandHelpList.stream().anyMatch(s -> s.invoke(sender, alias, args)); + } + } catch (CommandNoHelpException e) { + // Ignored + } catch (CommandFrameworkException e) { + commandSystemError(sender, e); + throw e; + } + } + + protected final List tabComplete(T sender, String alias, String[] args) throws IllegalArgumentException { + initialize(); + String string = args[args.length - 1].toLowerCase(); + return commandList.stream() + .filter(s -> !s.noTabComplete) + .map(s -> s.tabComplete(sender, args)) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(s -> !s.isEmpty()) + .filter(s -> s.toLowerCase().startsWith(string)) + .collect(Collectors.toList()); + } + + private void initialize() { + if (initialized) return; + createMapping(); + } + + private synchronized void createMapping() { + List methods = methods(); + for (Method method : methods) { + addMapper(Mapper.class, method, i -> i == 0, false, AbstractTypeMapper.class, (anno, typeMapper) -> { + if (anno.local()) { + localTypeMapper.putIfAbsent(anno.value(), (AbstractTypeMapper) typeMapper); + } else { + SWCommandUtils.getMAPPER_FUNCTIONS().putIfAbsent(anno.value(), typeMapper); + } + }); + addMapper(ClassMapper.class, method, i -> i == 0, false, AbstractTypeMapper.class, (anno, typeMapper) -> { + if (anno.local()) { + localTypeMapper.putIfAbsent(anno.value().getTypeName(), (AbstractTypeMapper) typeMapper); + } else { + SWCommandUtils.getMAPPER_FUNCTIONS().putIfAbsent(anno.value().getTypeName(), typeMapper); + } + }); + addGuard(Guard.class, method, i -> i == 0, false, AbstractGuardChecker.class, (anno, guardChecker) -> { + if (anno.local()) { + localGuardChecker.putIfAbsent(anno.value(), (AbstractGuardChecker) guardChecker); + } else { + SWCommandUtils.getGUARD_FUNCTIONS().putIfAbsent(anno.value(), guardChecker); + } + }); + addGuard(ClassGuard.class, method, i -> i == 0, false, AbstractGuardChecker.class, (anno, guardChecker) -> { + if (anno.local()) { + localGuardChecker.putIfAbsent(anno.value().getTypeName(), (AbstractGuardChecker) guardChecker); + } else { + SWCommandUtils.getGUARD_FUNCTIONS().putIfAbsent(anno.value().getTypeName(), guardChecker); + } + }); + + add(Register.class, method, i -> i > 0, true, null, (anno, parameters) -> { + if (!anno.help()) return; + if (parameters.length != 2) { + commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking parameters or has too many"); + } + if (!parameters[parameters.length - 1].isVarArgs()) { + commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking the varArgs parameters as last Argument"); + } + if (parameters[parameters.length - 1].getType().getComponentType() != String.class) { + commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking the varArgs parameters of type '" + String.class.getTypeName() + "' as last Argument"); + return; + } + commandHelpList.add(new SubCommand(this, method, anno.value(), new HashMap<>(), localGuardChecker, true, null, anno.noTabComplete())); + }); + } + for (Method method : methods) { + add(Register.class, method, i -> i > 0, true, null, (anno, parameters) -> { + if (anno.help()) return; + for (int i = 1; i < parameters.length; i++) { + Parameter parameter = parameters[i]; + Class clazz = parameter.getType(); + if (parameter.isVarArgs() && i == parameters.length - 1) { + clazz = parameter.getType().getComponentType(); + } + Mapper mapper = parameter.getAnnotation(Mapper.class); + if (clazz.isEnum() && mapper == null && !SWCommandUtils.getMAPPER_FUNCTIONS().containsKey(clazz.getTypeName())) { + continue; + } + String name = mapper != null ? mapper.value() : clazz.getTypeName(); + if (!SWCommandUtils.getMAPPER_FUNCTIONS().containsKey(name) && !localTypeMapper.containsKey(name)) { + commandSystemWarning(() -> "The parameter '" + parameter.toString() + "' is using an unsupported Mapper of type '" + name + "'"); + return; + } + } + commandList.add(new SubCommand(this, method, anno.value(), localTypeMapper, localGuardChecker, false, anno.description(), anno.noTabComplete())); + }); + + this.commandList.sort((o1, o2) -> { + int compare = Integer.compare(-o1.subCommand.length, -o2.subCommand.length); + if (compare != 0) { + return compare; + } else { + return Integer.compare(o1.comparableValue, o2.comparableValue); + } + }); + commandHelpList.sort((o1, o2) -> { + int compare = Integer.compare(-o1.subCommand.length, -o2.subCommand.length); + if (compare != 0) { + return compare; + } else { + return Integer.compare(o1.method.getDeclaringClass() == AbstractSWCommand.class ? 1 : 0, + o2.method.getDeclaringClass() == AbstractSWCommand.class ? 1 : 0); + } + }); + } + initialized = true; + } + + private void add(Class annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class returnType, BiConsumer consumer) { + T[] anno = SWCommandUtils.getAnnotation(method, annotation); + if (anno == null || anno.length == 0) return; + + Parameter[] parameters = method.getParameters(); + if (!parameterTester.test(parameters.length)) { + commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking parameters or has too many"); + return; + } + if (firstParameter && !clazz.isAssignableFrom(parameters[0].getType())) { + commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking the first parameter of type '" + clazz.getTypeName() + "'"); + return; + } + if (returnType != null && method.getReturnType() != returnType) { + commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking the desired return type '" + returnType.getTypeName() + "'"); + return; + } + Arrays.stream(anno).forEach(t -> consumer.accept(t, parameters)); + } + + private void addMapper(Class annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class returnType, BiConsumer> consumer) { + add(annotation, method, parameterTester, firstParameter, returnType, (anno, parameters) -> { + try { + method.setAccessible(true); + consumer.accept(anno, (AbstractTypeMapper) method.invoke(this)); + } catch (Exception e) { + throw new SecurityException(e.getMessage(), e); + } + }); + } + + private void addGuard(Class annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class returnType, BiConsumer> consumer) { + add(annotation, method, parameterTester, firstParameter, returnType, (anno, parameters) -> { + try { + method.setAccessible(true); + consumer.accept(anno, (AbstractGuardChecker) method.invoke(this)); + } catch (Exception e) { + throw new SecurityException(e.getMessage(), e); + } + }); + } + + // TODO: Implement this when Message System is ready + /* + public void addDefaultHelpMessage(String message) { + defaultHelpMessages.add(message); + } + */ + + private List methods() { + List methods = new ArrayList<>(); + Class current = getClass(); + while (current.getSuperclass() != AbstractSWCommand.class) { + methods.addAll(Arrays.asList(current.getDeclaredMethods())); + current = current.getSuperclass(); + } + return methods; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD}) + @Repeatable(Register.Registeres.class) + protected @interface Register { + String[] value() default {}; + + boolean help() default false; + + String[] description() default {}; + + boolean noTabComplete() default false; + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD}) + @interface Registeres { + Register[] value(); + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER, ElementType.METHOD}) + protected @interface Mapper { + String value(); + + boolean local() default false; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD}) + protected @interface ClassMapper { + Class value(); + + boolean local() default false; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER, ElementType.METHOD}) + protected @interface Guard { + String value() default ""; + + boolean local() default false; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD}) + protected @interface ClassGuard { + Class value(); + + boolean local() default false; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER}) + protected @interface StaticValue { + String[] value(); + + /** + * This is the short form for 'allowImplicitSwitchExpressions' + * and can be set to true if you want to allow int as well as boolean as annotated parameter types. + * The value array needs to be at least 2 long for this flag to be considered. + * While using an int, the value will represent the index into the value array. + * While using a boolean, the value array must only be 2 long and the value will be {@code false} + * for the first index and {@code true} for the second index. + */ + boolean allowISE() default false; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER}) + protected @interface OptionalValue { + /** + * Will pe parsed against the TypeMapper specified by the parameter or annotation. + */ + String value(); + } +} diff --git a/src/de/steamwar/command/AbstractTypeMapper.java b/src/de/steamwar/command/AbstractTypeMapper.java new file mode 100644 index 0000000..e5735fe --- /dev/null +++ b/src/de/steamwar/command/AbstractTypeMapper.java @@ -0,0 +1,12 @@ +package de.steamwar.command; + +import java.util.List; + +public interface AbstractTypeMapper { + /** + * The CommandSender can be null! + */ + T map(K sender, String[] previousArguments, String s); + + List tabCompletes(K sender, String[] previousArguments, String s); +} diff --git a/src/de/steamwar/command/CommandFrameworkException.java b/src/de/steamwar/command/CommandFrameworkException.java new file mode 100644 index 0000000..74db6d3 --- /dev/null +++ b/src/de/steamwar/command/CommandFrameworkException.java @@ -0,0 +1,76 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2020 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.command; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.lang.reflect.InvocationTargetException; + +public class CommandFrameworkException extends RuntimeException { + + private InvocationTargetException invocationTargetException; + private String alias; + private String[] args; + + private String message; + + CommandFrameworkException(InvocationTargetException invocationTargetException, String alias, String[] args) { + super(invocationTargetException); + this.invocationTargetException = invocationTargetException; + this.alias = alias; + this.args = args; + } + + public synchronized String getBuildStackTrace() { + if (message != null) { + return message; + } + StackTraceElement[] stackTraceElements = invocationTargetException.getCause().getStackTrace(); + StringBuilder st = new StringBuilder(); + st.append(invocationTargetException.getCause().getClass().getTypeName()); + if (invocationTargetException.getCause().getMessage() != null) { + st.append(": ").append(invocationTargetException.getCause().getMessage()); + } + st.append("\n"); + if (alias != null && !alias.isEmpty()) { + st.append("Performed command: ").append(alias).append(" ").append(String.join(" ", args)).append("\n"); + } + for (int i = 0; i < stackTraceElements.length - invocationTargetException.getStackTrace().length; i++) { + st.append("\tat ").append(stackTraceElements[i].toString()).append("\n"); + } + message = st.toString(); + return message; + } + + @Override + public void printStackTrace() { + printStackTrace(System.err); + } + + @Override + public void printStackTrace(PrintStream s) { + s.print(getBuildStackTrace()); + } + + @Override + public void printStackTrace(PrintWriter s) { + s.print(getBuildStackTrace()); + } +} diff --git a/src/de/steamwar/command/CommandNoHelpException.java b/src/de/steamwar/command/CommandNoHelpException.java new file mode 100644 index 0000000..e3d476a --- /dev/null +++ b/src/de/steamwar/command/CommandNoHelpException.java @@ -0,0 +1,25 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2020 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.command; + +class CommandNoHelpException extends RuntimeException { + + CommandNoHelpException() {} +} diff --git a/src/de/steamwar/command/CommandParseException.java b/src/de/steamwar/command/CommandParseException.java new file mode 100644 index 0000000..3d81ea6 --- /dev/null +++ b/src/de/steamwar/command/CommandParseException.java @@ -0,0 +1,26 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2020 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.command; + +public class CommandParseException extends RuntimeException { + + public CommandParseException() { + } +} diff --git a/src/de/steamwar/command/CommandPart.java b/src/de/steamwar/command/CommandPart.java new file mode 100644 index 0000000..6a8ff4b --- /dev/null +++ b/src/de/steamwar/command/CommandPart.java @@ -0,0 +1,196 @@ +package de.steamwar.command; + +import lombok.AllArgsConstructor; +import lombok.Setter; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.List; + +class CommandPart { + + private static final String[] EMPTY_ARRAY = new String[0]; + + @AllArgsConstructor + private static class CheckArgumentResult { + private final boolean success; + private final Object value; + } + + private AbstractTypeMapper typeMapper; + private AbstractGuardChecker guardChecker; + private Class varArgType; + private String optional; + private GuardCheckType guardCheckType; + + private CommandPart next = null; + + @Setter + private boolean ignoreAsArgument = false; + + public CommandPart(AbstractTypeMapper typeMapper, AbstractGuardChecker guardChecker, Class varArgType, String optional, GuardCheckType guardCheckType) { + this.typeMapper = typeMapper; + this.guardChecker = guardChecker; + this.varArgType = varArgType; + this.optional = optional; + this.guardCheckType = guardCheckType; + + validatePart(); + } + + public void setNext(CommandPart next) { + if (varArgType != null) { + throw new IllegalArgumentException("There can't be a next part if this is a vararg part!"); + } + this.next = next; + } + + private void validatePart() { + if (guardCheckType == GuardCheckType.TAB_COMPLETE) { + throw new IllegalArgumentException("Tab complete is not allowed as a guard check type!"); + } + if (optional != null && varArgType != null) { + throw new IllegalArgumentException("A vararg part can't have an optional part!"); + } + + if (optional != null) { + try { + typeMapper.map(null, EMPTY_ARRAY, optional); + } catch (Exception e) { + throw new IllegalArgumentException("The optional part is not valid!"); + } + } + } + + public void generateArgumentArray(List current, T sender, String[] args, int startIndex) { + if (varArgType != null) { + Object array = Array.newInstance(varArgType, args.length - startIndex); + for (int i = startIndex; i < args.length; i++) { + CheckArgumentResult validArgument = checkArgument(null, sender, args, i); + if (!validArgument.success) { + throw new CommandParseException(); + } + Array.set(array, i - startIndex, validArgument.value); + } + current.add(array); + return; + } + + CheckArgumentResult validArgument = checkArgument(null, sender, args, startIndex); + if (!validArgument.success && optional == null) { + throw new CommandParseException(); + } + if (!validArgument.success) { + if (!ignoreAsArgument) { + current.add(typeMapper.map(sender, EMPTY_ARRAY, optional)); + } + if (next != null) { + next.generateArgumentArray(current, sender, args, startIndex); + } + return; + } + if (!ignoreAsArgument) { + current.add(validArgument.value); + } + if (next != null) { + next.generateArgumentArray(current, sender, args, startIndex + 1); + } + } + + public boolean guardCheck(T sender, String[] args, int startIndex) { + if (varArgType != null) { + for (int i = startIndex; i < args.length; i++) { + GuardResult guardResult = checkGuard(guardCheckType, sender, args, i); + if (guardResult == GuardResult.DENIED) { + throw new CommandNoHelpException(); + } + if (guardResult == GuardResult.DENIED_WITH_HELP) { + return false; + } + } + return true; + } + + GuardResult guardResult = checkGuard(guardCheckType, sender, args, startIndex); + if (guardResult == GuardResult.DENIED) { + if (optional != null && next != null) { + return next.guardCheck(sender, args, startIndex); + } + throw new CommandNoHelpException(); + } + if (guardResult == GuardResult.DENIED_WITH_HELP) { + if (optional != null && next != null) { + return next.guardCheck(sender, args, startIndex); + } + return false; + } + if (next != null) { + return next.guardCheck(sender, args, startIndex + 1); + } + return true; + } + + public void generateTabComplete(List current, T sender, String[] args, int startIndex) { + if (varArgType != null) { + for (int i = startIndex; i < args.length - 1; i++) { + CheckArgumentResult validArgument = checkArgument(null, sender, args, i); + if (!validArgument.success) { + return; + } + } + List strings = typeMapper.tabCompletes(sender, Arrays.copyOf(args, args.length - 1), args[args.length - 1]); + if (strings != null) { + current.addAll(strings); + } + return; + } + + if (args.length - 1 > startIndex) { + CheckArgumentResult checkArgumentResult = checkArgument(GuardCheckType.TAB_COMPLETE, sender, args, startIndex); + if (checkArgumentResult.success && next != null) { + next.generateTabComplete(current, sender, args, startIndex + 1); + return; + } + if (optional != null && next != null) { + next.generateTabComplete(current, sender, args, startIndex); + } + return; + } + + List strings = typeMapper.tabCompletes(sender, Arrays.copyOf(args, startIndex), args[startIndex]); + if (strings != null) { + current.addAll(strings); + } + if (optional != null && next != null) { + next.generateTabComplete(current, sender, args, startIndex); + } + } + + private CheckArgumentResult checkArgument(GuardCheckType guardCheckType, T sender, String[] args, int index) { + try { + Object value = typeMapper.map(sender, Arrays.copyOf(args, index), args[index]); + if (value == null) { + return new CheckArgumentResult(false, null); + } + GuardResult guardResult = checkGuard(guardCheckType, sender, args, index); + switch (guardResult) { + case ALLOWED: + return new CheckArgumentResult(true, value); + case DENIED: + throw new CommandNoHelpException(); + case DENIED_WITH_HELP: + default: + return new CheckArgumentResult(false, null); + } + } catch (Exception e) { + return new CheckArgumentResult(false, null); + } + } + + private GuardResult checkGuard(GuardCheckType guardCheckType, T sender, String[] args, int index) { + if (guardChecker != null && guardCheckType != null) { + return guardChecker.guard(sender, guardCheckType, Arrays.copyOf(args, index), args[index]); + } + return GuardResult.ALLOWED; + } +} diff --git a/src/de/steamwar/command/GuardCheckType.java b/src/de/steamwar/command/GuardCheckType.java new file mode 100644 index 0000000..0f023b8 --- /dev/null +++ b/src/de/steamwar/command/GuardCheckType.java @@ -0,0 +1,26 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2020 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.command; + +public enum GuardCheckType { + COMMAND, + HELP_COMMAND, + TAB_COMPLETE +} diff --git a/src/de/steamwar/command/GuardResult.java b/src/de/steamwar/command/GuardResult.java new file mode 100644 index 0000000..9ffbf77 --- /dev/null +++ b/src/de/steamwar/command/GuardResult.java @@ -0,0 +1,26 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2020 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.command; + +public enum GuardResult { + ALLOWED, + DENIED_WITH_HELP, + DENIED +} diff --git a/src/de/steamwar/command/SWCommandUtils.java b/src/de/steamwar/command/SWCommandUtils.java new file mode 100644 index 0000000..52e1d75 --- /dev/null +++ b/src/de/steamwar/command/SWCommandUtils.java @@ -0,0 +1,241 @@ +package de.steamwar.command; + +import lombok.Getter; +import lombok.experimental.UtilityClass; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +@UtilityClass +public class SWCommandUtils { + + @Getter + private final Map> MAPPER_FUNCTIONS = new HashMap<>(); + + @Getter + private final Map> GUARD_FUNCTIONS = new HashMap<>(); + + static { + addMapper(boolean.class, Boolean.class, createMapper(Boolean::parseBoolean, s -> Arrays.asList("true", "false"))); + addMapper(float.class, Float.class, createMapper(numberMapper(Float::parseFloat), numberCompleter(Float::parseFloat))); + addMapper(double.class, Double.class, createMapper(numberMapper(Double::parseDouble), numberCompleter(Double::parseDouble))); + addMapper(int.class, Integer.class, createMapper(numberMapper(Integer::parseInt), numberCompleter(Integer::parseInt))); + addMapper(long.class, Long.class, createMapper(numberMapper(Long::parseLong), numberCompleter(Long::parseLong))); + MAPPER_FUNCTIONS.put(String.class.getTypeName(), createMapper(s -> s, Collections::singletonList)); + } + + private static void addMapper(Class clazz, Class alternativeClazz, AbstractTypeMapper mapper) { + MAPPER_FUNCTIONS.put(clazz.getTypeName(), mapper); + MAPPER_FUNCTIONS.put(alternativeClazz.getTypeName(), mapper); + } + + static CommandPart generateCommandPart(boolean help, String[] subCommand, Parameter[] parameters, Map> localTypeMapper, Map> localGuardChecker) { + CommandPart first = null; + CommandPart current = null; + for (String s : subCommand) { + CommandPart commandPart = new CommandPart(createMapper(s), null, null, null, help ? GuardCheckType.HELP_COMMAND : GuardCheckType.COMMAND); + commandPart.setIgnoreAsArgument(true); + if (current != null) { + current.setNext(commandPart); + } + current = commandPart; + if (first == null) { + first = current; + } + } + for (int i = 1; i < parameters.length; i++) { + Parameter parameter = parameters[i]; + AbstractTypeMapper typeMapper = getTypeMapper(parameter, localTypeMapper); + AbstractGuardChecker guardChecker = getGuardChecker(parameter, localGuardChecker); + Class varArgType = parameter.isVarArgs() ? parameter.getType().getComponentType() : null; + AbstractSWCommand.OptionalValue optionalValue = parameter.getAnnotation(AbstractSWCommand.OptionalValue.class); + + CommandPart commandPart = new CommandPart<>(typeMapper, guardChecker, varArgType, optionalValue != null ? optionalValue.value() : null, help ? GuardCheckType.HELP_COMMAND : GuardCheckType.COMMAND); + if (current != null) { + current.setNext(commandPart); + } + current = commandPart; + if (first == null) { + first = current; + } + } + return first; + } + + public static AbstractTypeMapper getTypeMapper(Parameter parameter, Map> localTypeMapper) { + Class clazz = parameter.getType(); + if (parameter.isVarArgs()) { + clazz = clazz.getComponentType(); + } + + AbstractSWCommand.ClassMapper classMapper = parameter.getAnnotation(AbstractSWCommand.ClassMapper.class); + AbstractSWCommand.Mapper mapper = parameter.getAnnotation(AbstractSWCommand.Mapper.class); + if (clazz.isEnum() && classMapper == null && mapper == null && !MAPPER_FUNCTIONS.containsKey(clazz.getTypeName()) && !localTypeMapper.containsKey(clazz.getTypeName())) { + return (AbstractTypeMapper) createEnumMapper((Class>) clazz); + } + + String name = clazz.getTypeName(); + if (classMapper != null) { + name = classMapper.value().getTypeName(); + } else if (mapper != null) { + name = mapper.value(); + } else { + AbstractSWCommand.StaticValue staticValue = parameter.getAnnotation(AbstractSWCommand.StaticValue.class); + if (staticValue != null) { + if (parameter.getType() == String.class) { + return createMapper(staticValue.value()); + } + if (staticValue.allowISE()) { + if ((parameter.getType() == boolean.class || parameter.getType() == Boolean.class) && staticValue.value().length == 2) { + List tabCompletes = new ArrayList<>(Arrays.asList(staticValue.value())); + return createMapper(s -> { + int index = tabCompletes.indexOf(s); + return index == -1 ? null : index != 0; + }, (commandSender, s) -> tabCompletes); + } + if ((parameter.getType() == int.class || parameter.getType() == Integer.class) && staticValue.value().length >= 2) { + List tabCompletes = new ArrayList<>(Arrays.asList(staticValue.value())); + return createMapper(s -> { + int index = tabCompletes.indexOf(s); + return index == -1 ? null : index; + }, (commandSender, s) -> tabCompletes); + } + if ((parameter.getType() == long.class || parameter.getType() == Long.class) && staticValue.value().length >= 2) { + List tabCompletes = new ArrayList<>(Arrays.asList(staticValue.value())); + return createMapper(s -> { + long index = tabCompletes.indexOf(s); + return index == -1 ? null : index; + }, (commandSender, s) -> tabCompletes); + } + } + } + } + AbstractTypeMapper typeMapper = localTypeMapper.getOrDefault(name, (AbstractTypeMapper) MAPPER_FUNCTIONS.getOrDefault(name, null)); + if (typeMapper == null) { + throw new IllegalArgumentException("No mapper found for " + name); + } + return typeMapper; + } + + public static AbstractGuardChecker getGuardChecker(Parameter parameter, Map> localGuardChecker) { + Class clazz = parameter.getType(); + if (parameter.isVarArgs()) { + clazz = clazz.getComponentType(); + } + + AbstractSWCommand.ClassGuard classGuard = parameter.getAnnotation(AbstractSWCommand.ClassGuard.class); + if (classGuard != null) { + if (classGuard.value() != null) { + return getGuardChecker(classGuard.value().getTypeName(), localGuardChecker); + } + return getGuardChecker(clazz.getTypeName(), localGuardChecker); + } + + AbstractSWCommand.Guard guard = parameter.getAnnotation(AbstractSWCommand.Guard.class); + if (guard != null) { + if (guard.value() != null && !guard.value().isEmpty()) { + return getGuardChecker(guard.value(), localGuardChecker); + } + return getGuardChecker(clazz.getTypeName(), localGuardChecker); + } + return null; + } + + private static AbstractGuardChecker getGuardChecker(String s, Map> localGuardChecker) { + AbstractGuardChecker guardChecker = localGuardChecker.getOrDefault(s, (AbstractGuardChecker) GUARD_FUNCTIONS.getOrDefault(s, null)); + if (guardChecker == null) { + throw new IllegalArgumentException("No guard found for " + s); + } + return guardChecker; + } + + public static void addMapper(Class clazz, AbstractTypeMapper mapper) { + addMapper(clazz.getTypeName(), mapper); + } + + public static void addMapper(String name, AbstractTypeMapper mapper) { + MAPPER_FUNCTIONS.putIfAbsent(name, mapper); + } + + public static void addGuard(Class clazz, AbstractGuardChecker guardChecker) { + addGuard(clazz.getTypeName(), guardChecker); + } + + public static void addGuard(String name, AbstractGuardChecker guardChecker) { + GUARD_FUNCTIONS.putIfAbsent(name, guardChecker); + } + + public static AbstractTypeMapper createMapper(String... values) { + List strings = Arrays.asList(values); + return createMapper((s) -> strings.contains(s) ? s : null, s -> strings); + } + + public static AbstractTypeMapper createMapper(Function mapper, Function> tabCompleter) { + return createMapper(mapper, (commandSender, s) -> tabCompleter.apply(s)); + } + + public static AbstractTypeMapper createMapper(Function mapper, BiFunction> tabCompleter) { + return new AbstractTypeMapper() { + @Override + public T map(K commandSender, String[] previousArguments, String s) { + return mapper.apply(s); + } + + @Override + public List tabCompletes(K commandSender, String[] previous, String s) { + return tabCompleter.apply(commandSender, s); + } + }; + } + + public static AbstractTypeMapper> createEnumMapper(Class> enumClass) { + Enum[] enums = enumClass.getEnumConstants(); + List strings = Arrays.stream(enums).map(Enum::name).map(String::toLowerCase).collect(Collectors.toList()); + return new AbstractTypeMapper>() { + @Override + public Enum map(Object commandSender, String[] previousArguments, String s) { + for (Enum e : enums) { + if (e.name().equalsIgnoreCase(s)) return e; + } + return null; + } + + @Override + public List tabCompletes(Object commandSender, String[] previousArguments, String s) { + return strings; + } + }; + } + + private static Function numberMapper(Function mapper) { + return s -> { + if (s.equalsIgnoreCase("nan")) return null; + try { + return mapper.apply(s); + } catch (NumberFormatException e) { + // Ignored + } + try { + return mapper.apply(s.replace(',', '.')); + } catch (NumberFormatException e) { + return null; + } + }; + } + + private static Function> numberCompleter(Function mapper) { + return s -> numberMapper(mapper).apply(s) != null + ? Collections.singletonList(s) + : Collections.emptyList(); + } + + static T[] getAnnotation(Method method, Class annotation) { + if (method.getAnnotations().length != 1) return null; + return method.getDeclaredAnnotationsByType(annotation); + } +} diff --git a/src/de/steamwar/command/SubCommand.java b/src/de/steamwar/command/SubCommand.java new file mode 100644 index 0000000..4b9a4a6 --- /dev/null +++ b/src/de/steamwar/command/SubCommand.java @@ -0,0 +1,99 @@ +package de.steamwar.command; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; + +public class SubCommand { + + private AbstractSWCommand abstractSWCommand; + Method method; + String[] description; + String[] subCommand; + private Predicate senderPredicate; + private Function senderFunction; + AbstractGuardChecker guardChecker; + boolean noTabComplete; + int comparableValue; + + private CommandPart commandPart; + + SubCommand(AbstractSWCommand abstractSWCommand, Method method, String[] subCommand, Map> localTypeMapper, Map> localGuardChecker, boolean help, String[] description, boolean noTabComplete) { + this.abstractSWCommand = abstractSWCommand; + this.method = method; + this.subCommand = subCommand; + this.description = description; + this.noTabComplete = noTabComplete; + + Parameter[] parameters = method.getParameters(); + comparableValue = parameters[parameters.length - 1].isVarArgs() ? Integer.MAX_VALUE : -parameters.length; + + guardChecker = SWCommandUtils.getGuardChecker(parameters[0], localGuardChecker); + + commandPart = SWCommandUtils.generateCommandPart(help, subCommand, parameters, localTypeMapper, localGuardChecker); + + senderPredicate = t -> parameters[0].getType().isAssignableFrom(t.getClass()); + senderFunction = t -> parameters[0].getType().cast(t); + } + + boolean invoke(T sender, String alias, String[] args) { + try { + if (!senderPredicate.test(sender)) { + return false; + } + + if (commandPart == null) { + if (args.length != 0) { + return false; + } + method.setAccessible(true); + method.invoke(abstractSWCommand, senderFunction.apply(sender)); + } else { + List objects = new ArrayList<>(); + commandPart.generateArgumentArray(objects, sender, args, 0); + if (guardChecker != null) { + GuardResult guardResult = guardChecker.guard(sender, GuardCheckType.COMMAND, new String[0], null); + switch (guardResult) { + case ALLOWED: + break; + case DENIED: + throw new CommandNoHelpException(); + case DENIED_WITH_HELP: + default: + return true; + } + } + commandPart.guardCheck(sender, args, 0); + objects.add(0, senderFunction.apply(sender)); + method.setAccessible(true); + method.invoke(abstractSWCommand, objects.toArray()); + } + } catch (CommandNoHelpException e) { + throw e; + } catch (CommandParseException e) { + return false; + } catch (InvocationTargetException e) { + throw new CommandFrameworkException(e, alias, args); + } catch (IllegalAccessException | RuntimeException e) { + throw new SecurityException(e.getMessage(), e); + } + return true; + } + + List tabComplete(T sender, String[] args) { + if (guardChecker != null && guardChecker.guard(sender, GuardCheckType.TAB_COMPLETE, new String[0], null) != GuardResult.ALLOWED) { + return null; + } + if (commandPart == null) { + return null; + } + List list = new ArrayList<>(); + commandPart.generateTabComplete(list, sender, args, 0); + return list; + } +}