324 Zeilen
13 KiB
Java
324 Zeilen
13 KiB
Java
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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<T> {
|
|
|
|
private Class<?> clazz; // This is used in createMappings()
|
|
|
|
private boolean initialized = false;
|
|
protected final List<SubCommand<T>> commandList = new ArrayList<>();
|
|
protected final List<SubCommand<T>> commandHelpList = new ArrayList<>();
|
|
|
|
private final Map<String, AbstractTypeMapper<T, ?>> localTypeMapper = new HashMap<>();
|
|
private final Map<String, AbstractGuardChecker<T>> localGuardChecker = new HashMap<>();
|
|
|
|
protected AbstractSWCommand(Class<T> clazz, String command) {
|
|
this(clazz, command, new String[0]);
|
|
}
|
|
|
|
protected AbstractSWCommand(Class<T> 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<String> 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<String> 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<Method> 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<T, ?>) 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<T, ?>) 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<T>) 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<T>) guardChecker);
|
|
} else {
|
|
SWCommandUtils.getGUARD_FUNCTIONS().putIfAbsent(anno.value().getTypeName(), guardChecker);
|
|
}
|
|
});
|
|
}
|
|
for (Method method : methods) {
|
|
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()));
|
|
});
|
|
|
|
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 <T extends Annotation> void add(Class<T> annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class<?> returnType, BiConsumer<T, Parameter[]> 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 <T extends Annotation> void addMapper(Class<T> annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class<?> returnType, BiConsumer<T, AbstractTypeMapper<?, ?>> consumer) {
|
|
add(annotation, method, parameterTester, firstParameter, returnType, (anno, parameters) -> {
|
|
try {
|
|
method.setAccessible(true);
|
|
consumer.accept(anno, (AbstractTypeMapper<T, ?>) method.invoke(this));
|
|
} catch (Exception e) {
|
|
throw new SecurityException(e.getMessage(), e);
|
|
}
|
|
});
|
|
}
|
|
|
|
private <T extends Annotation> void addGuard(Class<T> annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class<?> returnType, BiConsumer<T, AbstractGuardChecker<?>> consumer) {
|
|
add(annotation, method, parameterTester, firstParameter, returnType, (anno, parameters) -> {
|
|
try {
|
|
method.setAccessible(true);
|
|
consumer.accept(anno, (AbstractGuardChecker<T>) 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<Method> methods() {
|
|
List<Method> methods = new ArrayList<>();
|
|
Class<?> current = getClass();
|
|
while (current != 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();
|
|
}
|
|
}
|