More progress on command method parsing
This commit is contained in:
parent
da68a6bc87
commit
1e58ca3f13
4 changed files with 153 additions and 30 deletions
|
|
@ -23,15 +23,20 @@
|
||||||
//
|
//
|
||||||
package com.intellectualsites.commands.annotations;
|
package com.intellectualsites.commands.annotations;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
import com.google.common.reflect.TypeToken;
|
import com.google.common.reflect.TypeToken;
|
||||||
import com.intellectualsites.commands.Command;
|
import com.intellectualsites.commands.Command;
|
||||||
import com.intellectualsites.commands.CommandManager;
|
import com.intellectualsites.commands.CommandManager;
|
||||||
|
import com.intellectualsites.commands.arguments.CommandArgument;
|
||||||
import com.intellectualsites.commands.arguments.parser.ArgumentParser;
|
import com.intellectualsites.commands.arguments.parser.ArgumentParser;
|
||||||
import com.intellectualsites.commands.arguments.parser.ParserParameters;
|
import com.intellectualsites.commands.arguments.parser.ParserParameters;
|
||||||
|
import com.intellectualsites.commands.execution.CommandExecutionHandler;
|
||||||
import com.intellectualsites.commands.meta.CommandMeta;
|
import com.intellectualsites.commands.meta.CommandMeta;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Parameter;
|
import java.lang.reflect.Parameter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -39,6 +44,7 @@ import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.StringTokenizer;
|
import java.util.StringTokenizer;
|
||||||
import java.util.function.Predicate;
|
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]+))*")
|
private static final Predicate<String> PATTERN_ARGUMENT_LITERAL = Pattern.compile("([A-Za-z0-9]+)(|([A-Za-z0-9]+))*")
|
||||||
.asPredicate();
|
.asPredicate();
|
||||||
private static final Predicate<String> PATTERN_ARGUMENT_REQUIRED = Pattern.compile("<([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;
|
private final CommandManager<C, M> manager;
|
||||||
|
|
||||||
|
|
@ -91,40 +97,134 @@ public final class AnnotationParser<C, M extends CommandMeta> {
|
||||||
}
|
}
|
||||||
commandMethodPairs.add(new CommandMethodPair(method, commandMethod));
|
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
|
@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<>();
|
final Collection<Command<C, M>> commands = new ArrayList<>();
|
||||||
for (final CommandMethodPair commandMethodPair : methodPairs) {
|
for (final CommandMethodPair commandMethodPair : methodPairs) {
|
||||||
final CommandMethod commandMethod = commandMethodPair.getCommandMethod();
|
final CommandMethod commandMethod = commandMethodPair.getCommandMethod();
|
||||||
final Method method = commandMethodPair.getMethod();
|
final Method method = commandMethodPair.getMethod();
|
||||||
final Map<String, ArgumentMode> tokens = this.parseSyntax(commandMethod.value());
|
final LinkedHashMap<String, ArgumentMode> tokens = this.parseSyntax(commandMethod.value());
|
||||||
Command.Builder<C, M> builder = this.manager.commandBuilder(commandMethod.value(),
|
/* Determine command name */
|
||||||
Collections.emptyList(),
|
final String commandToken = commandMethod.value().split(" ")[0].split("\\|")[0];
|
||||||
manager.createDefaultCommandMeta());
|
@SuppressWarnings("ALL")
|
||||||
|
Command.Builder builder = this.manager.commandBuilder(commandToken,
|
||||||
|
Collections.emptyList(),
|
||||||
|
manager.createDefaultCommandMeta());
|
||||||
final Collection<ArgumentParameterPair> arguments = this.getArguments(method);
|
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) {
|
for (final ArgumentParameterPair argumentPair : arguments) {
|
||||||
final Parameter parameter = argumentPair.getParameter();
|
final CommandArgument<C, ?> argument = this.buildArgument(method,
|
||||||
final Collection<Annotation> annotations = Arrays.asList(parameter.getAnnotations());
|
tokens.get(argumentPair.getArgument().value()),
|
||||||
final TypeToken<?> token = TypeToken.of(parameter.getParameterizedType());
|
argumentPair);
|
||||||
final ParserParameters parameters = this.manager.getParserRegistry()
|
commandArguments.put(argument.getName(), argument);
|
||||||
.parseAnnotations(token, annotations);
|
|
||||||
final ArgumentParser<C, ?> parser = this.manager.getParserRegistry()
|
|
||||||
.createParser(token, parameters)
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException(
|
|
||||||
String.format("Parameter '%s' in method '%s' "
|
|
||||||
+ "has parser '%s' but no parser exists "
|
|
||||||
+ "for that type",
|
|
||||||
parameter.getName(), method.getName(),
|
|
||||||
token.toString())));
|
|
||||||
}
|
}
|
||||||
|
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;
|
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());
|
||||||
|
final ParserParameters parameters = this.manager.getParserRegistry()
|
||||||
|
.parseAnnotations(token, annotations);
|
||||||
|
final ArgumentParser<C, ?> parser = this.manager.getParserRegistry()
|
||||||
|
.createParser(token, parameters)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException(
|
||||||
|
String.format("Parameter '%s' in method '%s' "
|
||||||
|
+ "has parser '%s' but no parser exists "
|
||||||
|
+ "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());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
argumentBuilder.asRequired();
|
||||||
|
}
|
||||||
|
return argumentBuilder.manager(this.manager).withParser(parser).build();
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
LinkedHashMap<String, ArgumentMode> parseSyntax(@Nonnull final String syntax) {
|
LinkedHashMap<String, ArgumentMode> parseSyntax(@Nonnull final String syntax) {
|
||||||
final StringTokenizer stringTokenizer = new StringTokenizer(syntax, " ");
|
final StringTokenizer stringTokenizer = new StringTokenizer(syntax, " ");
|
||||||
|
|
@ -135,10 +235,12 @@ public final class AnnotationParser<C, M extends CommandMeta> {
|
||||||
map.put(token.substring(1, token.length() - 1), ArgumentMode.REQUIRED);
|
map.put(token.substring(1, token.length() - 1), ArgumentMode.REQUIRED);
|
||||||
} else if (PATTERN_ARGUMENT_OPTIONAL.test(token)) {
|
} else if (PATTERN_ARGUMENT_OPTIONAL.test(token)) {
|
||||||
map.put(token.substring(1, token.length() - 1), ArgumentMode.OPTIONAL);
|
map.put(token.substring(1, token.length() - 1), ArgumentMode.OPTIONAL);
|
||||||
} else {
|
} else if (PATTERN_ARGUMENT_LITERAL.test(token)) {
|
||||||
final String[] literals = token.split("\\|");
|
final String[] literals = token.split("\\|");
|
||||||
/* Actually use the other literals as well */
|
/* Actually use the other literals as well */
|
||||||
map.put(literals[0], ArgumentMode.LITERAL);
|
map.put(literals[0], ArgumentMode.LITERAL);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException(String.format("Unrecognizable syntax token '%s'", syntax));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
|
|
|
||||||
|
|
@ -42,13 +42,6 @@ public @interface Argument {
|
||||||
*/
|
*/
|
||||||
String value();
|
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
|
* Name of the argument parser
|
||||||
*
|
*
|
||||||
|
|
@ -56,4 +49,11 @@ public @interface Argument {
|
||||||
*/
|
*/
|
||||||
String parserName() default "";
|
String parserName() default "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default value
|
||||||
|
*
|
||||||
|
* @return Default value
|
||||||
|
*/
|
||||||
|
String defaultValue() default "";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,18 @@
|
||||||
package com.intellectualsites.commands.annotations;
|
package com.intellectualsites.commands.annotations;
|
||||||
|
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
|
import com.intellectualsites.commands.Command;
|
||||||
import com.intellectualsites.commands.CommandManager;
|
import com.intellectualsites.commands.CommandManager;
|
||||||
|
import com.intellectualsites.commands.annotations.specifier.Range;
|
||||||
import com.intellectualsites.commands.meta.SimpleCommandMeta;
|
import com.intellectualsites.commands.meta.SimpleCommandMeta;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
|
||||||
class AnnotationParserTest {
|
class AnnotationParserTest {
|
||||||
|
|
||||||
|
|
@ -54,4 +59,20 @@ class AnnotationParserTest {
|
||||||
Assertions.assertEquals(map, arguments);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ public class CommandArgument<C, T> implements Comparable<CommandArgument<?, ?>>
|
||||||
*/
|
*/
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public static <C, T> CommandArgument.Builder<C, T> ofType(@Nonnull final Class<T> clazz,
|
public static <C, T> CommandArgument.Builder<C, T> ofType(@Nonnull final Class<T> clazz,
|
||||||
@Nonnull final String name) {
|
@Nonnull final String name) {
|
||||||
return new Builder<>(clazz, name);
|
return new Builder<>(clazz, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue