From bd7635da0d686565fd45496e9966f14d336e3f1f Mon Sep 17 00:00:00 2001 From: yoyosource Date: Sun, 15 Jan 2023 11:23:58 +0100 Subject: [PATCH] Add CommandMetaData.ImplicitValidator for easier extension of implicit validators --- .../steamwar/command/AbstractSWCommand.java | 29 ++--- .../steamwar/command/AbstractTypeMapper.java | 2 + .../{MetaData.java => CommandMetaData.java} | 29 ++++- src/de/steamwar/command/CommandPart.java | 4 + .../command/ImplicitTypeValidators.java | 102 ++++++++++++++++++ src/de/steamwar/command/SubCommand.java | 48 +++++++-- 6 files changed, 192 insertions(+), 22 deletions(-) rename src/de/steamwar/command/{MetaData.java => CommandMetaData.java} (54%) create mode 100644 src/de/steamwar/command/ImplicitTypeValidators.java diff --git a/src/de/steamwar/command/AbstractSWCommand.java b/src/de/steamwar/command/AbstractSWCommand.java index 8ab7cbd..2793c3c 100644 --- a/src/de/steamwar/command/AbstractSWCommand.java +++ b/src/de/steamwar/command/AbstractSWCommand.java @@ -183,7 +183,7 @@ public abstract class AbstractSWCommand { private boolean validateMethod(Method method) { if (!checkType(method.getAnnotations(), method.getReturnType(), annotation -> { - MetaData.Method methodMetaData = annotation.annotationType().getAnnotation(MetaData.Method.class); + CommandMetaData.Method methodMetaData = annotation.annotationType().getAnnotation(CommandMetaData.Method.class); if (methodMetaData == null) return null; if (method.getParameterCount() > methodMetaData.maxParameterCount() || method.getParameterCount() < methodMetaData.minParameterCount()) return new Class[0]; return methodMetaData.value(); @@ -193,7 +193,7 @@ public abstract class AbstractSWCommand { Class type = parameter.getType(); if (parameter.isVarArgs()) type = type.getComponentType(); if (!checkType(parameter.getAnnotations(), type, annotation -> { - MetaData.Parameter parameterMetaData = annotation.annotationType().getAnnotation(MetaData.Parameter.class); + CommandMetaData.Parameter parameterMetaData = annotation.annotationType().getAnnotation(CommandMetaData.Parameter.class); if (parameterMetaData == null) return null; return parameterMetaData.value(); }, "The parameter '" + parameter + "'")) valid = false; @@ -269,7 +269,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Repeatable(Register.Registeres.class) - @MetaData.Method(value = void.class, minParameterCount = 1) + @CommandMetaData.Method(value = void.class, minParameterCount = 1) protected @interface Register { /** @@ -286,7 +286,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) - @MetaData.Method(value = void.class, minParameterCount = 1) + @CommandMetaData.Method(value = void.class, minParameterCount = 1) @interface Registeres { Register[] value(); } @@ -294,7 +294,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD}) - @MetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0) + @CommandMetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0) protected @interface Mapper { String value(); @@ -303,7 +303,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) - @MetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0) + @CommandMetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0) protected @interface ClassMapper { Class value(); @@ -312,7 +312,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) - @MetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0) + @CommandMetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0) protected @interface Cached { long cacheDuration() default 5; TimeUnit timeUnit() default TimeUnit.SECONDS; @@ -321,7 +321,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD}) - @MetaData.Method(value = AbstractValidator.class, maxParameterCount = 0) + @CommandMetaData.Method(value = AbstractValidator.class, maxParameterCount = 0) protected @interface Validator { String value() default ""; @@ -330,7 +330,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) - @MetaData.Method(value = AbstractValidator.class, maxParameterCount = 0) + @CommandMetaData.Method(value = AbstractValidator.class, maxParameterCount = 0) protected @interface ClassValidator { Class value(); @@ -341,7 +341,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) - @MetaData.Parameter({String.class, int.class, Integer.class, long.class, Long.class, boolean.class, Boolean.class}) + @CommandMetaData.Parameter({String.class, int.class, Integer.class, long.class, Long.class, boolean.class, Boolean.class}) protected @interface StaticValue { String[] value(); @@ -376,6 +376,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) + @CommandMetaData.ImplicitValidator(handler = ImplicitTypeValidators.ErrorMessageValidator.class, order = 1) protected @interface ErrorMessage { /** * Error message to be displayed when the parameter is invalid. @@ -398,13 +399,14 @@ public abstract class AbstractSWCommand { */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) - @MetaData.Parameter({String.class}) + @CommandMetaData.Parameter({String.class}) protected @interface Quotable { } @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) - @MetaData.Parameter({int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class}) + @CommandMetaData.Parameter({int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class}) + @CommandMetaData.ImplicitValidator(handler = ImplicitTypeValidators.MinValidator.class, order = 2) protected @interface Min { int intValue() default Integer.MIN_VALUE; long longValue() default Long.MIN_VALUE; @@ -416,7 +418,8 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) - @MetaData.Parameter({int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class}) + @CommandMetaData.Parameter({int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class}) + @CommandMetaData.ImplicitValidator(handler = ImplicitTypeValidators.MaxValidator.class, order = 2) protected @interface Max { int intValue() default Integer.MAX_VALUE; long longValue() default Long.MAX_VALUE; diff --git a/src/de/steamwar/command/AbstractTypeMapper.java b/src/de/steamwar/command/AbstractTypeMapper.java index 4a340d8..1c8550b 100644 --- a/src/de/steamwar/command/AbstractTypeMapper.java +++ b/src/de/steamwar/command/AbstractTypeMapper.java @@ -25,6 +25,8 @@ public interface AbstractTypeMapper extends AbstractValidator { /** * The CommandSender can be null! */ + // TODO: Change the 'previousArguments' to List or something like that + // TODO: Change T return value to Pair or something like that. This would make OptionalValue easier to implement as the Boolean would indicate if s should be consumed or not T map(K sender, String[] previousArguments, String s); @Override diff --git a/src/de/steamwar/command/MetaData.java b/src/de/steamwar/command/CommandMetaData.java similarity index 54% rename from src/de/steamwar/command/MetaData.java rename to src/de/steamwar/command/CommandMetaData.java index 5745fc4..c7f79c7 100644 --- a/src/de/steamwar/command/MetaData.java +++ b/src/de/steamwar/command/CommandMetaData.java @@ -24,8 +24,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -@interface MetaData { +public @interface CommandMetaData { + /** + * This annotation is only for internal use. + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) @interface Method { @@ -34,9 +37,33 @@ import java.lang.annotation.Target; int maxParameterCount() default Integer.MAX_VALUE; } + /** + * This annotation denotes what types are allowed as parameter types the annotation annotated with can use. + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) @interface Parameter { Class[] value(); } + + /** + * This annotation can be used in conjunction with a class that implement {@link AbstractValidator} to + * create custom validator short hands for commands. The validator class itself should contain a constructor + * with two parameters the first is the annotation this annotation is used on and the second is of type {@link Class}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.ANNOTATION_TYPE) + @interface ImplicitValidator { + /** + * The validator class that should be used. + */ + Class handler(); + + /** + * Defines when this validator should be processed. Negative numbers denote that this will be + * processed before {@link AbstractSWCommand.Validator} and positive numbers + * denote that this will be processed after {@link AbstractSWCommand.Validator}. + */ + int order(); + } } diff --git a/src/de/steamwar/command/CommandPart.java b/src/de/steamwar/command/CommandPart.java index dc8c6a9..2969872 100644 --- a/src/de/steamwar/command/CommandPart.java +++ b/src/de/steamwar/command/CommandPart.java @@ -192,4 +192,8 @@ class CommandPart { } return new CheckArgumentResult(true, value); } + + public Class getType() { + return varArgType != null ? varArgType : parameter.getType(); + } } diff --git a/src/de/steamwar/command/ImplicitTypeValidators.java b/src/de/steamwar/command/ImplicitTypeValidators.java new file mode 100644 index 0000000..7204ce5 --- /dev/null +++ b/src/de/steamwar/command/ImplicitTypeValidators.java @@ -0,0 +1,102 @@ +/* + * 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 lombok.experimental.UtilityClass; + +import java.lang.reflect.Array; +import java.util.function.Function; + +@UtilityClass +public class ImplicitTypeValidators { + + public static class ErrorMessageValidator implements AbstractValidator { + + private AbstractSWCommand.ErrorMessage errorMessage; + + public ErrorMessageValidator(AbstractSWCommand.ErrorMessage errorMessage, Class type) { + 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; + } + } + + public static class MinValidator implements AbstractValidator { + + private int value; + private Function comparator; + + public MinValidator(AbstractSWCommand.Min min, Class clazz) { + this.value = min.inclusive() ? 0 : 1; + + if (clazz == int.class || clazz == Integer.class) { + comparator = number -> Integer.compare(number.intValue(), min.intValue()); + } else if (clazz == long.class || clazz == Long.class) { + comparator = number -> Long.compare(number.longValue(), min.longValue()); + } else if (clazz == float.class || clazz == Float.class) { + comparator = number -> Float.compare(number.floatValue(), min.floatValue()); + } else if (clazz == double.class || clazz == Double.class) { + comparator = number -> Double.compare(number.doubleValue(), min.doubleValue()); + } else { + throw new IllegalArgumentException("Min annotation is not supported for " + clazz); + } + } + + @Override + public boolean validate(T sender, Number value, MessageSender messageSender) { + return (comparator.apply(value).intValue()) >= this.value; + } + } + + public static class MaxValidator implements AbstractValidator { + + private int value; + private Function comparator; + + public MaxValidator(AbstractSWCommand.Max max, Class clazz) { + this.value = max.inclusive() ? 0 : -1; + + if (clazz == int.class || clazz == Integer.class) { + comparator = number -> Integer.compare(number.intValue(), max.intValue()); + } else if (clazz == long.class || clazz == Long.class) { + comparator = number -> Long.compare(number.longValue(), max.longValue()); + } else if (clazz == float.class || clazz == Float.class) { + comparator = number -> Float.compare(number.floatValue(), max.floatValue()); + } else if (clazz == double.class || clazz == Double.class) { + comparator = number -> Double.compare(number.doubleValue(), max.doubleValue()); + } else { + throw new IllegalArgumentException("Max annotation is not supported for " + clazz); + } + } + + @Override + public boolean validate(T sender, Number value, MessageSender messageSender) { + return (comparator.apply(value).intValue()) <= this.value; + } + } +} diff --git a/src/de/steamwar/command/SubCommand.java b/src/de/steamwar/command/SubCommand.java index 9084fe7..da3576e 100644 --- a/src/de/steamwar/command/SubCommand.java +++ b/src/de/steamwar/command/SubCommand.java @@ -19,10 +19,13 @@ package de.steamwar.command; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -142,22 +145,16 @@ public class SubCommand { AbstractValidator validator = (AbstractValidator) SWCommandUtils.getValidator(parameter, localValidator); Class varArgType = parameter.isVarArgs() ? parameter.getType().getComponentType() : null; AbstractSWCommand.OptionalValue optionalValue = parameter.getAnnotation(AbstractSWCommand.OptionalValue.class); - AbstractSWCommand.Min min = parameter.getAnnotation(AbstractSWCommand.Min.class); - AbstractSWCommand.Max max = parameter.getAnnotation(AbstractSWCommand.Max.class); CommandPart commandPart = new CommandPart<>(command, typeMapper, varArgType, optionalValue != null ? optionalValue.value() : null, parameter, i); commandPart.setOnlyUseIfNoneIsGiven(optionalValue != null && optionalValue.onlyUINIG()); if (parameter.getAnnotation(AbstractSWCommand.Quotable.class) == null) { commandPart.addValidator((AbstractValidator) STRING_SPACE_FILTER); } + handleImplicitTypeValidator(parameter, commandPart, true); commandPart.addValidator(validator); commandPart.addValidator((AbstractValidator) SWCommandUtils.getErrorMessage(parameter)); - if (min != null) { - commandPart.addValidator((AbstractValidator) createMinValidator(varArgType != null ? varArgType : parameter.getType(), min)); - } - if (max != null) { - commandPart.addValidator((AbstractValidator) createMaxValidator(varArgType != null ? varArgType : parameter.getType(), max)); - } + handleImplicitTypeValidator(parameter, commandPart, false); if (parameter.getAnnotation(AbstractSWCommand.AllowNull.class) == null) { commandPart.addValidator((AbstractValidator) NULL_FILTER); } @@ -172,6 +169,41 @@ public class SubCommand { return first; } + private static void handleImplicitTypeValidator(Parameter parameter, CommandPart commandPart, boolean beforeValidatorAnnotation) { + Annotation[] annotations = parameter.getAnnotations(); + Map>> validators = new HashMap<>(); + for (Annotation annotation : annotations) { + CommandMetaData.ImplicitValidator implicitValidator = annotation.annotationType().getAnnotation(CommandMetaData.ImplicitValidator.class); + if (implicitValidator == null) continue; + if (beforeValidatorAnnotation && implicitValidator.order() >= 0) continue; + if (!beforeValidatorAnnotation && implicitValidator.order() <= 0) continue; + Class clazz = implicitValidator.handler(); + if (!AbstractValidator.class.isAssignableFrom(clazz)) continue; + Constructor[] constructors = clazz.getConstructors(); + if (constructors.length != 1) continue; + Constructor constructor = constructors[0]; + Class[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length != 2) continue; + AbstractValidator validator; + if (!annotation.annotationType().isAssignableFrom(parameterTypes[0])) continue; + if (!Class.class.isAssignableFrom(parameterTypes[1])) continue; + try { + validator = (AbstractValidator) constructor.newInstance(annotation, commandPart.getType()); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + validators.computeIfAbsent(implicitValidator.order(), integer -> new ArrayList<>()).add(validator); + } + List keys = new ArrayList<>(validators.keySet()); + keys.sort(Integer::compareTo); + for (Integer key : keys) { + List> list = validators.get(key); + for (AbstractValidator validator : list) { + commandPart.addValidator(validator); + } + } + } + private static final AbstractValidator NULL_FILTER = (sender, value, messageSender) -> value != null; private static final AbstractValidator STRING_SPACE_FILTER = (sender, value, messageSender) -> {