/* * 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); } }); } 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 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 && !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) -> { 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 != 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(); } }