/*
* 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();
}
}