diff --git a/bukkit/src/main/java/co/aikar/commands/BukkitCommandCompletions.java b/bukkit/src/main/java/co/aikar/commands/BukkitCommandCompletions.java index b7c3c9c7..e2de05d0 100644 --- a/bukkit/src/main/java/co/aikar/commands/BukkitCommandCompletions.java +++ b/bukkit/src/main/java/co/aikar/commands/BukkitCommandCompletions.java @@ -74,7 +74,7 @@ public class BukkitCommandCompletions extends CommandCompletions matchedPlayers = new ArrayList(); + ArrayList matchedPlayers = new ArrayList<>(); for (Player player : Bukkit.getOnlinePlayers()) { String name = player.getName(); if ((senderPlayer == null || senderPlayer.canSee(player)) && StringUtil.startsWithIgnoreCase(name, c.getInput())) { diff --git a/bukkit/src/main/java/co/aikar/commands/BukkitCommandManager.java b/bukkit/src/main/java/co/aikar/commands/BukkitCommandManager.java index fb37d9cd..66d8d5a4 100644 --- a/bukkit/src/main/java/co/aikar/commands/BukkitCommandManager.java +++ b/bukkit/src/main/java/co/aikar/commands/BukkitCommandManager.java @@ -35,8 +35,13 @@ import org.bukkit.command.CommandMap; import org.bukkit.command.CommandSender; import org.bukkit.command.SimpleCommandMap; import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemFactory; import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.scheduler.BukkitTask; +import org.bukkit.scoreboard.ScoreboardManager; import org.jetbrains.annotations.NotNull; import java.lang.reflect.Field; @@ -95,6 +100,15 @@ public class BukkitCommandManager extends CommandManager< } Bukkit.getOnlinePlayers().forEach(this::readPlayerLocale); }, 5, 5); + + registerDependency(plugin.getClass(), plugin); + registerDependency(Plugin.class, plugin); + registerDependency(JavaPlugin.class, plugin); + registerDependency(PluginManager.class, Bukkit.getPluginManager()); + registerDependency(Server.class, Bukkit.getServer()); + registerDependency(BukkitScheduler.class, Bukkit.getScheduler()); + registerDependency(ScoreboardManager.class, Bukkit.getScoreboardManager()); + registerDependency(ItemFactory.class, Bukkit.getItemFactory()); } @NotNull private CommandMap hookCommandMap() { @@ -261,7 +275,7 @@ public class BukkitCommandManager extends CommandManager< if (nmsPlayer != null) { Field localeField = nmsPlayer.getClass().getField("locale"); Object localeString = localeField.get(nmsPlayer); - if (localeString != null && localeString instanceof String) { + if (localeString instanceof String) { String[] split = ACFPatterns.UNDERSCORE.split((String) localeString); Locale locale = split.length > 1 ? new Locale(split[0], split[1]) : new Locale(split[0]); Locale prev = issuersLocale.put(player.getUniqueId(), locale); diff --git a/bukkit/src/main/java/co/aikar/commands/ProxyCommandMap.java b/bukkit/src/main/java/co/aikar/commands/ProxyCommandMap.java index 834ced43..e8b79c2a 100644 --- a/bukkit/src/main/java/co/aikar/commands/ProxyCommandMap.java +++ b/bukkit/src/main/java/co/aikar/commands/ProxyCommandMap.java @@ -87,7 +87,7 @@ class ProxyCommandMap extends SimpleCommandMap { @Override public void clearCommands() { - super.clearCommands();; + super.clearCommands(); proxied.clearCommands(); } diff --git a/bungee/src/main/java/co/aikar/commands/ACFBungeeUtil.java b/bungee/src/main/java/co/aikar/commands/ACFBungeeUtil.java index b845f4c1..0a962167 100644 --- a/bungee/src/main/java/co/aikar/commands/ACFBungeeUtil.java +++ b/bungee/src/main/java/co/aikar/commands/ACFBungeeUtil.java @@ -79,7 +79,7 @@ public class ACFBungeeUtil { public static final char COLOR_CHAR = '\u00A7'; public static String getLastColors(String input) { - String result = ""; + StringBuilder result = new StringBuilder(); int length = input.length(); // Search backwards from the end as it is faster @@ -90,7 +90,7 @@ public class ACFBungeeUtil { ChatColor color = ChatColor.getByChar(c); if (color != null) { - result = color.toString() + result; + result.insert(0, color.toString()); // Once we find a color or reset we can stop searching if (isChatColorAColor(color) || color.equals(ChatColor.RESET)) { @@ -99,7 +99,7 @@ public class ACFBungeeUtil { } } } - return result; + return result.toString(); } public static boolean isChatColorAColor(ChatColor chatColor) { diff --git a/bungee/src/main/java/co/aikar/commands/BungeeCommandManager.java b/bungee/src/main/java/co/aikar/commands/BungeeCommandManager.java index 88476928..06c65603 100644 --- a/bungee/src/main/java/co/aikar/commands/BungeeCommandManager.java +++ b/bungee/src/main/java/co/aikar/commands/BungeeCommandManager.java @@ -58,6 +58,10 @@ public class BungeeCommandManager extends CommandManager< this.formatters.put(MessageType.INFO, new BungeeMessageFormatter(ChatColor.BLUE, ChatColor.DARK_GREEN, ChatColor.GREEN)); this.formatters.put(MessageType.HELP, new BungeeMessageFormatter(ChatColor.AQUA, ChatColor.GREEN, ChatColor.YELLOW)); getLocales(); // auto load locales + + // TODO more default dependencies for bungee + registerDependency(plugin.getClass(), plugin); + registerDependency(Plugin.class, plugin); } public Plugin getPlugin() { diff --git a/core/src/main/java/co/aikar/commands/BaseCommand.java b/core/src/main/java/co/aikar/commands/BaseCommand.java index 830e9b0d..0361e101 100644 --- a/core/src/main/java/co/aikar/commands/BaseCommand.java +++ b/core/src/main/java/co/aikar/commands/BaseCommand.java @@ -123,6 +123,7 @@ public abstract class BaseCommand { onRegister(manager, this.commandName); } void onRegister(CommandManager manager, String cmd) { + manager.injectDependencies(this); this.manager = manager; final Class self = this.getClass(); CommandAlias rootCmdAliasAnno = self.getAnnotation(CommandAlias.class); diff --git a/core/src/main/java/co/aikar/commands/CommandManager.java b/core/src/main/java/co/aikar/commands/CommandManager.java index 9547c208..5f9eb924 100644 --- a/core/src/main/java/co/aikar/commands/CommandManager.java +++ b/core/src/main/java/co/aikar/commands/CommandManager.java @@ -23,15 +23,19 @@ package co.aikar.commands; -import co.aikar.commands.annotation.Conditions; +import co.aikar.commands.annotation.Dependency; import co.aikar.locales.MessageKeyProvider; +import com.google.common.collect.HashBasedTable; import com.google.common.collect.Lists; import com.google.common.collect.Sets; +import com.google.common.collect.Table; import org.jetbrains.annotations.NotNull; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; +import java.util.Arrays; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; @@ -53,18 +57,17 @@ public abstract class CommandManager < /** * This is a stack incase a command calls a command */ - static ThreadLocal> commandOperationContext = ThreadLocal.withInitial(() -> { - return new Stack() { - @Override - public synchronized CommandOperationContext peek() { - return super.size() == 0 ? null : super.peek(); - } - }; + static ThreadLocal> commandOperationContext = ThreadLocal.withInitial(() -> new Stack() { + @Override + public synchronized CommandOperationContext peek() { + return super.size() == 0 ? null : super.peek(); + } }); protected Map rootCommands = new HashMap<>(); protected final CommandReplacements replacements = new CommandReplacements(this); protected final CommandConditions conditions = new CommandConditions<>(this); protected ExceptionHandler defaultExceptionHandler = null; + protected Table, String, Object> dependencies = HashBasedTable.create(); protected boolean usePerIssuerLocale = false; protected List> localeChangedCallbacks = Lists.newArrayList(); @@ -377,6 +380,73 @@ public abstract class CommandManager < getLocales().loadMissingBundles(); } + /** + * Registers an instance of a class to be registered as an injectable dependency.
+ * The command manager will attempt to inject all fields in a command class that are annotated with + * {@link co.aikar.commands.annotation.Dependency} with the provided instance. + * + * @param clazz the class the injector should look for when injecting + * @param instance the instance of the class that should be injected + * @throws IllegalStateException when there is already an instance for the provided class registered + */ + public void registerDependency(Class clazz, T instance){ + registerDependency(clazz, clazz.getName(), instance); + } + + /** + * Registers an instance of a class to be registered as an injectable dependency.
+ * The command manager will attempt to inject all fields in a command class that are annotated with + * {@link co.aikar.commands.annotation.Dependency} with the provided instance. + * + * @param clazz the class the injector should look for when injecting + * @param key the key which needs to be present if that + * @param instance the instance of the class that should be injected + * @throws IllegalStateException when there is already an instance for the provided class registered + */ + public void registerDependency(Class clazz, String key, T instance){ + if(dependencies.containsRow(clazz) && dependencies.containsColumn(key)){ + throw new IllegalStateException("There is already an instance of " + clazz.getName() + " with the key " + key + " registered!"); + } + + dependencies.put(clazz, key, instance); + } + + /** + * Attempts to inject instances of classes registered with {@link CommandManager#registerDependency(Class, Object)} + * into all fields of the class and its superclasses that are marked with {@link Dependency}. + * + * @param baseCommand the instance which fields should be filled + */ + void injectDependencies(BaseCommand baseCommand) { + Class clazz = baseCommand.getClass(); + do { + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(Dependency.class)) { + Dependency dependency = field.getAnnotation(Dependency.class); + String key = (key = dependency.value()).equals("") ? field.getType().getName() : key; + Object object = dependencies.row(field.getType()).get(key); + if (object == null) { + throw new UnresolvedDependencyException("Could not find a registered instance of " + + field.getType().getName() + " with key " + key + " for field " + field.getName() + + " in class " + baseCommand.getClass().getName()); + } + + try { + boolean accessible = field.isAccessible(); + if (!accessible) { + field.setAccessible(true); + } + field.set(baseCommand, object); + field.setAccessible(accessible); + } catch (IllegalAccessException e) { + e.printStackTrace(); //TODO should we print our own exception here to make a more descriptive error? + } + } + } + clazz = clazz.getSuperclass(); + } while (!clazz.equals(BaseCommand.class)); + } + /** * @deprecated Use this with caution! If you enable and use Unstable API's, your next compile using ACF * may require you to update your implementation to those unstable API's diff --git a/core/src/main/java/co/aikar/commands/InvalidCommandArgument.java b/core/src/main/java/co/aikar/commands/InvalidCommandArgument.java index 46f521ee..f34c7bb2 100644 --- a/core/src/main/java/co/aikar/commands/InvalidCommandArgument.java +++ b/core/src/main/java/co/aikar/commands/InvalidCommandArgument.java @@ -32,7 +32,7 @@ public class InvalidCommandArgument extends Exception { final String[] replacements; public InvalidCommandArgument() { - this((String) null, true); + this(null, true); } public InvalidCommandArgument(boolean showSyntax) { this(null, showSyntax); diff --git a/core/src/main/java/co/aikar/commands/UnresolvedDependencyException.java b/core/src/main/java/co/aikar/commands/UnresolvedDependencyException.java new file mode 100644 index 00000000..0e0a0a8c --- /dev/null +++ b/core/src/main/java/co/aikar/commands/UnresolvedDependencyException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-2018 Daniel Ennis (Aikar) - MIT License + * + * 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 co.aikar.commands; + +/** + * Thrown when a command mananger couldn't find a registered instance for a field that is marked with + * {@link co.aikar.commands.annotation.Dependency} + */ +public class UnresolvedDependencyException extends RuntimeException { + UnresolvedDependencyException(String message) { + super(message); + } +} diff --git a/core/src/main/java/co/aikar/commands/annotation/Dependency.java b/core/src/main/java/co/aikar/commands/annotation/Dependency.java new file mode 100644 index 00000000..7b12ad33 --- /dev/null +++ b/core/src/main/java/co/aikar/commands/annotation/Dependency.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-2018 Daniel Ennis (Aikar) - MIT License + * + * 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 co.aikar.commands.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Dependency { + /** + * The key that should be used to lookup the instances, defaults to \"\" + * @return the key + */ + String value() default ""; +} diff --git a/example/src/main/java/co/aikar/acfexample/ACFExample.java b/example/src/main/java/co/aikar/acfexample/ACFExample.java index a4bd7a97..dd66162b 100644 --- a/example/src/main/java/co/aikar/acfexample/ACFExample.java +++ b/example/src/main/java/co/aikar/acfexample/ACFExample.java @@ -78,17 +78,27 @@ public final class ACFExample extends JavaPlugin { } }); - // 6: Register your commands - This first command demonstrates adding an exception handler to that command + // 6: (Optionally) Register dependencies - Dependencies can be injected into fields of command classes by + // marking them with @Dependency. Some classes, like your Plugin, are already registered by default. + SomeHandler someHandler = new SomeHandler(); + someHandler.setSomeField("Secret"); + commandManager.registerDependency(SomeHandler.class, someHandler); + commandManager.registerDependency(String.class, "Test3"); + commandManager.registerDependency(String.class, "test", "Test"); + commandManager.registerDependency(String.class, "test2", "Test2"); + + // 7: Register your commands - This first command demonstrates adding an exception handler to that command commandManager.registerCommand(new SomeCommand().setExceptionHandler((command, registeredCommand, sender, args, t) -> { sender.sendMessage(MessageType.ERROR, MessageKeys.ERROR_GENERIC_LOGGED); return true; // mark as handeled, default message will not be send to sender })); - // 7: Register an additional command. This one happens to share the same CommandAlias as the previous command + + // 8: Register an additional command. This one happens to share the same CommandAlias as the previous command // This means it simply registers additional sub commands under the same command, but organized into separate // Classes (Maybe different permission sets) commandManager.registerCommand(new SomeCommand_ExtraSubs()); - // 8: Register default exception handler for any command that doesn't supply its own + // 9: Register default exception handler for any command that doesn't supply its own commandManager.setDefaultExceptionHandler((command, registeredCommand, sender, args, t) -> { getLogger().warning("Error occured while executing command " + command.getName()); return false; // mark as unhandeled, sender will see default message diff --git a/example/src/main/java/co/aikar/acfexample/SomeCommand.java b/example/src/main/java/co/aikar/acfexample/SomeCommand.java index c4bcff77..89803b0e 100644 --- a/example/src/main/java/co/aikar/acfexample/SomeCommand.java +++ b/example/src/main/java/co/aikar/acfexample/SomeCommand.java @@ -29,6 +29,7 @@ import co.aikar.commands.annotation.CommandCompletion; import co.aikar.commands.annotation.CommandPermission; import co.aikar.commands.annotation.Conditions; import co.aikar.commands.annotation.Default; +import co.aikar.commands.annotation.Dependency; import co.aikar.commands.annotation.Optional; import co.aikar.commands.annotation.Subcommand; import co.aikar.commands.annotation.Values; @@ -36,6 +37,7 @@ import co.aikar.commands.contexts.OnlinePlayer; import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; @CommandAlias("acf|somecommand|sc|somcom") public class SomeCommand extends BaseCommand { @@ -46,6 +48,29 @@ public class SomeCommand extends BaseCommand { setContextFlags(SomeObject.class, "foo=bar"); } + // marking fields with @Dependency allows you to define instances that should be easily accessible to commands + @Dependency + private SomeHandler someHandler; + // some classes like your plugin are automatically registered for injection + @Dependency + private Plugin plugin; + // you can even use named injections to have multiple instances of the same type + @Dependency("test") + private String testString; + @Dependency("test2") + private String testString2; + @Dependency + private String testString3; + + @Subcommand("testDI") + public void onTestDI(CommandSender sender){ + sender.sendMessage("The value for the injected SomeHandler is " + someHandler.getSomeField()); + sender.sendMessage("Plugin is null? " + (plugin == null)); + sender.sendMessage("Test String 1: " + testString); + sender.sendMessage("Test String 2: " + testString2); + sender.sendMessage("Test String 3: " + testString3); + } + // %testcmd was defined in ACFExample plugin and defined as "test4|foobar|barbaz" // This means, /test4, /foobar and /barbaz all are aliased here. // functionally equivalent to @CommandAlias("test4|foobar|barbaz") but could be dynamic (Read from config) diff --git a/example/src/main/java/co/aikar/acfexample/SomeHandler.java b/example/src/main/java/co/aikar/acfexample/SomeHandler.java new file mode 100644 index 00000000..78bbb0ae --- /dev/null +++ b/example/src/main/java/co/aikar/acfexample/SomeHandler.java @@ -0,0 +1,14 @@ +package co.aikar.acfexample; + +public class SomeHandler { + + private String someField; + + public String getSomeField() { + return someField; + } + + public void setSomeField(String someField) { + this.someField = someField; + } +} diff --git a/sponge/src/main/java/co/aikar/commands/ACFSpongeUtil.java b/sponge/src/main/java/co/aikar/commands/ACFSpongeUtil.java index 9218f48c..55259157 100644 --- a/sponge/src/main/java/co/aikar/commands/ACFSpongeUtil.java +++ b/sponge/src/main/java/co/aikar/commands/ACFSpongeUtil.java @@ -74,7 +74,7 @@ public class ACFSpongeUtil { } public static List matchPlayer(String partialName) { - List matchedPlayers = new ArrayList(); + List matchedPlayers = new ArrayList<>(); for (Player iterPlayer : Sponge.getServer().getOnlinePlayers()) { String iterPlayerName = iterPlayer.getName(); diff --git a/sponge/src/main/java/co/aikar/commands/SpongeCommandContexts.java b/sponge/src/main/java/co/aikar/commands/SpongeCommandContexts.java index 8f550dbe..585e5aea 100644 --- a/sponge/src/main/java/co/aikar/commands/SpongeCommandContexts.java +++ b/sponge/src/main/java/co/aikar/commands/SpongeCommandContexts.java @@ -58,13 +58,12 @@ public class SpongeCommandContexts extends CommandContexts finalFilter.equals(ACFUtil.simplifyString(colour.getName()))); } Stream finalColours = colours; - TextColor match = Sponge.getRegistry().getType(TextColor.class, ACFUtil.simplifyString(first)).orElseThrow(() -> { + return Sponge.getRegistry().getType(TextColor.class, ACFUtil.simplifyString(first)).orElseThrow(() -> { String valid = finalColours .map(colour -> "" + ACFUtil.simplifyString(colour.getName()) + "") .collect(Collectors.joining(", ")); return new InvalidCommandArgument(MessageKeys.PLEASE_SPECIFY_ONE_OF, "{valid}", valid); }); - return match; }); registerContext(TextStyle.Base.class, c -> { String first = c.popFirstArg(); @@ -76,13 +75,12 @@ public class SpongeCommandContexts extends CommandContexts finalFilter.equals(ACFUtil.simplifyString(style.getName()))); } Stream finalStyles = styles; - TextStyle.Base match = Sponge.getRegistry().getType(TextStyle.Base.class, ACFUtil.simplifyString(first)).orElseThrow(() -> { + return Sponge.getRegistry().getType(TextStyle.Base.class, ACFUtil.simplifyString(first)).orElseThrow(() -> { String valid = finalStyles .map(style -> "" + ACFUtil.simplifyString(style.getName()) + "") .collect(Collectors.joining(", ")); return new InvalidCommandArgument(MessageKeys.PLEASE_SPECIFY_ONE_OF, "{valid}", valid); }); - return match; }); registerIssuerAwareContext(CommandSource.class, SpongeCommandExecutionContext::getSource); diff --git a/sponge/src/main/java/co/aikar/commands/SpongeCommandManager.java b/sponge/src/main/java/co/aikar/commands/SpongeCommandManager.java index ce51a36d..db54aca2 100644 --- a/sponge/src/main/java/co/aikar/commands/SpongeCommandManager.java +++ b/sponge/src/main/java/co/aikar/commands/SpongeCommandManager.java @@ -67,6 +67,9 @@ public class SpongeCommandManager extends CommandManager< this.formatters.put(MessageType.INFO, new SpongeMessageFormatter(TextColors.BLUE, TextColors.DARK_GREEN, TextColors.GREEN)); this.formatters.put(MessageType.HELP, new SpongeMessageFormatter(TextColors.AQUA, TextColors.GREEN, TextColors.YELLOW)); getLocales(); // auto load locales + + //TODO more default dependencies for sponge + registerDependency(plugin.getClass(), plugin); } public PluginContainer getPlugin() {