diff --git a/build.gradle b/build.gradle old mode 100644 new mode 100755 index f1c8306..d5f000a --- a/build.gradle +++ b/build.gradle @@ -81,6 +81,8 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:hamcrest:2.2' + + compileOnly 'org.xerial:sqlite-jdbc:3.36.0' } task buildResources { diff --git a/src/de/steamwar/ImplementationProvider.java b/src/de/steamwar/ImplementationProvider.java new file mode 100644 index 0000000..e5b795e --- /dev/null +++ b/src/de/steamwar/ImplementationProvider.java @@ -0,0 +1,34 @@ +/* + * 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; + +import java.lang.reflect.InvocationTargetException; + +public class ImplementationProvider { + private ImplementationProvider() {} + + public static T getImpl(String className) { + try { + return (T) Class.forName(className).getDeclaredConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException e) { + throw new SecurityException("Could not load implementation", e); + } + } +} diff --git a/src/de/steamwar/command/AbstractSWCommand.java b/src/de/steamwar/command/AbstractSWCommand.java index c23fd91..b8dc078 100644 --- a/src/de/steamwar/command/AbstractSWCommand.java +++ b/src/de/steamwar/command/AbstractSWCommand.java @@ -20,22 +20,26 @@ package de.steamwar.command; import java.lang.annotation.*; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; 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.BiPredicate; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; public abstract class AbstractSWCommand { + private static final Map>, List>> dependencyMap = new HashMap<>(); + private Class clazz; // This is used in createMappings() private boolean initialized = false; protected final List> commandList = new ArrayList<>(); - protected final List> commandHelpList = new ArrayList<>(); private final Map> localTypeMapper = new HashMap<>(); private final Map> localValidators = new HashMap<>(); @@ -46,6 +50,13 @@ public abstract class AbstractSWCommand { protected AbstractSWCommand(Class clazz, String command, String... aliases) { this.clazz = clazz; + + PartOf partOf = this.getClass().getAnnotation(PartOf.class); + if (partOf != null) { + dependencyMap.computeIfAbsent((Class>) partOf.value(), k -> new ArrayList<>()).add(this); + return; + } + createAndSafeCommand(command, aliases); unregister(); register(); @@ -65,22 +76,16 @@ public abstract class AbstractSWCommand { System.out.println(message.get()); } - protected void sendMessage(T sender, String message, Object[] args) {} + 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(errors::add, sender, alias, args))) { - if (!errors.isEmpty()) { - errors.forEach(Runnable::run); - return; - } - commandHelpList.stream().anyMatch(s -> s.invoke((ignore) -> { - }, sender, alias, args)); + errors.forEach(Runnable::run); } - } catch (CommandNoHelpException e) { - // Ignored } catch (CommandFrameworkException e) { commandSystemError(sender, e); throw e; @@ -96,79 +101,39 @@ public abstract class AbstractSWCommand { .filter(Objects::nonNull) .flatMap(Collection::stream) .filter(s -> !s.isEmpty()) - .filter(s -> s.toLowerCase().startsWith(string)) + .filter(s -> s.toLowerCase().startsWith(string) || string.startsWith(s.toLowerCase())) .distinct() .collect(Collectors.toList()); } - private void initialize() { + private synchronized void initialize() { if (initialized) return; - createMapping(); - } - - private synchronized void createMapping() { - 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) -> { + this.>add(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) -> { + this.>add(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); - } + this.>add(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); - } + this.>add(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) -> { - if (!anno.help()) return; - boolean error = false; - if (parameters.length != 2) { - commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking parameters or has too many"); - error = true; - } - if (!parameters[parameters.length - 1].isVarArgs()) { - commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking the varArgs parameters as last Argument"); - error = true; - } - if (parameters[parameters.length - 1].getType().getComponentType() != String.class) { - commandSystemWarning(() -> "The method '" + method.toString() + "' is lacking the varArgs parameters of type '" + String.class.getTypeName() + "' as last Argument"); - error = true; - } - if (error) return; - commandHelpList.add(new SubCommand<>(this, method, anno.value(), new HashMap<>(), new HashMap<>(), true, null, anno.noTabComplete())); - }); - - add(Register.class, method, i -> i > 0, true, null, (anno, parameters) -> { - if (anno.help()) return; + 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(); - } + if (parameter.isVarArgs()) clazz = clazz.getComponentType(); Mapper mapper = parameter.getAnnotation(Mapper.class); if (clazz.isEnum() && mapper == null && !SWCommandUtils.getMAPPER_FUNCTIONS().containsKey(clazz.getTypeName())) { continue; @@ -179,66 +144,94 @@ public abstract class AbstractSWCommand { return; } } - commandList.add(new SubCommand<>(this, method, anno.value(), localTypeMapper, localValidators, false, anno.description(), anno.noTabComplete())); + commandList.add(new SubCommand<>(this, method, anno.value(), localTypeMapper, localValidators, anno.description(), anno.noTabComplete())); }); } - this.commandList.sort((o1, o2) -> { - int compare = Integer.compare(-o1.subCommand.length, -o2.subCommand.length); - if (compare != 0) { - return compare; - } else { - return Integer.compare(o1.comparableValue, o2.comparableValue); - } - }); - commandHelpList.sort((o1, o2) -> { - int compare = Integer.compare(-o1.subCommand.length, -o2.subCommand.length); - if (compare != 0) { - return compare; - } else { - return Integer.compare(o1.method.getDeclaringClass() == AbstractSWCommand.class ? 1 : 0, - o2.method.getDeclaringClass() == AbstractSWCommand.class ? 1 : 0); - } - }); + if (dependencyMap.containsKey(this.getClass())) { + dependencyMap.get(this.getClass()).forEach(abstractSWCommand -> { + abstractSWCommand.localTypeMapper.putAll((Map) localTypeMapper); + abstractSWCommand.localValidators.putAll((Map) localValidators); + abstractSWCommand.initialize(); + commandList.addAll((Collection) abstractSWCommand.commandList); + }); + } + + Collections.sort(commandList); initialized = true; } - private void add(Class annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class returnType, BiConsumer consumer) { + private boolean validateMethod(Method method) { + if (!checkType(method.getAnnotations(), method.getReturnType(), false, annotation -> { + CommandMetaData.Method methodMetaData = annotation.annotationType().getAnnotation(CommandMetaData.Method.class); + if (methodMetaData == null) return (aClass, varArg) -> true; + if (method.getParameterCount() > methodMetaData.maxParameterCount() || method.getParameterCount() < methodMetaData.minParameterCount()) + return (aClass, varArg) -> false; + return (aClass, varArg) -> { + Class[] types = methodMetaData.value(); + if (types == null) return true; + for (Class type : types) { + if (type.isAssignableFrom(aClass)) return true; + } + return false; + }; + }, "The method '" + method + "'")) return false; + boolean valid = true; + for (Parameter parameter : method.getParameters()) { + if (!checkType(parameter.getAnnotations(), parameter.getType(), parameter.isVarArgs(), annotation -> { + CommandMetaData.Parameter parameterMetaData = annotation.annotationType().getAnnotation(CommandMetaData.Parameter.class); + if (parameterMetaData == null) return (aClass, varArg) -> true; + Class handler = parameterMetaData.handler(); + if (BiPredicate.class.isAssignableFrom(handler)) { + try { + return (BiPredicate, Boolean>) handler.getConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + } + } + return (aClass, varArg) -> { + if (varArg) aClass = aClass.getComponentType(); + Class[] types = parameterMetaData.value(); + if (types == null) return true; + for (Class current : types) { + if (current.isAssignableFrom(aClass)) return true; + } + return false; + }; + }, "The parameter '" + parameter + "'")) valid = false; + } + return valid; + } + + private boolean checkType(Annotation[] annotations, Class clazz, boolean varArg, Function, Boolean>> toApplicableTypes, String warning) { + boolean valid = true; + for (Annotation annotation : annotations) { + BiPredicate, Boolean> predicate = toApplicableTypes.apply(annotation); + if (!predicate.test(clazz, varArg)) { + commandSystemWarning(() -> warning + " is using an unsupported annotation of type '" + annotation.annotationType().getName() + "'"); + valid = false; + } + } + return valid; + } + + 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 add(Class annotation, Method method, BiConsumer consumer) { + add(annotation, method, false, (anno, parameters) -> { try { method.setAccessible(true); - consumer.accept(anno, (AbstractTypeMapper) method.invoke(this)); - } catch (Exception e) { - throw new SecurityException(e.getMessage(), e); - } - }); - } - - 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)); + consumer.accept(anno, (K) method.invoke(this)); } catch (Exception e) { throw new SecurityException(e.getMessage(), e); } @@ -262,12 +255,29 @@ public abstract class AbstractSWCommand { return methods; } + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE}) + public @interface PartOf { + Class value(); + } + + // --- Annotation for the command --- + + /** + * Annotation for registering a method as a command + */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Repeatable(Register.Registeres.class) + @CommandMetaData.Method(value = void.class, minParameterCount = 1) protected @interface Register { + + /** + * Identifier of subcommand + */ String[] value() default {}; + @Deprecated boolean help() default false; String[] description() default {}; @@ -276,6 +286,7 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) + @CommandMetaData.Method(value = void.class, minParameterCount = 1) @interface Registeres { Register[] value(); } @@ -283,14 +294,41 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD}) + @CommandMetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0) + @CommandMetaData.ImplicitTypeMapper(handler = Mapper.Handler.class) protected @interface Mapper { String value(); boolean local() default false; + + class Handler implements AbstractTypeMapper { + + private AbstractTypeMapper inner; + + public Handler(AbstractSWCommand.Mapper mapper, Map> localTypeMapper) { + inner = (AbstractTypeMapper) SWCommandUtils.getTypeMapper(mapper.value(), localTypeMapper); + } + + @Override + public Object map(T sender, PreviousArguments previousArguments, String s) { + return inner.map(sender, previousArguments, s); + } + + @Override + public boolean validate(T sender, Object value, MessageSender messageSender) { + return inner.validate(sender, value, messageSender); + } + + @Override + public Collection tabCompletes(T sender, PreviousArguments previousArguments, String s) { + return inner.tabCompletes(sender, previousArguments, s); + } + } } @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) + @CommandMetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0) protected @interface ClassMapper { Class value(); @@ -299,30 +337,58 @@ public abstract class AbstractSWCommand { @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) + @CommandMetaData.Method(value = AbstractTypeMapper.class, maxParameterCount = 0) protected @interface Cached { long cacheDuration() default 5; + TimeUnit timeUnit() default TimeUnit.SECONDS; + boolean global() default false; } @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD}) + @CommandMetaData.Method(value = AbstractValidator.class, maxParameterCount = 0) + @CommandMetaData.ImplicitValidator(handler = Validator.Handler.class, order = 0) protected @interface Validator { String value() default ""; boolean local() default false; + + boolean invert() default false; + + class Handler implements AbstractValidator { + + private AbstractValidator inner; + private boolean invert; + + public Handler(AbstractSWCommand.Validator validator, Class clazz, Map> localValidator) { + inner = (AbstractValidator) SWCommandUtils.getValidator(validator, clazz, localValidator); + invert = validator.invert(); + } + + @Override + public boolean validate(T sender, Object value, MessageSender messageSender) { + return inner.validate(sender, value, messageSender) ^ invert; + } + } } @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) + @CommandMetaData.Method(value = AbstractValidator.class, maxParameterCount = 0) protected @interface ClassValidator { Class value(); boolean local() default false; } + // --- Implicit TypeMapper --- + @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) + @CommandMetaData.Parameter({String.class, int.class, Integer.class, long.class, Long.class, boolean.class, Boolean.class}) + @CommandMetaData.ImplicitTypeMapper(handler = StaticValue.Handler.class) protected @interface StaticValue { String[] value(); @@ -336,7 +402,49 @@ public abstract class AbstractSWCommand { */ boolean allowISE() default false; - int[] falseValues() default { 0 }; + int[] falseValues() default {0}; + + class Handler implements AbstractTypeMapper { + + private AbstractTypeMapper inner; + + public Handler(StaticValue staticValue, Class clazz) { + if (clazz == String.class) { + inner = SWCommandUtils.createMapper(staticValue.value()); + return; + } + if (!staticValue.allowISE()) { + throw new IllegalArgumentException("The parameter type '" + clazz.getTypeName() + "' is not supported by the StaticValue annotation"); + } + if (clazz == boolean.class || clazz == Boolean.class) { + List tabCompletes = new ArrayList<>(Arrays.asList(staticValue.value())); + Set falseValues = new HashSet<>(); + for (int i : staticValue.falseValues()) falseValues.add(i); + inner = SWCommandUtils.createMapper(s -> { + int index = tabCompletes.indexOf(s); + return index == -1 ? null : !falseValues.contains(index); + }, (commandSender, s) -> tabCompletes); + } else if (clazz == int.class || clazz == Integer.class || clazz == long.class || clazz == Long.class) { + List tabCompletes = new ArrayList<>(Arrays.asList(staticValue.value())); + inner = SWCommandUtils.createMapper(s -> { + Number index = tabCompletes.indexOf(s); + return index.longValue() == -1 ? null : index; + }, (commandSender, s) -> tabCompletes); + } else { + throw new IllegalArgumentException("The parameter type '" + clazz.getTypeName() + "' is not supported by the StaticValue annotation"); + } + } + + @Override + public Object map(T sender, PreviousArguments previousArguments, String s) { + return inner.map(sender, previousArguments, s); + } + + @Override + public Collection tabCompletes(T sender, PreviousArguments previousArguments, String s) { + return inner.tabCompletes(sender, previousArguments, s); + } + } } @Retention(RetentionPolicy.RUNTIME) @@ -353,8 +461,11 @@ public abstract class AbstractSWCommand { boolean onlyUINIG() default false; } + // --- Implicit Validator --- + @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) + @CommandMetaData.ImplicitValidator(handler = ErrorMessage.Handler.class, order = Integer.MAX_VALUE) protected @interface ErrorMessage { /** * Error message to be displayed when the parameter is invalid. @@ -365,10 +476,219 @@ public abstract class AbstractSWCommand { * This is the short form for 'allowEmptyArrays'. */ boolean allowEAs() default true; + + class Handler implements AbstractValidator { + + private AbstractSWCommand.ErrorMessage errorMessage; + + public Handler(AbstractSWCommand.ErrorMessage errorMessage) { + 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; + } + } } @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) protected @interface AllowNull { } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER}) + @CommandMetaData.Parameter({int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class}) + @CommandMetaData.ImplicitValidator(handler = Min.Handler.class, order = 2) + protected @interface Min { + int intValue() default Integer.MIN_VALUE; + + long longValue() default Long.MIN_VALUE; + + float floatValue() default Float.MIN_VALUE; + + double doubleValue() default Double.MIN_VALUE; + + boolean inclusive() default true; + + class Handler implements AbstractValidator { + + private int value; + private Function comparator; + + public Handler(AbstractSWCommand.Min min, Class clazz) { + this.value = min.inclusive() ? 0 : 1; + this.comparator = createComparator("Min", clazz, min.intValue(), min.longValue(), min.floatValue(), min.doubleValue()); + } + + @Override + public boolean validate(T sender, Number value, MessageSender messageSender) { + if (value == null) return true; + return (comparator.apply(value).intValue()) >= this.value; + } + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER}) + @CommandMetaData.Parameter({int.class, Integer.class, long.class, Long.class, float.class, Float.class, double.class, Double.class}) + @CommandMetaData.ImplicitValidator(handler = Max.Handler.class, order = 2) + protected @interface Max { + int intValue() default Integer.MAX_VALUE; + + long longValue() default Long.MAX_VALUE; + + float floatValue() default Float.MAX_VALUE; + + double doubleValue() default Double.MAX_VALUE; + + boolean inclusive() default true; + + class Handler implements AbstractValidator { + + private int value; + private Function comparator; + + public Handler(AbstractSWCommand.Max max, Class clazz) { + this.value = max.inclusive() ? 0 : -1; + this.comparator = createComparator("Max", clazz, max.intValue(), max.longValue(), max.floatValue(), max.doubleValue()); + } + + @Override + public boolean validate(T sender, Number value, MessageSender messageSender) { + if (value == null) return true; + return (comparator.apply(value).intValue()) <= this.value; + } + } + } + + private static Function createComparator(String type, Class clazz, int iValue, long lValue, float fValue, double dValue) { + if (clazz == int.class || clazz == Integer.class) { + return number -> Integer.compare(number.intValue(), iValue); + } else if (clazz == long.class || clazz == Long.class) { + return number -> Long.compare(number.longValue(), lValue); + } else if (clazz == float.class || clazz == Float.class) { + return number -> Float.compare(number.floatValue(), fValue); + } else if (clazz == double.class || clazz == Double.class) { + return number -> Double.compare(number.doubleValue(), dValue); + } else { + throw new IllegalArgumentException(type + " annotation is not supported for " + clazz); + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER}) + @CommandMetaData.ImplicitTypeMapper(handler = Length.Handler.class) + protected @interface Length { + int min() default 0; + + int max() default Integer.MAX_VALUE; + + class Handler implements AbstractTypeMapper { + + private int min; + private int max; + private AbstractTypeMapper inner; + + public Handler(Length length, AbstractTypeMapper inner) { + this.min = length.min(); + this.max = length.max(); + this.inner = inner; + } + + @Override + public Object map(T sender, PreviousArguments previousArguments, String s) { + if (s.length() < min || s.length() > max) return null; + return inner.map(sender, previousArguments, s); + } + + @Override + public Collection tabCompletes(T sender, PreviousArguments previousArguments, String s) { + List tabCompletes = inner.tabCompletes(sender, previousArguments, s) + .stream() + .filter(str -> str.length() >= min) + .map(str -> str.substring(0, Math.min(str.length(), max))) + .collect(Collectors.toList()); + if (s.length() < min) { + tabCompletes.add(0, s); + } + return tabCompletes; + } + + @Override + public String normalize(T sender, String s) { + return inner.normalize(sender, s); + } + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER}) + @CommandMetaData.Parameter(handler = ArrayLength.Type.class) + @CommandMetaData.ImplicitTypeMapper(handler = ArrayLength.HandlerTypeMapper.class) + @CommandMetaData.ImplicitValidator(handler = ArrayLength.HandlerValidator.class, order = 1) + protected @interface ArrayLength { + int min() default 0; + + int max() default Integer.MAX_VALUE; + + class Type implements BiPredicate, Boolean> { + @Override + public boolean test(Class clazz, Boolean isVarArgs) { + return clazz.isArray(); + } + } + + class HandlerTypeMapper implements AbstractTypeMapper { + + private int max; + private AbstractTypeMapper inner; + + public HandlerTypeMapper(ArrayLength arrayLength, AbstractTypeMapper inner) { + this.max = arrayLength.max(); + this.inner = inner; + } + + @Override + public Object map(T sender, PreviousArguments previousArguments, String s) { + return inner.map(sender, previousArguments, s); + } + + @Override + public Collection tabCompletes(T sender, PreviousArguments previousArguments, String s) { + Object[] mapped = previousArguments.getMappedArg(0); + if (mapped.length >= max) return Collections.emptyList(); + return inner.tabCompletes(sender, previousArguments, s); + } + + @Override + public String normalize(T sender, String s) { + return inner.normalize(sender, s); + } + } + + class HandlerValidator implements AbstractValidator { + + private int min; + private int max; + + public HandlerValidator(ArrayLength arrayLength) { + this.min = arrayLength.min(); + this.max = arrayLength.max(); + } + + @Override + public boolean validate(T sender, Object value, MessageSender messageSender) { + if (value == null) return true; + int length = Array.getLength(value); + return length >= min && length <= max; + } + } + } } diff --git a/src/de/steamwar/command/AbstractTypeMapper.java b/src/de/steamwar/command/AbstractTypeMapper.java index 4a340d8..040f303 100644 --- a/src/de/steamwar/command/AbstractTypeMapper.java +++ b/src/de/steamwar/command/AbstractTypeMapper.java @@ -25,12 +25,38 @@ public interface AbstractTypeMapper extends AbstractValidator { /** * The CommandSender can be null! */ - T map(K sender, String[] previousArguments, String s); + @Deprecated + default T map(K sender, String[] previousArguments, String s) { + throw new IllegalArgumentException("This method is deprecated and should not be used anymore!"); + } + + /** + * The CommandSender can be null! + */ + default T map(K sender, PreviousArguments previousArguments, String s) { + return map(sender, previousArguments.userArgs, s); + } @Override default boolean validate(K sender, T value, MessageSender messageSender) { return true; } - Collection tabCompletes(K sender, String[] previousArguments, String s); + @Deprecated + default Collection tabCompletes(K sender, String[] previousArguments, String s) { + throw new IllegalArgumentException("This method is deprecated and should not be used anymore!"); + } + + default Collection tabCompletes(K sender, PreviousArguments previousArguments, String s) { + return tabCompletes(sender, previousArguments.userArgs, s); + } + + /** + * Normalize the cache key by sender and user provided argument.
+ * Note: The CommandSender can be null!
+ * Returning null and the empty string are equivalent. + */ + default String normalize(K sender, String s) { + return null; + } } diff --git a/src/de/steamwar/command/AbstractValidator.java b/src/de/steamwar/command/AbstractValidator.java index aab4876..7dc0b72 100644 --- a/src/de/steamwar/command/AbstractValidator.java +++ b/src/de/steamwar/command/AbstractValidator.java @@ -38,10 +38,12 @@ public interface AbstractValidator { */ boolean validate(K sender, T value, MessageSender messageSender); + @Deprecated default Validator validate(C value, MessageSender messageSender) { return new Validator<>(value, messageSender); } + @Deprecated @RequiredArgsConstructor class Validator { private final C value; diff --git a/src/de/steamwar/command/CommandFrameworkException.java b/src/de/steamwar/command/CommandFrameworkException.java index 53ee025..ea3d835 100644 --- a/src/de/steamwar/command/CommandFrameworkException.java +++ b/src/de/steamwar/command/CommandFrameworkException.java @@ -36,12 +36,6 @@ public class CommandFrameworkException extends RuntimeException { private String message; - static CommandFrameworkException commandGetExceptions(String type, Class clazzType, Executable executable, int index) { - return new CommandFrameworkException(throwable -> { - return CommandFrameworkException.class.getTypeName() + ": Error while getting " + type + " for " + clazzType.getTypeName() + " with parameter index " + index; - }, null, throwable -> Stream.empty(), executable.getDeclaringClass().getTypeName() + "." + executable.getName() + "(Unknown Source)"); - } - static CommandFrameworkException commandPartExceptions(String type, Throwable cause, String current, Class clazzType, Executable executable, int index) { return new CommandFrameworkException(e -> { return CommandFrameworkException.class.getTypeName() + ": Error while " + type + " (" + current + ") to type " + clazzType.getTypeName() + " with parameter index " + index; diff --git a/src/de/steamwar/command/CommandMetaData.java b/src/de/steamwar/command/CommandMetaData.java new file mode 100644 index 0000000..33909ac --- /dev/null +++ b/src/de/steamwar/command/CommandMetaData.java @@ -0,0 +1,92 @@ +/* + * 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.*; + +public @interface CommandMetaData { + + /** + * This annotation is only for internal use. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.ANNOTATION_TYPE) + @interface Method { + Class[] value(); + int minParameterCount() default 0; + 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() default {}; + Class handler() default void.class; + } + + /** + * This annotation can be used in conjunction with a class that implements {@link AbstractTypeMapper} to + * create a custom type mapper for a parameter. The class must have one of two constructors with the following + * types: + *
    + *
  • Annotation this annotation annotates
  • + *
  • {@link Class}
  • + *
  • {@link AbstractTypeMapper}, optional, if not present only one per parameter
  • + *
  • {@link java.util.Map} with types {@link String} and {@link AbstractValidator}
  • + *
+ */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.ANNOTATION_TYPE) + @interface ImplicitTypeMapper { + /** + * The validator class that should be used. + */ + Class handler(); + } + + /** + * This annotation can be used in conjunction with a class that implements {@link AbstractValidator} to + * create a custom validator short hands for commands. The validator class must have one constructor with + * one of the following types: + *
    + *
  • Annotation this annotation annotates
  • + *
  • {@link Class}
  • + *
  • {@link java.util.Map} with types {@link String} and {@link AbstractValidator}
  • + *
+ */ + @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 9f21ec8..5eb264c 100644 --- a/src/de/steamwar/command/CommandPart.java +++ b/src/de/steamwar/command/CommandPart.java @@ -24,6 +24,7 @@ import lombok.Setter; import java.lang.reflect.Array; import java.lang.reflect.Parameter; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -31,7 +32,8 @@ import java.util.function.Consumer; class CommandPart { - private static final String[] EMPTY_ARRAY = new String[0]; + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; @AllArgsConstructor private static class CheckArgumentResult { @@ -41,7 +43,7 @@ class CommandPart { private AbstractSWCommand command; private AbstractTypeMapper typeMapper; - private AbstractValidator validator; + private List> validators = new ArrayList<>(); private Class varArgType; private String optional; @@ -53,22 +55,25 @@ class CommandPart { @Setter private boolean onlyUseIfNoneIsGiven = false; - @Setter - private boolean allowNullValues = false; - private Parameter parameter; private int parameterIndex; - public CommandPart(AbstractSWCommand command, AbstractTypeMapper typeMapper, AbstractValidator validator, Class varArgType, String optional, Parameter parameter, int parameterIndex) { + public CommandPart(AbstractSWCommand command, AbstractTypeMapper typeMapper, Class varArgType, String optional, Parameter parameter, int parameterIndex) { this.command = command; this.typeMapper = typeMapper; - this.validator = validator; this.varArgType = varArgType; this.optional = optional; this.parameter = parameter; this.parameterIndex = parameterIndex; - validatePart(); + if (optional != null && varArgType != null) { + throw new IllegalArgumentException("A vararg part can't have an optional part! In method " + parameter.getDeclaringExecutable() + " with parameter " + parameterIndex); + } + } + + void addValidator(AbstractValidator validator) { + if (validator == null) return; + validators.add(validator); } public void setNext(CommandPart next) { @@ -78,41 +83,33 @@ class CommandPart { this.next = next; } - private void validatePart() { - if (optional != null && varArgType != null) { - throw new IllegalArgumentException("A vararg part can't have an optional part! In method " + parameter.getDeclaringExecutable() + " with parameter " + parameterIndex); - } - } - 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); - if (!validArgument.success) { - throw new CommandParseException(); - } + CheckArgumentResult validArgument = checkArgument(null, sender, args, current, 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(); + for (AbstractValidator validator : validators) { + if (!validator.validate(sender, array, (s, objects) -> { + errors.accept(() -> command.sendMessage(sender, s, objects)); + })) throw new CommandParseException(); } current.add(array); return; } - CheckArgumentResult validArgument = checkArgument(errors, sender, args, startIndex); + CheckArgumentResult validArgument = checkArgument(errors, sender, args, current, startIndex); if (!validArgument.success && optional == null) { throw new CommandParseException(); } if (!validArgument.success) { if (!ignoreAsArgument) { if (!onlyUseIfNoneIsGiven) { - current.add(typeMapper.map(sender, EMPTY_ARRAY, optional)); + current.add(typeMapper.map(sender, new PreviousArguments(EMPTY_STRING_ARRAY, EMPTY_OBJECT_ARRAY), optional)); } else if (startIndex >= args.length) { - current.add(typeMapper.map(sender, EMPTY_ARRAY, optional)); + current.add(typeMapper.map(sender, new PreviousArguments(EMPTY_STRING_ARRAY, EMPTY_OBJECT_ARRAY), optional)); } else { throw new CommandParseException(); } @@ -127,18 +124,23 @@ class CommandPart { } if (next != null) { next.generateArgumentArray(errors, current, sender, args, startIndex + 1); + } else if (startIndex + 1 < args.length) { + throw new CommandParseException(); } } - public void generateTabComplete(List current, T sender, String[] args, int startIndex) { + public void generateTabComplete(List current, T sender, String[] args, List mappedArgs, int startIndex) { if (varArgType != null) { + List currentArgs = new ArrayList<>(mappedArgs); + List varArgs = new ArrayList<>(); for (int i = startIndex; i < args.length - 1; i++) { - CheckArgumentResult validArgument = checkArgument((ignore) -> {}, sender, args, i); - if (!validArgument.success) { - return; - } + CheckArgumentResult validArgument = checkArgument((ignore) -> {}, sender, args, mappedArgs, i); + if (!validArgument.success) return; + varArgs.add(validArgument.value); } - Collection strings = tabCompletes(sender, args, args.length - 1); + + currentArgs.add(varArgs.toArray()); + Collection strings = tabCompletes(sender, args, currentArgs, args.length - 1); if (strings != null) { current.addAll(strings); } @@ -146,56 +148,65 @@ class CommandPart { } if (args.length - 1 > startIndex) { - CheckArgumentResult checkArgumentResult = checkArgument((ignore) -> {}, sender, args, startIndex); + CheckArgumentResult checkArgumentResult = checkArgument((ignore) -> {}, sender, args, mappedArgs, startIndex); if (checkArgumentResult.success && next != null) { - next.generateTabComplete(current, sender, args, startIndex + 1); + if (!ignoreAsArgument) { + mappedArgs.add(checkArgumentResult.value); + } + next.generateTabComplete(current, sender, args, mappedArgs, startIndex + 1); return; } if (optional != null && next != null) { - next.generateTabComplete(current, sender, args, startIndex); + next.generateTabComplete(current, sender, args, mappedArgs, startIndex); } return; } - Collection strings = tabCompletes(sender, args, startIndex); + Collection strings = tabCompletes(sender, args, mappedArgs, startIndex); if (strings != null) { current.addAll(strings); } if (optional != null && next != null) { - next.generateTabComplete(current, sender, args, startIndex); + next.generateTabComplete(current, sender, args, mappedArgs, startIndex); } } - private Collection tabCompletes(T sender, String[] args, int startIndex) { - return TabCompletionCache.tabComplete(sender, typeMapper, command, () -> { + private Collection tabCompletes(T sender, String[] args, List mappedArgs, int startIndex) { + return TabCompletionCache.tabComplete(sender, args[startIndex], (AbstractTypeMapper) typeMapper, () -> { try { - return typeMapper.tabCompletes(sender, Arrays.copyOf(args, startIndex), args[startIndex]); + return typeMapper.tabCompletes(sender, new PreviousArguments(Arrays.copyOf(args, startIndex), mappedArgs.toArray()), args[startIndex]); } catch (Throwable e) { throw CommandFrameworkException.commandPartExceptions("tabcompleting", e, args[startIndex], (varArgType != null ? varArgType : parameter.getType()), parameter.getDeclaringExecutable(), parameterIndex); } }); } - private CheckArgumentResult checkArgument(Consumer errors, T sender, String[] args, int index) { + private CheckArgumentResult checkArgument(Consumer errors, T sender, String[] args, List mappedArgs, int index) { Object value; try { - value = typeMapper.map(sender, Arrays.copyOf(args, index), args[index]); + value = typeMapper.map(sender, new PreviousArguments(Arrays.copyOf(args, index), mappedArgs.toArray()), args[index]); } catch (Exception e) { return new CheckArgumentResult(false, null); } - if (validator != null && errors != null) { + boolean success = true; + for (AbstractValidator validator : validators) { try { if (!validator.validate(sender, value, (s, objects) -> { errors.accept(() -> { command.sendMessage(sender, s, objects); }); })) { - return new CheckArgumentResult(false, null); + success = false; + value = null; } } catch (Throwable e) { throw CommandFrameworkException.commandPartExceptions("validating", e, args[index], (varArgType != null ? varArgType : parameter.getType()), parameter.getDeclaringExecutable(), parameterIndex); } } - return new CheckArgumentResult(allowNullValues || value != null, value); + return new CheckArgumentResult(success, value); + } + + public Class getType() { + return varArgType != null ? varArgType : parameter.getType(); } } diff --git a/src/de/steamwar/command/PreviousArguments.java b/src/de/steamwar/command/PreviousArguments.java new file mode 100644 index 0000000..e9851c0 --- /dev/null +++ b/src/de/steamwar/command/PreviousArguments.java @@ -0,0 +1,62 @@ +/* + * 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.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class PreviousArguments { + + public final String[] userArgs; + public final Object[] mappedArgs; + + public PreviousArguments(String[] userArgs, Object[] mappedArgs) { + this.userArgs = userArgs; + this.mappedArgs = mappedArgs; + } + + public String getUserArg(int index) { + return userArgs[userArgs.length - index - 1]; + } + + public T getMappedArg(int index) { + return (T) mappedArgs[mappedArgs.length - index - 1]; + } + + public Optional getFirst(Class clazz) { + for (Object o : mappedArgs) { + if (clazz.isInstance(o)) { + return Optional.of((T) o); + } + } + return Optional.empty(); + } + + public List getAll(Class clazz) { + List list = new ArrayList<>(); + for (Object o : mappedArgs) { + if (clazz.isInstance(o)) { + list.add((T) o); + } + } + return list; + } +} diff --git a/src/de/steamwar/command/SWCommandUtils.java b/src/de/steamwar/command/SWCommandUtils.java index 32c46c4..c06d584 100644 --- a/src/de/steamwar/command/SWCommandUtils.java +++ b/src/de/steamwar/command/SWCommandUtils.java @@ -23,7 +23,6 @@ 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.*; @@ -42,12 +41,12 @@ public class SWCommandUtils { private SWTypeMapperCreator swTypeMapperCreator = (mapper, tabCompleter) -> new AbstractTypeMapper() { @Override - public Object map(Object sender, String[] previousArguments, String s) { + public Object map(Object sender, PreviousArguments previousArguments, String s) { return mapper.apply(s); } @Override - public Collection tabCompletes(Object sender, String[] previousArguments, String s) { + public Collection tabCompletes(Object sender, PreviousArguments previousArguments, String s) { return ((BiFunction>) tabCompleter).apply(sender, s); } }; @@ -58,10 +57,10 @@ public class SWCommandUtils { if (s.equalsIgnoreCase("false")) return false; return null; }, s -> Arrays.asList("true", "false"))); - addMapper(float.class, Float.class, createMapper(numberMapper(Float::parseFloat), numberCompleter(Float::parseFloat))); - addMapper(double.class, Double.class, createMapper(numberMapper(Double::parseDouble), numberCompleter(Double::parseDouble))); - addMapper(int.class, Integer.class, createMapper(numberMapper(Integer::parseInt), numberCompleter(Integer::parseInt))); - addMapper(long.class, Long.class, createMapper(numberMapper(Long::parseLong), numberCompleter(Long::parseLong))); + addMapper(float.class, Float.class, createMapper(numberMapper(Float::parseFloat), numberCompleter(Float::parseFloat, true))); + addMapper(double.class, Double.class, createMapper(numberMapper(Double::parseDouble), numberCompleter(Double::parseDouble, true))); + addMapper(int.class, Integer.class, createMapper(numberMapper(Integer::parseInt), numberCompleter(Integer::parseInt, false))); + addMapper(long.class, Long.class, createMapper(numberMapper(Long::parseLong), numberCompleter(Long::parseLong, false))); MAPPER_FUNCTIONS.put(String.class.getTypeName(), createMapper(s -> s, Collections::singletonList)); } @@ -74,85 +73,7 @@ public class SWCommandUtils { MAPPER_FUNCTIONS.put(alternativeClazz.getTypeName(), mapper); } - static CommandPart generateCommandPart(AbstractSWCommand command, boolean help, String[] subCommand, Parameter[] parameters, Map> localTypeMapper, Map> localValidator) { - CommandPart first = null; - CommandPart current = null; - for (String s : subCommand) { - CommandPart commandPart = new CommandPart(command, createMapper(s), null, null, null, null, -1); - commandPart.setIgnoreAsArgument(true); - if (current != null) { - current.setNext(commandPart); - } - current = commandPart; - if (first == null) { - first = current; - } - } - for (int i = 1; i < parameters.length; i++) { - Parameter parameter = parameters[i]; - AbstractTypeMapper typeMapper = getTypeMapper(parameter, localTypeMapper); - AbstractValidator validator = (AbstractValidator) getValidator(parameter, localValidator); - Class varArgType = parameter.isVarArgs() ? parameter.getType().getComponentType() : null; - AbstractSWCommand.OptionalValue optionalValue = parameter.getAnnotation(AbstractSWCommand.OptionalValue.class); - AbstractSWCommand.AllowNull allowNull = parameter.getAnnotation(AbstractSWCommand.AllowNull.class); - - CommandPart commandPart = new CommandPart<>(command, typeMapper, validator, varArgType, optionalValue != null ? optionalValue.value() : null, parameter, i); - commandPart.setOnlyUseIfNoneIsGiven(optionalValue != null && optionalValue.onlyUINIG()); - commandPart.setAllowNullValues(allowNull != null); - if (current != null) { - current.setNext(commandPart); - } - current = commandPart; - if (first == null) { - first = current; - } - } - return first; - } - - public static AbstractTypeMapper getTypeMapper(Parameter parameter, Map> localTypeMapper) { - Class clazz = parameter.getType(); - if (parameter.isVarArgs()) { - clazz = clazz.getComponentType(); - } - - 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 createEnumMapper((Class>) clazz); - } - - String name = clazz.getTypeName(); - if (classMapper != null) { - name = classMapper.value().getTypeName(); - } else if (mapper != null) { - name = mapper.value(); - } else { - AbstractSWCommand.StaticValue staticValue = parameter.getAnnotation(AbstractSWCommand.StaticValue.class); - if (staticValue != null) { - if (parameter.getType() == String.class) { - return createMapper(staticValue.value()); - } - if (staticValue.allowISE()) { - if ((parameter.getType() == boolean.class || parameter.getType() == Boolean.class)) { - List tabCompletes = new ArrayList<>(Arrays.asList(staticValue.value())); - Set falseValues = new HashSet<>(); - for (int i : staticValue.falseValues()) falseValues.add(i); - return createMapper(s -> { - int index = tabCompletes.indexOf(s); - return index == -1 ? null : !falseValues.contains(index); - }, (commandSender, s) -> tabCompletes); - } - if ((parameter.getType() == int.class || parameter.getType() == Integer.class || parameter.getType() == long.class || parameter.getType() == Long.class) && staticValue.value().length >= 2) { - List tabCompletes = new ArrayList<>(Arrays.asList(staticValue.value())); - return createMapper(s -> { - Number index = tabCompletes.indexOf(s); - return index.longValue() == -1 ? null : index; - }, (commandSender, s) -> tabCompletes); - } - } - } - } + public static AbstractTypeMapper getTypeMapper(String name, Map> localTypeMapper) { AbstractTypeMapper typeMapper = localTypeMapper.getOrDefault(name, (AbstractTypeMapper) MAPPER_FUNCTIONS.getOrDefault(name, null)); if (typeMapper == null) { throw new IllegalArgumentException("No mapper found for " + name); @@ -160,45 +81,24 @@ public class SWCommandUtils { return typeMapper; } - public static AbstractValidator getValidator(Parameter parameter, Map> localValidator) { + public static AbstractTypeMapper getTypeMapper(Parameter parameter, Map> localTypeMapper) { 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); + if (parameter.isVarArgs()) { + clazz = clazz.getComponentType(); } - - 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); + if (clazz.isEnum() && !MAPPER_FUNCTIONS.containsKey(clazz.getTypeName()) && !localTypeMapper.containsKey(clazz.getTypeName())) { + return createEnumMapper((Class>) clazz); } - - 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; + return getTypeMapper(clazz.getTypeName(), localTypeMapper); } - private static AbstractValidator getValidator(String s, Map> localGuardChecker) { - AbstractValidator validator = localGuardChecker.getOrDefault(s, (AbstractValidator) VALIDATOR_FUNCTIONS.getOrDefault(s, null)); - if (validator == null) { + public static AbstractValidator getValidator(AbstractSWCommand.Validator validator, Class type, Map> localValidator) { + String s = validator.value() != null && !validator.value().isEmpty() ? validator.value() : type.getTypeName(); + AbstractValidator concreteValidator = localValidator.getOrDefault(s, (AbstractValidator) VALIDATOR_FUNCTIONS.getOrDefault(s, null)); + if (concreteValidator == null) { throw new IllegalArgumentException("No validator found for " + s); } - return validator; + return concreteValidator; } public static void addMapper(Class clazz, AbstractTypeMapper mapper) { @@ -255,10 +155,25 @@ public class SWCommandUtils { }; } - private static Function> numberCompleter(Function mapper) { - return s -> numberMapper(mapper).apply(s) != null - ? Collections.singletonList(s) - : Collections.emptyList(); + private static Function> numberCompleter(Function mapper, boolean comma) { + return s -> { + if (numberMapper(mapper).apply(s) == null) { + return Collections.emptyList(); + } + List strings = new ArrayList<>(); + if (s.length() == 0) { + strings.add("-"); + } else { + strings.add(s); + } + for (int i = 0; i < 10; i++) { + strings.add(s + i); + } + if (comma && (!s.contains(".") || !s.contains(","))) { + strings.add(s + "."); + } + return strings; + }; } static T[] getAnnotation(Method method, Class annotation) { diff --git a/src/de/steamwar/command/SubCommand.java b/src/de/steamwar/command/SubCommand.java index 9eb6e4f..58fa919 100644 --- a/src/de/steamwar/command/SubCommand.java +++ b/src/de/steamwar/command/SubCommand.java @@ -19,17 +19,20 @@ 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; import java.util.function.Function; import java.util.function.Predicate; -public class SubCommand { +public class SubCommand implements Comparable> { private AbstractSWCommand abstractSWCommand; Method method; @@ -39,11 +42,12 @@ public class SubCommand { private Function senderFunction; AbstractValidator validator; boolean noTabComplete; - int comparableValue; + + private Parameter[] parameters; private CommandPart commandPart; - SubCommand(AbstractSWCommand abstractSWCommand, Method method, String[] subCommand, Map> localTypeMapper, Map> localValidator, boolean help, String[] description, boolean noTabComplete) { + SubCommand(AbstractSWCommand abstractSWCommand, Method method, String[] subCommand, Map> localTypeMapper, Map> localValidator, String[] description, boolean noTabComplete) { this.abstractSWCommand = abstractSWCommand; this.method = method; try { @@ -55,17 +59,34 @@ public class SubCommand { this.description = description; this.noTabComplete = noTabComplete; - Parameter[] parameters = method.getParameters(); - comparableValue = parameters[parameters.length - 1].isVarArgs() ? Integer.MAX_VALUE : -parameters.length; + parameters = method.getParameters(); + AbstractSWCommand.Validator validator = parameters[0].getAnnotation(AbstractSWCommand.Validator.class); + if (validator != null) { + this.validator = (AbstractValidator) SWCommandUtils.getValidator(validator, parameters[0].getType(), localValidator); + } - validator = (AbstractValidator) SWCommandUtils.getValidator(parameters[0], localValidator); - - commandPart = SWCommandUtils.generateCommandPart(abstractSWCommand, help, subCommand, parameters, localTypeMapper, localValidator); + commandPart = generateCommandPart(abstractSWCommand, subCommand, parameters, localTypeMapper, localValidator); senderPredicate = t -> parameters[0].getType().isAssignableFrom(t.getClass()); senderFunction = t -> parameters[0].getType().cast(t); } + @Override + public int compareTo(SubCommand o) { + int tLength = parameters.length + subCommand.length; + int oLength = o.parameters.length + o.subCommand.length; + + boolean tVarArgs = parameters[parameters.length - 1].isVarArgs(); + boolean oVarArgs = o.parameters[o.parameters.length - 1].isVarArgs(); + + if (tVarArgs) tLength *= -1; + if (oVarArgs) oLength *= -1; + + if (tVarArgs && oVarArgs) return Integer.compare(tLength, oLength); + + return -Integer.compare(tLength, oLength); + } + boolean invoke(Consumer errors, T sender, String alias, String[] args) { try { if (!senderPredicate.test(sender)) { @@ -97,7 +118,7 @@ public class SubCommand { objects.add(0, senderFunction.apply(sender)); method.invoke(abstractSWCommand, objects.toArray()); } - } catch (CommandNoHelpException | CommandFrameworkException e) { + } catch (CommandFrameworkException e) { throw e; } catch (CommandParseException e) { return false; @@ -110,18 +131,155 @@ public class SubCommand { } List tabComplete(T sender, String[] args) { - if (validator != null) { - if (!validator.validate(sender, sender, (s, objects) -> { - // ignore - })) { - return null; - } + if (validator != null && !validator.validate(sender, sender, (s, objects) -> {})) { + return null; } if (commandPart == null) { return null; } List list = new ArrayList<>(); - commandPart.generateTabComplete(list, sender, args, 0); + commandPart.generateTabComplete(list, sender, args, new ArrayList<>(), 0); return list; } + + private static CommandPart generateCommandPart(AbstractSWCommand command, String[] subCommand, Parameter[] parameters, Map> localTypeMapper, Map> localValidator) { + CommandPart first = null; + CommandPart current = null; + for (String s : subCommand) { + CommandPart commandPart = new CommandPart(command, SWCommandUtils.createMapper(s), null, null, null, -1); + commandPart.addValidator(NULL_FILTER); + commandPart.setIgnoreAsArgument(true); + if (current != null) { + current.setNext(commandPart); + } + current = commandPart; + if (first == null) { + first = current; + } + } + for (int i = 1; i < parameters.length; i++) { + Parameter parameter = parameters[i]; + AbstractTypeMapper typeMapper = handleImplicitTypeMapper(parameter, localTypeMapper); + Class varArgType = parameter.isVarArgs() ? parameter.getType().getComponentType() : null; + AbstractSWCommand.OptionalValue optionalValue = parameter.getAnnotation(AbstractSWCommand.OptionalValue.class); + + CommandPart commandPart = new CommandPart<>(command, typeMapper, varArgType, optionalValue != null ? optionalValue.value() : null, parameter, i); + commandPart.setOnlyUseIfNoneIsGiven(optionalValue != null && optionalValue.onlyUINIG()); + handleImplicitTypeValidator(parameter, commandPart, localValidator); + if (parameter.getAnnotation(AbstractSWCommand.AllowNull.class) == null) { + commandPart.addValidator((AbstractValidator) NULL_FILTER); + } + if (current != null) { + current.setNext(commandPart); + } + current = commandPart; + if (first == null) { + first = current; + } + } + return first; + } + + private static AbstractTypeMapper handleImplicitTypeMapper(Parameter parameter, Map> localTypeMapper) { + Class type = parameter.getType(); + if (parameter.isVarArgs()) { + type = type.getComponentType(); + } + + Annotation[] annotations = parameter.getAnnotations(); + Constructor sourceConstructor = null; + Annotation sourceAnnotation = null; + List> parentConstructors = new ArrayList<>(); + List parentAnnotations = new ArrayList<>(); + for (Annotation annotation : annotations) { + CommandMetaData.ImplicitTypeMapper implicitTypeMapper = annotation.annotationType().getAnnotation(CommandMetaData.ImplicitTypeMapper.class); + if (implicitTypeMapper == null) continue; + Class clazz = implicitTypeMapper.handler(); + if (!AbstractTypeMapper.class.isAssignableFrom(clazz)) continue; + Constructor[] constructors = clazz.getConstructors(); + if (constructors.length != 1) continue; + Constructor constructor = constructors[0]; + if (needsTypeMapper(constructor)) { + parentConstructors.add(constructor); + parentAnnotations.add(annotation); + } else { + if (sourceAnnotation != null) { + throw new IllegalArgumentException("Multiple source type mappers found for parameter " + parameter); + } + sourceConstructor = constructor; + sourceAnnotation = annotation; + } + } + + AbstractTypeMapper current; + if (sourceAnnotation != null) { + current = createInstance(sourceConstructor, sourceAnnotation, type, localTypeMapper); + } else { + current = (AbstractTypeMapper) SWCommandUtils.getTypeMapper(parameter, localTypeMapper); + } + for (int i = 0; i < parentConstructors.size(); i++) { + Constructor constructor = parentConstructors.get(i); + Annotation annotation = parentAnnotations.get(i); + current = createInstance(constructor, annotation, type, localTypeMapper, current); + } + return current; + } + + private static boolean needsTypeMapper(Constructor constructor) { + Class[] parameterTypes = constructor.getParameterTypes(); + for (Class parameterType : parameterTypes) { + if (AbstractTypeMapper.class.isAssignableFrom(parameterType)) { + return true; + } + } + return false; + } + + private static void handleImplicitTypeValidator(Parameter parameter, CommandPart commandPart, Map> localValidator) { + 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; + Class clazz = implicitValidator.handler(); + if (!AbstractValidator.class.isAssignableFrom(clazz)) continue; + Constructor[] constructors = clazz.getConstructors(); + if (constructors.length != 1) continue; + AbstractValidator validator = createInstance(constructors[0], annotation, commandPart.getType(), localValidator); + 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 T createInstance(Constructor constructor, Object... parameter) { + Class[] types = constructor.getParameterTypes(); + List objects = new ArrayList<>(); + for (Class clazz : types) { + boolean found = false; + for (Object o : parameter) { + if (clazz.isAssignableFrom(o.getClass())) { + objects.add(o); + found = true; + break; + } + } + if (!found) { + throw new RuntimeException("Could not find type " + clazz + " for constructor " + constructor); + } + } + try { + return (T) constructor.newInstance(objects.toArray()); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private static final AbstractValidator NULL_FILTER = (sender, value, messageSender) -> value != null; } diff --git a/src/de/steamwar/command/TabCompletionCache.java b/src/de/steamwar/command/TabCompletionCache.java index 6f2d0ff..551ce19 100644 --- a/src/de/steamwar/command/TabCompletionCache.java +++ b/src/de/steamwar/command/TabCompletionCache.java @@ -51,25 +51,29 @@ public class TabCompletionCache { @AllArgsConstructor private static class Key { private Object sender; + private String arg; 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) { + Collection tabComplete(Object sender, String arg, AbstractTypeMapper typeMapper, Supplier> tabCompleteSupplier) { if (!cached.contains(typeMapper)) return tabCompleteSupplier.get(); - Key key = global.contains(typeMapper) ? new Key(null, typeMapper) : new Key(sender, typeMapper); + + String normalizedArg = typeMapper.normalize(sender, arg); + if (normalizedArg == null) normalizedArg = ""; + Key key = new Key(global.contains(typeMapper) ? null : sender, normalizedArg, typeMapper); + TabCompletions tabCompletions = tabCompletionCache.computeIfAbsent(key, ignore -> { - return new TabCompletions(command, System.currentTimeMillis(), tabCompleteSupplier.get()); + return new TabCompletions(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); + + if (System.currentTimeMillis() - tabCompletions.timestamp > cacheDuration.get(typeMapper)) { + tabCompletions.tabCompletions = tabCompleteSupplier.get(); } tabCompletions.timestamp = System.currentTimeMillis(); return tabCompletions.tabCompletions; diff --git a/src/de/steamwar/linkage/LinkageProcessor.java b/src/de/steamwar/linkage/LinkageProcessor.java index 7965140..f547817 100644 --- a/src/de/steamwar/linkage/LinkageProcessor.java +++ b/src/de/steamwar/linkage/LinkageProcessor.java @@ -285,6 +285,7 @@ public class LinkageProcessor extends AbstractProcessor { private LinkageType resolveSingle(TypeMirror typeMirror) { String qualifier = typeMirror.toString(); + if (qualifier.contains("<")) qualifier = qualifier.substring(0, qualifier.indexOf('<')); qualifier = qualifier.substring(qualifier.lastIndexOf('.') + 1); try { return (LinkageType) Class.forName("de.steamwar.linkage.types." + qualifier + "_" + context.name()).getDeclaredConstructor().newInstance(); diff --git a/src/de/steamwar/sql/BauweltMember.java b/src/de/steamwar/sql/BauweltMember.java new file mode 100644 index 0000000..aa3ed93 --- /dev/null +++ b/src/de/steamwar/sql/BauweltMember.java @@ -0,0 +1,79 @@ +/* + 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.sql; + +import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Table; +import lombok.Getter; + +import java.util.*; + +public class BauweltMember { + private static final Map memberCache = new HashMap<>(); + + public static void clear() { + memberCache.clear(); + } + + private static final Table table = new Table<>(BauweltMember.class); + private static final SelectStatement getMember = table.select(Table.PRIMARY); + private static final SelectStatement getMembers = table.selectFields("BauweltID"); + + public static BauweltMember getBauMember(UUID ownerID, UUID memberID){ + return getBauMember(SteamwarUser.get(ownerID).getId(), SteamwarUser.get(memberID).getId()); + } + + public static BauweltMember getBauMember(int ownerID, int memberID){ + BauweltMember member = memberCache.get(memberID); + if(member != null) + return member; + return getMember.select(ownerID, memberID); + } + + public static List getMembers(UUID bauweltID){ + return getMembers(SteamwarUser.get(bauweltID).getId()); + } + + public static List getMembers(int bauweltID){ + return getMembers.listSelect(bauweltID); + } + + @Getter + @Field(keys = {Table.PRIMARY}) + private final int bauweltID; + @Getter + @Field(keys = {Table.PRIMARY}) + private final int memberID; + @Getter + @Field + private final boolean worldEdit; + @Getter + @Field + private final boolean world; + + public BauweltMember(int bauweltID, int memberID, boolean worldEdit, boolean world) { + this.bauweltID = bauweltID; + this.memberID = memberID; + this.worldEdit = worldEdit; + this.world = world; + memberCache.put(memberID, this); + } +} diff --git a/src/de/steamwar/sql/CheckedSchematic.java b/src/de/steamwar/sql/CheckedSchematic.java new file mode 100644 index 0000000..71fe24a --- /dev/null +++ b/src/de/steamwar/sql/CheckedSchematic.java @@ -0,0 +1,71 @@ +/* + 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.sql; + +import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.sql.Timestamp; +import java.util.List; + +@AllArgsConstructor +public class CheckedSchematic { + + private static final Table table = new Table<>(CheckedSchematic.class); + private static final SelectStatement statusOfNode = new SelectStatement<>(table, "SELECT * FROM CheckedSchematic WHERE NodeId = ? AND DeclineReason != 'Prüfvorgang abgebrochen' ORDER BY EndTime DESC"); + + public static List getLastDeclinedOfNode(int node){ + return statusOfNode.listSelect(node); + } + + @Field(nullable = true) + private final Integer nodeId; + @Field + private final int nodeOwner; + @Field + private final String nodeName; + @Getter + @Field + private final int validator; + @Getter + @Field + private final Timestamp startTime; + @Getter + @Field + private final Timestamp endTime; + @Getter + @Field + private final String declineReason; + + public int getNode() { + return nodeId; + } + + public String getSchemName() { + return nodeName; + } + + public int getSchemOwner() { + return nodeOwner; + } +} diff --git a/src/de/steamwar/sql/Event.java b/src/de/steamwar/sql/Event.java new file mode 100644 index 0000000..a4c30e6 --- /dev/null +++ b/src/de/steamwar/sql/Event.java @@ -0,0 +1,75 @@ +/* + 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.sql; + +import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.sql.Timestamp; + +@AllArgsConstructor +public class Event { + + private static final Table table = new Table<>(Event.class); + private static final SelectStatement byId = table.select(Table.PRIMARY); + + public static Event get(int eventID){ + return byId.select(eventID); + } + + @Getter + @Field(keys = {Table.PRIMARY}, autoincrement = true) + private final int eventID; + @Getter + @Field(keys = {"eventName"}) + private final String eventName; + @Getter + @Field + private final Timestamp deadline; + @Getter + @Field + private final Timestamp start; + @Getter + @Field + private final Timestamp end; + @Getter + @Field + private final int maximumTeamMembers; + @Field(nullable = true) + private final SchematicType schemType; + @Field + private final boolean publicSchemsOnly; + @Field + private final boolean spectateSystem; + + public boolean publicSchemsOnly() { + return publicSchemsOnly; + } + public boolean spectateSystem(){ + return spectateSystem; + } + + public SchematicType getSchematicType() { + return schemType; + } +} diff --git a/src/de/steamwar/sql/EventFight.java b/src/de/steamwar/sql/EventFight.java new file mode 100644 index 0000000..60e4831 --- /dev/null +++ b/src/de/steamwar/sql/EventFight.java @@ -0,0 +1,72 @@ +/* + 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.sql; + +import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Statement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +public class EventFight { + + private static final Table table = new Table<>(EventFight.class); + private static final SelectStatement byId = table.select(Table.PRIMARY); + private static final Statement setResult = table.update(Table.PRIMARY, "Ergebnis"); + private static final Statement setFight = table.update(Table.PRIMARY, "Fight"); + + public static EventFight get(int fightID) { + return byId.select(fightID); + } + + @Getter + @Field + private final int eventID; + @Getter + @Field(keys = {Table.PRIMARY}, autoincrement = true) + private final int fightID; + @Getter + @Field + private final int teamBlue; + @Getter + @Field + private final int teamRed; + @Getter + @Field + private final int kampfleiter; + @Getter + @Field(def = "0") + private int ergebnis; + @Field(nullable = true) + private int fight; + + public void setErgebnis(int winner) { + this.ergebnis = winner; + setResult.update(winner, fightID); + } + + public void setFight(int fight) { + //Fight.FightID, not EventFight.FightID + this.fight = fight; + setFight.update(fight, fightID); + } +} diff --git a/src/de/steamwar/sql/Fight.java b/src/de/steamwar/sql/Fight.java new file mode 100644 index 0000000..0f9eae6 --- /dev/null +++ b/src/de/steamwar/sql/Fight.java @@ -0,0 +1,61 @@ +/* + 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.sql; + +import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.Statement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; + +import java.sql.Timestamp; + +@AllArgsConstructor +public class Fight { + + private static final Table table = new Table<>(Fight.class); + private static final Statement insert = table.insertFields(true, "GameMode", "Server", "StartTime", "Duration", "BlueLeader", "RedLeader", "BlueSchem", "RedSchem", "Win", "WinCondition"); + + @Field(keys = {Table.PRIMARY}, autoincrement = true) + private final int fightID; + @Field + private final String gameMode; + @Field + private final String server; + @Field + private final Timestamp startTime; + @Field + private final int duration; + @Field + private final int blueLeader; + @Field + private final int redLeader; + @Field(nullable = true) + private final Integer blueSchem; + @Field(nullable = true) + private final Integer redSchem; + @Field + private final int win; + @Field + private final String wincondition; + + public static int create(String gamemode, String server, Timestamp starttime, int duration, int blueleader, int redleader, Integer blueschem, Integer redschem, int win, String wincondition){ + return insert.insertGetKey(gamemode, server, starttime, duration, blueleader, redleader, blueschem, redschem, win, wincondition); + } +} diff --git a/src/de/steamwar/sql/FightPlayer.java b/src/de/steamwar/sql/FightPlayer.java new file mode 100644 index 0000000..cc529b8 --- /dev/null +++ b/src/de/steamwar/sql/FightPlayer.java @@ -0,0 +1,49 @@ +/* + 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.sql; + +import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.Statement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class FightPlayer { + + private static final Table table = new Table<>(FightPlayer.class); + private static final Statement create = table.insertAll(); + + @Field(keys = {Table.PRIMARY}) + private final int fightID; + @Field(keys = {Table.PRIMARY}) + private final int userID; + @Field + private final int team; + @Field + private final String kit; + @Field + private final int kills; + @Field + private final boolean isOut; + + public static void create(int fightID, int userID, boolean blue, String kit, int kills, boolean isOut) { + create.update(fightID, userID, blue ? 1 : 2, kit, kills, isOut); + } +} diff --git a/src/de/steamwar/sql/NoClipboardException.java b/src/de/steamwar/sql/NoClipboardException.java new file mode 100644 index 0000000..9743348 --- /dev/null +++ b/src/de/steamwar/sql/NoClipboardException.java @@ -0,0 +1,23 @@ +/* + 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.sql; + +public class NoClipboardException extends RuntimeException { +} diff --git a/src/de/steamwar/sql/NodeDownload.java b/src/de/steamwar/sql/NodeDownload.java new file mode 100644 index 0000000..585b3db --- /dev/null +++ b/src/de/steamwar/sql/NodeDownload.java @@ -0,0 +1,69 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2021 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.sql; + +import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.Statement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Timestamp; +import java.time.Instant; + +@AllArgsConstructor +public class NodeDownload { + + private static final char[] HEX = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + private static final String LINK_BASE = "https://steamwar.de/download.php?schem="; + + private static final Table table = new Table<>(NodeDownload.class); + private static final Statement insert = table.insertFields("NodeId", "Link"); + + @Field(keys = {Table.PRIMARY}) + private final int nodeId; + @Field + private final String link; + @Field(def = "CURRENT_TIMESTAMP") + private final Timestamp timestamp; + + public static String getLink(SchematicNode schem){ + if(schem.isDir()) + throw new SecurityException("Can not Download Directorys"); + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException(e); + } + digest.reset(); + digest.update((Instant.now().toString() + schem.getOwner() + schem.getId()).getBytes()); + String hash = base16encode(digest.digest()); + insert.update(schem.getId(), hash); + return LINK_BASE + hash; + } + public static String base16encode(byte[] byteArray) { + StringBuilder hexBuffer = new StringBuilder(byteArray.length * 2); + for (byte b : byteArray) + hexBuffer.append(HEX[(b >>> 4) & 0xF]).append(HEX[b & 0xF]); + return hexBuffer.toString(); + } +} diff --git a/src/de/steamwar/sql/NodeMember.java b/src/de/steamwar/sql/NodeMember.java new file mode 100644 index 0000000..e25764e --- /dev/null +++ b/src/de/steamwar/sql/NodeMember.java @@ -0,0 +1,91 @@ +/* + * 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.sql; + +import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Statement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +@AllArgsConstructor +public class NodeMember { + + public static void init() { + // enforce class initialization + } + + private static final Table table = new Table<>(NodeMember.class); + private static final SelectStatement getNodeMember = table.select(Table.PRIMARY); + private static final SelectStatement getNodeMembers = table.selectFields("NodeId"); + private static final SelectStatement getSchematics = table.selectFields("UserId"); + private static final Statement create = table.insert(Table.PRIMARY); + private static final Statement delete = table.delete(Table.PRIMARY); + private static final Statement updateParent = table.update(Table.PRIMARY, "ParentId"); + + @Field(keys = {Table.PRIMARY}) + private final int nodeId; + @Field(keys = {Table.PRIMARY}) + private final int userId; + @Field(nullable = true, def = "null") + private Integer parentId; + + public int getNode() { + return nodeId; + } + + public int getMember() { + return userId; + } + + public Optional getParent() { + return Optional.ofNullable(parentId); + } + + public void delete() { + delete.update(nodeId, userId); + } + + public static NodeMember createNodeMember(int node, int member) { + create.update(node, member); + return new NodeMember(node, member, null); + } + + public static NodeMember getNodeMember(int node, int member) { + return getNodeMember.select(node, member); + } + + public static Set getNodeMembers(int node) { + return new HashSet<>(getNodeMembers.listSelect(node)); + } + + public static Set getSchematics(int member) { + return new HashSet<>(getSchematics.listSelect(member)); + } + + public void setParentId(Integer parentId) { + this.parentId = parentId; + updateParent.update(this.parentId, nodeId, userId); + } +} diff --git a/src/de/steamwar/sql/Punishment.java b/src/de/steamwar/sql/Punishment.java new file mode 100644 index 0000000..086f445 --- /dev/null +++ b/src/de/steamwar/sql/Punishment.java @@ -0,0 +1,122 @@ +/* + * 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.sql; + +import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.SqlTypeMapper; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.sql.Timestamp; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +@AllArgsConstructor +public class Punishment { + + static { + SqlTypeMapper.nameEnumMapper(PunishmentType.class); + } + + private static final Table table = new Table<>(Punishment.class, "Punishments"); + private static final SelectStatement getPunishments = new SelectStatement<>(table, "SELECT * FROM Punishments WHERE PunishmentId IN (SELECT MAX(PunishmentId) FROM Punishments WHERE UserId = ? GROUP BY Type)"); + private static final SelectStatement getPunishment = new SelectStatement<>(table, "SELECT * FROM Punishments WHERE UserId = ? AND Type = ? ORDER BY PunishmentId DESC LIMIT 1"); + + @Field(keys = {Table.PRIMARY}, autoincrement = true) + private final int punishmentId; + @Field + private final int userId; + @Field + @Getter + private final int punisher; + @Field + @Getter + private final PunishmentType type; + @Field + @Getter + private final Timestamp startTime; + @Field + @Getter + private final Timestamp endTime; + @Field + @Getter + private final boolean perma; + @Field + @Getter + private final String reason; + + public static Punishment getPunishmentOfPlayer(int user, PunishmentType type) { + return getPunishment.select(user, type); + } + + public static Map getPunishmentsOfPlayer(int user) { + return getPunishments.listSelect(user).stream().collect(Collectors.toMap(Punishment::getType, punishment -> punishment)); + } + + public static boolean isPunished(SteamwarUser user, Punishment.PunishmentType type, Consumer callback) { + Punishment punishment = Punishment.getPunishmentOfPlayer(user.getId(), type); + if(punishment == null || !punishment.isCurrent()) { + return false; + } else { + callback.accept(punishment); + return true; + } + } + + @Deprecated // Not multiling, misleading title + public String getBantime(Timestamp endTime, boolean perma) { + if (perma) { + return "permanent"; + } else { + return endTime.toLocalDateTime().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")); + } + } + + public int getUserId() { + return userId; + } + + public boolean isCurrent() { + return isPerma() || getEndTime().after(new Date()); + } + + @AllArgsConstructor + @Getter + public enum PunishmentType { + Ban(false, "BAN_TEAM", "BAN_PERMA", "BAN_UNTIL", "UNBAN_ERROR", "UNBAN"), + Mute( false, "MUTE_TEAM", "MUTE_PERMA", "MUTE_UNTIL", "UNMUTE_ERROR", "UNMUTE"), + NoSchemReceiving(false, "NOSCHEMRECEIVING_TEAM", "NOSCHEMRECEIVING_PERMA", "NOSCHEMRECEIVING_UNTIL", "UNNOSCHEMRECEIVING_ERROR", "UNNOSCHEMRECEIVING"), + NoSchemSharing(false, "NOSCHEMSHARING_TEAM", "NOSCHEMSHARING_PERMA", "NOSCHEMSHARING_UNTIL", "UNNOSCHEMSHARING_ERROR", "UNNOSCHEMSHARING"), + NoSchemSubmitting(true, "NOSCHEMSUBMITTING_TEAM", "NOSCHEMSUBMITTING_PERMA", "NOSCHEMSUBMITTING_UNTIL", "UNNOSCHEMSUBMITTING_ERROR", "UNNOSCHEMSUBMITTING"), + NoDevServer(true, "NODEVSERVER_TEAM", "NODEVSERVER_PERMA", "NODEVSERVER_UNTIL", "UNNODEVSERVER_ERROR", "UNNODEVSERVER"); + + private final boolean needsAdmin; + private final String teamMessage; + private final String playerMessagePerma; + private final String playerMessageUntil; + private final String usageNotPunished; + private final String unpunishmentMessage; + } +} diff --git a/src/de/steamwar/sql/Replay.java b/src/de/steamwar/sql/Replay.java new file mode 100644 index 0000000..a90e239 --- /dev/null +++ b/src/de/steamwar/sql/Replay.java @@ -0,0 +1,74 @@ +/* + * 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.sql; + +import de.steamwar.sql.internal.*; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.sql.SQLException; + +@AllArgsConstructor +public class Replay { + + static { + new SqlTypeMapper<>(File.class, "BLOB", (rs, identifier) -> { + try { + File file = File.createTempFile("replay", ".replay"); + file.deleteOnExit(); + Files.copy(rs.getBinaryStream(identifier), file.toPath(), StandardCopyOption.REPLACE_EXISTING); + return file; + } catch (IOException e) { + throw new SQLException(e); + } + }, (st, index, value) -> { + try { + st.setBinaryStream(index, new FileInputStream(value)); + } catch (FileNotFoundException e) { + throw new SQLException(e); + } + }); + } + + private static final Table table = new Table<>(Replay.class); + private static final SelectStatement get = table.select(Table.PRIMARY); + + public static final Statement insert = table.insertAll(); + + public static Replay get(int fightID) { + return get.select(fightID); + } + + public static void save(int fightID, File file) { + insert.update(fightID, file); + } + + @Field(keys = {Table.PRIMARY}) + private final int fightID; + @Getter + @Field + private final File replay; +} diff --git a/src/de/steamwar/sql/SQLWrapper.java b/src/de/steamwar/sql/SQLWrapper.java new file mode 100644 index 0000000..cff7dec --- /dev/null +++ b/src/de/steamwar/sql/SQLWrapper.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.sql; + +import de.steamwar.ImplementationProvider; + +import java.util.List; +import java.util.Map; + +public interface SQLWrapper { + SQLWrapper impl = ImplementationProvider.getImpl("de.steamwar.sql.SQLWrapperImpl"); + + void loadSchemTypes(List tmpTypes, Map tmpFromDB); + + void additionalExceptionMetadata(StringBuilder builder); +} diff --git a/src/de/steamwar/sql/SWException.java b/src/de/steamwar/sql/SWException.java new file mode 100644 index 0000000..0a8ece3 --- /dev/null +++ b/src/de/steamwar/sql/SWException.java @@ -0,0 +1,61 @@ +/* + * 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.sql; + +import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.Statement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; + +import java.io.File; +import java.sql.Timestamp; + +@AllArgsConstructor +public class SWException { + + public static void init() { + // force class initialialisation + } + + private static final String CWD = System.getProperty("user.dir"); + private static final String SERVER_NAME = new File(CWD).getName(); + + private static final Table table = new Table<>(SWException.class, "Exception"); + private static final Statement insert = table.insertFields("server", "message", "stacktrace"); + + @Field(keys = {Table.PRIMARY}, autoincrement = true) + private final int id; + @Field(def = "CURRENT_TIMESTAMP") + private final Timestamp time; + @Field + private final String server; + @Field + private final String message; + @Field + private final String stacktrace; + + public static void log(String message, String stacktrace){ + StringBuilder msgBuilder = new StringBuilder(message); + SQLWrapper.impl.additionalExceptionMetadata(msgBuilder); + msgBuilder.append("\nCWD: ").append(CWD); + + insert.update(SERVER_NAME, msgBuilder.toString(), stacktrace); + } +} diff --git a/src/de/steamwar/sql/SchemElo.java b/src/de/steamwar/sql/SchemElo.java new file mode 100644 index 0000000..64090ce --- /dev/null +++ b/src/de/steamwar/sql/SchemElo.java @@ -0,0 +1,44 @@ +/* + * 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.sql; + +import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class SchemElo { + + private static final Table table = new Table<>(SchemElo.class); + private static final SelectStatement select = table.select(Table.PRIMARY); + + @Field(keys = {Table.PRIMARY}) + private final int schemId; + @Field + private final int elo; + @Field(keys = {Table.PRIMARY}) + private final int season; + + public static int getElo(SchematicNode node, int season) { + SchemElo elo = select.select(node, season); + return elo != null ? elo.elo : 0; + } +} diff --git a/src/de/steamwar/sql/SchematicNode.java b/src/de/steamwar/sql/SchematicNode.java new file mode 100644 index 0000000..0e64522 --- /dev/null +++ b/src/de/steamwar/sql/SchematicNode.java @@ -0,0 +1,564 @@ +/* + * 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.sql; + +import de.steamwar.sql.internal.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.*; +import java.util.function.Predicate; + +public class SchematicNode { + + static { + new SqlTypeMapper<>(SchematicNode.class, null, (rs, identifier) -> { throw new SecurityException("SchematicNode cannot be used as type (recursive select)"); }, (st, index, value) -> st.setInt(index, value.nodeId)); + } + + private static final Map>> TAB_CACHE = new HashMap<>(); + public static void clear() { + TAB_CACHE.clear(); + } + + private static final String nodeSelector = "SELECT NodeId, NodeOwner, NodeOwner AS EffectiveOwner, NodeName, ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, ReplaceColor, AllowReplay, NodeFormat FROM SchematicNode "; + + private static final Table table = new Table<>(SchematicNode.class); + private static final Statement create = table.insertFields(true, "NodeOwner", "NodeName", "ParentNode", "NodeItem", "NodeType"); + private static final Statement update = table.update(Table.PRIMARY, "NodeName", "ParentNode", "NodeItem", "NodeType", "NodeRank", "ReplaceColor", "AllowReplay", "NodeFormat"); + private static final Statement delete = table.delete(Table.PRIMARY); + + private static final SelectStatement byId = new SelectStatement<>(table, nodeSelector + "WHERE NodeId = ?"); + private static final SelectStatement byOwnerNameParent = new SelectStatement<>(table, nodeSelector + "WHERE NodeOwner = ? AND NodeName = ? AND ParentNode " + Statement.NULL_SAFE_EQUALS + "?"); + private static final SelectStatement byParent = new SelectStatement<>(table, nodeSelector + "WHERE ParentNode" + Statement.NULL_SAFE_EQUALS + "? ORDER BY NodeName"); + private static final SelectStatement dirsByParent = new SelectStatement<>(table, nodeSelector + "WHERE ParentNode" + Statement.NULL_SAFE_EQUALS + "? AND NodeType is NULL ORDER BY NodeName"); + private static final SelectStatement byOwnerType = new SelectStatement<>(table, nodeSelector + "WHERE NodeOwner = ? AND NodeType = ? ORDER BY NodeName"); + private static final SelectStatement byType = new SelectStatement<>(table, nodeSelector + "WHERE NodeType = ? ORDER BY NodeName"); + private static final SelectStatement all = new SelectStatement<>(table, "SELECT * FROM EffectiveSchematicNode WHERE EffectiveOwner = ? ORDER BY NodeName"); + private static final SelectStatement list = new SelectStatement<>(table, "SELECT SchematicNode.NodeId, NodeOwner, ? AS EffectiveOwner, NodeName, NM.ParentId AS ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, ReplaceColor, AllowReplay, NodeFormat FROM SchematicNode INNER JOIN NodeMember NM on SchematicNode.NodeId = NM.NodeId WHERE NM.ParentId " + Statement.NULL_SAFE_EQUALS + "? AND NM.UserId = ? UNION ALL SELECT SchematicNode.NodeId, NodeOwner, ? AS EffectiveOwner, NodeName, ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, ReplaceColor, AllowReplay, NodeFormat FROM SchematicNode WHERE (? IS NULL AND ParentNode IS NULL AND NodeOwner = ?) OR (? IS NOT NULL AND ParentNode = ?) ORDER BY NodeName"); + private static final SelectStatement byParentName = new SelectStatement<>(table, "SELECT SchematicNode.NodeId, NodeOwner, ? AS EffectiveOwner, NodeName, NM.ParentId AS ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, ReplaceColor, AllowReplay, NodeFormat FROM SchematicNode INNER JOIN NodeMember NM on SchematicNode.NodeId = NM.NodeId WHERE NM.ParentId " + Statement.NULL_SAFE_EQUALS + "? AND NM.UserId = ? AND SchematicNode.NodeName = ? UNION ALL SELECT SchematicNode.NodeId, NodeOwner, ? AS EffectiveOwner, NodeName, ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, ReplaceColor, AllowReplay, NodeFormat FROM SchematicNode WHERE ((? IS NULL AND ParentNode IS NULL AND NodeOwner = ?) OR (? IS NOT NULL AND ParentNode = ?)) AND NodeName = ?"); + private static final SelectStatement schematicAccessibleForUser = new SelectStatement<>(table, "SELECT COUNT(DISTINCT NodeId) FROM EffectiveSchematicNode WHERE EffectiveOwner = ? AND NodeId = ?"); + private static final SelectStatement accessibleByUserTypeInParent = new SelectStatement<>(table, "WITH RECURSIVE RSN AS (SELECT NodeId, ParentNode FROM EffectiveSchematicNode WHERE NodeType = ? AND EffectiveOwner = ? UNION SELECT SN.NodeId, SN.ParentNode FROM RSN, EffectiveSchematicNode SN WHERE SN.NodeId = RSN.ParentNode AND EffectiveOwner = ?) SELECT SN.NodeId, SN.NodeOwner, ? AS EffectiveOwner, SN.NodeName, RSN.ParentNode, SN.LastUpdate, SN.NodeItem, SN.NodeType, SN.NodeRank, SN.ReplaceColor, SN.AllowReplay, SN.NodeFormat FROM RSN INNER JOIN SchematicNode SN ON RSN.NodeId = SN.NodeId WHERE RSN.ParentNode" + Statement.NULL_SAFE_EQUALS + "?"); + private static final SelectStatement accessibleByUserType = new SelectStatement<>(table, "SELECT * FROM EffectiveSchematicNode WHERE EffectiveOwner = ? AND NodeType = ?"); + private static final SelectStatement byIdAndUser = new SelectStatement<>(table, "SELECT NodeId, NodeOwner, ? AS EffectiveOwner, NodeName, ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, ReplaceColor, AllowReplay, NodeFormat FROM SchematicNode WHERE NodeId = ?"); + private static final SelectStatement allParentsOfNode = new SelectStatement<>(table, "WITH RECURSIVE R AS (SELECT NodeId, ParentNode FROM EffectiveSchematicNode WHERE NodeId = ? AND EffectiveOwner = ? UNION SELECT E.NodeId, E.ParentNode FROM R, EffectiveSchematicNode E WHERE R.ParentNode = E.NodeId AND E.EffectiveOwner = ?) SELECT SN.NodeId, SN.NodeOwner, ? AS EffectiveOwner, SN.NodeName, R.ParentNode, SN.LastUpdate, SN.NodeItem, SN.NodeType, SN.NodeRank, SN.ReplaceColor, SN.AllowReplay, SN.NodeFormat FROM R INNER JOIN SchematicNode SN ON SN.NodeId = R.NodeId"); + + static { + NodeMember.init(); + } + + @Field(keys = {Table.PRIMARY}, autoincrement = true) + private final int nodeId; + @Field(keys = {"OwnerNameParent"}) + private final int nodeOwner; + @Field(def = "0") + @Getter + private final int effectiveOwner; + @Field(keys = {"OwnerNameParent"}) + private String nodeName; + @Field(keys = {"OwnerNameParent"}, nullable = true) + private Integer parentNode; + @Field(def = "CURRENT_TIMESTAMP") + private Timestamp lastUpdate; + @Field(def = "''") + private String nodeItem; + @Field(def = "'normal'", nullable = true) + private SchematicType nodeType; + @Field(def = "0") + private int nodeRank; + @Field(def = "1") + private boolean replaceColor; + @Field(def = "1") + private boolean allowReplay; + @Setter(AccessLevel.PACKAGE) + @Field(def = "1") + private boolean nodeFormat; + + private String brCache; + + public SchematicNode( + int nodeId, + int nodeOwner, + int effectiveOwner, + String nodeName, + Integer parentNode, + Timestamp lastUpdate, + String nodeItem, + SchematicType nodeType, + int nodeRank, + boolean replaceColor, + boolean allowReplay, + boolean nodeFormat + ) { + this.nodeId = nodeId; + this.nodeOwner = nodeOwner; + this.effectiveOwner = effectiveOwner; + this.nodeName = nodeName; + this.parentNode = parentNode; + this.nodeItem = nodeItem; + this.nodeType = nodeType; + this.lastUpdate = lastUpdate; + this.nodeRank = nodeRank; + this.replaceColor = replaceColor; + this.allowReplay = allowReplay; + this.nodeFormat = nodeFormat; + } + + public static List getAll(SteamwarUser user) { + return all.listSelect(user); + } + + public static Map> getAllMap(SteamwarUser user) { + return map(getAll(user)); + } + + public static List list(SteamwarUser user, Integer schematicId) { + return list.listSelect(user, schematicId, user, user, schematicId, user, schematicId, schematicId); + } + + public static SchematicNode byParentName(SteamwarUser user, Integer schematicId, String name) { + return byParentName.select(user, schematicId, user, name, user, schematicId, user, schematicId, schematicId, name); + } + + public static List accessibleByUserType(SteamwarUser user, SchematicType type) { + return accessibleByUserType.listSelect(user, type); + } + + public static Map> accessibleByUserTypeMap(SteamwarUser user, SchematicType type) { + return map(accessibleByUserType(user, type)); + } + + public static boolean schematicAccessibleForUser(SteamwarUser user, Integer schematicId) { + return schematicAccessibleForUser.select(user, schematicId) != null; + } + + public static List accessibleByUserTypeParent(SteamwarUser user, SchematicType type, Integer parentId) { + return accessibleByUserTypeInParent.listSelect(type, user, user, user, parentId); + } + + public static SchematicNode byIdAndUser(SteamwarUser user, Integer id) { + return byIdAndUser.select(user, id); + } + + public static List parentsOfNode(SteamwarUser user, Integer id) { + return allParentsOfNode.listSelect(id, user, user, user); + } + + private static Map> map(List in) { + Map> map = new HashMap<>(); + for (SchematicNode effectiveSchematicNode : in) { + map.computeIfAbsent(effectiveSchematicNode.getOptionalParent().orElse(0), k -> new ArrayList<>()).add(effectiveSchematicNode); + } + return map; + } + + public static SchematicNode createSchematic(int owner, String name, Integer parent) { + return createSchematicNode(owner, name, parent, SchematicType.Normal.toDB(), ""); + } + + public static SchematicNode createSchematicDirectory(int owner, String name, Integer parent) { + return createSchematicNode(owner, name, parent, null, ""); + } + + public static SchematicNode createSchematicNode(int owner, String name, Integer parent, String type, String item) { + if (parent != null && parent == 0) + parent = null; + int nodeId = create.insertGetKey(owner, name, parent, item, type); + return getSchematicNode(nodeId); + } + + public static SchematicNode getSchematicNode(int owner, String name, SchematicNode parent) { + return getSchematicNode(owner, name, parent.getId()); + } + + public static SchematicNode getSchematicNode(int owner, String name, Integer parent) { + return byOwnerNameParent.select(owner, name, parent); + } + + public static List getSchematicNodeInNode(SchematicNode parent) { + return getSchematicNodeInNode(parent.getId()); + } + + public static List getSchematicNodeInNode(Integer parent) { + return byParent.listSelect(parent); + } + + public static List getSchematicDirectoryInNode(Integer parent) { + return dirsByParent.listSelect(parent); + } + + @Deprecated + public static SchematicNode getSchematicDirectory(String name, SchematicNode parent) { + return getSchematicNode(name, parent.getId()); + } + + @Deprecated + public static SchematicNode getSchematicDirectory(String name, Integer parent) { + return getSchematicNode(name, parent); + } + + public static SchematicNode getSchematicNode(String name, Integer parent) { + return byParentName.select(name, parent); + } + + public static SchematicNode getSchematicNode(int id) { + return byId.select(id); + } + + public static List getAccessibleSchematicsOfTypeInParent(int owner, String schemType, Integer parent) { + return accessibleByUserTypeParent(SteamwarUser.get(owner), SchematicType.fromDB(schemType), parent); + } + + public static List getAllAccessibleSchematicsOfType(int user, String schemType) { + return accessibleByUserType(SteamwarUser.get(user), SchematicType.fromDB(schemType)); + } + + public static List getAllSchematicsOfType(int owner, String schemType) { + return byOwnerType.listSelect(owner, schemType); + } + + @Deprecated + public static List getAllSchematicsOfType(String schemType) { + return byType.listSelect(schemType); + } + + public static List getAllSchematicsOfType(SchematicType schemType) { + return byType.listSelect(schemType); + } + + public static List deepGet(Integer parent, Predicate filter) { + List finalList = new ArrayList<>(); + List nodes = SchematicNode.getSchematicNodeInNode(parent); + nodes.forEach(node -> { + if (node.isDir()) { + finalList.addAll(deepGet(node.getId(), filter)); + } else { + if (filter.test(node)) + finalList.add(node); + } + }); + return finalList; + } + + @Deprecated + public static List getSchematicsAccessibleByUser(int user, Integer parent) { + return list(SteamwarUser.get(user), parent); + } + + @Deprecated + public static List getAllSchematicsAccessibleByUser(int user) { + return getAll(SteamwarUser.get(user)); + } + + public static List getAllParentsOfNode(SchematicNode node) { + return getAllParentsOfNode(node.getId()); + } + + public static List getAllParentsOfNode(int node) { + return allParentsOfNode.listSelect(node); + } + + public static SchematicNode getNodeFromPath(SteamwarUser user, String s) { + if (s.startsWith("/")) { + s = s.substring(1); + } + if (s.endsWith("/")) { + s = s.substring(0, s.length() - 1); + } + if (s.isEmpty()) { + return null; + } + if (s.contains("/")) { + String[] layers = s.split("/"); + Optional currentNode = Optional.ofNullable(SchematicNode.byParentName(user, null, layers[0])); + for (int i = 1; i < layers.length; i++) { + int finalI = i; + Optional node = currentNode.map(effectiveSchematicNode -> SchematicNode.byParentName(user, effectiveSchematicNode.getId(), layers[finalI])); + if (!node.isPresent()) { + return null; + } else { + currentNode = node; + if (!currentNode.map(SchematicNode::isDir).orElse(false) && i != layers.length - 1) { + return null; + } + } + } + return currentNode.orElse(null); + } else { + return SchematicNode.byParentName(user, null, s); + } + } + + public static List filterSchems(int user, Predicate filter) { + List finalList = new ArrayList<>(); + List nodes = getSchematicsAccessibleByUser(user, null); + nodes.forEach(node -> { + if (node.isDir()) { + finalList.addAll(deepGet(node.getId(), filter)); + } else { + if (filter.test(node)) + finalList.add(node); + } + }); + return finalList; + } + + public int getId() { + return nodeId; + } + + public int getOwner() { + return nodeOwner; + } + + public String getName() { + return nodeName; + } + + public void setName(String name) { + this.nodeName = name; + updateDB(); + } + + public Integer getParent() { + return parentNode; + } + + public Optional getOptionalParent() { + return Optional.ofNullable(parentNode); + } + + public void setParent(Integer parent) { + this.parentNode = parent; + updateDB(); + } + + public String getItem() { + if (nodeItem.isEmpty()) { + return isDir() ? "CHEST" : "CAULDRON_ITEM"; + } + return nodeItem; + } + + public void setItem(String item) { + this.nodeItem = item; + updateDB(); + } + + @Deprecated + public String getType() { + return nodeType.name(); + } + + @Deprecated + public void setType(String type) { + if(isDir()) + throw new SecurityException("Node is Directory"); + this.nodeType = SchematicType.fromDB(type); + updateDB(); + } + + public boolean isDir() { + return nodeType == null; + } + + public boolean getSchemFormat() { + if(isDir()) + throw new SecurityException("Node is Directory"); + return nodeFormat; + } + + public int getRank() { + if(isDir()) + throw new SecurityException("Node is Directory"); + return nodeRank; + } + + @Deprecated + public int getRankUnsafe() { + return nodeRank; + } + + public void setRank(int rank) { + if(isDir()) + throw new SecurityException("Node is Directory"); + this.nodeRank = rank; + } + + public SchematicType getSchemtype() { + if(isDir()) + throw new SecurityException("Is Directory"); + return nodeType; + } + + public void setSchemtype(SchematicType type) { + if(isDir()) + throw new SecurityException("Is Directory"); + this.nodeType = type; + updateDB(); + } + + public boolean replaceColor() { + return replaceColor; + } + + public void setReplaceColor(boolean replaceColor) { + if(isDir()) + throw new SecurityException("Is Directory"); + this.replaceColor = replaceColor; + updateDB(); + } + + public boolean allowReplay() { + return allowReplay; + } + + public void setAllowReplay(boolean allowReplay) { + if(isDir()) + throw new SecurityException("Is Directory"); + this.allowReplay = allowReplay; + updateDB(); + } + + public SchematicNode getParentNode() { + if(parentNode == null) return null; + return SchematicNode.getSchematicNode(parentNode); + } + + public int getElo(int season) { + return SchemElo.getElo(this, season); + } + + public boolean accessibleByUser(int user) { + return NodeMember.getNodeMember(nodeId, user) != null; + } + + public Set getMembers() { + return NodeMember.getNodeMembers(nodeId); + } + + public Timestamp getLastUpdate() { + return lastUpdate; + } + + private void updateDB() { + this.lastUpdate = Timestamp.from(Instant.now()); + update.update(nodeName, parentNode, nodeItem, nodeType, nodeRank, replaceColor, allowReplay, nodeFormat, nodeId); + TAB_CACHE.clear(); + } + + public void delete() { + delete.update(nodeId); + } + + @Override + public int hashCode() { + return nodeId; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SchematicNode)) + return false; + + return ((SchematicNode) obj).getId() == nodeId; + } + + public String generateBreadcrumbs(SteamwarUser user) { + return byIdAndUser(user, nodeId).generateBreadcrumbs(); + } + + public String generateBreadcrumbs(String split, SteamwarUser user) { + return byIdAndUser(user, nodeId).generateBreadcrumbs(split); + } + + public String generateBreadcrumbs() { + if(brCache == null) { + brCache = generateBreadcrumbs("/"); + } + return brCache; + } + + public String generateBreadcrumbs(String split) { + StringBuilder builder = new StringBuilder(getName()); + Optional currentNode = Optional.of(this); + if(currentNode.map(SchematicNode::isDir).orElse(false)) { + builder.append(split); + } + while (currentNode.isPresent()) { + currentNode = currentNode.flatMap(schematicNode -> Optional.ofNullable(NodeMember.getNodeMember(schematicNode.getId(), effectiveOwner)).map(NodeMember::getParent).orElse(schematicNode.getOptionalParent())).map(SchematicNode::getSchematicNode); + currentNode.ifPresent(node -> builder.insert(0, split).insert(0, node.getName())); + } + return builder.toString(); + } + + private static final List FORBIDDEN_NAMES = Collections.unmodifiableList(Arrays.asList("public")); + public static boolean invalidSchemName(String[] layers) { + for (String layer : layers) { + if (layer.isEmpty()) { + return true; + } + if (layer.contains("/") || + layer.contains("\\") || + layer.contains("<") || + layer.contains(">") || + layer.contains("^") || + layer.contains("°") || + layer.contains("'") || + layer.contains("\"") || + layer.contains(" ")) { + return true; + } + if(FORBIDDEN_NAMES.contains(layer.toLowerCase())) { + return true; + } + } + return false; + } + + public static List getNodeTabcomplete(SteamwarUser user, String s) { + boolean sws = s.startsWith("/"); + if (sws) { + s = s.substring(1); + } + int index = s.lastIndexOf("/"); + String cacheKey = index == -1 ? "" : s.substring(0, index); + if(TAB_CACHE.containsKey(user.getId()) && TAB_CACHE.get(user.getId()).containsKey(cacheKey)) { + return new ArrayList<>(TAB_CACHE.get(user.getId()).get(cacheKey)); + } + List list = new ArrayList<>(); + if (s.contains("/")) { + String preTab = s.substring(0, s.lastIndexOf("/") + 1); + SchematicNode pa = SchematicNode.getNodeFromPath(user, preTab); + if (pa == null) return Collections.emptyList(); + List nodes = SchematicNode.list(user, pa.getId()); + String br = pa.generateBreadcrumbs(); + nodes.forEach(node -> list.add((sws ? "/" : "") + br + node.getName() + (node.isDir() ? "/" : ""))); + } else { + List nodes = SchematicNode.list(user, null); + nodes.forEach(node -> list.add((sws ? "/" : "") + node.getName() + (node.isDir() ? "/" : ""))); + } + list.remove("//copy"); + TAB_CACHE.computeIfAbsent(user.getId(), integer -> new HashMap<>()).putIfAbsent(cacheKey, list); + return list; + } +} diff --git a/src/de/steamwar/sql/SchematicType.java b/src/de/steamwar/sql/SchematicType.java new file mode 100644 index 0000000..d87065c --- /dev/null +++ b/src/de/steamwar/sql/SchematicType.java @@ -0,0 +1,116 @@ +/* + 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.sql; + +import de.steamwar.sql.internal.SqlTypeMapper; + +import java.util.*; + +public class SchematicType { + + public static final SchematicType Normal = new SchematicType("Normal", "", Type.NORMAL, null, "STONE_BUTTON"); + + private static final Map fromDB; + private static final List types; + + static { + List tmpTypes = new LinkedList<>(); + Map tmpFromDB = new HashMap<>(); + + tmpTypes.add(Normal); + tmpFromDB.put(Normal.name().toLowerCase(), Normal); + + SQLWrapper.impl.loadSchemTypes(tmpTypes, tmpFromDB); + + fromDB = Collections.unmodifiableMap(tmpFromDB); + types = Collections.unmodifiableList(tmpTypes); + } + + static { + new SqlTypeMapper<>(SchematicType.class, "VARCHAR(16)", (rs, identifier) -> { + String t = rs.getString(identifier); + return t != null ? fromDB.get(t) : null; + }, (st, index, value) -> st.setString(index, value.toDB())); + } + + private final String name; + private final String kuerzel; + private final Type type; + private final SchematicType checkType; + private final String material; + + SchematicType(String name, String kuerzel, Type type, SchematicType checkType, String material){ + this.name = name; + this.kuerzel = kuerzel; + this.type = type; + this.checkType = checkType; + this.material = material; + } + + public boolean isAssignable(){ + return type == Type.NORMAL || (type == Type.FIGHT_TYPE && checkType != null); + } + + public SchematicType checkType(){ + return checkType; + } + + public boolean check(){ + return type == Type.CHECK_TYPE; + } + + public boolean fightType(){ + return type == Type.FIGHT_TYPE; + } + + public boolean writeable(){ + return type == Type.NORMAL; + } + + public String name(){ + return name; + } + + public String getKuerzel() { + return kuerzel; + } + + public String getMaterial() { + return material; + } + + public String toDB(){ + return name.toLowerCase(); + } + + public static SchematicType fromDB(String input){ + return fromDB.get(input.toLowerCase()); + } + + public static List values(){ + return types; + } + + enum Type{ + NORMAL, + CHECK_TYPE, + FIGHT_TYPE + } +} diff --git a/src/de/steamwar/sql/Season.java b/src/de/steamwar/sql/Season.java new file mode 100644 index 0000000..8768ad8 --- /dev/null +++ b/src/de/steamwar/sql/Season.java @@ -0,0 +1,54 @@ +/* + * 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.sql; + +import java.util.Calendar; + +public class Season { + private Season() {} + + public static int getSeason() { + Calendar calendar = Calendar.getInstance(); + int yearIndex = calendar.get(Calendar.MONTH) / 4; + return (calendar.get(Calendar.YEAR) * 3 + yearIndex); + } + + public static String getSeasonStart() { + Calendar calendar = Calendar.getInstance(); + return calendar.get(Calendar.YEAR) + "-" + (calendar.get(Calendar.MONTH) / 4 * 3 + 1) + "-1"; + } + + public static String convertSeasonToString(int season){ + if (season == -1) return ""; + int yearSeason = season % 3; + int year = (season - yearSeason) / 3; + return String.format("%d-%d", year, yearSeason); + } + + public static int convertSeasonToNumber(String season){ + if (season.isEmpty()) return -1; + String[] split = season.split("-"); + try { + return Integer.parseInt(split[0]) * 3 + Integer.parseInt(split[1]); + } catch (NumberFormatException e) { + return -1; + } + } +} diff --git a/src/de/steamwar/sql/SteamwarUser.java b/src/de/steamwar/sql/SteamwarUser.java new file mode 100644 index 0000000..874643f --- /dev/null +++ b/src/de/steamwar/sql/SteamwarUser.java @@ -0,0 +1,171 @@ +/* + * 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.sql; + +import de.steamwar.sql.internal.*; +import lombok.Getter; + +import java.util.*; + +public class SteamwarUser { + + static { + new SqlTypeMapper<>(UUID.class, "CHAR(36)", (rs, identifier) -> UUID.fromString(rs.getString(identifier)), (st, index, value) -> st.setString(index, value.toString())); + new SqlTypeMapper<>(Locale.class, "VARCHAR(32)", (rs, identifier) -> { + String l = rs.getString(identifier); + return l != null ? Locale.forLanguageTag(l) : null; + }, (st, index, value) -> st.setString(index, value.toLanguageTag())); + SqlTypeMapper.nameEnumMapper(UserGroup.class); + new SqlTypeMapper<>(SteamwarUser.class, null, (rs, identifier) -> { throw new SecurityException("SteamwarUser cannot be used as type (recursive select)"); }, (st, index, value) -> st.setInt(index, value.id)); + } + + private static final Table table = new Table<>(SteamwarUser.class, "UserData"); + private static final Statement insert = table.insertFields("UUID", "UserName"); + private static final SelectStatement byID = table.selectFields("id"); + private static final SelectStatement byUUID = table.selectFields("UUID"); + private static final SelectStatement byName = table.selectFields("UserName"); + private static final SelectStatement byDiscord = table.selectFields("DiscordId"); + private static final SelectStatement byTeam = table.selectFields("Team"); + private static final SelectStatement getServerTeam = new SelectStatement<>(table, "SELECT * FROM UserData WHERE UserGroup != 'Member' AND UserGroup != 'YouTuber'"); + private static final Statement updateName = table.update(Table.PRIMARY, "UserName"); + private static final Statement updateLocale = table.update(Table.PRIMARY, "Locale", "ManualLocale"); + private static final Statement updateTeam = table.update(Table.PRIMARY, "Team"); + private static final Statement updateLeader = table.update(Table.PRIMARY, "Leader"); + private static final Statement updateDiscord = table.update(Table.PRIMARY, "DiscordId"); + + private static final Map usersById = new HashMap<>(); + private static final Map usersByUUID = new HashMap<>(); + private static final Map usersByName = new HashMap<>(); + private static final Map usersByDiscord = new HashMap<>(); + public static void clear() { + usersById.clear(); + usersByName.clear(); + usersByUUID.clear(); + usersByDiscord.clear(); + } + + public static void invalidate(int userId) { + SteamwarUser user = usersById.remove(userId); + if (user == null) + return; + usersByName.remove(user.getUserName()); + usersByUUID.remove(user.getUUID()); + } + + @Getter + @Field(keys = {Table.PRIMARY}, autoincrement = true) + private final int id; + @Field(keys = {"uuid"}) + private final UUID uuid; + @Getter + @Field + private String userName; + @Getter + @Field(def = "'Member'") + private final UserGroup userGroup; + @Getter + @Field(def = "0") + private int team; + @Field(def = "0") + private boolean leader; + @Field(nullable = true) + private Locale locale; + @Field(def = "0") + private boolean manualLocale; + @Field(keys = {"discordId"}, nullable = true) + private Long discordId; + + public SteamwarUser(int id, UUID uuid, String userName, UserGroup userGroup, int team, boolean leader, Locale locale, boolean manualLocale, Long discordId) { + this.id = id; + this.uuid = uuid; + this.userName = userName; + this.userGroup = userGroup; + this.team = team; + this.leader = leader; + this.locale = locale; + this.manualLocale = manualLocale; + this.discordId = discordId != null && discordId != 0 ? discordId : null; + + usersById.put(id, this); + usersByName.put(userName.toLowerCase(), this); + usersByUUID.put(uuid, this); + if (this.discordId != null) { + usersByDiscord.put(discordId, this); + } + } + + public UUID getUUID() { + return uuid; + } + + public Locale getLocale() { + if(locale != null) + return locale; + return Locale.getDefault(); + } + + public void setLocale(Locale locale, boolean manualLocale) { + if (locale == null || (this.manualLocale && !manualLocale)) + return; + + this.locale = locale; + this.manualLocale = manualLocale; + updateLocale.update(locale.toLanguageTag(), manualLocale, id); + } + + public static SteamwarUser get(String userName){ + SteamwarUser user = usersByName.get(userName.toLowerCase()); + if(user != null) + return user; + return byName.select(userName); + } + + public static SteamwarUser get(UUID uuid){ + SteamwarUser user = usersByUUID.get(uuid); + if(user != null) + return user; + return byUUID.select(uuid); + } + + public static SteamwarUser get(int id) { + SteamwarUser user = usersById.get(id); + if(user != null) + return user; + return byID.select(id); + } + + public static SteamwarUser get(Long discordId) { + if(usersByDiscord.containsKey(discordId)) + return usersByDiscord.get(discordId); + return byDiscord.select(discordId); + } + + public static void createOrUpdateUsername(UUID uuid, String userName) { + insert.update(uuid, userName); + } + + public static List getServerTeam() { + return getServerTeam.listSelect(); + } + + public static List getTeam(int teamId) { + return byTeam.listSelect(teamId); + } +} diff --git a/src/de/steamwar/sql/Team.java b/src/de/steamwar/sql/Team.java new file mode 100644 index 0000000..10e6892 --- /dev/null +++ b/src/de/steamwar/sql/Team.java @@ -0,0 +1,61 @@ +/* + 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.sql; + +import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; +import java.util.stream.Collectors; + +@AllArgsConstructor +public class Team { + + private static final Table table = new Table<>(Team.class); + private static final SelectStatement select = table.select(Table.PRIMARY); + + @Field(keys = {Table.PRIMARY}) + @Getter + private final int teamId; + @Field + @Getter + private final String teamKuerzel; + @Field + @Getter + private final String teamName; + @Field(def = "'8'") + @Getter + private final String teamColor; + + private static final Team pub = new Team(0, "PUB", "Öffentlich", "8"); + + public static Team get(int id) { + if(id == 0) + return pub; + return select.select(id); + } + + public List getMembers(){ + return SteamwarUser.getTeam(teamId).stream().map(SteamwarUser::getId).collect(Collectors.toList()); + } +} diff --git a/src/de/steamwar/sql/TeamTeilnahme.java b/src/de/steamwar/sql/TeamTeilnahme.java new file mode 100644 index 0000000..533b830 --- /dev/null +++ b/src/de/steamwar/sql/TeamTeilnahme.java @@ -0,0 +1,54 @@ +/* + * 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.sql; + +import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; + +import java.util.Set; +import java.util.stream.Collectors; + +@AllArgsConstructor +public class TeamTeilnahme { + + private static final Table table = new Table<>(TeamTeilnahme.class); + private static final SelectStatement select = table.select(Table.PRIMARY); + private static final SelectStatement selectTeams = table.selectFields("EventID"); + private static final SelectStatement selectEvents = table.selectFields("TeamID"); + + @Field(keys = {Table.PRIMARY}) + private final int teamId; + @Field(keys = {Table.PRIMARY}) + private final int eventId; + + public static boolean nimmtTeil(int teamID, int eventID){ + return select.select(teamID, eventID) != null; + } + + public static Set getTeams(int eventID){ + return selectTeams.listSelect(eventID).stream().map(tt -> Team.get(tt.teamId)).collect(Collectors.toSet()); // suboptimal performance (O(n) database queries) + } + + public static Set getEvents(int teamID){ + return selectEvents.listSelect(teamID).stream().map(tt -> Event.get(tt.eventId)).collect(Collectors.toSet()); // suboptimal performance (O(n) database queries) + } +} diff --git a/src/de/steamwar/sql/UserConfig.java b/src/de/steamwar/sql/UserConfig.java new file mode 100644 index 0000000..f705dfd --- /dev/null +++ b/src/de/steamwar/sql/UserConfig.java @@ -0,0 +1,73 @@ +/* + * 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.sql; + +import de.steamwar.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Statement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; + +import java.util.UUID; + +@AllArgsConstructor +public class UserConfig { + + private static final Table table = new Table<>(UserConfig.class); + private static final SelectStatement select = table.select(Table.PRIMARY); + private static final Statement insert = table.insertAll(); + private static final Statement delete = table.delete(Table.PRIMARY); + + @Field(keys = {Table.PRIMARY}) + private final int user; + @Field(keys = {Table.PRIMARY}) + private final String config; + @Field + private final String value; + + public static String getConfig(UUID player, String config) { + return getConfig(SteamwarUser.get(player).getId(), config); + } + + public static String getConfig(int player, String config) { + UserConfig value = select.select(player, config); + return value != null ? value.value : null; + } + + public static void updatePlayerConfig(UUID uuid, String config, String value) { + updatePlayerConfig(SteamwarUser.get(uuid).getId(), config, value); + } + + public static void updatePlayerConfig(int id, String config, String value) { + if (value == null) { + removePlayerConfig(id, config); + return; + } + insert.update(id, config, value); + } + + public static void removePlayerConfig(UUID uuid, String config) { + removePlayerConfig(SteamwarUser.get(uuid).getId(), config); + } + + public static void removePlayerConfig(int id, String config) { + delete.update(id, config); + } +} diff --git a/src/de/steamwar/sql/UserGroup.java b/src/de/steamwar/sql/UserGroup.java new file mode 100644 index 0000000..cef4fee --- /dev/null +++ b/src/de/steamwar/sql/UserGroup.java @@ -0,0 +1,45 @@ +/* + 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.sql; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +public enum UserGroup { + Admin("§4", "§e", true, true, true), + Developer("§3", "§f", true, true, true), + Moderator("§c", "§f", true, true, true), + Supporter("§9", "§f", false, true, true), + Builder("§2", "§f", false, true, false), + YouTuber("§5", "§f", false, false, false), + Member("§7", "§7", false, false, false); + + @Getter + private final String colorCode; + @Getter + private final String chatColorCode; + @Getter + private final boolean adminGroup; + @Getter + private final boolean teamGroup; + @Getter + private final boolean checkSchematics; +} \ No newline at end of file diff --git a/src/de/steamwar/sql/internal/Field.java b/src/de/steamwar/sql/internal/Field.java new file mode 100644 index 0000000..90bfa1d --- /dev/null +++ b/src/de/steamwar/sql/internal/Field.java @@ -0,0 +1,34 @@ +/* + * 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.sql.internal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Field { + String[] keys() default {}; + String def() default ""; + boolean nullable() default false; + boolean autoincrement() default false; +} diff --git a/src/de/steamwar/sql/internal/SQLConfig.java b/src/de/steamwar/sql/internal/SQLConfig.java new file mode 100644 index 0000000..3153ecb --- /dev/null +++ b/src/de/steamwar/sql/internal/SQLConfig.java @@ -0,0 +1,34 @@ +/* + * 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.sql.internal; + +import de.steamwar.ImplementationProvider; + +import java.util.logging.Logger; + +public interface SQLConfig { + SQLConfig impl = ImplementationProvider.getImpl("de.steamwar.sql.SQLConfigImpl"); + + Logger getLogger(); + + int maxConnections(); + + +} diff --git a/src/de/steamwar/sql/internal/SelectStatement.java b/src/de/steamwar/sql/internal/SelectStatement.java new file mode 100644 index 0000000..e17dcb7 --- /dev/null +++ b/src/de/steamwar/sql/internal/SelectStatement.java @@ -0,0 +1,72 @@ +/* + * 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.sql.internal; + +import java.lang.reflect.InvocationTargetException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class SelectStatement extends Statement { + private final Table table; + + SelectStatement(Table table, String... kfields) { + this(table, "SELECT " + Arrays.stream(table.fields).map(f -> f.identifier).collect(Collectors.joining(", ")) + " FROM " + table.name + " WHERE " + Arrays.stream(kfields).map(f -> f + " = ?").collect(Collectors.joining(" AND "))); + } + + public SelectStatement(Table table, String sql) { + super(sql); + this.table = table; + } + + public T select(Object... values) { + return select(rs -> { + if (rs.next()) + return read(rs); + return null; + }, values); + } + + public List listSelect(Object... values) { + return select(rs -> { + List result = new ArrayList<>(); + while (rs.next()) + result.add(read(rs)); + + return result; + }, values); + } + + private T read(ResultSet rs) throws SQLException { + Object[] params = new Object[table.fields.length]; + for(int i = 0; i < params.length; i++) { + params[i] = table.fields[i].read(rs); + } + + try { + return table.constructor.newInstance(params); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new SecurityException(e); + } + } +} diff --git a/src/de/steamwar/sql/internal/SqlTypeMapper.java b/src/de/steamwar/sql/internal/SqlTypeMapper.java new file mode 100644 index 0000000..34c6173 --- /dev/null +++ b/src/de/steamwar/sql/internal/SqlTypeMapper.java @@ -0,0 +1,110 @@ +/* + * 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.sql.internal; + +import java.io.InputStream; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.Map; + +public final class SqlTypeMapper { + private static final Map, SqlTypeMapper> mappers = new IdentityHashMap<>(); + + public static SqlTypeMapper getMapper(Class clazz) { + return (SqlTypeMapper) mappers.get(clazz); + } + + public static > void ordinalEnumMapper(Class type) { + T[] enumConstants = type.getEnumConstants(); + new SqlTypeMapper<>( + type, + "INTEGER(" + (int)Math.ceil(enumConstants.length/256.0) + ")", + (rs, identifier) -> enumConstants[rs.getInt(identifier)], + (st, index, value) -> st.setInt(index, value.ordinal()) + ); + } + + public static > void nameEnumMapper(Class type) { + new SqlTypeMapper<>( + type, + "VARCHAR(" + Arrays.stream(type.getEnumConstants()).map(e -> e.name().length()).max(Integer::compareTo).orElse(0) + ")", + (rs, identifier) -> Enum.valueOf(type, rs.getString(identifier)), + (st, index, value) -> st.setString(index, value.name()) + ); + } + + static { + primitiveMapper(boolean.class, Boolean.class, "BOOLEAN", ResultSet::getBoolean, PreparedStatement::setBoolean); + primitiveMapper(byte.class, Byte.class, "INTEGER(1)", ResultSet::getByte, PreparedStatement::setByte); + primitiveMapper(short.class, Short.class, "INTEGER(2)", ResultSet::getShort, PreparedStatement::setShort); + primitiveMapper(int.class, Integer.class, "INTEGER", ResultSet::getInt, PreparedStatement::setInt); + primitiveMapper(long.class, Long.class, "INTEGER(8)", ResultSet::getLong, PreparedStatement::setLong); + primitiveMapper(float.class, Float.class, "REAL", ResultSet::getFloat, PreparedStatement::setFloat); + primitiveMapper(double.class, Double.class, "REAL", ResultSet::getDouble, PreparedStatement::setDouble); + new SqlTypeMapper<>(String.class, "TEXT", ResultSet::getString, PreparedStatement::setString); + new SqlTypeMapper<>(Timestamp.class, "TIMESTAMP", ResultSet::getTimestamp, PreparedStatement::setTimestamp); + new SqlTypeMapper<>(InputStream.class, "BLOB", ResultSet::getBinaryStream, PreparedStatement::setBinaryStream); + } + + private static void primitiveMapper(Class primitive, Class wrapped, String sqlType, SQLReader reader, SQLWriter writer) { + new SqlTypeMapper<>(primitive, sqlType, reader, writer); + new SqlTypeMapper<>(wrapped, sqlType, (rs, identifier) -> { + T value = reader.read(rs, identifier); + return rs.wasNull() ? null : value; + }, writer); + } + + private final String sqlType; + private final SQLReader reader; + private final SQLWriter writer; + + public SqlTypeMapper(Class clazz, String sqlType, SQLReader reader, SQLWriter writer) { + this.sqlType = sqlType; + this.reader = reader; + this.writer = writer; + mappers.put(clazz, this); + } + + public T read(ResultSet rs, String identifier) throws SQLException { + return reader.read(rs, identifier); + } + + public void write(PreparedStatement st, int index, Object value) throws SQLException { + writer.write(st, index, (T) value); + } + + public String sqlType() { + return sqlType; + } + + @FunctionalInterface + public interface SQLReader { + T read(ResultSet rs, String identifier) throws SQLException; + } + + @FunctionalInterface + public interface SQLWriter { + void write(PreparedStatement st, int index, T value) throws SQLException; + } +} diff --git a/src/de/steamwar/sql/internal/Statement.java b/src/de/steamwar/sql/internal/Statement.java new file mode 100644 index 0000000..e842d5f --- /dev/null +++ b/src/de/steamwar/sql/internal/Statement.java @@ -0,0 +1,294 @@ +/* + * 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.sql.internal; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.sql.*; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class Statement implements AutoCloseable { + + private static final Logger logger = SQLConfig.impl.getLogger(); + + private static final List statements = new ArrayList<>(); + private static final Deque connections = new ArrayDeque<>(); + private static final int MAX_CONNECTIONS; + private static final Supplier conProvider; + static final Consumer> schemaCreator; + static final String ON_DUPLICATE_KEY; + static final UnaryOperator upsertWrapper; + public static final String NULL_SAFE_EQUALS; + + private static final boolean MYSQL_MODE; + private static final boolean PRODUCTION_DATABASE; + + static { + File file = new File(System.getProperty("user.home"), "mysql.properties"); + MYSQL_MODE = file.exists(); + + if(MYSQL_MODE) { + Properties properties = new Properties(); + try { + properties.load(new FileReader(file)); + } catch (IOException e) { + throw new SecurityException("Could not load SQL connection", e); + } + + String url = "jdbc:mysql://" + properties.getProperty("host") + ":" + properties.getProperty("port") + "/" + properties.getProperty("database") + "?useServerPrepStmts=true"; + String user = properties.getProperty("user"); + String password = properties.getProperty("password"); + + PRODUCTION_DATABASE = "core".equals(properties.getProperty("database")); + MAX_CONNECTIONS = SQLConfig.impl.maxConnections(); + conProvider = () -> { + try { + return DriverManager.getConnection(url, user, password); + } catch (SQLException e) { + throw new SecurityException("Could not create MySQL connection", e); + } + }; + schemaCreator = table -> {}; + ON_DUPLICATE_KEY = " ON DUPLICATE KEY UPDATE "; + upsertWrapper = f -> f + " = VALUES(" + f + ")"; + NULL_SAFE_EQUALS = " <=> "; + } else { + Connection connection; + + try { + Class.forName("org.sqlite.JDBC"); + connection = DriverManager.getConnection("jdbc:sqlite:" + System.getProperty("user.home") + "/standalone.db"); + } catch (SQLException | ClassNotFoundException e) { + throw new SecurityException("Could not create sqlite connection", e); + } + + PRODUCTION_DATABASE = false; + MAX_CONNECTIONS = 1; + conProvider = () -> connection; + schemaCreator = Table::ensureExistanceInSqlite; + ON_DUPLICATE_KEY = " ON CONFLICT DO UPDATE SET "; + upsertWrapper = f -> f + " = " + f; + NULL_SAFE_EQUALS = " IS "; + } + } + + private static int connectionBudget = MAX_CONNECTIONS; + + public static void closeAll() { + synchronized (connections) { + while(connectionBudget < MAX_CONNECTIONS) { + if(connections.isEmpty()) + waitOnConnections(); + else + closeConnection(aquireConnection()); + } + } + } + + public static boolean mysqlMode() { + return MYSQL_MODE; + } + + public static boolean productionDatabase() { + return PRODUCTION_DATABASE; + } + + private final boolean returnGeneratedKeys; + private final String sql; + private final Map cachedStatements = new HashMap<>(); + + public Statement(String sql) { + this(sql, false); + } + + public Statement(String sql, boolean returnGeneratedKeys) { + this.sql = sql; + this.returnGeneratedKeys = returnGeneratedKeys; + synchronized (statements) { + statements.add(this); + } + } + + public T select(ResultSetUser user, Object... objects) { + return withConnection(st -> { + ResultSet rs = st.executeQuery(); + T result = user.use(rs); + rs.close(); + return result; + }, objects); + } + + public void update(Object... objects) { + withConnection(PreparedStatement::executeUpdate, objects); + } + + public int insertGetKey(Object... objects) { + return withConnection(st -> { + st.executeUpdate(); + ResultSet rs = st.getGeneratedKeys(); + rs.next(); + return rs.getInt(1); + }, objects); + } + + public String getSql() { + return sql; + } + + private T withConnection(SQLRunnable runnable, Object... objects) { + Connection connection = aquireConnection(); + T result; + + try { + result = tryWithConnection(connection, runnable, objects); + } catch (Throwable e) { + if(connectionInvalid(connection)) { + closeConnection(connection); + + return withConnection(runnable, objects); + } else { + synchronized (connections) { + connections.push(connection); + connections.notify(); + } + + throw new SecurityException("Failing sql statement", e); + } + } + + synchronized (connections) { + connections.push(connection); + connections.notify(); + } + + return result; + } + + private boolean connectionInvalid(Connection connection) { + try { + return connection.isClosed() || !connection.isValid(1); + } catch (SQLException e) { + logger.log(Level.INFO, "Could not check SQL connection status", e); // No database logging possible at this state + return true; + } + } + + private T tryWithConnection(Connection connection, SQLRunnable runnable, Object... objects) throws SQLException { + PreparedStatement st = cachedStatements.get(connection); + if(st == null) { + if(returnGeneratedKeys) + st = connection.prepareStatement(sql, java.sql.Statement.RETURN_GENERATED_KEYS); + else + st = connection.prepareStatement(sql); + cachedStatements.put(connection, st); + } + + for (int i = 0; i < objects.length; i++) { + Object o = objects[i]; + if(o != null) + SqlTypeMapper.getMapper(o.getClass()).write(st, i+1, o); + else + st.setNull(i+1, Types.NULL); + } + + return runnable.run(st); + } + + @Override + public void close() { + cachedStatements.values().forEach(st -> closeStatement(st, false)); + cachedStatements.clear(); + synchronized (statements) { + statements.remove(this); + } + } + + private void close(Connection connection) { + PreparedStatement st = cachedStatements.remove(connection); + if(st != null) + closeStatement(st, true); + } + + private static Connection aquireConnection() { + synchronized (connections) { + while(connections.isEmpty() && connectionBudget == 0) + waitOnConnections(); + + if(!connections.isEmpty()) { + return connections.pop(); + } else { + Connection connection = conProvider.get(); + connectionBudget--; + return connection; + } + } + } + + private static void closeConnection(Connection connection) { + synchronized (statements) { + for (Statement statement : statements) { + statement.close(connection); + } + } + try { + connection.close(); + } catch (SQLException e) { + logger.log(Level.INFO, "Could not close connection", e); + } + + synchronized (connections) { + connectionBudget++; + connections.notify(); + } + } + + private static void waitOnConnections() { + synchronized (connections) { + try { + connections.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + private static void closeStatement(PreparedStatement st, boolean silent) { + try { + st.close(); + } catch (SQLException e) { + if(!silent) + logger.log(Level.INFO, "Could not close statement", e); + } + } + + public interface ResultSetUser { + T use(ResultSet rs) throws SQLException; + } + + private interface SQLRunnable { + T run(PreparedStatement st) throws SQLException; + } +} diff --git a/src/de/steamwar/sql/internal/Table.java b/src/de/steamwar/sql/internal/Table.java new file mode 100644 index 0000000..cbad10a --- /dev/null +++ b/src/de/steamwar/sql/internal/Table.java @@ -0,0 +1,137 @@ +/* + * 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.sql.internal; + +import java.lang.reflect.Constructor; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class Table { + public static final String PRIMARY = "primary"; + + final String name; + final TableField[] fields; + private final Map> fieldsByIdentifier = new HashMap<>(); + final Constructor constructor; + + private final Map[]> keys; + + + public Table(Class clazz) { + this(clazz, clazz.getSimpleName()); + } + + public Table(Class clazz, String name) { + this.name = name; + this.fields = Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(Field.class)).map(TableField::new).toArray(TableField[]::new); + try { + this.constructor = clazz.getDeclaredConstructor(Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(Field.class)).map(java.lang.reflect.Field::getType).toArray(Class[]::new)); + } catch (NoSuchMethodException e) { + throw new SecurityException(e); + } + + keys = Arrays.stream(fields).flatMap(field -> Arrays.stream(field.field.keys())).distinct().collect(Collectors.toMap(Function.identity(), key -> Arrays.stream(fields).filter(field -> Arrays.asList(field.field.keys()).contains(key)).toArray(TableField[]::new))); + + for (TableField field : fields) { + fieldsByIdentifier.put(field.identifier.toLowerCase(), field); + } + + Statement.schemaCreator.accept(this); + } + + public SelectStatement select(String name) { + return selectFields(keyFields(name)); + } + public SelectStatement selectFields(String... kfields) { + return new SelectStatement<>(this, kfields); + } + + public Statement update(String name, String... fields) { + return updateFields(fields, keyFields(name)); + } + + public Statement updateField(String field, String... kfields) { + return updateFields(new String[]{field}, kfields); + } + + public Statement updateFields(String[] fields, String... kfields) { + return new Statement("UPDATE " + name + " SET " + Arrays.stream(fields).map(f -> f + " = ?").collect(Collectors.joining(", ")) + " WHERE " + Arrays.stream(kfields).map(f -> f + " = ?").collect(Collectors.joining(" AND "))); + } + + public Statement insert(String name) { + return insertFields(keyFields(name)); + } + + public Statement insertAll() { + return insertFields(false, Arrays.stream(fields).map(f -> f.identifier).toArray(String[]::new)); + } + + public Statement insertFields(String... fields) { + return insertFields(false, fields); + } + + public Statement insertFields(boolean returnGeneratedKeys, String... fields) { + List nonKeyFields = Arrays.stream(fields).filter(f -> fieldsByIdentifier.get(f.toLowerCase()).field.keys().length == 0).collect(Collectors.toList()); + return new Statement("INSERT INTO " + name + " (" + String.join(", ", fields) + ") VALUES (" + Arrays.stream(fields).map(f -> "?").collect(Collectors.joining(", ")) + ")" + (nonKeyFields.isEmpty() ? "" : Statement.ON_DUPLICATE_KEY + nonKeyFields.stream().map(Statement.upsertWrapper).collect(Collectors.joining(", "))), returnGeneratedKeys); + } + + public Statement delete(String name) { + return deleteFields(keyFields(name)); + } + + public Statement deleteFields(String... kfields) { + return new Statement("DELETE FROM " + name + " WHERE " + Arrays.stream(kfields).map(f -> f + " = ?").collect(Collectors.joining(" AND "))); + } + + void ensureExistanceInSqlite() { + try (Statement statement = new Statement( + "CREATE TABLE IF NOT EXISTS " + name + "(" + + Arrays.stream(fields).map(field -> field.identifier + " " + field.mapper.sqlType() + (field.field.nullable() ? " DEFAULT NULL" : " NOT NULL") + (field.field.nullable() || field.field.def().equals("") ? "" : " DEFAULT " + field.field.def())).collect(Collectors.joining(", ")) + + keys.entrySet().stream().map(key -> (key.getKey().equals(PRIMARY) ? ", PRIMARY KEY(" : ", UNIQUE (") + Arrays.stream(key.getValue()).map(field -> field.identifier).collect(Collectors.joining(", ")) + ")").collect(Collectors.joining(" ")) + + ")")) { + statement.update(); + } + } + + private String[] keyFields(String name) { + return Arrays.stream(keys.get(name)).map(f -> f.identifier).toArray(String[]::new); + } + + static class TableField { + + final String identifier; + + final SqlTypeMapper mapper; + private final Field field; + + private TableField(java.lang.reflect.Field field) { + this.identifier = field.getName(); + this.mapper = SqlTypeMapper.getMapper(field.getType()); + this.field = field.getAnnotation(Field.class); + } + + T read(ResultSet rs) throws SQLException { + return mapper.read(rs, identifier); + } + } +} diff --git a/steamwarci.yml b/steamwarci.yml index 73f8039..47930f4 100644 --- a/steamwarci.yml +++ b/steamwarci.yml @@ -1,5 +1,6 @@ -build: +setup: - "ln -s /home/gitea/lib" - - "cp ~/gradle.properties ." - - "chmod u+x build.gradle" + +build: - "./gradlew buildProject" + - "./gradlew --stop" diff --git a/testsrc/de/steamwar/command/ArgumentCommand.java b/testsrc/de/steamwar/command/ArgumentCommand.java index a107270..41f59c7 100644 --- a/testsrc/de/steamwar/command/ArgumentCommand.java +++ b/testsrc/de/steamwar/command/ArgumentCommand.java @@ -57,4 +57,18 @@ public class ArgumentCommand extends TestSWCommand { public void argument(String sender, String arg) { throw new ExecutionIdentifier("RunArgument with String"); } + + @Register + public void minLengthArgument(String sender, @Length(min = 3) @StaticValue({"he", "hello"}) String arg) { + throw new ExecutionIdentifier("RunLengthArgument with String"); + } + + @Register + public void minAndMaxLengthArgument(String sender, @Length(min = 3, max = 3) @StaticValue({"wo", "world"}) String arg) { + throw new ExecutionIdentifier("RunLengthArgument with String"); + } + + public void arrayLengthArgument(String sender, @ArrayLength(max = 3) @StaticValue({"one", "two", "three"}) String... args) { + throw new ExecutionIdentifier("RunArrayLengthArgument with String"); + } } diff --git a/testsrc/de/steamwar/command/ArgumentCommandTest.java b/testsrc/de/steamwar/command/ArgumentCommandTest.java index e57edd0..b46c184 100644 --- a/testsrc/de/steamwar/command/ArgumentCommandTest.java +++ b/testsrc/de/steamwar/command/ArgumentCommandTest.java @@ -44,6 +44,7 @@ public class ArgumentCommandTest { ArgumentCommand cmd = new ArgumentCommand(); try { cmd.execute("test", "", new String[]{"true", "false"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunArgument with Boolean"); } @@ -54,6 +55,7 @@ public class ArgumentCommandTest { ArgumentCommand cmd = new ArgumentCommand(); try { cmd.execute("test", "", new String[]{"0.0", "0.0", "0.0"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunArgument with Float"); } @@ -64,6 +66,7 @@ public class ArgumentCommandTest { ArgumentCommand cmd = new ArgumentCommand(); try { cmd.execute("test", "", new String[]{"0.0", "0.0", "0.0", "0.0"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunArgument with Double"); } @@ -74,6 +77,7 @@ public class ArgumentCommandTest { ArgumentCommand cmd = new ArgumentCommand(); try { cmd.execute("test", "", new String[]{"0"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunArgument with Integer"); } @@ -84,26 +88,17 @@ public class ArgumentCommandTest { ArgumentCommand cmd = new ArgumentCommand(); try { cmd.execute("test", "", new String[]{"0", "0"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunArgument with Long"); } } - @Test - public void testString() { - ArgumentCommand cmd = new ArgumentCommand(); - try { - cmd.execute("test", "", new String[]{"Hello World"}); - } catch (Exception e) { - assertCMDFramework(e, ExecutionIdentifier.class, "RunArgument with String"); - } - } - @Test public void testTabComplete() { ArgumentCommand cmd = new ArgumentCommand(); List strings = cmd.tabComplete("test", "", new String[]{""}); - assertTabCompletes(strings, "true", "false"); + assertTabCompletes(strings, "true", "false", "hello", "wor"); } @Test @@ -111,5 +106,23 @@ public class ArgumentCommandTest { ArgumentCommand cmd = new ArgumentCommand(); List strings = cmd.tabComplete("test", "", new String[]{"t"}); assertTabCompletes(strings, "true", "t"); + + strings = cmd.tabComplete("test", "", new String[]{"h"}); + assertTabCompletes(strings, "h", "hello"); + + strings = cmd.tabComplete("test", "", new String[]{"hel"}); + assertTabCompletes(strings, "hel", "hello"); + + strings = cmd.tabComplete("test", "", new String[]{"w"}); + assertTabCompletes(strings, "w", "wor"); + + strings = cmd.tabComplete("test", "", new String[]{"wor"}); + assertTabCompletes(strings, "wor"); + + strings = cmd.tabComplete("test", "", new String[]{"worl"}); + assertTabCompletes(strings, "wor", "worl"); + + strings = cmd.tabComplete("test", "", new String[]{"one", "two", "three", "one"}); + assertTabCompletes(strings); } } diff --git a/testsrc/de/steamwar/command/BetterExceptionCommand.java b/testsrc/de/steamwar/command/BetterExceptionCommand.java index cd38179..feb47fc 100644 --- a/testsrc/de/steamwar/command/BetterExceptionCommand.java +++ b/testsrc/de/steamwar/command/BetterExceptionCommand.java @@ -39,17 +39,18 @@ public class BetterExceptionCommand extends TestSWCommand { public TestTypeMapper tabCompleteException() { return new TestTypeMapper() { @Override - public String map(String sender, String[] previousArguments, String s) { + public String map(String sender, PreviousArguments previousArguments, String s) { return null; } @Override public boolean validate(String sender, String value, MessageSender messageSender) { + System.out.println("Validate: " + value); throw new SecurityException(); } @Override - public Collection tabCompletes(String sender, String[] previousArguments, String s) { + public Collection tabCompletes(String sender, PreviousArguments previousArguments, String s) { throw new SecurityException(); } }; diff --git a/testsrc/de/steamwar/command/CacheCommand.java b/testsrc/de/steamwar/command/CacheCommand.java index 1771bca..7cfdb72 100644 --- a/testsrc/de/steamwar/command/CacheCommand.java +++ b/testsrc/de/steamwar/command/CacheCommand.java @@ -41,15 +41,14 @@ public class CacheCommand extends TestSWCommand { @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) { + public Integer map(String sender, PreviousArguments previousArguments, String s) { return Integer.parseInt(s); } @Override - public Collection tabCompletes(String sender, String[] previousArguments, String s) { + public Collection tabCompletes(String sender, PreviousArguments previousArguments, String s) { return Arrays.asList(count.getAndIncrement() + ""); } }; diff --git a/testsrc/de/steamwar/command/CacheCommandTest.java b/testsrc/de/steamwar/command/CacheCommandTest.java index 122528f..4a6e183 100644 --- a/testsrc/de/steamwar/command/CacheCommandTest.java +++ b/testsrc/de/steamwar/command/CacheCommandTest.java @@ -36,6 +36,14 @@ public class CacheCommandTest { assertThat(tabCompletions1, is(equalTo(tabCompletions2))); } + @Test + public void testCachingWithDifferentMessages() { + CacheCommand cmd = new CacheCommand(); + List tabCompletions1 = cmd.tabComplete("test", "", new String[]{""}); + List tabCompletions2 = cmd.tabComplete("test", "", new String[]{"0"}); + assertThat(tabCompletions1, is(equalTo(tabCompletions2))); + } + @Test public void testCachingWithDifferentSenders() { CacheCommand cmd = new CacheCommand(); diff --git a/testsrc/de/steamwar/command/NullMapperCommand.java b/testsrc/de/steamwar/command/NullMapperCommand.java index e47eab7..30f4061 100644 --- a/testsrc/de/steamwar/command/NullMapperCommand.java +++ b/testsrc/de/steamwar/command/NullMapperCommand.java @@ -43,7 +43,7 @@ public class NullMapperCommand extends TestSWCommand { public TestTypeMapper typeMapper() { return new TestTypeMapper() { @Override - public String map(String sender, String[] previousArguments, String s) { + public String map(String sender, PreviousArguments previousArguments, String s) { if (s.equals("Hello World")) { return null; } @@ -51,7 +51,7 @@ public class NullMapperCommand extends TestSWCommand { } @Override - public Collection tabCompletes(String sender, String[] previousArguments, String s) { + public Collection tabCompletes(String sender, PreviousArguments previousArguments, String s) { return null; } }; diff --git a/src/de/steamwar/command/CommandNoHelpException.java b/testsrc/de/steamwar/command/NumberValidatorCommand.java similarity index 62% rename from src/de/steamwar/command/CommandNoHelpException.java rename to testsrc/de/steamwar/command/NumberValidatorCommand.java index e3d476a..9ba53a6 100644 --- a/src/de/steamwar/command/CommandNoHelpException.java +++ b/testsrc/de/steamwar/command/NumberValidatorCommand.java @@ -1,7 +1,7 @@ /* * This file is a part of the SteamWar software. * - * Copyright (C) 2020 SteamWar.de-Serverteam + * 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 @@ -19,7 +19,17 @@ package de.steamwar.command; -class CommandNoHelpException extends RuntimeException { - - CommandNoHelpException() {} +import de.steamwar.command.dto.ExecutionIdentifier; +import de.steamwar.command.dto.TestSWCommand; + +public class NumberValidatorCommand extends TestSWCommand { + + public NumberValidatorCommand() { + super("numberValidator"); + } + + @Register + public void test(String sender, @Min(intValue = 0) @Max(intValue = 10) int i) { + throw new ExecutionIdentifier("RunNumberValidator with int"); + } } diff --git a/testsrc/de/steamwar/command/NumberValidatorCommandTest.java b/testsrc/de/steamwar/command/NumberValidatorCommandTest.java new file mode 100644 index 0000000..c45b4a8 --- /dev/null +++ b/testsrc/de/steamwar/command/NumberValidatorCommandTest.java @@ -0,0 +1,51 @@ +/* + * 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; + +public class NumberValidatorCommandTest { + + @Test + public void testMinValue() { + NumberValidatorCommand command = new NumberValidatorCommand(); + command.execute("sender", "", new String[]{"-1"}); + } + + @Test + public void testMaxValue() { + NumberValidatorCommand command = new NumberValidatorCommand(); + command.execute("sender", "", new String[]{"11"}); + } + + @Test + public void testValidValue() { + try { + NumberValidatorCommand command = new NumberValidatorCommand(); + command.execute("sender", "", new String[]{"2"}); + assert false; + } catch (Exception e) { + assertCMDFramework(e, ExecutionIdentifier.class, "RunNumberValidator with int"); + } + } +} diff --git a/testsrc/de/steamwar/command/PartOfCommand.java b/testsrc/de/steamwar/command/PartOfCommand.java new file mode 100644 index 0000000..9c66a4b --- /dev/null +++ b/testsrc/de/steamwar/command/PartOfCommand.java @@ -0,0 +1,70 @@ +/* + * 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.TestTypeMapper; + +import java.util.Arrays; +import java.util.Collection; + +public class PartOfCommand { + + public static class ParentCommand extends TestSWCommand { + + public ParentCommand() { + super("parent"); + } + + @Register + public void execute(String s, String... args) { + throw new ExecutionIdentifier("ParentCommand with String..."); + } + + @Mapper("test") + public TestTypeMapper typeMapper() { + return new TestTypeMapper() { + @Override + public Integer map(String sender, PreviousArguments previousArguments, String s) { + return -1; + } + + @Override + public Collection tabCompletes(String sender, PreviousArguments previousArguments, String s) { + return Arrays.asList(s); + } + }; + } + } + + @AbstractSWCommand.PartOf(ParentCommand.class) + public static class SubCommand extends TestSWCommand { + + public SubCommand() { + super(null); + } + + @Register + public void execute(String s, @Mapper("test") int i) { + throw new ExecutionIdentifier("SubCommand with int " + i); + } + } +} diff --git a/testsrc/de/steamwar/command/PartOfCommandTest.java b/testsrc/de/steamwar/command/PartOfCommandTest.java new file mode 100644 index 0000000..726384e --- /dev/null +++ b/testsrc/de/steamwar/command/PartOfCommandTest.java @@ -0,0 +1,42 @@ +/* + * 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 PartOfCommandTest { + + @Test + public void testMerging() { + PartOfCommand.ParentCommand command = new PartOfCommand.ParentCommand(); + new PartOfCommand.SubCommand(); + try { + command.execute("test", "", new String[]{"0"}); + assertThat(true, is(false)); + } catch (Exception e) { + assertCMDFramework(e, ExecutionIdentifier.class, "SubCommand with int -1"); + } + } +} diff --git a/testsrc/de/steamwar/command/PreviousArgumentCommand.java b/testsrc/de/steamwar/command/PreviousArgumentCommand.java new file mode 100644 index 0000000..454283c --- /dev/null +++ b/testsrc/de/steamwar/command/PreviousArgumentCommand.java @@ -0,0 +1,54 @@ +/* + * 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.TestTypeMapper; + +import java.util.Arrays; +import java.util.Collection; + +public class PreviousArgumentCommand extends TestSWCommand { + + public PreviousArgumentCommand() { + super("previous"); + } + + @Register + public void genericCommand(String s, int i, String l) { + throw new ExecutionIdentifier(l); + } + + @ClassMapper(value = String.class, local = true) + public TestTypeMapper typeMapper() { + return new TestTypeMapper() { + @Override + public String map(String sender, PreviousArguments previousArguments, String s) { + return "RunTypeMapper_" + previousArguments.getMappedArg(0) + "_" + previousArguments.getMappedArg(0).getClass().getSimpleName(); + } + + @Override + public Collection tabCompletes(String sender, PreviousArguments previousArguments, String s) { + return Arrays.asList(previousArguments.getMappedArg(0) + ""); + } + }; + } +} diff --git a/testsrc/de/steamwar/command/PreviousArgumentCommandTest.java b/testsrc/de/steamwar/command/PreviousArgumentCommandTest.java new file mode 100644 index 0000000..941b1c3 --- /dev/null +++ b/testsrc/de/steamwar/command/PreviousArgumentCommandTest.java @@ -0,0 +1,56 @@ +/* + * 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 java.util.List; + +import static de.steamwar.AssertionUtils.assertCMDFramework; +import static de.steamwar.AssertionUtils.assertTabCompletes; + +public class PreviousArgumentCommandTest { + + @Test + public void testPrevArg1() { + PreviousArgumentCommand command = new PreviousArgumentCommand(); + List strings = command.tabComplete("", "", new String[]{"1", ""}); + assertTabCompletes(strings, "1"); + } + + @Test + public void testPrevArg2() { + PreviousArgumentCommand command = new PreviousArgumentCommand(); + List strings = command.tabComplete("", "", new String[]{"2", ""}); + assertTabCompletes(strings, "2"); + } + + @Test + public void testPrevArgExecute() { + PreviousArgumentCommand command = new PreviousArgumentCommand(); + try { + command.execute("", "", new String[]{"2", "2"}); + assert false; + } catch (Exception e) { + assertCMDFramework(e, ExecutionIdentifier.class, "RunTypeMapper_2_Integer"); + } + } +} diff --git a/testsrc/de/steamwar/command/SimpleCommand.java b/testsrc/de/steamwar/command/SimpleCommand.java index 95aa43d..7388633 100644 --- a/testsrc/de/steamwar/command/SimpleCommand.java +++ b/testsrc/de/steamwar/command/SimpleCommand.java @@ -19,7 +19,6 @@ package de.steamwar.command; -import de.steamwar.command.AbstractSWCommand.Register; import de.steamwar.command.dto.ExecutionIdentifier; import de.steamwar.command.dto.TestSWCommand; @@ -29,7 +28,7 @@ public class SimpleCommand extends TestSWCommand { super("simple"); } - @Register(value = "a", help = true) + @Register(value = "a", noTabComplete = true) public void test(String s, String... varargs) { throw new ExecutionIdentifier("RunSimple with Varargs"); } diff --git a/testsrc/de/steamwar/command/StaticValueCommandTest.java b/testsrc/de/steamwar/command/StaticValueCommandTest.java index 665250a..75a7dfd 100644 --- a/testsrc/de/steamwar/command/StaticValueCommandTest.java +++ b/testsrc/de/steamwar/command/StaticValueCommandTest.java @@ -41,11 +41,13 @@ public class StaticValueCommandTest { StaticValueCommand cmd = new StaticValueCommand(); try { cmd.execute("", "", new String[] {"hello"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunStaticValue with hello"); } try { cmd.execute("", "", new String[] {"world"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunStaticValue with world"); } @@ -56,16 +58,19 @@ public class StaticValueCommandTest { StaticValueCommand cmd = new StaticValueCommand(); try { cmd.execute("", "", new String[] {"-a"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunStaticValue with false"); } try { cmd.execute("", "", new String[] {"-b"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunStaticValue with true"); } try { cmd.execute("", "", new String[] {"-c"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunStaticValue with true"); } @@ -76,16 +81,19 @@ public class StaticValueCommandTest { StaticValueCommand cmd = new StaticValueCommand(); try { cmd.execute("", "", new String[] {"-d"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunStaticValue with true"); } try { cmd.execute("", "", new String[] {"-e"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunStaticValue with false"); } try { cmd.execute("", "", new String[] {"-f"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunStaticValue with true"); } @@ -96,16 +104,19 @@ public class StaticValueCommandTest { StaticValueCommand cmd = new StaticValueCommand(); try { cmd.execute("", "", new String[] {"-g"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunStaticValue with int 0"); } try { cmd.execute("", "", new String[] {"-h"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunStaticValue with int 1"); } try { cmd.execute("", "", new String[] {"-i"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunStaticValue with int 2"); } @@ -116,16 +127,19 @@ public class StaticValueCommandTest { StaticValueCommand cmd = new StaticValueCommand(); try { cmd.execute("", "", new String[] {"-j"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunStaticValue with long 0"); } try { cmd.execute("", "", new String[] {"-k"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunStaticValue with long 1"); } try { cmd.execute("", "", new String[] {"-l"}); + assert false; } catch (Exception e) { assertCMDFramework(e, ExecutionIdentifier.class, "RunStaticValue with long 2"); } diff --git a/testsrc/de/steamwar/command/SubCMDSortingCommand.java b/testsrc/de/steamwar/command/SubCMDSortingCommand.java new file mode 100644 index 0000000..0f4e151 --- /dev/null +++ b/testsrc/de/steamwar/command/SubCMDSortingCommand.java @@ -0,0 +1,50 @@ +/* + * 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; + +public class SubCMDSortingCommand extends TestSWCommand { + + public SubCMDSortingCommand() { + super("subcmdsorting"); + } + + @Register + public void test(String s) { + throw new ExecutionIdentifier("Command with 0 parameters"); + } + + @Register + public void test(String s, String args) { + throw new ExecutionIdentifier("Command with 1 parameter"); + } + + @Register + public void test(String s, String i1, String i2, String i3, String... args) { + throw new ExecutionIdentifier("Command with 3+n parameters"); + } + + @Register + public void test(String s, String... args) { + throw new ExecutionIdentifier("Command with n parameters"); + } +} diff --git a/testsrc/de/steamwar/command/SubCMDSortingCommandTest.java b/testsrc/de/steamwar/command/SubCMDSortingCommandTest.java new file mode 100644 index 0000000..1f3203c --- /dev/null +++ b/testsrc/de/steamwar/command/SubCMDSortingCommandTest.java @@ -0,0 +1,83 @@ +/* + * 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; + +public class SubCMDSortingCommandTest { + + @Test + public void testNoArgs() { + SubCMDSortingCommand cmd = new SubCMDSortingCommand(); + try { + cmd.execute("", "", new String[]{}); + assert false; + } catch (Exception e) { + assertCMDFramework(e, ExecutionIdentifier.class, "Command with 0 parameters"); + } + } + + @Test + public void testOneArgs() { + SubCMDSortingCommand cmd = new SubCMDSortingCommand(); + try { + cmd.execute("", "", new String[]{"Hello"}); + assert false; + } catch (Exception e) { + assertCMDFramework(e, ExecutionIdentifier.class, "Command with 1 parameter"); + } + } + + @Test + public void testOneArgsVarArg() { + SubCMDSortingCommand cmd = new SubCMDSortingCommand(); + try { + cmd.execute("", "", new String[]{"Hello", "World"}); + assert false; + } catch (Exception e) { + assertCMDFramework(e, ExecutionIdentifier.class, "Command with n parameters"); + } + } + + @Test + public void testThreeArgsVarArg() { + SubCMDSortingCommand cmd = new SubCMDSortingCommand(); + try { + cmd.execute("", "", new String[]{"Hello", "World", "YoyoNow", "Hugo"}); + assert false; + } catch (Exception e) { + assertCMDFramework(e, ExecutionIdentifier.class, "Command with 3+n parameters"); + } + } + + @Test + public void testThreeArgsVarArg2() { + SubCMDSortingCommand cmd = new SubCMDSortingCommand(); + try { + cmd.execute("", "", new String[]{"Hello", "World", "YoyoNow"}); + assert false; + } catch (Exception e) { + assertCMDFramework(e, ExecutionIdentifier.class, "Command with 3+n parameters"); + } + } +} diff --git a/testsrc/de/steamwar/command/dto/TestSWCommand.java b/testsrc/de/steamwar/command/dto/TestSWCommand.java index 5fd3b3f..e0ac34b 100644 --- a/testsrc/de/steamwar/command/dto/TestSWCommand.java +++ b/testsrc/de/steamwar/command/dto/TestSWCommand.java @@ -51,9 +51,11 @@ public class TestSWCommand extends AbstractSWCommand { @Override protected void commandSystemError(String sender, CommandFrameworkException e) { + System.out.println("CommandSystemError: " + e.getMessage()); } @Override protected void commandSystemWarning(Supplier message) { + System.out.println("CommandSystemWarning: " + message.get()); } }