diff --git a/build.gradle b/build.gradle
old mode 100644
new mode 100755
index f1c8306..d5f000a
--- a/build.gradle
+++ b/build.gradle
@@ -81,6 +81,8 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.hamcrest:hamcrest:2.2'
+
+ compileOnly 'org.xerial:sqlite-jdbc:3.36.0'
}
task buildResources {
diff --git a/src/de/steamwar/ImplementationProvider.java b/src/de/steamwar/ImplementationProvider.java
new file mode 100644
index 0000000..e5b795e
--- /dev/null
+++ b/src/de/steamwar/ImplementationProvider.java
@@ -0,0 +1,34 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2022 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;
+
+import java.lang.reflect.InvocationTargetException;
+
+public class ImplementationProvider {
+ private ImplementationProvider() {}
+
+ public static T getImpl(String className) {
+ try {
+ return (T) Class.forName(className).getDeclaredConstructor().newInstance();
+ } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException e) {
+ throw new SecurityException("Could not load implementation", e);
+ }
+ }
+}
diff --git a/src/de/steamwar/command/AbstractSWCommand.java b/src/de/steamwar/command/AbstractSWCommand.java
index c23fd91..b8dc078 100644
--- a/src/de/steamwar/command/AbstractSWCommand.java
+++ b/src/de/steamwar/command/AbstractSWCommand.java
@@ -20,22 +20,26 @@
package de.steamwar.command;
import java.lang.annotation.*;
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
-import java.util.function.IntPredicate;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public abstract class AbstractSWCommand {
+ private static final Map>, List>> dependencyMap = new HashMap<>();
+
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> localValidators = new HashMap<>();
@@ -46,6 +50,13 @@ public abstract class AbstractSWCommand {
protected AbstractSWCommand(Class clazz, String command, String... aliases) {
this.clazz = clazz;
+
+ PartOf partOf = this.getClass().getAnnotation(PartOf.class);
+ if (partOf != null) {
+ dependencyMap.computeIfAbsent((Class>) partOf.value(), k -> new ArrayList<>()).add(this);
+ return;
+ }
+
createAndSafeCommand(command, aliases);
unregister();
register();
@@ -65,22 +76,16 @@ public abstract class AbstractSWCommand {
System.out.println(message.get());
}
- protected void sendMessage(T sender, String message, Object[] args) {}
+ protected void sendMessage(T sender, String message, Object[] args) {
+ }
protected final void execute(T sender, String alias, String[] args) {
initialize();
List errors = new ArrayList<>();
try {
if (!commandList.stream().anyMatch(s -> s.invoke(errors::add, sender, alias, args))) {
- if (!errors.isEmpty()) {
- errors.forEach(Runnable::run);
- return;
- }
- commandHelpList.stream().anyMatch(s -> s.invoke((ignore) -> {
- }, sender, alias, args));
+ errors.forEach(Runnable::run);
}
- } catch (CommandNoHelpException e) {
- // Ignored
} catch (CommandFrameworkException e) {
commandSystemError(sender, e);
throw e;
@@ -96,79 +101,39 @@ public abstract class AbstractSWCommand {
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(s -> !s.isEmpty())
- .filter(s -> s.toLowerCase().startsWith(string))
+ .filter(s -> s.toLowerCase().startsWith(string) || string.startsWith(s.toLowerCase()))
.distinct()
.collect(Collectors.toList());
}
- private void initialize() {
+ private synchronized void initialize() {
if (initialized) return;
- createMapping();
- }
-
- private synchronized void createMapping() {
- List methods = methods();
+ List methods = methods().stream()
+ .filter(this::validateMethod)
+ .collect(Collectors.toList());
for (Method method : methods) {
Cached cached = method.getAnnotation(Cached.class);
- addMapper(Mapper.class, method, i -> i == 0, false, AbstractTypeMapper.class, (anno, typeMapper) -> {
+ this.>add(Mapper.class, method, (anno, typeMapper) -> {
TabCompletionCache.add(typeMapper, cached);
- if (anno.local()) {
- localTypeMapper.putIfAbsent(anno.value(), (AbstractTypeMapper) typeMapper);
- } else {
- SWCommandUtils.getMAPPER_FUNCTIONS().putIfAbsent(anno.value(), typeMapper);
- }
+ (anno.local() ? ((Map) localTypeMapper) : SWCommandUtils.getMAPPER_FUNCTIONS()).put(anno.value(), typeMapper);
});
- addMapper(ClassMapper.class, method, i -> i == 0, false, AbstractTypeMapper.class, (anno, typeMapper) -> {
+ this.>add(ClassMapper.class, method, (anno, typeMapper) -> {
TabCompletionCache.add(typeMapper, cached);
- if (anno.local()) {
- localTypeMapper.putIfAbsent(anno.value().getTypeName(), (AbstractTypeMapper) typeMapper);
- } else {
- SWCommandUtils.getMAPPER_FUNCTIONS().putIfAbsent(anno.value().getTypeName(), typeMapper);
- }
+ (anno.local() ? ((Map) localTypeMapper) : SWCommandUtils.getMAPPER_FUNCTIONS()).put(anno.value().getName(), typeMapper);
});
- addValidator(Validator.class, method, i -> i == 0, false, AbstractValidator.class, (anno, validator) -> {
- if (anno.local()) {
- localValidators.putIfAbsent(anno.value(), (AbstractValidator) validator);
- } else {
- SWCommandUtils.getVALIDATOR_FUNCTIONS().putIfAbsent(anno.value(), validator);
- }
+ this.>add(Validator.class, method, (anno, validator) -> {
+ (anno.local() ? ((Map) localValidators) : SWCommandUtils.getVALIDATOR_FUNCTIONS()).put(anno.value(), validator);
});
- addValidator(ClassValidator.class, method, i -> i == 0, false, AbstractValidator.class, (anno, validator) -> {
- if (anno.local()) {
- localValidators.putIfAbsent(anno.value().getTypeName(), (AbstractValidator) validator);
- } else {
- SWCommandUtils.getVALIDATOR_FUNCTIONS().putIfAbsent(anno.value().getTypeName(), validator);
- }
+ this.>add(ClassValidator.class, method, (anno, validator) -> {
+ (anno.local() ? ((Map) localValidators) : SWCommandUtils.getVALIDATOR_FUNCTIONS()).put(anno.value().getName(), validator);
});
}
for (Method method : methods) {
- add(Register.class, method, i -> i > 0, true, null, (anno, parameters) -> {
- if (!anno.help()) return;
- boolean error = false;
- if (parameters.length != 2) {
- commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking parameters or has too many");
- error = true;
- }
- if (!parameters[parameters.length - 1].isVarArgs()) {
- commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking the varArgs parameters as last Argument");
- error = true;
- }
- 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");
- error = true;
- }
- if (error) return;
- commandHelpList.add(new SubCommand<>(this, method, anno.value(), new HashMap<>(), new HashMap<>(), true, null, anno.noTabComplete()));
- });
-
- add(Register.class, method, i -> i > 0, true, null, (anno, parameters) -> {
- if (anno.help()) return;
+ add(Register.class, method, true, (anno, parameters) -> {
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();
- }
+ if (parameter.isVarArgs()) clazz = clazz.getComponentType();
Mapper mapper = parameter.getAnnotation(Mapper.class);
if (clazz.isEnum() && mapper == null && !SWCommandUtils.getMAPPER_FUNCTIONS().containsKey(clazz.getTypeName())) {
continue;
@@ -179,66 +144,94 @@ public abstract class AbstractSWCommand {
return;
}
}
- commandList.add(new SubCommand<>(this, method, anno.value(), localTypeMapper, localValidators, false, anno.description(), anno.noTabComplete()));
+ commandList.add(new SubCommand<>(this, method, anno.value(), localTypeMapper, localValidators, 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);
- }
- });
+ if (dependencyMap.containsKey(this.getClass())) {
+ dependencyMap.get(this.getClass()).forEach(abstractSWCommand -> {
+ abstractSWCommand.localTypeMapper.putAll((Map) localTypeMapper);
+ abstractSWCommand.localValidators.putAll((Map) localValidators);
+ abstractSWCommand.initialize();
+ commandList.addAll((Collection) abstractSWCommand.commandList);
+ });
+ }
+
+ Collections.sort(commandList);
initialized = true;
}
- private void add(Class annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class> returnType, BiConsumer consumer) {
+ private boolean validateMethod(Method method) {
+ if (!checkType(method.getAnnotations(), method.getReturnType(), false, annotation -> {
+ CommandMetaData.Method methodMetaData = annotation.annotationType().getAnnotation(CommandMetaData.Method.class);
+ if (methodMetaData == null) return (aClass, varArg) -> true;
+ if (method.getParameterCount() > methodMetaData.maxParameterCount() || method.getParameterCount() < methodMetaData.minParameterCount())
+ return (aClass, varArg) -> false;
+ return (aClass, varArg) -> {
+ Class>[] types = methodMetaData.value();
+ if (types == null) return true;
+ for (Class> type : types) {
+ if (type.isAssignableFrom(aClass)) return true;
+ }
+ return false;
+ };
+ }, "The method '" + method + "'")) return false;
+ boolean valid = true;
+ for (Parameter parameter : method.getParameters()) {
+ if (!checkType(parameter.getAnnotations(), parameter.getType(), parameter.isVarArgs(), annotation -> {
+ CommandMetaData.Parameter parameterMetaData = annotation.annotationType().getAnnotation(CommandMetaData.Parameter.class);
+ if (parameterMetaData == null) return (aClass, varArg) -> true;
+ Class> handler = parameterMetaData.handler();
+ if (BiPredicate.class.isAssignableFrom(handler)) {
+ try {
+ return (BiPredicate, Boolean>) handler.getConstructor().newInstance();
+ } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
+ NoSuchMethodException e) {
+ }
+ }
+ return (aClass, varArg) -> {
+ if (varArg) aClass = aClass.getComponentType();
+ Class>[] types = parameterMetaData.value();
+ if (types == null) return true;
+ for (Class> current : types) {
+ if (current.isAssignableFrom(aClass)) return true;
+ }
+ return false;
+ };
+ }, "The parameter '" + parameter + "'")) valid = false;
+ }
+ return valid;
+ }
+
+ private boolean checkType(Annotation[] annotations, Class> clazz, boolean varArg, Function, Boolean>> toApplicableTypes, String warning) {
+ boolean valid = true;
+ for (Annotation annotation : annotations) {
+ BiPredicate, Boolean> predicate = toApplicableTypes.apply(annotation);
+ if (!predicate.test(clazz, varArg)) {
+ commandSystemWarning(() -> warning + " is using an unsupported annotation of type '" + annotation.annotationType().getName() + "'");
+ valid = false;
+ }
+ }
+ return valid;
+ }
+
+ private void add(Class annotation, Method method, boolean firstParameter, 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 && !returnType.isAssignableFrom(method.getReturnType())) {
- 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) -> {
+ private void add(Class annotation, Method method, BiConsumer consumer) {
+ add(annotation, method, false, (anno, parameters) -> {
try {
method.setAccessible(true);
- consumer.accept(anno, (AbstractTypeMapper) method.invoke(this));
- } catch (Exception e) {
- throw new SecurityException(e.getMessage(), e);
- }
- });
- }
-
- private void addValidator(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, (AbstractValidator) method.invoke(this));
+ consumer.accept(anno, (K) method.invoke(this));
} catch (Exception e) {
throw new SecurityException(e.getMessage(), e);
}
@@ -262,12 +255,29 @@ public abstract class AbstractSWCommand {
return methods;
}
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.TYPE})
+ public @interface PartOf {
+ Class> value();
+ }
+
+ // --- Annotation for the command ---
+
+ /**
+ * Annotation for registering a method as a command
+ */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Repeatable(Register.Registeres.class)
+ @CommandMetaData.Method(value = void.class, minParameterCount = 1)
protected @interface Register {
+
+ /**
+ * Identifier of subcommand
+ */
String[] value() default {};
+ @Deprecated
boolean help() default false;
String[] description() default {};
@@ -276,6 +286,7 @@ public abstract class AbstractSWCommand {
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
+ @CommandMetaData.Method(value = void.class, minParameterCount = 1)
@interface Registeres {
Register[] value();
}
@@ -283,14 +294,41 @@ public abstract class AbstractSWCommand {
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD})
+ @CommandMetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0)
+ @CommandMetaData.ImplicitTypeMapper(handler = Mapper.Handler.class)
protected @interface Mapper {
String value();
boolean local() default false;
+
+ class Handler implements AbstractTypeMapper {
+
+ private AbstractTypeMapper inner;
+
+ public Handler(AbstractSWCommand.Mapper mapper, Map> localTypeMapper) {
+ inner = (AbstractTypeMapper) SWCommandUtils.getTypeMapper(mapper.value(), localTypeMapper);
+ }
+
+ @Override
+ public Object map(T sender, PreviousArguments previousArguments, String s) {
+ return inner.map(sender, previousArguments, s);
+ }
+
+ @Override
+ public boolean validate(T sender, Object value, MessageSender messageSender) {
+ return inner.validate(sender, value, messageSender);
+ }
+
+ @Override
+ public Collection tabCompletes(T sender, PreviousArguments previousArguments, String s) {
+ return inner.tabCompletes(sender, previousArguments, s);
+ }
+ }
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
+ @CommandMetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0)
protected @interface ClassMapper {
Class> value();
@@ -299,30 +337,58 @@ public abstract class AbstractSWCommand {
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
+ @CommandMetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0)
protected @interface Cached {
long cacheDuration() default 5;
+
TimeUnit timeUnit() default TimeUnit.SECONDS;
+
boolean global() default false;
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD})
+ @CommandMetaData.Method(value = AbstractValidator.class, maxParameterCount = 0)
+ @CommandMetaData.ImplicitValidator(handler = Validator.Handler.class, order = 0)
protected @interface Validator {
String value() default "";
boolean local() default false;
+
+ boolean invert() default false;
+
+ class Handler implements AbstractValidator {
+
+ private AbstractValidator inner;
+ private boolean invert;
+
+ public Handler(AbstractSWCommand.Validator validator, Class> clazz, Map> localValidator) {
+ inner = (AbstractValidator) SWCommandUtils.getValidator(validator, clazz, localValidator);
+ invert = validator.invert();
+ }
+
+ @Override
+ public boolean validate(T sender, Object value, MessageSender messageSender) {
+ return inner.validate(sender, value, messageSender) ^ invert;
+ }
+ }
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
+ @CommandMetaData.Method(value = AbstractValidator.class, maxParameterCount = 0)
protected @interface ClassValidator {
Class> value();
boolean local() default false;
}
+ // --- Implicit TypeMapper ---
+
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
+ @CommandMetaData.Parameter({String.class, int.class, Integer.class, long.class, Long.class, boolean.class, Boolean.class})
+ @CommandMetaData.ImplicitTypeMapper(handler = StaticValue.Handler.class)
protected @interface StaticValue {
String[] value();
@@ -336,7 +402,49 @@ public abstract class AbstractSWCommand {
*/
boolean allowISE() default false;
- int[] falseValues() default { 0 };
+ int[] falseValues() default {0};
+
+ class Handler implements AbstractTypeMapper {
+
+ private AbstractTypeMapper inner;
+
+ public Handler(StaticValue staticValue, Class> clazz) {
+ if (clazz == String.class) {
+ inner = SWCommandUtils.createMapper(staticValue.value());
+ return;
+ }
+ if (!staticValue.allowISE()) {
+ throw new IllegalArgumentException("The parameter type '" + clazz.getTypeName() + "' is not supported by the StaticValue annotation");
+ }
+ if (clazz == boolean.class || clazz == Boolean.class) {
+ List tabCompletes = new ArrayList<>(Arrays.asList(staticValue.value()));
+ Set falseValues = new HashSet<>();
+ for (int i : staticValue.falseValues()) falseValues.add(i);
+ inner = SWCommandUtils.createMapper(s -> {
+ int index = tabCompletes.indexOf(s);
+ return index == -1 ? null : !falseValues.contains(index);
+ }, (commandSender, s) -> tabCompletes);
+ } else if (clazz == int.class || clazz == Integer.class || clazz == long.class || clazz == Long.class) {
+ List tabCompletes = new ArrayList<>(Arrays.asList(staticValue.value()));
+ inner = SWCommandUtils.createMapper(s -> {
+ Number index = tabCompletes.indexOf(s);
+ return index.longValue() == -1 ? null : index;
+ }, (commandSender, s) -> tabCompletes);
+ } else {
+ throw new IllegalArgumentException("The parameter type '" + clazz.getTypeName() + "' is not supported by the StaticValue annotation");
+ }
+ }
+
+ @Override
+ public Object map(T sender, PreviousArguments previousArguments, String s) {
+ return inner.map(sender, previousArguments, s);
+ }
+
+ @Override
+ public Collection tabCompletes(T sender, PreviousArguments previousArguments, String s) {
+ return inner.tabCompletes(sender, previousArguments, s);
+ }
+ }
}
@Retention(RetentionPolicy.RUNTIME)
@@ -353,8 +461,11 @@ public abstract class AbstractSWCommand {
boolean onlyUINIG() default false;
}
+ // --- Implicit Validator ---
+
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
+ @CommandMetaData.ImplicitValidator(handler = ErrorMessage.Handler.class, order = Integer.MAX_VALUE)
protected @interface ErrorMessage {
/**
* Error message to be displayed when the parameter is invalid.
@@ -365,10 +476,219 @@ public abstract class AbstractSWCommand {
* This is the short form for 'allowEmptyArrays'.
*/
boolean allowEAs() default true;
+
+ class Handler implements AbstractValidator {
+
+ private AbstractSWCommand.ErrorMessage errorMessage;
+
+ public Handler(AbstractSWCommand.ErrorMessage errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ @Override
+ public boolean validate(T sender, Object value, MessageSender messageSender) {
+ if (value == null) messageSender.send(errorMessage.value());
+ if (!errorMessage.allowEAs() && value != null && value.getClass().isArray() && Array.getLength(value) == 0) {
+ messageSender.send(errorMessage.value());
+ return false;
+ }
+ return value != null;
+ }
+ }
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
protected @interface AllowNull {
}
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER})
+ @CommandMetaData.Parameter({int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class})
+ @CommandMetaData.ImplicitValidator(handler = Min.Handler.class, order = 2)
+ protected @interface Min {
+ int intValue() default Integer.MIN_VALUE;
+
+ long longValue() default Long.MIN_VALUE;
+
+ float floatValue() default Float.MIN_VALUE;
+
+ double doubleValue() default Double.MIN_VALUE;
+
+ boolean inclusive() default true;
+
+ class Handler implements AbstractValidator {
+
+ private int value;
+ private Function comparator;
+
+ public Handler(AbstractSWCommand.Min min, Class> clazz) {
+ this.value = min.inclusive() ? 0 : 1;
+ this.comparator = createComparator("Min", clazz, min.intValue(), min.longValue(), min.floatValue(), min.doubleValue());
+ }
+
+ @Override
+ public boolean validate(T sender, Number value, MessageSender messageSender) {
+ if (value == null) return true;
+ return (comparator.apply(value).intValue()) >= this.value;
+ }
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER})
+ @CommandMetaData.Parameter({int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class})
+ @CommandMetaData.ImplicitValidator(handler = Max.Handler.class, order = 2)
+ protected @interface Max {
+ int intValue() default Integer.MAX_VALUE;
+
+ long longValue() default Long.MAX_VALUE;
+
+ float floatValue() default Float.MAX_VALUE;
+
+ double doubleValue() default Double.MAX_VALUE;
+
+ boolean inclusive() default true;
+
+ class Handler implements AbstractValidator {
+
+ private int value;
+ private Function comparator;
+
+ public Handler(AbstractSWCommand.Max max, Class> clazz) {
+ this.value = max.inclusive() ? 0 : -1;
+ this.comparator = createComparator("Max", clazz, max.intValue(), max.longValue(), max.floatValue(), max.doubleValue());
+ }
+
+ @Override
+ public boolean validate(T sender, Number value, MessageSender messageSender) {
+ if (value == null) return true;
+ return (comparator.apply(value).intValue()) <= this.value;
+ }
+ }
+ }
+
+ private static Function createComparator(String type, Class> clazz, int iValue, long lValue, float fValue, double dValue) {
+ if (clazz == int.class || clazz == Integer.class) {
+ return number -> Integer.compare(number.intValue(), iValue);
+ } else if (clazz == long.class || clazz == Long.class) {
+ return number -> Long.compare(number.longValue(), lValue);
+ } else if (clazz == float.class || clazz == Float.class) {
+ return number -> Float.compare(number.floatValue(), fValue);
+ } else if (clazz == double.class || clazz == Double.class) {
+ return number -> Double.compare(number.doubleValue(), dValue);
+ } else {
+ throw new IllegalArgumentException(type + " annotation is not supported for " + clazz);
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER})
+ @CommandMetaData.ImplicitTypeMapper(handler = Length.Handler.class)
+ protected @interface Length {
+ int min() default 0;
+
+ int max() default Integer.MAX_VALUE;
+
+ class Handler implements AbstractTypeMapper {
+
+ private int min;
+ private int max;
+ private AbstractTypeMapper inner;
+
+ public Handler(Length length, AbstractTypeMapper inner) {
+ this.min = length.min();
+ this.max = length.max();
+ this.inner = inner;
+ }
+
+ @Override
+ public Object map(T sender, PreviousArguments previousArguments, String s) {
+ if (s.length() < min || s.length() > max) return null;
+ return inner.map(sender, previousArguments, s);
+ }
+
+ @Override
+ public Collection tabCompletes(T sender, PreviousArguments previousArguments, String s) {
+ List tabCompletes = inner.tabCompletes(sender, previousArguments, s)
+ .stream()
+ .filter(str -> str.length() >= min)
+ .map(str -> str.substring(0, Math.min(str.length(), max)))
+ .collect(Collectors.toList());
+ if (s.length() < min) {
+ tabCompletes.add(0, s);
+ }
+ return tabCompletes;
+ }
+
+ @Override
+ public String normalize(T sender, String s) {
+ return inner.normalize(sender, s);
+ }
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.PARAMETER})
+ @CommandMetaData.Parameter(handler = ArrayLength.Type.class)
+ @CommandMetaData.ImplicitTypeMapper(handler = ArrayLength.HandlerTypeMapper.class)
+ @CommandMetaData.ImplicitValidator(handler = ArrayLength.HandlerValidator.class, order = 1)
+ protected @interface ArrayLength {
+ int min() default 0;
+
+ int max() default Integer.MAX_VALUE;
+
+ class Type implements BiPredicate, Boolean> {
+ @Override
+ public boolean test(Class> clazz, Boolean isVarArgs) {
+ return clazz.isArray();
+ }
+ }
+
+ class HandlerTypeMapper implements AbstractTypeMapper {
+
+ private int max;
+ private AbstractTypeMapper inner;
+
+ public HandlerTypeMapper(ArrayLength arrayLength, AbstractTypeMapper inner) {
+ this.max = arrayLength.max();
+ this.inner = inner;
+ }
+
+ @Override
+ public Object map(T sender, PreviousArguments previousArguments, String s) {
+ return inner.map(sender, previousArguments, s);
+ }
+
+ @Override
+ public Collection tabCompletes(T sender, PreviousArguments previousArguments, String s) {
+ Object[] mapped = previousArguments.getMappedArg(0);
+ if (mapped.length >= max) return Collections.emptyList();
+ return inner.tabCompletes(sender, previousArguments, s);
+ }
+
+ @Override
+ public String normalize(T sender, String s) {
+ return inner.normalize(sender, s);
+ }
+ }
+
+ class HandlerValidator implements AbstractValidator {
+
+ private int min;
+ private int max;
+
+ public HandlerValidator(ArrayLength arrayLength) {
+ this.min = arrayLength.min();
+ this.max = arrayLength.max();
+ }
+
+ @Override
+ public boolean validate(T sender, Object value, MessageSender messageSender) {
+ if (value == null) return true;
+ int length = Array.getLength(value);
+ return length >= min && length <= max;
+ }
+ }
+ }
}
diff --git a/src/de/steamwar/command/AbstractTypeMapper.java b/src/de/steamwar/command/AbstractTypeMapper.java
index 4a340d8..040f303 100644
--- a/src/de/steamwar/command/AbstractTypeMapper.java
+++ b/src/de/steamwar/command/AbstractTypeMapper.java
@@ -25,12 +25,38 @@ public interface AbstractTypeMapper extends AbstractValidator {
/**
* The CommandSender can be null!
*/
- T map(K sender, String[] previousArguments, String s);
+ @Deprecated
+ default T map(K sender, String[] previousArguments, String s) {
+ throw new IllegalArgumentException("This method is deprecated and should not be used anymore!");
+ }
+
+ /**
+ * The CommandSender can be null!
+ */
+ default T map(K sender, PreviousArguments previousArguments, String s) {
+ return map(sender, previousArguments.userArgs, s);
+ }
@Override
default boolean validate(K sender, T value, MessageSender messageSender) {
return true;
}
- Collection tabCompletes(K sender, String[] previousArguments, String s);
+ @Deprecated
+ default Collection tabCompletes(K sender, String[] previousArguments, String s) {
+ throw new IllegalArgumentException("This method is deprecated and should not be used anymore!");
+ }
+
+ default Collection tabCompletes(K sender, PreviousArguments previousArguments, String s) {
+ return tabCompletes(sender, previousArguments.userArgs, s);
+ }
+
+ /**
+ * Normalize the cache key by sender and user provided argument.
+ * Note: The CommandSender can be null!
+ * Returning null and the empty string are equivalent.
+ */
+ default String normalize(K sender, String s) {
+ return null;
+ }
}
diff --git a/src/de/steamwar/command/AbstractValidator.java b/src/de/steamwar/command/AbstractValidator.java
index aab4876..7dc0b72 100644
--- a/src/de/steamwar/command/AbstractValidator.java
+++ b/src/de/steamwar/command/AbstractValidator.java
@@ -38,10 +38,12 @@ public interface AbstractValidator {
*/
boolean validate(K sender, T value, MessageSender messageSender);
+ @Deprecated
default Validator validate(C value, MessageSender messageSender) {
return new Validator<>(value, messageSender);
}
+ @Deprecated
@RequiredArgsConstructor
class Validator {
private final C value;
diff --git a/src/de/steamwar/command/CommandFrameworkException.java b/src/de/steamwar/command/CommandFrameworkException.java
index 53ee025..ea3d835 100644
--- a/src/de/steamwar/command/CommandFrameworkException.java
+++ b/src/de/steamwar/command/CommandFrameworkException.java
@@ -36,12 +36,6 @@ public class CommandFrameworkException extends RuntimeException {
private String message;
- static CommandFrameworkException commandGetExceptions(String type, Class> clazzType, Executable executable, int index) {
- return new CommandFrameworkException(throwable -> {
- return CommandFrameworkException.class.getTypeName() + ": Error while getting " + type + " for " + clazzType.getTypeName() + " with parameter index " + index;
- }, null, throwable -> Stream.empty(), executable.getDeclaringClass().getTypeName() + "." + executable.getName() + "(Unknown Source)");
- }
-
static CommandFrameworkException commandPartExceptions(String type, Throwable cause, String current, Class> clazzType, Executable executable, int index) {
return new CommandFrameworkException(e -> {
return CommandFrameworkException.class.getTypeName() + ": Error while " + type + " (" + current + ") to type " + clazzType.getTypeName() + " with parameter index " + index;
diff --git a/src/de/steamwar/command/CommandMetaData.java b/src/de/steamwar/command/CommandMetaData.java
new file mode 100644
index 0000000..33909ac
--- /dev/null
+++ b/src/de/steamwar/command/CommandMetaData.java
@@ -0,0 +1,92 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2022 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.*;
+
+public @interface CommandMetaData {
+
+ /**
+ * This annotation is only for internal use.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.ANNOTATION_TYPE)
+ @interface Method {
+ Class>[] value();
+ int minParameterCount() default 0;
+ int maxParameterCount() default Integer.MAX_VALUE;
+ }
+
+ /**
+ * This annotation denotes what types are allowed as parameter types the annotation annotated with can use.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.ANNOTATION_TYPE)
+ @interface Parameter {
+ Class>[] value() default {};
+ Class> handler() default void.class;
+ }
+
+ /**
+ * This annotation can be used in conjunction with a class that implements {@link AbstractTypeMapper} to
+ * create a custom type mapper for a parameter. The class must have one of two constructors with the following
+ * types:
+ *
+ *
Annotation this annotation annotates
+ *
{@link Class}
+ *
{@link AbstractTypeMapper}, optional, if not present only one per parameter
+ *
{@link java.util.Map} with types {@link String} and {@link AbstractValidator}
+ *
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.ANNOTATION_TYPE)
+ @interface ImplicitTypeMapper {
+ /**
+ * The validator class that should be used.
+ */
+ Class> handler();
+ }
+
+ /**
+ * This annotation can be used in conjunction with a class that implements {@link AbstractValidator} to
+ * create a custom validator short hands for commands. The validator class must have one constructor with
+ * one of the following types:
+ *
+ *
Annotation this annotation annotates
+ *
{@link Class}
+ *
{@link java.util.Map} with types {@link String} and {@link AbstractValidator}
+ *
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.ANNOTATION_TYPE)
+ @interface ImplicitValidator {
+ /**
+ * The validator class that should be used.
+ */
+ Class> handler();
+
+ /**
+ * Defines when this validator should be processed. Negative numbers denote that this will be
+ * processed before {@link AbstractSWCommand.Validator} and positive numbers
+ * denote that this will be processed after {@link AbstractSWCommand.Validator}.
+ */
+ int order();
+ }
+}
diff --git a/src/de/steamwar/command/CommandPart.java b/src/de/steamwar/command/CommandPart.java
index 9f21ec8..5eb264c 100644
--- a/src/de/steamwar/command/CommandPart.java
+++ b/src/de/steamwar/command/CommandPart.java
@@ -24,6 +24,7 @@ import lombok.Setter;
import java.lang.reflect.Array;
import java.lang.reflect.Parameter;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@@ -31,7 +32,8 @@ import java.util.function.Consumer;
class CommandPart {
- private static final String[] EMPTY_ARRAY = new String[0];
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+ private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
@AllArgsConstructor
private static class CheckArgumentResult {
@@ -41,7 +43,7 @@ class CommandPart {
private AbstractSWCommand command;
private AbstractTypeMapper typeMapper;
- private AbstractValidator validator;
+ private List> validators = new ArrayList<>();
private Class> varArgType;
private String optional;
@@ -53,22 +55,25 @@ class CommandPart {
@Setter
private boolean onlyUseIfNoneIsGiven = false;
- @Setter
- private boolean allowNullValues = false;
-
private Parameter parameter;
private int parameterIndex;
- public CommandPart(AbstractSWCommand command, AbstractTypeMapper typeMapper, AbstractValidator validator, Class> varArgType, String optional, Parameter parameter, int parameterIndex) {
+ public CommandPart(AbstractSWCommand command, AbstractTypeMapper typeMapper, Class> varArgType, String optional, Parameter parameter, int parameterIndex) {
this.command = command;
this.typeMapper = typeMapper;
- this.validator = validator;
this.varArgType = varArgType;
this.optional = optional;
this.parameter = parameter;
this.parameterIndex = parameterIndex;
- validatePart();
+ if (optional != null && varArgType != null) {
+ throw new IllegalArgumentException("A vararg part can't have an optional part! In method " + parameter.getDeclaringExecutable() + " with parameter " + parameterIndex);
+ }
+ }
+
+ void addValidator(AbstractValidator validator) {
+ if (validator == null) return;
+ validators.add(validator);
}
public void setNext(CommandPart next) {
@@ -78,41 +83,33 @@ class CommandPart {
this.next = next;
}
- private void validatePart() {
- if (optional != null && varArgType != null) {
- throw new IllegalArgumentException("A vararg part can't have an optional part! In method " + parameter.getDeclaringExecutable() + " with parameter " + parameterIndex);
- }
- }
-
public void generateArgumentArray(Consumer errors, List