Add MethodMetaData and ParameterMetaData for better type safety
Alle Prüfungen waren erfolgreich
SteamWarCI Build successful
Alle Prüfungen waren erfolgreich
SteamWarCI Build successful
Dieser Commit ist enthalten in:
Ursprung
4b8310d4f8
Commit
f2c0a2a16b
@ -25,6 +25,7 @@ import java.lang.reflect.Parameter;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.function.IntPredicate;
|
import java.util.function.IntPredicate;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -133,49 +134,32 @@ public abstract class AbstractSWCommand<T> {
|
|||||||
|
|
||||||
private synchronized void initialize() {
|
private synchronized void initialize() {
|
||||||
if (initialized) return;
|
if (initialized) return;
|
||||||
List<Method> methods = methods();
|
List<Method> methods = methods().stream()
|
||||||
|
.filter(this::validateMethod)
|
||||||
|
.collect(Collectors.toList());
|
||||||
for (Method method : methods) {
|
for (Method method : methods) {
|
||||||
Cached cached = method.getAnnotation(Cached.class);
|
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);
|
TabCompletionCache.add(typeMapper, cached);
|
||||||
if (anno.local()) {
|
(anno.local() ? ((Map) localTypeMapper) : SWCommandUtils.getMAPPER_FUNCTIONS()).put(anno.value(), typeMapper);
|
||||||
localTypeMapper.putIfAbsent(anno.value(), (AbstractTypeMapper<T, ?>) typeMapper);
|
|
||||||
} else {
|
|
||||||
SWCommandUtils.getMAPPER_FUNCTIONS().putIfAbsent(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);
|
TabCompletionCache.add(typeMapper, cached);
|
||||||
if (anno.local()) {
|
(anno.local() ? ((Map) localTypeMapper) : SWCommandUtils.getMAPPER_FUNCTIONS()).put(anno.value().getName(), typeMapper);
|
||||||
localTypeMapper.putIfAbsent(anno.value().getTypeName(), (AbstractTypeMapper<T, ?>) typeMapper);
|
|
||||||
} else {
|
|
||||||
SWCommandUtils.getMAPPER_FUNCTIONS().putIfAbsent(anno.value().getTypeName(), typeMapper);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
addValidator(Validator.class, method, i -> i == 0, false, AbstractValidator.class, (anno, validator) -> {
|
addValidator(Validator.class, method, (anno, validator) -> {
|
||||||
if (anno.local()) {
|
(anno.local() ? ((Map) localValidators) : SWCommandUtils.getVALIDATOR_FUNCTIONS()).put(anno.value(), validator);
|
||||||
localValidators.putIfAbsent(anno.value(), (AbstractValidator<T, ?>) validator);
|
|
||||||
} else {
|
|
||||||
SWCommandUtils.getVALIDATOR_FUNCTIONS().putIfAbsent(anno.value(), validator);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
addValidator(ClassValidator.class, method, i -> i == 0, false, AbstractValidator.class, (anno, validator) -> {
|
addValidator(ClassValidator.class, method, (anno, validator) -> {
|
||||||
if (anno.local()) {
|
(anno.local() ? ((Map) localValidators) : SWCommandUtils.getVALIDATOR_FUNCTIONS()).put(anno.value().getName(), validator);
|
||||||
localValidators.putIfAbsent(anno.value().getTypeName(), (AbstractValidator<T, ?>) validator);
|
|
||||||
} else {
|
|
||||||
SWCommandUtils.getVALIDATOR_FUNCTIONS().putIfAbsent(anno.value().getTypeName(), validator);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (Method method : methods) {
|
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++) {
|
for (int i = 1; i < parameters.length; i++) {
|
||||||
Parameter parameter = parameters[i];
|
Parameter parameter = parameters[i];
|
||||||
Class<?> clazz = parameter.getType();
|
Class<?> clazz = parameter.getType();
|
||||||
if (parameter.isVarArgs() && i == parameters.length - 1) {
|
if (parameter.isVarArgs()) clazz = clazz.getComponentType();
|
||||||
clazz = parameter.getType().getComponentType();
|
|
||||||
}
|
|
||||||
checkAnnotationApplicability(parameter, clazz);
|
|
||||||
Mapper mapper = parameter.getAnnotation(Mapper.class);
|
Mapper mapper = parameter.getAnnotation(Mapper.class);
|
||||||
if (clazz.isEnum() && mapper == null && !SWCommandUtils.getMAPPER_FUNCTIONS().containsKey(clazz.getTypeName())) {
|
if (clazz.isEnum() && mapper == null && !SWCommandUtils.getMAPPER_FUNCTIONS().containsKey(clazz.getTypeName())) {
|
||||||
continue;
|
continue;
|
||||||
@ -201,12 +185,42 @@ public abstract class AbstractSWCommand<T> {
|
|||||||
initialized = true;
|
initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkAnnotationApplicability(Parameter parameter, Class<?> clazz) {
|
private boolean validateMethod(Method method) {
|
||||||
Annotation[] annotations = parameter.getAnnotations();
|
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<Annotation, Class<?>[]> toApplicableTypes, String warning) {
|
||||||
|
boolean valid = true;
|
||||||
for (Annotation annotation : annotations) {
|
for (Annotation annotation : annotations) {
|
||||||
ApplicableTypes applicableTypes = annotation.annotationType().getAnnotation(ApplicableTypes.class);
|
Class<?>[] types = toApplicableTypes.apply(annotation);
|
||||||
if (applicableTypes == null) continue;
|
if (types == null) continue;
|
||||||
Class<?>[] types = applicableTypes.value();
|
|
||||||
boolean applicable = false;
|
boolean applicable = false;
|
||||||
for (Class<?> type : types) {
|
for (Class<?> type : types) {
|
||||||
if (type.isAssignableFrom(clazz)) {
|
if (type.isAssignableFrom(clazz)) {
|
||||||
@ -215,33 +229,27 @@ public abstract class AbstractSWCommand<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!applicable) {
|
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 <T extends Annotation> void add(Class<T> annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class<?> returnType, BiConsumer<T, Parameter[]> consumer) {
|
private <T extends Annotation> void add(Class<T> annotation, Method method, boolean firstParameter, BiConsumer<T, Parameter[]> consumer) {
|
||||||
T[] anno = SWCommandUtils.getAnnotation(method, annotation);
|
T[] anno = SWCommandUtils.getAnnotation(method, annotation);
|
||||||
if (anno == null || anno.length == 0) return;
|
if (anno == null || anno.length == 0) return;
|
||||||
|
|
||||||
Parameter[] parameters = method.getParameters();
|
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())) {
|
if (firstParameter && !clazz.isAssignableFrom(parameters[0].getType())) {
|
||||||
commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking the first parameter of type '" + clazz.getTypeName() + "'");
|
commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking the first parameter of type '" + clazz.getTypeName() + "'");
|
||||||
return;
|
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));
|
Arrays.stream(anno).forEach(t -> consumer.accept(t, parameters));
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T extends Annotation> void addMapper(Class<T> annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class<?> returnType, BiConsumer<T, AbstractTypeMapper<?, ?>> consumer) {
|
private <T extends Annotation> void addMapper(Class<T> annotation, Method method, BiConsumer<T, AbstractTypeMapper<?, ?>> consumer) {
|
||||||
add(annotation, method, parameterTester, firstParameter, returnType, (anno, parameters) -> {
|
add(annotation, method, false, (anno, parameters) -> {
|
||||||
try {
|
try {
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
consumer.accept(anno, (AbstractTypeMapper<T, ?>) method.invoke(this));
|
consumer.accept(anno, (AbstractTypeMapper<T, ?>) method.invoke(this));
|
||||||
@ -251,8 +259,8 @@ public abstract class AbstractSWCommand<T> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T extends Annotation> void addValidator(Class<T> annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class<?> returnType, BiConsumer<T, AbstractValidator<T, ?>> consumer) {
|
private <T extends Annotation> void addValidator(Class<T> annotation, Method method, BiConsumer<T, AbstractValidator<T, ?>> consumer) {
|
||||||
add(annotation, method, parameterTester, firstParameter, returnType, (anno, parameters) -> {
|
add(annotation, method, false, (anno, parameters) -> {
|
||||||
try {
|
try {
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
consumer.accept(anno, (AbstractValidator<T, ?>) method.invoke(this));
|
consumer.accept(anno, (AbstractValidator<T, ?>) method.invoke(this));
|
||||||
@ -287,6 +295,7 @@ public abstract class AbstractSWCommand<T> {
|
|||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target({ElementType.METHOD})
|
@Target({ElementType.METHOD})
|
||||||
@Repeatable(Register.Registeres.class)
|
@Repeatable(Register.Registeres.class)
|
||||||
|
@MethodMetaData(possibleReturnTypes = void.class, minParameterCount = 1)
|
||||||
protected @interface Register {
|
protected @interface Register {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -303,6 +312,7 @@ public abstract class AbstractSWCommand<T> {
|
|||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target({ElementType.METHOD})
|
@Target({ElementType.METHOD})
|
||||||
|
@MethodMetaData(possibleReturnTypes = void.class, minParameterCount = 1)
|
||||||
@interface Registeres {
|
@interface Registeres {
|
||||||
Register[] value();
|
Register[] value();
|
||||||
}
|
}
|
||||||
@ -310,6 +320,7 @@ public abstract class AbstractSWCommand<T> {
|
|||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target({ElementType.PARAMETER, ElementType.METHOD})
|
@Target({ElementType.PARAMETER, ElementType.METHOD})
|
||||||
|
@MethodMetaData(possibleReturnTypes = AbstractTypeMapper.class, minParameterCount = 0, maxParameterCount = 0)
|
||||||
protected @interface Mapper {
|
protected @interface Mapper {
|
||||||
String value();
|
String value();
|
||||||
|
|
||||||
@ -318,6 +329,7 @@ public abstract class AbstractSWCommand<T> {
|
|||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target({ElementType.METHOD})
|
@Target({ElementType.METHOD})
|
||||||
|
@MethodMetaData(possibleReturnTypes = AbstractTypeMapper.class, minParameterCount = 0, maxParameterCount = 0)
|
||||||
protected @interface ClassMapper {
|
protected @interface ClassMapper {
|
||||||
Class<?> value();
|
Class<?> value();
|
||||||
|
|
||||||
@ -326,6 +338,7 @@ public abstract class AbstractSWCommand<T> {
|
|||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target({ElementType.METHOD})
|
@Target({ElementType.METHOD})
|
||||||
|
@MethodMetaData(possibleReturnTypes = AbstractTypeMapper.class, minParameterCount = 0, maxParameterCount = 0)
|
||||||
protected @interface Cached {
|
protected @interface Cached {
|
||||||
long cacheDuration() default 5;
|
long cacheDuration() default 5;
|
||||||
TimeUnit timeUnit() default TimeUnit.SECONDS;
|
TimeUnit timeUnit() default TimeUnit.SECONDS;
|
||||||
@ -334,6 +347,7 @@ public abstract class AbstractSWCommand<T> {
|
|||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target({ElementType.PARAMETER, ElementType.METHOD})
|
@Target({ElementType.PARAMETER, ElementType.METHOD})
|
||||||
|
@MethodMetaData(possibleReturnTypes = AbstractValidator.class, minParameterCount = 0, maxParameterCount = 0)
|
||||||
protected @interface Validator {
|
protected @interface Validator {
|
||||||
String value() default "";
|
String value() default "";
|
||||||
|
|
||||||
@ -342,6 +356,7 @@ public abstract class AbstractSWCommand<T> {
|
|||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target({ElementType.METHOD})
|
@Target({ElementType.METHOD})
|
||||||
|
@MethodMetaData(possibleReturnTypes = AbstractValidator.class, minParameterCount = 0, maxParameterCount = 0)
|
||||||
protected @interface ClassValidator {
|
protected @interface ClassValidator {
|
||||||
Class<?> value();
|
Class<?> value();
|
||||||
|
|
||||||
@ -352,7 +367,7 @@ public abstract class AbstractSWCommand<T> {
|
|||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target({ElementType.PARAMETER})
|
@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 {
|
protected @interface StaticValue {
|
||||||
String[] value();
|
String[] value();
|
||||||
|
|
||||||
@ -409,13 +424,13 @@ public abstract class AbstractSWCommand<T> {
|
|||||||
*/
|
*/
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target({ElementType.PARAMETER})
|
@Target({ElementType.PARAMETER})
|
||||||
@ApplicableTypes({String.class})
|
@ParameterMetaData(possibleTypes = {String.class})
|
||||||
protected @interface Quotable {
|
protected @interface Quotable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target({ElementType.PARAMETER})
|
@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 {
|
protected @interface Min {
|
||||||
int intValue() default Integer.MIN_VALUE;
|
int intValue() default Integer.MIN_VALUE;
|
||||||
long longValue() default Long.MIN_VALUE;
|
long longValue() default Long.MIN_VALUE;
|
||||||
@ -427,7 +442,7 @@ public abstract class AbstractSWCommand<T> {
|
|||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target({ElementType.PARAMETER})
|
@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 {
|
protected @interface Max {
|
||||||
int intValue() default Integer.MAX_VALUE;
|
int intValue() default Integer.MAX_VALUE;
|
||||||
long longValue() default Long.MAX_VALUE;
|
long longValue() default Long.MAX_VALUE;
|
||||||
|
33
src/de/steamwar/command/MethodMetaData.java
Normale Datei
33
src/de/steamwar/command/MethodMetaData.java
Normale Datei
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
@ -26,6 +26,6 @@ import java.lang.annotation.Target;
|
|||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target(ElementType.ANNOTATION_TYPE)
|
@Target(ElementType.ANNOTATION_TYPE)
|
||||||
@interface ApplicableTypes {
|
@interface ParameterMetaData {
|
||||||
Class<?>[] value();
|
Class<?>[] possibleTypes();
|
||||||
}
|
}
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren