From 11dcee8b39b2baf1d51bc97067a2ec608ee32631 Mon Sep 17 00:00:00 2001 From: Mariell Date: Sat, 24 Mar 2018 15:59:43 +0100 Subject: [PATCH] Use MethodHandles for invocation (#112) * Use method handles for invocation * Fix parsing, add more safety and add for named MessageChannels, and format * Add methodhandle support to BaseCommand --- .../java/co/aikar/commands/BaseCommand.java | 20 ++++++-- .../co/aikar/commands/RegisteredCommand.java | 22 +++++++-- .../aikar/commands/JDACommandCompletions.java | 1 + .../co/aikar/commands/JDACommandConfig.java | 2 +- .../co/aikar/commands/JDACommandContexts.java | 49 +++++++++++++------ .../co/aikar/commands/JDACommandEvent.java | 1 + .../commands/JDACommandExecutionContext.java | 3 +- .../co/aikar/commands/JDACommandManager.java | 21 ++++---- .../java/co/aikar/commands/JDAOptions.java | 3 +- .../co/aikar/commands/JDARootCommand.java | 4 +- .../co/aikar/commands/annotation/Author.java | 18 +++++++ .../aikar/commands/annotation/SelfUser.java | 11 +++++ 12 files changed, 114 insertions(+), 41 deletions(-) create mode 100644 jda/src/main/java/co/aikar/commands/annotation/Author.java create mode 100644 jda/src/main/java/co/aikar/commands/annotation/SelfUser.java diff --git a/core/src/main/java/co/aikar/commands/BaseCommand.java b/core/src/main/java/co/aikar/commands/BaseCommand.java index 9b2502d3..aab0330c 100644 --- a/core/src/main/java/co/aikar/commands/BaseCommand.java +++ b/core/src/main/java/co/aikar/commands/BaseCommand.java @@ -42,7 +42,10 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; +import org.jetbrains.annotations.Nullable; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -68,7 +71,8 @@ public abstract class BaseCommand { public static final String DEFAULT = "__default"; final SetMultimap subCommands = HashMultimap.create(); final Map, String> contextFlags = Maps.newHashMap(); - private Method preCommandHandler; + @Nullable private MethodHandle preCommandHandler = null; + private Method preCommandReflectiveHandler = null; @SuppressWarnings("WeakerAccess") private String execLabel; @@ -245,7 +249,12 @@ public abstract class BaseCommand { } } else if (preCommand) { if (this.preCommandHandler == null) { - this.preCommandHandler = method; + this.preCommandReflectiveHandler = method; + try { + this.preCommandHandler = MethodHandles.lookup().unreflect(method); + } catch (IllegalAccessException ignored) { + // We use preCommandReflectiveHandler whenever the methodhandle is used instead + } } else { ACFUtil.sneaky(new IllegalStateException("Multiple @PreCommand commands, duplicate on " + method.getDeclaringClass().getName() + "#" + method.getName())); } @@ -564,7 +573,7 @@ public abstract class BaseCommand { } private boolean checkPrecommand(CommandOperationContext commandOperationContext, RegisteredCommand cmd, CommandIssuer issuer, String[] args) { - Method pre = this.preCommandHandler; + Method pre = this.preCommandReflectiveHandler; if (pre != null) { try { Class[] types = pre.getParameterTypes(); @@ -585,8 +594,11 @@ public abstract class BaseCommand { } } + if (preCommandHandler != null) { + return (boolean) preCommandHandler.invoke(this, parameters); + } return (boolean) pre.invoke(this, parameters); - } catch (IllegalAccessException | InvocationTargetException e) { + } catch (Throwable e) { this.manager.log(LogLevel.ERROR, "Exception encountered while command pre-processing", e); } } diff --git a/core/src/main/java/co/aikar/commands/RegisteredCommand.java b/core/src/main/java/co/aikar/commands/RegisteredCommand.java index 10dd076d..0f5c1050 100644 --- a/core/src/main/java/co/aikar/commands/RegisteredCommand.java +++ b/core/src/main/java/co/aikar/commands/RegisteredCommand.java @@ -37,6 +37,8 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.jetbrains.annotations.Nullable; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; @@ -51,6 +53,7 @@ import java.util.stream.Collectors; public class RegisteredCommand > { final BaseCommand scope; final Method method; + @Nullable final MethodHandle methodHandle; final CommandParameter[] parameters; final CommandManager manager; final List registeredSubcommands = new ArrayList<>(); @@ -79,6 +82,14 @@ public class RegisteredCommand passedArgs = resolveContexts(sender, args); if (passedArgs == null) return; - method.invoke(scope, passedArgs.values().toArray()); - } catch (Exception e) { - handleException(sender, args, e); + if (methodHandle != null) { + methodHandle.invoke(passedArgs.values().toArray()); + } else { + // The offchance the method handle wasn't unreflected, we're going to use the slower, working reflection. + method.invoke(passedArgs.values().toArray()); + } + } catch (Throwable throwable) { + handleException(sender, args, throwable instanceof Exception ? (Exception) throwable : new Exception(throwable)); } postCommand(); } diff --git a/jda/src/main/java/co/aikar/commands/JDACommandCompletions.java b/jda/src/main/java/co/aikar/commands/JDACommandCompletions.java index 3c14e2d9..481eb56a 100644 --- a/jda/src/main/java/co/aikar/commands/JDACommandCompletions.java +++ b/jda/src/main/java/co/aikar/commands/JDACommandCompletions.java @@ -7,6 +7,7 @@ import java.util.List; public class JDACommandCompletions extends CommandCompletions> { private boolean initialized; + public JDACommandCompletions(CommandManager manager) { super(manager); this.initialized = true; diff --git a/jda/src/main/java/co/aikar/commands/JDACommandConfig.java b/jda/src/main/java/co/aikar/commands/JDACommandConfig.java index 87fc9051..6eb97afc 100644 --- a/jda/src/main/java/co/aikar/commands/JDACommandConfig.java +++ b/jda/src/main/java/co/aikar/commands/JDACommandConfig.java @@ -6,7 +6,7 @@ import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; public class JDACommandConfig implements CommandConfig { - protected @NotNull List commandPrefixes = new CopyOnWriteArrayList<>(new String[] {"!"}); + protected @NotNull List commandPrefixes = new CopyOnWriteArrayList<>(new String[]{"!"}); public JDACommandConfig() { diff --git a/jda/src/main/java/co/aikar/commands/JDACommandContexts.java b/jda/src/main/java/co/aikar/commands/JDACommandContexts.java index 93debc93..70a069cb 100644 --- a/jda/src/main/java/co/aikar/commands/JDACommandContexts.java +++ b/jda/src/main/java/co/aikar/commands/JDACommandContexts.java @@ -1,11 +1,14 @@ package co.aikar.commands; +import co.aikar.commands.annotation.Author; import co.aikar.commands.annotation.Optional; +import co.aikar.commands.annotation.SelfUser; import net.dv8tion.jda.core.JDA; import net.dv8tion.jda.core.entities.ChannelType; import net.dv8tion.jda.core.entities.Guild; import net.dv8tion.jda.core.entities.Message; import net.dv8tion.jda.core.entities.MessageChannel; +import net.dv8tion.jda.core.entities.TextChannel; import net.dv8tion.jda.core.entities.User; import net.dv8tion.jda.core.events.message.MessageReceivedEvent; @@ -21,10 +24,9 @@ public class JDACommandContexts extends CommandContexts c.getIssuer().getIssuer()); - this.registerIssuerOnlyContext(Message.class, c -> { - MessageReceivedEvent event = c.getIssuer().getIssuer(); - return event.getMessage(); - }); + this.registerIssuerOnlyContext(Message.class, c -> c.issuer.getIssuer().getMessage()); + this.registerIssuerOnlyContext(ChannelType.class, c -> c.issuer.getIssuer().getChannelType()); + this.registerIssuerOnlyContext(JDA.class, c -> jda); this.registerIssuerOnlyContext(Guild.class, c -> { MessageReceivedEvent event = c.getIssuer().getIssuer(); if (event.isFromType(ChannelType.PRIVATE) && !c.hasAnnotation(Optional.class)) { @@ -34,23 +36,39 @@ public class JDACommandContexts extends CommandContexts { - MessageReceivedEvent event = c.getIssuer().getIssuer(); - return event.getChannel(); + if (c.hasAnnotation(Author.class)) { + return c.issuer.getIssuer().getChannel(); + } + String argument = c.popFirstArg(); + MessageChannel channel = null; + if (argument.startsWith("<#")) { + channel = jda.getTextChannelById(argument.substring(2, argument.length() - 1)); + } else { + List channelList = c.issuer.getEvent().getGuild().getTextChannelsByName(argument.toLowerCase(), true); + if (channelList.size() > 1) { + throw new InvalidCommandArgument("Too many channels were found with the given name. Try with the `#channelname` syntax.", false); + } else if (channelList.size() == 1) { + channel = channelList.get(0); + } + } + if (channel == null) { + throw new InvalidCommandArgument("Couldn't find the channel with that name or ID."); + } + return channel; }); - this.registerIssuerOnlyContext(ChannelType.class, c -> { - MessageReceivedEvent event = c.getIssuer().getIssuer(); - return event.getChannelType(); - }); - - - this.registerIssuerOnlyContext(JDA.class, c -> jda); this.registerContext(User.class, c -> { + if (c.hasAnnotation(SelfUser.class)) { + return jda.getSelfUser(); + } String arg = c.popFirstArg(); User user = null; - if (arg.startsWith("@")) { - user = jda.getUserById(arg.substring(1)); + if (arg.startsWith("<@")) { + user = jda.getUserById(arg.substring(2, arg.length() - 1)); } else { List users = jda.getUsersByName(arg, true); + if (users.size() > 1) { + throw new InvalidCommandArgument("Too many users were found with the given name. Try with the `@username#0000` syntax.", false); + } if (!users.isEmpty()) { user = users.get(0); } @@ -61,5 +79,4 @@ public class JDACommandContexts extends CommandContexts { +public class JDACommandExecutionContext extends CommandExecutionContext { JDACommandExecutionContext(RegisteredCommand cmd, CommandParameter param, JDACommandEvent sender, List args, int index, Map passedArgs) { super(cmd, param, sender, args, index, passedArgs); } diff --git a/jda/src/main/java/co/aikar/commands/JDACommandManager.java b/jda/src/main/java/co/aikar/commands/JDACommandManager.java index 19d13c71..e280c301 100644 --- a/jda/src/main/java/co/aikar/commands/JDACommandManager.java +++ b/jda/src/main/java/co/aikar/commands/JDACommandManager.java @@ -25,18 +25,16 @@ public class JDACommandManager extends CommandManager< > { private final JDA jda; - + protected JDACommandCompletions completions; + protected JDACommandContexts contexts; + protected JDALocales locales; + protected Map commands = Maps.newHashMap(); private Logger logger; private CommandConfig defaultConfig; private CommandConfigProvider configProvider; private CommandPermissionResolver permissionResolver; - protected JDACommandCompletions completions; - protected JDACommandContexts contexts; - protected JDALocales locales; private long botOwner = 0L; - protected Map commands = Maps.newHashMap(); - public JDACommandManager(JDA jda) { this(jda, null); } @@ -78,6 +76,10 @@ public class JDACommandManager extends CommandManager< }); } + public static JDAOptions options() { + return new JDAOptions(); + } + void initializeBotOwner() { if (botOwner == 0L) { if (jda.getAccountType() == AccountType.BOT) { @@ -94,11 +96,6 @@ public class JDACommandManager extends CommandManager< return botOwner; } - - public static JDAOptions options() { - return new JDAOptions(); - } - public JDA getJDA() { return jda; } @@ -218,7 +215,7 @@ public class JDACommandManager extends CommandManager< void dispatchEvent(MessageReceivedEvent event) { Message message = event.getMessage(); - String msg = message.getContentDisplay(); + String msg = message.getContentRaw(); CommandConfig config = getCommandConfig(event); diff --git a/jda/src/main/java/co/aikar/commands/JDAOptions.java b/jda/src/main/java/co/aikar/commands/JDAOptions.java index c54a0d45..92a3ea8a 100644 --- a/jda/src/main/java/co/aikar/commands/JDAOptions.java +++ b/jda/src/main/java/co/aikar/commands/JDAOptions.java @@ -8,7 +8,8 @@ public class JDAOptions { CommandConfigProvider configProvider = null; CommandPermissionResolver permissionResolver = null; - public JDAOptions() {} + public JDAOptions() { + } public JDAOptions defaultConfig(@NotNull CommandConfig defaultConfig) { this.defaultConfig = defaultConfig; diff --git a/jda/src/main/java/co/aikar/commands/JDARootCommand.java b/jda/src/main/java/co/aikar/commands/JDARootCommand.java index 073be220..5c48c408 100644 --- a/jda/src/main/java/co/aikar/commands/JDARootCommand.java +++ b/jda/src/main/java/co/aikar/commands/JDARootCommand.java @@ -8,12 +8,12 @@ import java.util.List; public class JDARootCommand implements RootCommand { - private JDACommandManager manager; private final String name; + boolean isRegistered = false; + private JDACommandManager manager; private BaseCommand defCommand; private SetMultimap subCommands = HashMultimap.create(); private List children = new ArrayList<>(); - boolean isRegistered = false; JDARootCommand(JDACommandManager manager, String name) { this.manager = manager; diff --git a/jda/src/main/java/co/aikar/commands/annotation/Author.java b/jda/src/main/java/co/aikar/commands/annotation/Author.java new file mode 100644 index 00000000..46c3d025 --- /dev/null +++ b/jda/src/main/java/co/aikar/commands/annotation/Author.java @@ -0,0 +1,18 @@ +package co.aikar.commands.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The {@link Author} annotation is to define whether the parameter should be the author object from the event or + * parsed from the user's input. + *

+ * Using this on a User/Member will fetch the author, while if not, it'll parse the input. + * The same happens with channels, guilds, and so on. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Author { +} diff --git a/jda/src/main/java/co/aikar/commands/annotation/SelfUser.java b/jda/src/main/java/co/aikar/commands/annotation/SelfUser.java new file mode 100644 index 00000000..d609174f --- /dev/null +++ b/jda/src/main/java/co/aikar/commands/annotation/SelfUser.java @@ -0,0 +1,11 @@ +package co.aikar.commands.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface SelfUser { +}