From f2c0a2a16b98af98c90c6389dec656f137baf5d4 Mon Sep 17 00:00:00 2001 From: yoyosource Date: Sun, 18 Dec 2022 18:14:44 +0100 Subject: [PATCH] Add MethodMetaData and ParameterMetaData for better type safety --- .../steamwar/command/AbstractSWCommand.java | 121 ++++++++++-------- src/de/steamwar/command/MethodMetaData.java | 33 +++++ ...cableTypes.java => ParameterMetaData.java} | 4 +- 3 files changed, 103 insertions(+), 55 deletions(-) create mode 100644 src/de/steamwar/command/MethodMetaData.java rename src/de/steamwar/command/{ApplicableTypes.java => ParameterMetaData.java} (94%) diff --git a/src/de/steamwar/command/AbstractSWCommand.java b/src/de/steamwar/command/AbstractSWCommand.java index 9ff2e28..30af847 100644 --- a/src/de/steamwar/command/AbstractSWCommand.java +++ b/src/de/steamwar/command/AbstractSWCommand.java @@ -25,6 +25,7 @@ import java.lang.reflect.Parameter; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.function.IntPredicate; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -133,49 +134,32 @@ public abstract class AbstractSWCommand { private synchronized void initialize() { if (initialized) return; - List methods = methods(); + List methods = methods().stream() + .filter(this::validateMethod) + .collect(Collectors.toList()); for (Method method : methods) { Cached cached = method.getAnnotation(Cached.class); - addMapper(Mapper.class, method, i -> i == 0, false, AbstractTypeMapper.class, (anno, typeMapper) -> { + addMapper(Mapper.class, method, (anno, typeMapper) -> { TabCompletionCache.add(typeMapper, cached); - if (anno.local()) { - localTypeMapper.putIfAbsent(anno.value(), (AbstractTypeMapper) typeMapper); - } else { - SWCommandUtils.getMAPPER_FUNCTIONS().putIfAbsent(anno.value(), typeMapper); - } + (anno.local() ? ((Map) localTypeMapper) : SWCommandUtils.getMAPPER_FUNCTIONS()).put(anno.value(), typeMapper); }); - addMapper(ClassMapper.class, method, i -> i == 0, false, AbstractTypeMapper.class, (anno, typeMapper) -> { + addMapper(ClassMapper.class, method, (anno, typeMapper) -> { TabCompletionCache.add(typeMapper, cached); - if (anno.local()) { - localTypeMapper.putIfAbsent(anno.value().getTypeName(), (AbstractTypeMapper) typeMapper); - } else { - SWCommandUtils.getMAPPER_FUNCTIONS().putIfAbsent(anno.value().getTypeName(), typeMapper); - } + (anno.local() ? ((Map) localTypeMapper) : SWCommandUtils.getMAPPER_FUNCTIONS()).put(anno.value().getName(), typeMapper); }); - 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(Validator.class, method, (anno, validator) -> { + (anno.local() ? ((Map) localValidators) : SWCommandUtils.getVALIDATOR_FUNCTIONS()).put(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); - } + addValidator(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, i -> i > 0, true, null, (anno, parameters) -> { + 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() && i == parameters.length - 1) { - clazz = parameter.getType().getComponentType(); - } - checkAnnotationApplicability(parameter, clazz); + if (parameter.isVarArgs()) clazz = clazz.getComponentType(); Mapper mapper = parameter.getAnnotation(Mapper.class); if (clazz.isEnum() && mapper == null && !SWCommandUtils.getMAPPER_FUNCTIONS().containsKey(clazz.getTypeName())) { continue; @@ -201,12 +185,42 @@ public abstract class AbstractSWCommand { initialized = true; } - private void checkAnnotationApplicability(Parameter parameter, Class clazz) { - Annotation[] annotations = parameter.getAnnotations(); + private boolean validateMethod(Method method) { + if (!checkType(method.getAnnotations(), method.getReturnType(), annotation -> { + MethodMetaData methodMetaData = annotation.annotationType().getAnnotation(MethodMetaData.class); + if (methodMetaData == null) return null; + if (method.getParameterCount() > methodMetaData.maxParameterCount()) { + return new Class[0]; + } + if (method.getParameterCount() < methodMetaData.minParameterCount()) { + return new Class[0]; + } + return methodMetaData.possibleReturnTypes(); + }, "The method '" + method + "'")) { + return false; + } + boolean valid = true; + for (Parameter parameter : method.getParameters()) { + Class type = parameter.getType(); + if (parameter.isVarArgs()) { + type = type.getComponentType(); + } + if (!checkType(parameter.getAnnotations(), type, annotation -> { + ParameterMetaData parameterMetaData = annotation.annotationType().getAnnotation(ParameterMetaData.class); + if (parameterMetaData == null) return null; + return parameterMetaData.possibleTypes(); + }, "The parameter '" + parameter + "'")) { + valid = false; + } + } + return valid; + } + + private boolean checkType(Annotation[] annotations, Class clazz, Function[]> toApplicableTypes, String warning) { + boolean valid = true; for (Annotation annotation : annotations) { - ApplicableTypes applicableTypes = annotation.annotationType().getAnnotation(ApplicableTypes.class); - if (applicableTypes == null) continue; - Class[] types = applicableTypes.value(); + Class[] types = toApplicableTypes.apply(annotation); + if (types == null) continue; boolean applicable = false; for (Class type : types) { if (type.isAssignableFrom(clazz)) { @@ -215,33 +229,27 @@ public abstract class AbstractSWCommand { } } if (!applicable) { - commandSystemWarning(() -> "The parameter '" + parameter.toString() + "' is using an unsupported annotation of type '" + annotation.annotationType().getName() + "'"); + commandSystemWarning(() -> warning + " is using an unsupported annotation of type '" + annotation.annotationType().getName() + "'"); + valid = false; } } + return valid; } - private void add(Class annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class returnType, BiConsumer consumer) { + 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 (!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) -> { + private void addMapper(Class annotation, Method method, BiConsumer> consumer) { + add(annotation, method, false, (anno, parameters) -> { try { method.setAccessible(true); consumer.accept(anno, (AbstractTypeMapper) method.invoke(this)); @@ -251,8 +259,8 @@ 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) -> { + private void addValidator(Class annotation, Method method, BiConsumer> consumer) { + add(annotation, method, false, (anno, parameters) -> { try { method.setAccessible(true); consumer.accept(anno, (AbstractValidator) method.invoke(this)); @@ -287,6 +295,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Repeatable(Register.Registeres.class) + @MethodMetaData(possibleReturnTypes = void.class, minParameterCount = 1) protected @interface Register { /** @@ -303,6 +312,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) + @MethodMetaData(possibleReturnTypes = void.class, minParameterCount = 1) @interface Registeres { Register[] value(); } @@ -310,6 +320,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD}) + @MethodMetaData(possibleReturnTypes = AbstractTypeMapper.class, minParameterCount = 0, maxParameterCount = 0) protected @interface Mapper { String value(); @@ -318,6 +329,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) + @MethodMetaData(possibleReturnTypes = AbstractTypeMapper.class, minParameterCount = 0, maxParameterCount = 0) protected @interface ClassMapper { Class value(); @@ -326,6 +338,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) + @MethodMetaData(possibleReturnTypes = AbstractTypeMapper.class, minParameterCount = 0, maxParameterCount = 0) protected @interface Cached { long cacheDuration() default 5; TimeUnit timeUnit() default TimeUnit.SECONDS; @@ -334,6 +347,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD}) + @MethodMetaData(possibleReturnTypes = AbstractValidator.class, minParameterCount = 0, maxParameterCount = 0) protected @interface Validator { String value() default ""; @@ -342,6 +356,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) + @MethodMetaData(possibleReturnTypes = AbstractValidator.class, minParameterCount = 0, maxParameterCount = 0) protected @interface ClassValidator { Class value(); @@ -352,7 +367,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) - @ApplicableTypes({String.class, int.class, Integer.class, boolean.class, Boolean.class}) + @ParameterMetaData(possibleTypes = {String.class, int.class, Integer.class, long.class, Long.class, boolean.class, Boolean.class}) protected @interface StaticValue { String[] value(); @@ -409,13 +424,13 @@ public abstract class AbstractSWCommand { */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) - @ApplicableTypes({String.class}) + @ParameterMetaData(possibleTypes = {String.class}) protected @interface Quotable { } @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) - @ApplicableTypes({int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class}) + @ParameterMetaData(possibleTypes = {int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class}) protected @interface Min { int intValue() default Integer.MIN_VALUE; long longValue() default Long.MIN_VALUE; @@ -427,7 +442,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) - @ApplicableTypes({int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class}) + @ParameterMetaData(possibleTypes = {int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class}) protected @interface Max { int intValue() default Integer.MAX_VALUE; long longValue() default Long.MAX_VALUE; diff --git a/src/de/steamwar/command/MethodMetaData.java b/src/de/steamwar/command/MethodMetaData.java new file mode 100644 index 0000000..4975ca8 --- /dev/null +++ b/src/de/steamwar/command/MethodMetaData.java @@ -0,0 +1,33 @@ +/* + * 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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +@interface MethodMetaData { + Class[] possibleReturnTypes(); + int minParameterCount() default 0; + int maxParameterCount() default Integer.MAX_VALUE; +} diff --git a/src/de/steamwar/command/ApplicableTypes.java b/src/de/steamwar/command/ParameterMetaData.java similarity index 94% rename from src/de/steamwar/command/ApplicableTypes.java rename to src/de/steamwar/command/ParameterMetaData.java index 2385b43..c7c0467 100644 --- a/src/de/steamwar/command/ApplicableTypes.java +++ b/src/de/steamwar/command/ParameterMetaData.java @@ -26,6 +26,6 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) -@interface ApplicableTypes { - Class[] value(); +@interface ParameterMetaData { + Class[] possibleTypes(); }