/*
* 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.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.BiPredicate;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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> helpCommandList = new ArrayList<>();
private final Map> localTypeMapper = new HashMap<>();
private final Map> localValidators = 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;
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();
}
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 void sendMessage(T sender, String message, Object[] args) {
}
protected void initialisePartOf(AbstractSWCommand parent) {
}
protected final void execute(T sender, String alias, String[] args) {
initialize();
List errors = new ArrayList<>();
try {
if (commandList.stream().noneMatch(s -> s.invoke(errors::add, sender, alias, args))) {
errors.forEach(Runnable::run);
} else return;
if (errors.isEmpty() && helpCommandList.stream().noneMatch(s -> s.invoke(errors::add, sender, alias, args))) {
errors.forEach(Runnable::run);
}
} 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 Stream.concat(commandList.stream(), helpCommandList.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) || string.startsWith(s.toLowerCase()))
.distinct()
.collect(Collectors.toList());
}
private synchronized void initialize() {
if (initialized) return;
List methods = methods().stream()
.filter(this::validateMethod)
.collect(Collectors.toList());
for (Method method : methods) {
Cached cached = method.getAnnotation(Cached.class);
this.>add(Mapper.class, method, (anno, typeMapper) -> {
TabCompletionCache.add(typeMapper, cached);
(anno.local() ? ((Map) localTypeMapper) : SWCommandUtils.getMAPPER_FUNCTIONS()).put(anno.value(), typeMapper);
});
this.>add(ClassMapper.class, method, (anno, typeMapper) -> {
TabCompletionCache.add(typeMapper, cached);
(anno.local() ? ((Map) localTypeMapper) : SWCommandUtils.getMAPPER_FUNCTIONS()).put(anno.value().getName(), typeMapper);
});
this.>add(Validator.class, method, (anno, validator) -> {
(anno.local() ? ((Map) localValidators) : SWCommandUtils.getVALIDATOR_FUNCTIONS()).put(anno.value(), 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, true, (anno, parameters) -> {
for (int i = 1; i < parameters.length; i++) {
Parameter parameter = parameters[i];
Class> clazz = parameter.getType();
if (parameter.isVarArgs()) clazz = clazz.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, localValidators, anno.description(), anno.noTabComplete()));
});
}
if (dependencyMap.containsKey(this.getClass())) {
dependencyMap.get(this.getClass()).forEach(abstractSWCommand -> {
abstractSWCommand.localTypeMapper.putAll((Map) localTypeMapper);
abstractSWCommand.localValidators.putAll((Map) localValidators);
abstractSWCommand.initialisePartOf(this);
abstractSWCommand.initialize();
commandList.addAll((Collection) abstractSWCommand.commandList);
});
}
Collections.sort(commandList);
commandList.removeIf(subCommand -> {
if (subCommand.isHelp) {
helpCommandList.add(subCommand);
return true;
} else {
return false;
}
});
initialized = true;
}
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 (firstParameter && !clazz.isAssignableFrom(parameters[0].getType())) {
commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking the first parameter of type '" + clazz.getTypeName() + "'");
return;
}
Arrays.stream(anno).forEach(t -> consumer.accept(t, parameters));
}
private void add(Class annotation, Method method, BiConsumer consumer) {
add(annotation, method, false, (anno, parameters) -> {
try {
method.setAccessible(true);
consumer.accept(anno, (K) 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 != AbstractSWCommand.class) {
methods.addAll(Arrays.asList(current.getDeclaredMethods()));
current = current.getSuperclass();
}
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 {};
boolean noTabComplete() default false;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@CommandMetaData.Method(value = void.class, minParameterCount = 1)
@interface Registeres {
Register[] value();
}
}
@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();
boolean local() default false;
}
@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();
/**
* 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 {@link #falseValues()} defines which indices are
* considered {@code false} or {@code true}.
*/
boolean allowISE() default false;
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)
@Target({ElementType.PARAMETER})
protected @interface OptionalValue {
/**
* Will pe parsed against the TypeMapper specified by the parameter or annotation.
*/
String value();
/**
* The method name stands for 'onlyUseIfNoneIsGiven'.
*/
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.
*/
String value();
/**
* 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;
}
}
}
}