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
This commit is contained in:
Mariell
2018-03-24 15:59:43 +01:00
committed by Daniel Ennis
parent a44e48ef9a
commit 11dcee8b39
12 changed files with 114 additions and 41 deletions
@@ -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<String, RegisteredCommand> subCommands = HashMultimap.create();
final Map<Class<?>, 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);
}
}
@@ -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 <CEC extends CommandExecutionContext<CEC, ? extends CommandIssuer>> {
final BaseCommand scope;
final Method method;
@Nullable final MethodHandle methodHandle;
final CommandParameter<CEC>[] parameters;
final CommandManager manager;
final List<String> registeredSubcommands = new ArrayList<>();
@@ -79,6 +82,14 @@ public class RegisteredCommand <CEC extends CommandExecutionContext<CEC, ? exten
}
this.command = command + (!annotations.hasAnnotation(method, CommandAlias.class, false) && !prefSubCommand.isEmpty() ? prefSubCommand : "");
this.method = method;
MethodHandle methodHandle = null;
try {
methodHandle = MethodHandles.lookup().unreflect(method);
} catch (IllegalAccessException ignored) {
// As the method has been found, it cannot not have permission for it.
// In the case that it does however manage to fail this, keep it null and handle it later on.
}
this.methodHandle = methodHandle;
this.prefSubCommand = prefSubCommand;
this.permission = annotations.getAnnotationValue(method, CommandPermission.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY);
@@ -139,9 +150,14 @@ public class RegisteredCommand <CEC extends CommandExecutionContext<CEC, ? exten
Map<String, Object> 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();
}
@@ -7,6 +7,7 @@ import java.util.List;
public class JDACommandCompletions extends CommandCompletions<CommandCompletionContext<?>> {
private boolean initialized;
public JDACommandCompletions(CommandManager manager) {
super(manager);
this.initialized = true;
@@ -6,7 +6,7 @@ import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class JDACommandConfig implements CommandConfig {
protected @NotNull List<String> commandPrefixes = new CopyOnWriteArrayList<>(new String[] {"!"});
protected @NotNull List<String> commandPrefixes = new CopyOnWriteArrayList<>(new String[]{"!"});
public JDACommandConfig() {
@@ -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<JDACommandExecutionConte
this.jda = this.manager.getJDA();
this.registerIssuerOnlyContext(JDACommandEvent.class, CommandExecutionContext::getIssuer);
this.registerIssuerOnlyContext(MessageReceivedEvent.class, c -> 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<JDACommandExecutionConte
}
});
this.registerIssuerOnlyContext(MessageChannel.class, c -> {
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<TextChannel> 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<User> 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<JDACommandExecutionConte
return user;
});
}
}
@@ -59,6 +59,7 @@ public class JDACommandEvent implements CommandIssuer {
public void sendMessage(Message message) {
this.event.getChannel().sendMessage(message).queue();
}
public void sendMessage(MessageEmbed message) {
this.event.getChannel().sendMessage(message).queue();
}
@@ -1,10 +1,9 @@
package co.aikar.commands;
import java.lang.reflect.Parameter;
import java.util.List;
import java.util.Map;
public class JDACommandExecutionContext extends CommandExecutionContext<JDACommandExecutionContext,JDACommandEvent> {
public class JDACommandExecutionContext extends CommandExecutionContext<JDACommandExecutionContext, JDACommandEvent> {
JDACommandExecutionContext(RegisteredCommand cmd, CommandParameter param, JDACommandEvent sender, List<String> args, int index, Map<String, Object> passedArgs) {
super(cmd, param, sender, args, index, passedArgs);
}
@@ -25,18 +25,16 @@ public class JDACommandManager extends CommandManager<
> {
private final JDA jda;
protected JDACommandCompletions completions;
protected JDACommandContexts contexts;
protected JDALocales locales;
protected Map<String, JDARootCommand> 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<String, JDARootCommand> 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);
@@ -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;
@@ -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<String, RegisteredCommand> subCommands = HashMultimap.create();
private List<BaseCommand> children = new ArrayList<>();
boolean isRegistered = false;
JDARootCommand(JDACommandManager manager, String name) {
this.manager = manager;
@@ -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.
* <p>
* 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 {
}
@@ -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 {
}