More progress on command method parsing

This commit is contained in:
Alexander Söderberg 2020-09-18 14:34:14 +02:00
parent da68a6bc87
commit 1e58ca3f13
No known key found for this signature in database
GPG key ID: C0207FF7EA146678
4 changed files with 153 additions and 30 deletions

View file

@ -23,15 +23,20 @@
//
package com.intellectualsites.commands.annotations;
import com.google.common.collect.Maps;
import com.google.common.reflect.TypeToken;
import com.intellectualsites.commands.Command;
import com.intellectualsites.commands.CommandManager;
import com.intellectualsites.commands.arguments.CommandArgument;
import com.intellectualsites.commands.arguments.parser.ArgumentParser;
import com.intellectualsites.commands.arguments.parser.ParserParameters;
import com.intellectualsites.commands.execution.CommandExecutionHandler;
import com.intellectualsites.commands.meta.CommandMeta;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
@ -39,6 +44,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.function.Predicate;
@ -55,7 +61,7 @@ public final class AnnotationParser<C, M extends CommandMeta> {
private static final Predicate<String> PATTERN_ARGUMENT_LITERAL = Pattern.compile("([A-Za-z0-9]+)(|([A-Za-z0-9]+))*")
.asPredicate();
private static final Predicate<String> PATTERN_ARGUMENT_REQUIRED = Pattern.compile("<([A-Za-z0-9]+)>").asPredicate();
private static final Predicate<String> PATTERN_ARGUMENT_OPTIONAL = Pattern.compile("\\[([A-Za-z0-9]+)\\]").asPredicate();
private static final Predicate<String> PATTERN_ARGUMENT_OPTIONAL = Pattern.compile("\\[([A-Za-z0-9]+)]").asPredicate();
private final CommandManager<C, M> manager;
@ -91,21 +97,100 @@ public final class AnnotationParser<C, M extends CommandMeta> {
}
commandMethodPairs.add(new CommandMethodPair(method, commandMethod));
}
return this.construct(commandMethodPairs);
final Collection<Command<C, M>> commands = this.construct(instance, commandMethodPairs);
for (final Command<C, M> command : commands) {
this.manager.command(command);
}
return commands;
}
@Nonnull
private Collection<Command<C, M>> construct(@Nonnull final Collection<CommandMethodPair> methodPairs) {
@SuppressWarnings("unchecked")
private Collection<Command<C, M>> construct(@Nonnull final Object instance,
@Nonnull final Collection<CommandMethodPair> methodPairs) {
final Collection<Command<C, M>> commands = new ArrayList<>();
for (final CommandMethodPair commandMethodPair : methodPairs) {
final CommandMethod commandMethod = commandMethodPair.getCommandMethod();
final Method method = commandMethodPair.getMethod();
final Map<String, ArgumentMode> tokens = this.parseSyntax(commandMethod.value());
Command.Builder<C, M> builder = this.manager.commandBuilder(commandMethod.value(),
final LinkedHashMap<String, ArgumentMode> tokens = this.parseSyntax(commandMethod.value());
/* Determine command name */
final String commandToken = commandMethod.value().split(" ")[0].split("\\|")[0];
@SuppressWarnings("ALL")
Command.Builder builder = this.manager.commandBuilder(commandToken,
Collections.emptyList(),
manager.createDefaultCommandMeta());
final Collection<ArgumentParameterPair> arguments = this.getArguments(method);
final Map<String, CommandArgument<C, ?>> commandArguments = Maps.newHashMap();
/* Go through all annotated parameters and build up the argument tree */
for (final ArgumentParameterPair argumentPair : arguments) {
final CommandArgument<C, ?> argument = this.buildArgument(method,
tokens.get(argumentPair.getArgument().value()),
argumentPair);
commandArguments.put(argument.getName(), argument);
}
boolean commandNameFound = false;
/* Build the command tree */
for (final Map.Entry<String, ArgumentMode> entry : tokens.entrySet()) {
if (!commandNameFound) {
commandNameFound = true;
continue;
}
if (entry.getValue() == ArgumentMode.LITERAL) {
builder = builder.literal(entry.getKey());
} else {
final CommandArgument<C, ?> argument = commandArguments.get(entry.getKey());
if (argument == null) {
throw new IllegalArgumentException(String.format(
"Found no mapping for argument '%s' in method '%s'",
entry.getKey(), method.getName()
));
}
builder = builder.argument(argument);
}
}
/* Decorate command data */
builder = builder.withPermission(commandMethod.permission()).withSenderType(commandMethod.requiredSender());
/* Construct the handler */
final CommandExecutionHandler<C> commandExecutionHandler = commandContext -> {
final List<Object> parameters = new ArrayList<>(method.getParameterCount());
for (final Parameter parameter : method.getParameters()) {
if (parameter.isAnnotationPresent(Argument.class)) {
final Argument argument = parameter.getAnnotation(Argument.class);
final CommandArgument<C, ?> commandArgument = commandArguments.get(argument.value());
if (commandArgument.isRequired()) {
parameters.add(commandContext.getRequired(argument.value()));
} else {
final Object optional = commandContext.get(argument.value()).orElse(null);
parameters.add(optional);
}
} else {
if (parameter.getType().isAssignableFrom(commandContext.getSender().getClass())) {
parameters.add(commandContext.getSender());
} else {
throw new IllegalArgumentException(String.format(
"Unknown command parameter '%s' in method '%s'",
parameter.getName(), method.getName()
));
}
}
}
try {
method.invoke(instance, parameters.toArray());
} catch (final IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
};
builder = builder.handler(commandExecutionHandler);
commands.add(builder.build());
}
return commands;
}
@Nonnull
@SuppressWarnings("unchecked")
private CommandArgument<C, ?> buildArgument(@Nonnull final Method method,
@Nullable final ArgumentMode argumentMode,
@Nonnull final ArgumentParameterPair argumentPair) {
final Parameter parameter = argumentPair.getParameter();
final Collection<Annotation> annotations = Arrays.asList(parameter.getAnnotations());
final TypeToken<?> token = TypeToken.of(parameter.getParameterizedType());
@ -119,10 +204,25 @@ public final class AnnotationParser<C, M extends CommandMeta> {
+ "for that type",
parameter.getName(), method.getName(),
token.toString())));
if (argumentMode == null || argumentMode == ArgumentMode.LITERAL) {
throw new IllegalArgumentException(String.format(
"Invalid command argument '%s' in method '%s': "
+ "Missing syntax mapping", argumentPair.getArgument().value(), method.getName()));
}
final Argument argument = argumentPair.getArgument();
@SuppressWarnings("ALL")
final CommandArgument.Builder argumentBuilder = CommandArgument.ofType(parameter.getType(),
argument.value());
if (argumentMode == ArgumentMode.OPTIONAL) {
if (argument.defaultValue().isEmpty()) {
argumentBuilder.asOptional();
} else {
argumentBuilder.asOptionalWithDefault(argument.defaultValue());
}
return commands;
} else {
argumentBuilder.asRequired();
}
return argumentBuilder.manager(this.manager).withParser(parser).build();
}
@Nonnull
@ -135,10 +235,12 @@ public final class AnnotationParser<C, M extends CommandMeta> {
map.put(token.substring(1, token.length() - 1), ArgumentMode.REQUIRED);
} else if (PATTERN_ARGUMENT_OPTIONAL.test(token)) {
map.put(token.substring(1, token.length() - 1), ArgumentMode.OPTIONAL);
} else {
} else if (PATTERN_ARGUMENT_LITERAL.test(token)) {
final String[] literals = token.split("\\|");
/* Actually use the other literals as well */
map.put(literals[0], ArgumentMode.LITERAL);
} else {
throw new IllegalArgumentException(String.format("Unrecognizable syntax token '%s'", syntax));
}
}
return map;

View file

@ -42,13 +42,6 @@ public @interface Argument {
*/
String value();
/**
* Whether or not the argument is required
*
* @return {@code true} if the argument is required, else {@code false}
*/
boolean required() default true;
/**
* Name of the argument parser
*
@ -56,4 +49,11 @@ public @interface Argument {
*/
String parserName() default "";
/**
* Get the default value
*
* @return Default value
*/
String defaultValue() default "";
}

View file

@ -24,13 +24,18 @@
package com.intellectualsites.commands.annotations;
import com.google.common.collect.Maps;
import com.intellectualsites.commands.Command;
import com.intellectualsites.commands.CommandManager;
import com.intellectualsites.commands.annotations.specifier.Range;
import com.intellectualsites.commands.meta.SimpleCommandMeta;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.CompletionException;
class AnnotationParserTest {
@ -54,4 +59,20 @@ class AnnotationParserTest {
Assertions.assertEquals(map, arguments);
}
@Test
void testMethodConstruction() {
final Collection<Command<TestCommandSender, SimpleCommandMeta>> commands = annotationParser.parse(this);
Assertions.assertFalse(commands.isEmpty());
manager.executeCommand(new TestCommandSender(), "test 10").join();
Assertions.assertThrows(CompletionException.class, () ->
manager.executeCommand(new TestCommandSender(), "test 101").join());
}
@CommandMethod("test <int> [string]")
public void testCommand(@Nonnull final TestCommandSender sender,
@Argument("int") @Range(max = "100") final int argument,
@Nonnull @Argument(value = "string", defaultValue = "potato") final String string) {
System.out.printf("Received int: %d and string '%s'\n", argument, string);
}
}