diff --git a/build.gradle b/build.gradle
index 6c65867..376b05d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -57,6 +57,16 @@ sourceSets {
exclude '**/*.java', '**/*.kt'
}
}
+
+ test {
+ java {
+ srcDirs = ['testsrc']
+ }
+ resources {
+ srcDirs = ['testsrc']
+ exclude '**/*.java', '**/*.kt'
+ }
+ }
}
repositories {
@@ -68,6 +78,9 @@ dependencies {
testCompileOnly 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok:1.18.22'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.22'
+
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.hamcrest:hamcrest:2.2'
}
task buildProject {
diff --git a/src/de/steamwar/command/AbstractGuardChecker.java b/src/de/steamwar/command/AbstractGuardChecker.java
new file mode 100644
index 0000000..f5f2597
--- /dev/null
+++ b/src/de/steamwar/command/AbstractGuardChecker.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+@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..fd371df
--- /dev/null
+++ b/src/de/steamwar/command/AbstractSWCommand.java
@@ -0,0 +1,323 @@
+/*
+ * 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.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().isAssignableFrom(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..744b726
--- /dev/null
+++ b/src/de/steamwar/command/AbstractTypeMapper.java
@@ -0,0 +1,31 @@
+/*
+ * 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.util.Collection;
+
+public interface AbstractTypeMapper {
+ /**
+ * The CommandSender can be null!
+ */
+ T map(K sender, String[] previousArguments, String s);
+
+ Collection 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..0688caf
--- /dev/null
+++ b/src/de/steamwar/command/CommandPart.java
@@ -0,0 +1,216 @@
+/*
+ * 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 lombok.AllArgsConstructor;
+import lombok.Setter;
+
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.Collection;
+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