diff --git a/src/de/steamwar/command/AbstractGuardChecker.java b/src/de/steamwar/command/AbstractGuardChecker.java index f5f2597..dd4c555 100644 --- a/src/de/steamwar/command/AbstractGuardChecker.java +++ b/src/de/steamwar/command/AbstractGuardChecker.java @@ -19,6 +19,7 @@ package de.steamwar.command; +@Deprecated @FunctionalInterface public interface AbstractGuardChecker { /** diff --git a/src/de/steamwar/command/AbstractSWCommand.java b/src/de/steamwar/command/AbstractSWCommand.java index d654df7..1de5785 100644 --- a/src/de/steamwar/command/AbstractSWCommand.java +++ b/src/de/steamwar/command/AbstractSWCommand.java @@ -23,6 +23,7 @@ import java.lang.annotation.*; 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.Supplier; @@ -38,6 +39,7 @@ public abstract class AbstractSWCommand { private final Map> localTypeMapper = new HashMap<>(); private final Map> localGuardChecker = new HashMap<>(); + private final Map> localValidators = new HashMap<>(); protected AbstractSWCommand(Class clazz, String command) { this(clazz, command, new String[0]); @@ -64,11 +66,18 @@ public abstract class AbstractSWCommand { System.out.println(message.get()); } + 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(sender, alias, args))) { - commandHelpList.stream().anyMatch(s -> s.invoke(sender, alias, args)); + 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)); } } catch (CommandNoHelpException e) { // Ignored @@ -88,6 +97,7 @@ public abstract class AbstractSWCommand { .flatMap(Collection::stream) .filter(s -> !s.isEmpty()) .filter(s -> s.toLowerCase().startsWith(string)) + .distinct() .collect(Collectors.toList()); } @@ -99,7 +109,9 @@ public abstract class AbstractSWCommand { private synchronized void createMapping() { List methods = methods(); for (Method method : methods) { + Cached cached = method.getAnnotation(Cached.class); addMapper(Mapper.class, method, i -> i == 0, false, AbstractTypeMapper.class, (anno, typeMapper) -> { + TabCompletionCache.add(typeMapper, cached); if (anno.local()) { localTypeMapper.putIfAbsent(anno.value(), (AbstractTypeMapper) typeMapper); } else { @@ -107,6 +119,7 @@ public abstract class AbstractSWCommand { } }); addMapper(ClassMapper.class, method, i -> i == 0, false, AbstractTypeMapper.class, (anno, typeMapper) -> { + TabCompletionCache.add(typeMapper, cached); if (anno.local()) { localTypeMapper.putIfAbsent(anno.value().getTypeName(), (AbstractTypeMapper) typeMapper); } else { @@ -127,6 +140,20 @@ public abstract class AbstractSWCommand { SWCommandUtils.getGUARD_FUNCTIONS().putIfAbsent(anno.value().getTypeName(), guardChecker); } }); + 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); + } + }); + 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); + } + }); } for (Method method : methods) { add(Register.class, method, i -> i > 0, true, null, (anno, parameters) -> { @@ -141,7 +168,7 @@ public abstract class AbstractSWCommand { 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())); + commandHelpList.add(new SubCommand<>(this, method, anno.value(), new HashMap<>(), new HashMap<>(), localGuardChecker, true, null, anno.noTabComplete())); }); add(Register.class, method, i -> i > 0, true, null, (anno, parameters) -> { @@ -162,7 +189,7 @@ public abstract class AbstractSWCommand { return; } } - commandList.add(new SubCommand<>(this, method, anno.value(), localTypeMapper, localGuardChecker, false, anno.description(), anno.noTabComplete())); + commandList.add(new SubCommand<>(this, method, anno.value(), localTypeMapper, localValidators, localGuardChecker, false, anno.description(), anno.noTabComplete())); }); } @@ -217,6 +244,18 @@ public abstract class AbstractSWCommand { }); } + 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)); + } catch (Exception e) { + throw new SecurityException(e.getMessage(), e); + } + }); + } + + @Deprecated private void addGuard(Class annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class returnType, BiConsumer> consumer) { add(annotation, method, parameterTester, firstParameter, returnType, (anno, parameters) -> { try { @@ -280,6 +319,15 @@ public abstract class AbstractSWCommand { boolean local() default false; } + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD}) + protected @interface Cached { + long cacheDuration() default 5; + TimeUnit timeUnit() default TimeUnit.SECONDS; + boolean global() default false; + } + + @Deprecated @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD}) protected @interface Guard { @@ -288,6 +336,7 @@ public abstract class AbstractSWCommand { boolean local() default false; } + @Deprecated @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) protected @interface ClassGuard { @@ -296,6 +345,22 @@ public abstract class AbstractSWCommand { boolean local() default false; } + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER, ElementType.METHOD}) + protected @interface Validator { + String value() default ""; + + boolean local() default false; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD}) + protected @interface ClassValidator { + Class value(); + + boolean local() default false; + } + @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) protected @interface StaticValue { @@ -320,4 +385,18 @@ public abstract class AbstractSWCommand { */ String value(); } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER}) + 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; + } } diff --git a/src/de/steamwar/command/AbstractTypeMapper.java b/src/de/steamwar/command/AbstractTypeMapper.java index 744b726..4a340d8 100644 --- a/src/de/steamwar/command/AbstractTypeMapper.java +++ b/src/de/steamwar/command/AbstractTypeMapper.java @@ -21,11 +21,16 @@ package de.steamwar.command; import java.util.Collection; -public interface AbstractTypeMapper { +public interface AbstractTypeMapper extends AbstractValidator { /** * The CommandSender can be null! */ T map(K sender, String[] previousArguments, String s); + @Override + default boolean validate(K sender, T value, MessageSender messageSender) { + return true; + } + Collection tabCompletes(K sender, String[] previousArguments, String s); } diff --git a/src/de/steamwar/command/AbstractValidator.java b/src/de/steamwar/command/AbstractValidator.java new file mode 100644 index 0000000..60efd19 --- /dev/null +++ b/src/de/steamwar/command/AbstractValidator.java @@ -0,0 +1,39 @@ +/* + * 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; + +@FunctionalInterface +public interface AbstractValidator { + + /** + * Validates the given value. + * + * @param sender The sender of the command. + * @param value The value to validate or null if mapping returned null. + * @param messageSender The message sender to send messages to the player. Never send messages directly to the player. + * @return The result of the validation. + */ + boolean validate(K sender, T value, MessageSender messageSender); + + @FunctionalInterface + interface MessageSender { + void send(String s, Object... args); + } +} diff --git a/src/de/steamwar/command/CommandPart.java b/src/de/steamwar/command/CommandPart.java index 0688caf..d5a5220 100644 --- a/src/de/steamwar/command/CommandPart.java +++ b/src/de/steamwar/command/CommandPart.java @@ -23,9 +23,11 @@ import lombok.AllArgsConstructor; import lombok.Setter; import java.lang.reflect.Array; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.function.Consumer; class CommandPart { @@ -37,7 +39,9 @@ class CommandPart { private final Object value; } + private AbstractSWCommand command; private AbstractTypeMapper typeMapper; + private AbstractValidator validator; private AbstractGuardChecker guardChecker; private Class varArgType; private String optional; @@ -48,8 +52,10 @@ class CommandPart { @Setter private boolean ignoreAsArgument = false; - public CommandPart(AbstractTypeMapper typeMapper, AbstractGuardChecker guardChecker, Class varArgType, String optional, GuardCheckType guardCheckType) { + public CommandPart(AbstractSWCommand command, AbstractTypeMapper typeMapper, AbstractValidator validator, AbstractGuardChecker guardChecker, Class varArgType, String optional, GuardCheckType guardCheckType) { + this.command = command; this.typeMapper = typeMapper; + this.validator = validator; this.guardChecker = guardChecker; this.varArgType = varArgType; this.optional = optional; @@ -82,21 +88,26 @@ class CommandPart { } } - public void generateArgumentArray(List current, T sender, String[] args, int startIndex) { + public void generateArgumentArray(Consumer errors, List current, T sender, String[] args, int startIndex) { if (varArgType != null) { Object array = Array.newInstance(varArgType, args.length - startIndex); for (int i = startIndex; i < args.length; i++) { - CheckArgumentResult validArgument = checkArgument(null, sender, args, i); + CheckArgumentResult validArgument = checkArgument(null, null, sender, args, i); if (!validArgument.success) { throw new CommandParseException(); } Array.set(array, i - startIndex, validArgument.value); } + if (validator != null && !validator.validate(sender, array, (s, objects) -> { + errors.accept(() -> command.sendMessage(sender, s, objects)); + })) { + throw new CommandParseException(); + } current.add(array); return; } - CheckArgumentResult validArgument = checkArgument(null, sender, args, startIndex); + CheckArgumentResult validArgument = checkArgument(errors, null, sender, args, startIndex); if (!validArgument.success && optional == null) { throw new CommandParseException(); } @@ -105,7 +116,7 @@ class CommandPart { current.add(typeMapper.map(sender, EMPTY_ARRAY, optional)); } if (next != null) { - next.generateArgumentArray(current, sender, args, startIndex); + next.generateArgumentArray(errors, current, sender, args, startIndex); } return; } @@ -113,7 +124,7 @@ class CommandPart { current.add(validArgument.value); } if (next != null) { - next.generateArgumentArray(current, sender, args, startIndex + 1); + next.generateArgumentArray(errors, current, sender, args, startIndex + 1); } } @@ -153,12 +164,12 @@ class CommandPart { public void generateTabComplete(List current, T sender, String[] args, int startIndex) { if (varArgType != null) { for (int i = startIndex; i < args.length - 1; i++) { - CheckArgumentResult validArgument = checkArgument(GuardCheckType.TAB_COMPLETE, sender, args, i); + CheckArgumentResult validArgument = checkArgument((ignore) -> {}, GuardCheckType.TAB_COMPLETE, sender, args, i); if (!validArgument.success) { return; } } - Collection strings = typeMapper.tabCompletes(sender, Arrays.copyOf(args, args.length - 1), args[args.length - 1]); + Collection strings = tabCompletes(sender, args, args.length - 1); if (strings != null) { current.addAll(strings); } @@ -166,7 +177,7 @@ class CommandPart { } if (args.length - 1 > startIndex) { - CheckArgumentResult checkArgumentResult = checkArgument(GuardCheckType.TAB_COMPLETE, sender, args, startIndex); + CheckArgumentResult checkArgumentResult = checkArgument((ignore) -> {}, GuardCheckType.TAB_COMPLETE, sender, args, startIndex); if (checkArgumentResult.success && next != null) { next.generateTabComplete(current, sender, args, startIndex + 1); return; @@ -177,7 +188,7 @@ class CommandPart { return; } - Collection strings = typeMapper.tabCompletes(sender, Arrays.copyOf(args, startIndex), args[startIndex]); + Collection strings = tabCompletes(sender, args, startIndex); if (strings != null) { current.addAll(strings); } @@ -186,12 +197,29 @@ class CommandPart { } } - private CheckArgumentResult checkArgument(GuardCheckType guardCheckType, T sender, String[] args, int index) { + private Collection tabCompletes(T sender, String[] args, int startIndex) { + return TabCompletionCache.tabComplete(sender, typeMapper, command, () -> { + return typeMapper.tabCompletes(sender, Arrays.copyOf(args, startIndex), args[startIndex]); + }); + } + + private CheckArgumentResult checkArgument(Consumer errors, GuardCheckType guardCheckType, T sender, String[] args, int index) { try { Object value = typeMapper.map(sender, Arrays.copyOf(args, index), args[index]); + if (validator != null && errors != null) { + if (!validator.validate(sender, value, (s, objects) -> { + errors.accept(() -> { + command.sendMessage(sender, s, objects); + }); + })) { + return new CheckArgumentResult(false, null); + } + return new CheckArgumentResult(value != null, value); + } if (value == null) { return new CheckArgumentResult(false, null); } + GuardResult guardResult = checkGuard(guardCheckType, sender, args, index); switch (guardResult) { case ALLOWED: diff --git a/src/de/steamwar/command/GuardCheckType.java b/src/de/steamwar/command/GuardCheckType.java index 0f023b8..d2e6ed0 100644 --- a/src/de/steamwar/command/GuardCheckType.java +++ b/src/de/steamwar/command/GuardCheckType.java @@ -19,6 +19,7 @@ package de.steamwar.command; +@Deprecated public enum GuardCheckType { COMMAND, HELP_COMMAND, diff --git a/src/de/steamwar/command/GuardResult.java b/src/de/steamwar/command/GuardResult.java index 9ffbf77..3e6aafc 100644 --- a/src/de/steamwar/command/GuardResult.java +++ b/src/de/steamwar/command/GuardResult.java @@ -19,6 +19,7 @@ package de.steamwar.command; +@Deprecated public enum GuardResult { ALLOWED, DENIED_WITH_HELP, diff --git a/src/de/steamwar/command/SWCommandUtils.java b/src/de/steamwar/command/SWCommandUtils.java index aaf0c7a..9620631 100644 --- a/src/de/steamwar/command/SWCommandUtils.java +++ b/src/de/steamwar/command/SWCommandUtils.java @@ -23,11 +23,13 @@ import lombok.Getter; import lombok.experimental.UtilityClass; import java.lang.annotation.Annotation; +import java.lang.reflect.Array; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.*; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.stream.Collectors; @UtilityClass public class SWCommandUtils { @@ -36,8 +38,12 @@ public class SWCommandUtils { private final Map> MAPPER_FUNCTIONS = new HashMap<>(); @Getter + @Deprecated private final Map> GUARD_FUNCTIONS = new HashMap<>(); + @Getter + private final Map> VALIDATOR_FUNCTIONS = new HashMap<>(); + private SWTypeMapperCreator swTypeMapperCreator = (mapper, tabCompleter) -> new AbstractTypeMapper() { @Override public Object map(Object sender, String[] previousArguments, String s) { @@ -72,11 +78,11 @@ public class SWCommandUtils { MAPPER_FUNCTIONS.put(alternativeClazz.getTypeName(), mapper); } - static CommandPart generateCommandPart(boolean help, String[] subCommand, Parameter[] parameters, Map> localTypeMapper, Map> localGuardChecker) { + static CommandPart generateCommandPart(AbstractSWCommand command, boolean help, String[] subCommand, Parameter[] parameters, Map> localTypeMapper, Map> localValidator, Map> localGuardChecker) { CommandPart first = null; CommandPart current = null; for (String s : subCommand) { - CommandPart commandPart = new CommandPart(createMapper(s), null, null, null, help ? GuardCheckType.HELP_COMMAND : GuardCheckType.COMMAND); + CommandPart commandPart = new CommandPart(command, createMapper(s), null, null, null, null, help ? GuardCheckType.HELP_COMMAND : GuardCheckType.COMMAND); commandPart.setIgnoreAsArgument(true); if (current != null) { current.setNext(commandPart); @@ -89,11 +95,12 @@ public class SWCommandUtils { for (int i = 1; i < parameters.length; i++) { Parameter parameter = parameters[i]; AbstractTypeMapper typeMapper = getTypeMapper(parameter, localTypeMapper); + AbstractValidator validator = (AbstractValidator) getValidator(parameter, localValidator); AbstractGuardChecker guardChecker = getGuardChecker(parameter, localGuardChecker); Class varArgType = parameter.isVarArgs() ? parameter.getType().getComponentType() : null; AbstractSWCommand.OptionalValue optionalValue = parameter.getAnnotation(AbstractSWCommand.OptionalValue.class); - CommandPart commandPart = new CommandPart<>(typeMapper, guardChecker, varArgType, optionalValue != null ? optionalValue.value() : null, help ? GuardCheckType.HELP_COMMAND : GuardCheckType.COMMAND); + CommandPart commandPart = new CommandPart<>(command, typeMapper, validator, guardChecker, varArgType, optionalValue != null ? optionalValue.value() : null, help ? GuardCheckType.HELP_COMMAND : GuardCheckType.COMMAND); if (current != null) { current.setNext(commandPart); } @@ -114,7 +121,7 @@ public class SWCommandUtils { AbstractSWCommand.ClassMapper classMapper = parameter.getAnnotation(AbstractSWCommand.ClassMapper.class); AbstractSWCommand.Mapper mapper = parameter.getAnnotation(AbstractSWCommand.Mapper.class); if (clazz.isEnum() && classMapper == null && mapper == null && !MAPPER_FUNCTIONS.containsKey(clazz.getTypeName()) && !localTypeMapper.containsKey(clazz.getTypeName())) { - return (AbstractTypeMapper) createEnumMapper((Class>) clazz); + return createEnumMapper((Class>) clazz); } String name = clazz.getTypeName(); @@ -160,6 +167,48 @@ public class SWCommandUtils { return typeMapper; } + public static AbstractValidator getValidator(Parameter parameter, Map> localValidator) { + Class clazz = parameter.getType(); + + AbstractSWCommand.ClassValidator classValidator = parameter.getAnnotation(AbstractSWCommand.ClassValidator.class); + if (classValidator != null) { + if (classValidator.value() != null) { + return getValidator(classValidator.value().getTypeName(), localValidator); + } + return getValidator(clazz.getTypeName(), localValidator); + } + + AbstractSWCommand.Validator validator = parameter.getAnnotation(AbstractSWCommand.Validator.class); + if (validator != null) { + if (validator.value() != null && !validator.value().isEmpty()) { + return getValidator(validator.value(), localValidator); + } + return getValidator(clazz.getTypeName(), localValidator); + } + + AbstractSWCommand.ErrorMessage errorMessage = parameter.getAnnotation(AbstractSWCommand.ErrorMessage.class); + if (errorMessage != null) { + return (AbstractValidator) (sender, value, 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; + }; + } + return null; + } + + private static AbstractValidator getValidator(String s, Map> localGuardChecker) { + AbstractValidator validator = localGuardChecker.getOrDefault(s, (AbstractValidator) VALIDATOR_FUNCTIONS.getOrDefault(s, null)); + if (validator == null) { + throw new IllegalArgumentException("No validator found for " + s); + } + return validator; + } + + @Deprecated public static AbstractGuardChecker getGuardChecker(Parameter parameter, Map> localGuardChecker) { Class clazz = parameter.getType(); if (parameter.isVarArgs()) { @@ -184,6 +233,7 @@ public class SWCommandUtils { return null; } + @Deprecated private static AbstractGuardChecker getGuardChecker(String s, Map> localGuardChecker) { AbstractGuardChecker guardChecker = localGuardChecker.getOrDefault(s, (AbstractGuardChecker) GUARD_FUNCTIONS.getOrDefault(s, null)); if (guardChecker == null) { @@ -200,17 +250,28 @@ public class SWCommandUtils { MAPPER_FUNCTIONS.putIfAbsent(name, mapper); } + public static void addValidator(Class clazz, AbstractValidator validator) { + addValidator(clazz.getTypeName(), validator); + } + + public static void addValidator(String name, AbstractValidator validator) { + VALIDATOR_FUNCTIONS.putIfAbsent(name, validator); + } + + @Deprecated public static void addGuard(Class clazz, AbstractGuardChecker guardChecker) { addGuard(clazz.getTypeName(), guardChecker); } + @Deprecated public static void addGuard(String name, AbstractGuardChecker guardChecker) { GUARD_FUNCTIONS.putIfAbsent(name, guardChecker); } public static , K> T createMapper(String... values) { - List strings = Arrays.asList(values); - return createMapper((s) -> strings.contains(s) ? s : null, s -> strings); + List strings = Arrays.stream(values).map(String::toLowerCase).collect(Collectors.toList()); + List tabCompletes = Arrays.asList(values); + return createMapper(s -> strings.contains(s.toLowerCase()) ? s : null, s -> tabCompletes); } public static , K, V> T createMapper(Function mapper, Function> tabCompleter) { @@ -252,7 +313,7 @@ public class SWCommandUtils { } static T[] getAnnotation(Method method, Class annotation) { - if (method.getAnnotations().length != 1) return null; + if (method.getAnnotations().length == 0) return null; return method.getDeclaredAnnotationsByType(annotation); } } diff --git a/src/de/steamwar/command/SubCommand.java b/src/de/steamwar/command/SubCommand.java index 603e6d3..d3a266c 100644 --- a/src/de/steamwar/command/SubCommand.java +++ b/src/de/steamwar/command/SubCommand.java @@ -25,6 +25,7 @@ import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -36,13 +37,14 @@ public class SubCommand { String[] subCommand; private Predicate senderPredicate; private Function senderFunction; + AbstractValidator validator; AbstractGuardChecker guardChecker; boolean noTabComplete; int comparableValue; private CommandPart commandPart; - SubCommand(AbstractSWCommand abstractSWCommand, Method method, String[] subCommand, Map> localTypeMapper, Map> localGuardChecker, boolean help, String[] description, boolean noTabComplete) { + SubCommand(AbstractSWCommand abstractSWCommand, Method method, String[] subCommand, Map> localTypeMapper, Map> localValidator, Map> localGuardChecker, boolean help, String[] description, boolean noTabComplete) { this.abstractSWCommand = abstractSWCommand; this.method = method; this.subCommand = subCommand; @@ -52,15 +54,16 @@ public class SubCommand { Parameter[] parameters = method.getParameters(); comparableValue = parameters[parameters.length - 1].isVarArgs() ? Integer.MAX_VALUE : -parameters.length; + validator = (AbstractValidator) SWCommandUtils.getValidator(parameters[0], localValidator); guardChecker = SWCommandUtils.getGuardChecker(parameters[0], localGuardChecker); - commandPart = SWCommandUtils.generateCommandPart(help, subCommand, parameters, localTypeMapper, localGuardChecker); + commandPart = SWCommandUtils.generateCommandPart(abstractSWCommand, help, subCommand, parameters, localTypeMapper, localValidator, localGuardChecker); senderPredicate = t -> parameters[0].getType().isAssignableFrom(t.getClass()); senderFunction = t -> parameters[0].getType().cast(t); } - boolean invoke(T sender, String alias, String[] args) { + boolean invoke(Consumer errors, T sender, String alias, String[] args) { try { if (!senderPredicate.test(sender)) { return false; @@ -70,12 +73,36 @@ public class SubCommand { if (args.length != 0) { return false; } + if (validator != null) { + if (!validator.validate(sender, sender, (s, objectArgs) -> { + abstractSWCommand.sendMessage(sender, s, objectArgs); + })) { + throw new CommandNoHelpException(); + } + } else if (guardChecker != null) { + GuardResult guardResult = guardChecker.guard(sender, GuardCheckType.COMMAND, new String[0], null); + switch (guardResult) { + case ALLOWED: + break; + case DENIED: + throw new CommandNoHelpException(); + case DENIED_WITH_HELP: + default: + return true; + } + } method.setAccessible(true); method.invoke(abstractSWCommand, senderFunction.apply(sender)); } else { List objects = new ArrayList<>(); - commandPart.generateArgumentArray(objects, sender, args, 0); - if (guardChecker != null) { + commandPart.generateArgumentArray(errors, objects, sender, args, 0); + if (validator != null) { + if (!validator.validate(sender, sender, (s, objectArgs) -> { + abstractSWCommand.sendMessage(sender, s, objectArgs); + })) { + throw new CommandNoHelpException(); + } + } else if (guardChecker != null) { GuardResult guardResult = guardChecker.guard(sender, GuardCheckType.COMMAND, new String[0], null); switch (guardResult) { case ALLOWED: @@ -105,7 +132,13 @@ public class SubCommand { } List tabComplete(T sender, String[] args) { - if (guardChecker != null && guardChecker.guard(sender, GuardCheckType.TAB_COMPLETE, new String[0], null) != GuardResult.ALLOWED) { + if (validator != null) { + if (!validator.validate(sender, sender, (s, objects) -> { + // ignore + })) { + return null; + } + } else if (guardChecker != null && guardChecker.guard(sender, GuardCheckType.TAB_COMPLETE, new String[0], null) != GuardResult.ALLOWED) { return null; } if (commandPart == null) { diff --git a/src/de/steamwar/command/TabCompletionCache.java b/src/de/steamwar/command/TabCompletionCache.java new file mode 100644 index 0000000..5cf555c --- /dev/null +++ b/src/de/steamwar/command/TabCompletionCache.java @@ -0,0 +1,84 @@ +/* + * 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 lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.experimental.UtilityClass; + +import java.util.*; +import java.util.function.Supplier; + +@UtilityClass +public class TabCompletionCache { + + private Map tabCompletionCache = new HashMap<>(); + Set> cached = new HashSet<>(); + Set> global = new HashSet<>(); + Map, Long> cacheDuration = new HashMap<>(); + + void add(AbstractTypeMapper typeMapper, AbstractSWCommand.Cached cached) { + if (cached != null) { + TabCompletionCache.cached.add(typeMapper); + if (cached.global()) TabCompletionCache.global.add(typeMapper); + TabCompletionCache.cacheDuration.put(typeMapper, cached.timeUnit().toMillis(cached.cacheDuration())); + } + } + + @EqualsAndHashCode + @AllArgsConstructor + private static class Key { + private Object sender; + private AbstractTypeMapper typeMapper; + } + + @AllArgsConstructor + private static class TabCompletions { + private AbstractSWCommand command; + private long timestamp; + private Collection tabCompletions; + } + + Collection tabComplete(Object sender, AbstractTypeMapper typeMapper, AbstractSWCommand command, Supplier> tabCompleteSupplier) { + if (!cached.contains(typeMapper)) return tabCompleteSupplier.get(); + Key key = global.contains(typeMapper) ? new Key(null, typeMapper) : new Key(sender, typeMapper); + TabCompletions tabCompletions = tabCompletionCache.computeIfAbsent(key, ignore -> { + return new TabCompletions(command, System.currentTimeMillis(), tabCompleteSupplier.get()); + }); + if (tabCompletions.command != command || System.currentTimeMillis() - tabCompletions.timestamp > cacheDuration.get(typeMapper)) { + tabCompletions = new TabCompletions(command, System.currentTimeMillis(), tabCompleteSupplier.get()); + tabCompletionCache.put(key, tabCompletions); + } + tabCompletions.timestamp = System.currentTimeMillis(); + return tabCompletions.tabCompletions; + } + + public void invalidateOldEntries() { + Set toRemove = new HashSet<>(); + for (Map.Entry tabCompletionsEntry : tabCompletionCache.entrySet()) { + if (System.currentTimeMillis() - tabCompletionsEntry.getValue().timestamp > cacheDuration.get(tabCompletionsEntry.getKey().typeMapper)) { + toRemove.add(tabCompletionsEntry.getKey()); + } + } + for (Key key : toRemove) { + tabCompletionCache.remove(key); + } + } +} diff --git a/src/de/steamwar/network/packets/MetaInfos.java b/src/de/steamwar/network/packets/MetaInfos.java new file mode 100644 index 0000000..61632d7 --- /dev/null +++ b/src/de/steamwar/network/packets/MetaInfos.java @@ -0,0 +1,23 @@ +/* + * 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.network.packets; + +public interface MetaInfos { +} diff --git a/src/de/steamwar/network/packets/NetworkPacket.java b/src/de/steamwar/network/packets/NetworkPacket.java index b1d6bf3..987e1ef 100644 --- a/src/de/steamwar/network/packets/NetworkPacket.java +++ b/src/de/steamwar/network/packets/NetworkPacket.java @@ -27,6 +27,13 @@ import java.io.*; @EqualsAndHashCode public abstract class NetworkPacket implements Serializable { + private static final long serialVersionUID = -3168992457669156473L; + private transient MetaInfos metaInfos; + + public MetaInfos getMetaInfos() { + return metaInfos; + } + @SneakyThrows public byte[] serialize() { ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -40,6 +47,12 @@ public abstract class NetworkPacket implements Serializable { PacketHandler.handlePacket(deserialize(data)); } + public static void handle(MetaInfos metaInfos, byte[] data) { + NetworkPacket networkPacket = deserialize(data); + networkPacket.metaInfos = metaInfos; + PacketHandler.handlePacket(networkPacket); + } + @SneakyThrows public static NetworkPacket deserialize(byte[] data) { ByteArrayInputStream bais = new ByteArrayInputStream(data); diff --git a/src/de/steamwar/network/packets/PacketHandler.java b/src/de/steamwar/network/packets/PacketHandler.java index f23afe5..671eb46 100644 --- a/src/de/steamwar/network/packets/PacketHandler.java +++ b/src/de/steamwar/network/packets/PacketHandler.java @@ -47,6 +47,7 @@ public abstract class PacketHandler { if(method.getParameterCount() != 1 || !NetworkPacket.class.isAssignableFrom(method.getParameterTypes()[0])) { continue; } + if (method.isAnnotationPresent(Handler.class)) { Class packetClass = (Class) method.getParameterTypes()[0]; HANDLER_MAP.put(packetClass, method); diff --git a/src/de/steamwar/network/packets/server/LocaleInvalidationPacket.java b/src/de/steamwar/network/packets/server/LocaleInvalidationPacket.java new file mode 100644 index 0000000..1e24547 --- /dev/null +++ b/src/de/steamwar/network/packets/server/LocaleInvalidationPacket.java @@ -0,0 +1,35 @@ +/* + * 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.network.packets.server; + +import de.steamwar.network.packets.NetworkPacket; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Getter +public class LocaleInvalidationPacket extends NetworkPacket { + private static final long serialVersionUID = 1113963147008168582L; + private int playerId; +} diff --git a/testsrc/de/steamwar/command/CacheCommand.java b/testsrc/de/steamwar/command/CacheCommand.java new file mode 100644 index 0000000..1771bca --- /dev/null +++ b/testsrc/de/steamwar/command/CacheCommand.java @@ -0,0 +1,57 @@ +/* + * 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 de.steamwar.command.dto.TestSWCommand; +import de.steamwar.command.dto.TestTypeMapper; + +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicInteger; + +public class CacheCommand extends TestSWCommand { + + public CacheCommand() { + super("typemapper"); + } + + @Register + public void test(String sender, int tabCompleteTest) { + } + + private AtomicInteger count = new AtomicInteger(); + + @Cached + @Mapper(value = "int", local = true) + public AbstractTypeMapper typeMapper() { + System.out.println("TypeMapper register"); + return new TestTypeMapper() { + @Override + public Integer map(String sender, String[] previousArguments, String s) { + return Integer.parseInt(s); + } + + @Override + public Collection tabCompletes(String sender, String[] previousArguments, String s) { + return Arrays.asList(count.getAndIncrement() + ""); + } + }; + } +} diff --git a/testsrc/de/steamwar/command/CacheCommandTest.java b/testsrc/de/steamwar/command/CacheCommandTest.java new file mode 100644 index 0000000..122528f --- /dev/null +++ b/testsrc/de/steamwar/command/CacheCommandTest.java @@ -0,0 +1,46 @@ +/* + * 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 org.junit.Test; + +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class CacheCommandTest { + + @Test + public void testCaching() { + CacheCommand cmd = new CacheCommand(); + List tabCompletions1 = cmd.tabComplete("test", "", new String[]{""}); + List tabCompletions2 = cmd.tabComplete("test", "", new String[]{""}); + assertThat(tabCompletions1, is(equalTo(tabCompletions2))); + } + + @Test + public void testCachingWithDifferentSenders() { + CacheCommand cmd = new CacheCommand(); + List tabCompletions1 = cmd.tabComplete("test", "", new String[]{""}); + List tabCompletions2 = cmd.tabComplete("test2", "", new String[]{""}); + assertThat(tabCompletions1, is(not(equalTo(tabCompletions2)))); + } +} diff --git a/testsrc/de/steamwar/command/GuardCommandTest.java b/testsrc/de/steamwar/command/GuardCommandTest.java index 233333c..51eae0e 100644 --- a/testsrc/de/steamwar/command/GuardCommandTest.java +++ b/testsrc/de/steamwar/command/GuardCommandTest.java @@ -35,7 +35,7 @@ public class GuardCommandTest { try { cmd.execute("test", "", new String[0]); } catch (Exception e) { - assertCMDFramework(e, ExecutionIdentifier.class, "RunTypeMapper"); + assertThat(e.getMessage(), is("GuardChecker COMMAND")); } } diff --git a/testsrc/de/steamwar/command/ValidatorCommand.java b/testsrc/de/steamwar/command/ValidatorCommand.java new file mode 100644 index 0000000..ea95a5e --- /dev/null +++ b/testsrc/de/steamwar/command/ValidatorCommand.java @@ -0,0 +1,60 @@ +/* + * 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 de.steamwar.command.dto.ExecutionIdentifier; +import de.steamwar.command.dto.TestSWCommand; +import de.steamwar.command.dto.TestValidator; + +public class ValidatorCommand extends TestSWCommand { + + public ValidatorCommand() { + super("testvalidator"); + } + + @Register + public void test(@Validator String sender) { + throw new ExecutionIdentifier("RunTest"); + } + + @Override + protected void sendMessage(String sender, String message, Object[] args) { + if (message.equals("Hello World")) { + throw new ExecutionIdentifier("RunSendMessageWithHelloWorldParameter"); + } + } + + @Register + public void onError(String sender, @ErrorMessage("Hello World") int error) { + throw new ExecutionIdentifier("RunOnError"); + } + + @Register + public void onError(String sender, double error) { + throw new ExecutionIdentifier("RunOnErrorDouble"); + } + + @ClassValidator(value = String.class, local = true) + public TestValidator validator() { + return (sender, value, messageSender) -> { + throw new ExecutionIdentifier("RunValidator"); + }; + } +} diff --git a/testsrc/de/steamwar/command/ValidatorCommandTest.java b/testsrc/de/steamwar/command/ValidatorCommandTest.java new file mode 100644 index 0000000..d9712e3 --- /dev/null +++ b/testsrc/de/steamwar/command/ValidatorCommandTest.java @@ -0,0 +1,63 @@ +/* + * 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 de.steamwar.command.dto.ExecutionIdentifier; +import org.junit.Test; + +import static de.steamwar.AssertionUtils.assertCMDFramework; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class ValidatorCommandTest { + + @Test + public void testValidator() { + ValidatorCommand cmd = new ValidatorCommand(); + try { + cmd.execute("test", "", new String[0]); + assertThat(true, is(false)); + } catch (Exception e) { + assertThat(e.getMessage(), is("RunValidator")); + } + } + + @Test + public void testErrorMessage() { + ValidatorCommand cmd = new ValidatorCommand(); + try { + cmd.execute("test", "", new String[]{"Hello"}); + assertThat(true, is(false)); + } catch (Exception e) { + assertThat(e.getMessage(), is("RunSendMessageWithHelloWorldParameter")); + } + } + + @Test + public void testErrorNoMessage() { + ValidatorCommand cmd = new ValidatorCommand(); + try { + cmd.execute("test", "", new String[]{"0.0"}); + assertThat(true, is(false)); + } catch (Exception e) { + assertCMDFramework(e, ExecutionIdentifier.class, "RunOnErrorDouble"); + } + } +} diff --git a/testsrc/de/steamwar/command/dto/TestValidator.java b/testsrc/de/steamwar/command/dto/TestValidator.java new file mode 100644 index 0000000..8cf0495 --- /dev/null +++ b/testsrc/de/steamwar/command/dto/TestValidator.java @@ -0,0 +1,25 @@ +/* + * 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.dto; + +import de.steamwar.command.AbstractValidator; + +public interface TestValidator extends AbstractValidator { +}