// // MIT License // // Copyright (c) 2021 Alexander Söderberg & Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // package cloud.commandframework.annotations; import cloud.commandframework.ArgumentDescription; import cloud.commandframework.Command; import cloud.commandframework.CommandManager; import cloud.commandframework.annotations.injection.ParameterInjectorRegistry; import cloud.commandframework.annotations.injection.RawArgs; import cloud.commandframework.annotations.parsers.MethodArgumentParser; import cloud.commandframework.annotations.parsers.Parser; import cloud.commandframework.annotations.specifier.Completions; import cloud.commandframework.annotations.suggestions.MethodSuggestionsProvider; import cloud.commandframework.annotations.suggestions.Suggestions; import cloud.commandframework.arguments.CommandArgument; import cloud.commandframework.arguments.flags.CommandFlag; import cloud.commandframework.arguments.parser.ArgumentParseResult; import cloud.commandframework.arguments.parser.ArgumentParser; import cloud.commandframework.arguments.parser.ParserParameter; import cloud.commandframework.arguments.parser.ParserParameters; import cloud.commandframework.arguments.parser.StandardParameters; import cloud.commandframework.arguments.preprocessor.RegexPreprocessor; import cloud.commandframework.captions.Caption; import cloud.commandframework.context.CommandContext; import cloud.commandframework.execution.CommandExecutionHandler; import cloud.commandframework.extra.confirmation.CommandConfirmationManager; import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.meta.SimpleCommandMeta; import io.leangen.geantyref.TypeToken; import java.util.function.Predicate; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Queue; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; /** * Parser that parses class instances {@link Command commands} * * @param Command sender type */ public final class AnnotationParser { private final SyntaxParser syntaxParser = new SyntaxParser(); private final ArgumentExtractor argumentExtractor = new ArgumentExtractor(); private final CommandManager manager; private final Map, Function> annotationMappers; private final Map, Function, @NonNull Queue<@NonNull String>, @NonNull ArgumentParseResult>>> preprocessorMappers; private final Map, BiFunction, Command.Builder>> builderModifiers; private final Map, Function, MethodCommandExecutionHandler>> commandMethodFactories; private final Class commandSenderClass; private final MetaFactory metaFactory; private final FlagExtractor flagExtractor; /** * Construct a new annotation parser * * @param manager Command manager instance * @param commandSenderClass Command sender class * @param metaMapper Function that is used to create {@link CommandMeta} instances from annotations on the * command methods. These annotations will be mapped to * {@link ParserParameter}. Mappers for the * parser parameters can be registered using {@link #registerAnnotationMapper(Class, Function)} */ public AnnotationParser( final @NonNull CommandManager manager, final @NonNull Class commandSenderClass, final @NonNull Function<@NonNull ParserParameters, @NonNull CommandMeta> metaMapper ) { this.commandSenderClass = commandSenderClass; this.manager = manager; this.metaFactory = new MetaFactory(this, metaMapper); this.annotationMappers = new HashMap<>(); this.preprocessorMappers = new HashMap<>(); this.builderModifiers = new HashMap<>(); this.commandMethodFactories = new HashMap<>(); this.flagExtractor = new FlagExtractor(manager); this.registerAnnotationMapper(CommandDescription.class, d -> ParserParameters.single(StandardParameters.DESCRIPTION, d.value())); this.registerPreprocessorMapper(Regex.class, annotation -> RegexPreprocessor.of( annotation.value(), Caption.of(annotation.failureCaption()) )); this.getParameterInjectorRegistry().registerInjector( String[].class, (context, annotations) -> annotations.annotation(RawArgs.class) == null ? null : context.getRawInput().toArray(new String[0]) ); } @SuppressWarnings("unchecked") static @Nullable A getAnnotationRecursively( final @NonNull AnnotationAccessor annotations, final @NonNull Class clazz, final @NonNull Set> checkedAnnotations ) { A innerCandidate = null; for (final Annotation annotation : annotations.annotations()) { if (!checkedAnnotations.add(annotation.annotationType())) { continue; } if (annotation.annotationType().equals(clazz)) { return (A) annotation; } if (annotation.annotationType().getPackage().getName().startsWith("java.lang")) { continue; } final A inner = getAnnotationRecursively( AnnotationAccessor.of(annotation.annotationType()), clazz, checkedAnnotations ); if (inner != null) { innerCandidate = inner; } } return innerCandidate; } static @Nullable A getMethodOrClassAnnotation( final @NonNull Method method, final @NonNull Class clazz ) { A annotation = getAnnotationRecursively( AnnotationAccessor.of(method), clazz, new HashSet<>() ); if (annotation == null) { annotation = getAnnotationRecursively( AnnotationAccessor.of(method.getDeclaringClass()), clazz, new HashSet<>() ); } return annotation; } static boolean methodOrClassHasAnnotation( final @NonNull Method method, final @NonNull Class clazz ) { return getMethodOrClassAnnotation(method, clazz) != null; } /** * Returns the command manager that was used to create this parser * * @return Command manager * @since 1.6.0 */ public @NonNull CommandManager manager() { return this.manager; } /** * Registers a new command execution method factory. This allows for the registration of * custom command method execution strategies. * * @param predicate The predicate that decides whether or not to apply the custom execution handler to the given method * @param function The function that produces the command execution handler * @since 1.6.0 */ public void registerCommandExecutionMethodFactory( final @NonNull Predicate<@NonNull Method> predicate, final @NonNull Function, MethodCommandExecutionHandler> function ) { this.commandMethodFactories.put(predicate, function); } /** * Register a builder modifier for a specific annotation. The builder modifiers are * allowed to act on a {@link Command.Builder} after all arguments have been added * to the builder. This allows for modifications of the builder instance before * the command is registered to the command manager. * * @param annotation Annotation (class) that the builder modifier reacts to * @param builderModifier Modifier that acts on the given annotation and the incoming builder. Command builders * are immutable, so the modifier should return the instance of the command builder that is * returned as a result of any operation on the builder * @param Annotation type */ public void registerBuilderModifier( final @NonNull Class annotation, final @NonNull BiFunction, Command.Builder> builderModifier ) { this.builderModifiers.put(annotation, builderModifier); } /** * Register an annotation mapper * * @param annotation Annotation class * @param mapper Mapping function * @param Annotation type */ public void registerAnnotationMapper( final @NonNull Class annotation, final @NonNull Function<@NonNull A, @NonNull ParserParameters> mapper ) { this.annotationMappers.put(annotation, mapper); } /** * Register a preprocessor mapper * * @param annotation Annotation class * @param preprocessorMapper Preprocessor mapper * @param Annotation type */ public void registerPreprocessorMapper( final @NonNull Class annotation, final @NonNull Function, @NonNull Queue<@NonNull String>, @NonNull ArgumentParseResult>> preprocessorMapper ) { this.preprocessorMappers.put(annotation, preprocessorMapper); } /** * Get the parameter injector registry instance that is used to inject non-{@link Argument argument} parameters * into {@link CommandMethod} annotated {@link Method methods} * * @return Parameter injector registry * @since 1.2.0 */ public @NonNull ParameterInjectorRegistry getParameterInjectorRegistry() { return this.manager.parameterInjectorRegistry(); } /** * Scan a class instance of {@link CommandMethod} annotations and attempt to * compile them into {@link Command} instances * * @param instance Instance to scan * @param Type of the instance * @return Collection of parsed annotations */ @SuppressWarnings({"deprecation", "unchecked", "rawtypes"}) public @NonNull Collection<@NonNull Command> parse(final @NonNull T instance) { /* Start by registering all @Suggestion annotated methods */ this.parseSuggestions(instance); /* Then register all parsers */ this.parseParsers(instance); /* Then construct commands from @CommandMethod annotated classes */ final Method[] methods = instance.getClass().getDeclaredMethods(); final Collection commandMethodPairs = new ArrayList<>(); for (final Method method : methods) { final CommandMethod commandMethod = method.getAnnotation(CommandMethod.class); if (commandMethod == null) { continue; } if (!method.isAccessible()) { method.setAccessible(true); } if (Modifier.isStatic(method.getModifiers())) { throw new IllegalArgumentException(String.format( "@CommandMethod annotated method '%s' is static! @CommandMethod annotated methods should not be static.", method.getName() )); } commandMethodPairs.add(new CommandMethodPair(method, commandMethod)); } final Collection> commands = this.construct(instance, commandMethodPairs); for (final Command command : commands) { ((CommandManager) this.manager).command(command); } return commands; } @SuppressWarnings("deprecation") private void parseSuggestions(final @NonNull T instance) { for (final Method method : instance.getClass().getMethods()) { final Suggestions suggestions = method.getAnnotation(Suggestions.class); if (suggestions == null) { continue; } if (!method.isAccessible()) { method.setAccessible(true); } if (method.getParameterCount() != 2 || !method.getReturnType().equals(List.class) || !method.getParameters()[0].getType().equals(CommandContext.class) || !method.getParameters()[1].getType().equals(String.class) ) { throw new IllegalArgumentException(String.format( "@Suggestions annotated method '%s' in class '%s' does not have the correct signature", method.getName(), instance.getClass().getCanonicalName() )); } try { this.manager.getParserRegistry().registerSuggestionProvider( suggestions.value(), new MethodSuggestionsProvider<>(instance, method) ); } catch (final Exception e) { throw new RuntimeException(e); } } } @SuppressWarnings("deprecation") private void parseParsers(final @NonNull T instance) { for (final Method method : instance.getClass().getMethods()) { final Parser parser = method.getAnnotation(Parser.class); if (parser == null) { continue; } if (!method.isAccessible()) { method.setAccessible(true); } if (method.getParameterCount() != 2 || method.getReturnType().equals(Void.class) || !method.getParameters()[0].getType().equals(CommandContext.class) || !method.getParameters()[1].getType().equals(Queue.class) ) { throw new IllegalArgumentException(String.format( "@Parser annotated method '%s' in class '%s' does not have the correct signature", method.getName(), instance.getClass().getCanonicalName() )); } try { final BiFunction, String, List> suggestionsProvider; if (parser.suggestions().isEmpty()) { suggestionsProvider = (context, input) -> Collections.emptyList(); } else { suggestionsProvider = this.manager.getParserRegistry().getSuggestionProvider(parser.suggestions()) .orElseThrow(() -> new NullPointerException( String.format( "Cannot find the suggestions provider with name '%s'", parser.suggestions() ) )); } final MethodArgumentParser methodArgumentParser = new MethodArgumentParser<>( suggestionsProvider, instance, method ); final Function> parserFunction = parameters -> methodArgumentParser; if (parser.name().isEmpty()) { this.manager.getParserRegistry().registerParserSupplier( TypeToken.get(method.getGenericReturnType()), parserFunction ); } else { this.manager.getParserRegistry().registerNamedParserSupplier( parser.name(), parserFunction ); } } catch (final Exception e) { throw new RuntimeException(e); } } } @SuppressWarnings("unchecked") private @NonNull Collection<@NonNull Command> construct( final @NonNull Object instance, final @NonNull Collection<@NonNull CommandMethodPair> methodPairs ) { final CommandMethod classCommandMethod = instance.getClass().getAnnotation(CommandMethod.class); final String syntaxPrefix = classCommandMethod == null ? "" : (classCommandMethod.value() + " "); final Collection> commands = new ArrayList<>(); for (final CommandMethodPair commandMethodPair : methodPairs) { final CommandMethod commandMethod = commandMethodPair.getCommandMethod(); final Method method = commandMethodPair.getMethod(); final String syntax = syntaxPrefix + commandMethod.value(); final List tokens = this.syntaxParser.apply(syntax); /* Determine command name */ final String commandToken = syntax.split(" ")[0].split("\\|")[0]; @SuppressWarnings("rawtypes") final CommandManager manager = this.manager; final SimpleCommandMeta.Builder metaBuilder = SimpleCommandMeta.builder() .with(this.metaFactory.apply(method)); if (methodOrClassHasAnnotation(method, Confirmation.class)) { metaBuilder.with(CommandConfirmationManager.META_CONFIRMATION_REQUIRED, true); } @SuppressWarnings("rawtypes") Command.Builder builder = manager.commandBuilder( commandToken, tokens.get(0).getMinor(), metaBuilder.build() ); final Collection arguments = this.argumentExtractor.apply(method); final Collection> flags = this.flagExtractor.apply(method); final Map> commandArguments = new HashMap<>(); final Map, String> argumentDescriptions = new HashMap<>(); /* Go through all annotated parameters and build up the argument tree */ for (final ArgumentParameterPair argumentPair : arguments) { final CommandArgument argument = this.buildArgument( method, this.findSyntaxFragment(tokens, argumentPair.getArgument().value()), argumentPair ); commandArguments.put(argument.getName(), argument); argumentDescriptions.put(argument, argumentPair.getArgument().description()); } boolean commandNameFound = false; /* Build the command tree */ for (final SyntaxFragment token : tokens) { if (!commandNameFound) { commandNameFound = true; continue; } if (token.getArgumentMode() == ArgumentMode.LITERAL) { builder = builder.literal(token.getMajor(), token.getMinor().toArray(new String[0])); } else { final CommandArgument argument = commandArguments.get(token.getMajor()); if (argument == null) { throw new IllegalArgumentException(String.format( "Found no mapping for argument '%s' in method '%s'", token.getMajor(), method.getName() )); } final String description = argumentDescriptions.getOrDefault(argument, ""); builder = builder.argument(argument, ArgumentDescription.of(description)); } } /* Try to find the command sender type */ Class senderType = null; for (final Parameter parameter : method.getParameters()) { if (parameter.isAnnotationPresent(Argument.class)) { continue; } if (this.commandSenderClass.isAssignableFrom(parameter.getType())) { senderType = (Class) parameter.getType(); break; } } final CommandPermission commandPermission = getMethodOrClassAnnotation(method, CommandPermission.class); if (commandPermission != null) { builder = builder.permission(commandPermission.value()); } if (commandMethod.requiredSender() != Object.class) { builder = builder.senderType(commandMethod.requiredSender()); } else if (senderType != null) { builder = builder.senderType(senderType); } try { final MethodCommandExecutionHandler.CommandMethodContext context = new MethodCommandExecutionHandler.CommandMethodContext<>( instance, commandArguments, method, this.getParameterInjectorRegistry() ); /* Create the command execution handler */ CommandExecutionHandler commandExecutionHandler = new MethodCommandExecutionHandler<>(context); for (final Map.Entry, Function, MethodCommandExecutionHandler>> entry : this.commandMethodFactories.entrySet()) { if (entry.getKey().test(method)) { commandExecutionHandler = entry.getValue().apply(context); /* Once we have our custom handler, we stop */ break; } } builder = builder.handler(commandExecutionHandler); } catch (final Exception e) { throw new RuntimeException("Failed to construct command execution handler", e); } /* Check if the command should be hidden */ if (methodOrClassHasAnnotation(method, Hidden.class)) { builder = builder.hidden(); } /* Apply flags */ for (final CommandFlag flag : flags) { builder = builder.flag(flag); } for (final Annotation annotation : method.getDeclaredAnnotations()) { @SuppressWarnings("rawtypes") final BiFunction builderModifier = this.builderModifiers.get(annotation.annotationType()); if (builderModifier == null) { continue; } builder = (Command.Builder) builderModifier.apply(annotation, builder); } /* Construct and register the command */ final Command builtCommand = builder.build(); commands.add(builtCommand); /* Check if we need to construct a proxy */ if (method.isAnnotationPresent(ProxiedBy.class)) { final ProxiedBy proxyAnnotation = method.getAnnotation(ProxiedBy.class); final String proxy = proxyAnnotation.value(); if (proxy.contains(" ")) { throw new IllegalArgumentException("@ProxiedBy proxies may only contain single literals"); } Command.Builder proxyBuilder = manager.commandBuilder(proxy, builtCommand.getCommandMeta()) .proxies(builtCommand); if (proxyAnnotation.hidden()) { proxyBuilder = proxyBuilder.hidden(); } manager.command(proxyBuilder.build()); } } return commands; } private @NonNull SyntaxFragment findSyntaxFragment( final @NonNull List<@NonNull SyntaxFragment> fragments, final @NonNull String argumentName ) { for (final SyntaxFragment fragment : fragments) { if (fragment.getArgumentMode() != ArgumentMode.LITERAL && fragment.getMajor().equals(argumentName)) { return fragment; } } throw new IllegalArgumentException("Argument is not declared in syntax: " + argumentName); } @SuppressWarnings("unchecked") private @NonNull CommandArgument buildArgument( final @NonNull Method method, final @Nullable SyntaxFragment syntaxFragment, final @NonNull ArgumentParameterPair argumentPair ) { final Parameter parameter = argumentPair.getParameter(); final Collection annotations = Arrays.asList(parameter.getAnnotations()); final TypeToken token = TypeToken.get(parameter.getParameterizedType()); final ParserParameters parameters = this.manager.getParserRegistry() .parseAnnotations(token, annotations); /* Create the argument parser */ final ArgumentParser parser; if (argumentPair.getArgument().parserName().isEmpty()) { 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.getType().getTypeName() ))); } else { parser = this.manager.getParserRegistry() .createParser(argumentPair.getArgument().parserName(), 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.getType().getTypeName() ))); } /* Check whether or not the corresponding method parameter actually exists */ if (syntaxFragment == null || syntaxFragment.getArgumentMode() == 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(); /* Create the argument builder */ @SuppressWarnings("rawtypes") final CommandArgument.Builder argumentBuilder = CommandArgument.ofType( parameter.getType(), argument.value() ); /* Set the argument requirement status */ if (syntaxFragment.getArgumentMode() == ArgumentMode.OPTIONAL) { if (argument.defaultValue().isEmpty()) { argumentBuilder.asOptional(); } else { argumentBuilder.asOptionalWithDefault(argument.defaultValue()); } } else { argumentBuilder.asRequired(); } /* Check for Completions annotation */ final Completions completions = parameter.getDeclaredAnnotation(Completions.class); if (completions != null) { final List suggestions = Arrays.asList( completions.value().replace(" ", "").split(",") ); argumentBuilder.withSuggestionsProvider((commandContext, input) -> suggestions); } else if (!argument.suggestions().isEmpty()) { /* Check whether or not a suggestion provider should be set */ final String suggestionProviderName = argument.suggestions(); final Optional, String, List>> suggestionsFunction = this.manager.getParserRegistry().getSuggestionProvider(suggestionProviderName); argumentBuilder.withSuggestionsProvider( suggestionsFunction.orElseThrow(() -> new IllegalArgumentException(String.format( "There is no suggestion provider with name '%s'. Did you forget to register it?", suggestionProviderName ))) ); } /* Build the argument */ final CommandArgument builtArgument = argumentBuilder.manager(this.manager).withParser(parser).build(); /* Add preprocessors */ for (final Annotation annotation : annotations) { @SuppressWarnings("rawtypes") final Function preprocessorMapper = this.preprocessorMappers.get(annotation.annotationType()); if (preprocessorMapper != null) { final BiFunction<@NonNull CommandContext, @NonNull Queue<@NonNull String>, @NonNull ArgumentParseResult> preprocessor = (BiFunction, Queue, ArgumentParseResult>) preprocessorMapper.apply(annotation); builtArgument.addPreprocessor(preprocessor); } } /* Yay, we're done */ return builtArgument; } @NonNull Map<@NonNull Class<@NonNull ? extends Annotation>, @NonNull Function<@NonNull ? extends Annotation, @NonNull ParserParameters>> getAnnotationMappers() { return this.annotationMappers; } }