Java tutorial
/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.royaldev.royalcommands.rcommands; import com.google.common.base.Preconditions; import com.google.gson.Gson; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.royaldev.royalcommands.AuthorizationHandler; import org.royaldev.royalcommands.Config; import org.royaldev.royalcommands.MessageColor; import org.royaldev.royalcommands.RUtils; import org.royaldev.royalcommands.RoyalCommands; import org.royaldev.royalcommands.shaded.mkremins.fanciful.FancyMessage; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.StringWriter; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; public abstract class BaseCommand implements CommandExecutor { protected final RoyalCommands plugin; /** * The AuthorizationHandler for this command. This is essentially an alias of this.plugin.ah. */ protected final AuthorizationHandler ah; private final String name; private final boolean checkPermissions; private final List<Flag> expectedFlags = new ArrayList<>(); private final List<String> helpNames = Arrays.asList("help", "h", "?"); /** * Constructs a BaseCommand. This command has some backend utilities to help speed up command development and make * usage better overall. * * @param instance The instance of RoyalCommands that this command is being registered with * @param name The name of this command * @param checkPermissions Whether to check permissions using the auto-generated permission */ public BaseCommand(final RoyalCommands instance, final String name, final boolean checkPermissions) { this.plugin = instance; this.ah = this.plugin.ah; this.name = name; this.checkPermissions = checkPermissions; } /** * The body of the command to be run. Depending on the constructor * ({@link #BaseCommand(org.royaldev.royalcommands.RoyalCommands, String, boolean)}), permissions will have already * been checked. The command name matching the name of this command is already checked. All unhandled exceptions * will be caught and displayed to the user in a friendly format. * * @param cs The CommandSender using the command * @param cmd The Command being used * @param label The label of the command (alias) * @param args The arguments passed to the command * @return true to not display usage, false to display usage (essentially) */ protected abstract boolean runCommand(final CommandSender cs, final Command cmd, final String label, final String[] args); /** * Gets a link to a Hastebin paste with the given content. * * @param paste Content to paste * @return Link to Hastebin paste with the given content * @throws IOException Upon any issue */ private String hastebin(final String paste) throws IOException { final URL url = new URL("http://hastebin.com/documents"); final HttpURLConnection huc = (HttpURLConnection) url.openConnection(); huc.setRequestMethod("POST"); huc.setDoOutput(true); final DataOutputStream dos = new DataOutputStream(huc.getOutputStream()); dos.writeBytes(paste); dos.flush(); dos.close(); final BufferedReader br = new BufferedReader(new InputStreamReader(huc.getInputStream())); String inputLine; final StringBuilder sb = new StringBuilder(); while ((inputLine = br.readLine()) != null) sb.append(inputLine); br.close(); final HastebinData hd = new Gson().fromJson(sb.toString(), HastebinData.class); return "http://hastebin.com/" + hd.getKey() + ".txt"; } /** * Schedules a thread-safe implementation of {@link #hastebin(String)}. * * @param cs CommandSender to send the generated link to * @param paste Content to paste */ private void scheduleErrorHastebin(final CommandSender cs, final String paste) { this.plugin.getServer().getScheduler().runTaskAsynchronously(this.plugin, new Runnable() { @Override public void run() { String tempURL = null; if (Config.hastebinErrors) { try { tempURL = BaseCommand.this.hastebin(paste); } catch (IOException ex) { ex.printStackTrace(); tempURL = null; } } final String url = tempURL; BaseCommand.this.plugin.getServer().getScheduler().runTask(BaseCommand.this.plugin, new Runnable() { @Override public void run() { if (url != null) { BaseCommand.this.plugin.getLogger().warning("Error paste: " + url); // @formatter:off new FancyMessage("Click ").color(MessageColor.NEGATIVE._()).then("here") .color(MessageColor.NEUTRAL._()).tooltip("Click here to find out more.") .link(url).then(" to find out more.").color(MessageColor.NEGATIVE._()).send(cs); // @formatter:on } else { new FancyMessage(Config.hastebinErrors ? "An error occurred while trying to paste the stack trace." : "Error pasting is disabled.").color(MessageColor.NEGATIVE._()).send(cs); } } }); } }); } private void showFlagHelp(final CommandSender cs, final Command cmd, final String label) { cs.sendMessage(cmd.getDescription()); cs.sendMessage(cmd.getUsage().replaceFirst("<command>", label)); cs.sendMessage(MessageColor.POSITIVE + "Expected flags:"); for (final Flag f : this.getExpectedFlags()) { String message = " " + MessageColor.NEUTRAL + "-" + f.getNames(); if (f.getType() != null) message += " [" + f.getType().getSimpleName() + "]"; cs.sendMessage(message); } } protected void addExpectedFlag(final Flag f) { Preconditions.checkNotNull(f, "f cannot be null"); if (this.expectedFlags.contains(f)) throw new IllegalArgumentException("Flag already exists!"); this.expectedFlags.add(f); } /** * Gets the CommandArguments from the given arguments. This allows for flags to be used. * * @param args Arguments to generate CommandArguments from * @return CommandArguments * @see org.royaldev.royalcommands.rcommands.CACommand */ protected CommandArguments getCommandArguments(final String[] args) { return new CommandArguments(args); } public List<Flag> getExpectedFlags() { return this.expectedFlags; } /** * Gets the name of this command. * * @return Name */ public String getName() { return this.name; } /** * Handles an exception. Generates a useful debug paste and sends it to the user if enabled. Also prints the stack * trace to the console and tells the user that an exception occurred. * * @param cs The CommandSender using the command * @param cmd The Command being used * @param label The label of the command (alias) * @param args The arguments passed to the command * @param message Message to be shown about the exception * @param t The exception thrown */ protected void handleException(final CommandSender cs, final Command cmd, final String label, final String[] args, final Throwable t, final String message) { new FancyMessage(message).color(MessageColor.NEGATIVE._()).send(cs); t.printStackTrace(); if (Config.hastebinErrors) { final StringBuilder sb = new StringBuilder(); sb.append("An error occurred while handling a command. Please report this to jkcclemens or WizardCM.\n") .append("They are available at #bukkit @ irc.royaldev.org. If you don't know what that means, then\n") .append("go to the following URL: https://irc.royaldev.org/#bukkit\n\n") .append("---DEBUG INFO---\n\n"); try { sb.append("RoyalCommands Version\n\t").append(this.plugin.getDescription().getVersion()); } catch (final Throwable tt) { sb.append("Could not get RoyalCommands version:\n"); final StringWriter sw = new StringWriter(); tt.printStackTrace(new PrintWriter(sw)); sb.append(sw.toString()); } if (cs != null) { sb.append("\n\nCommandSender\n").append("\tName:\t\t").append(cs.getName()).append("\n") .append("\tClass:\t\t").append(cs.getClass().getName()); } else sb.append("CommandSender:\t\tnull"); if (cmd != null) { sb.append("\n\nCommand\n").append("\tName:\t\t").append(cmd.getName()).append("\n") .append("\tClass:\t\t").append(cmd.getClass().getName()); } else sb.append("\n\nCommand:\t\tnull"); sb.append("\n\nLabel\n\t").append(label); sb.append("\n\nArguments"); if (args != null) { for (final String arg : args) sb.append("\n\t").append(arg); } else sb.append("\n\tnull"); final StringWriter sw = new StringWriter(); t.printStackTrace(new PrintWriter(sw)); sb.append("\n\n---STRACK TRACE---\n\n").append(sw.toString()); this.scheduleErrorHastebin(cs, sb.toString()); } } protected void handleException(final CommandSender cs, final Command cmd, final String label, final String[] args, final Throwable t) { this.handleException(cs, cmd, label, args, t, "An exception occurred while processing that command."); } /** * The real onCommand that will be called by Bukkit. This checks for the command name to match, permissions if * specified, and runs the * {@link #runCommand(org.bukkit.command.CommandSender, org.bukkit.command.Command, String, String[])} method. If * any exception is unhandled in that method, it will be handled and displayed to the user in a friendly format. * <p/> * Due to the nature of this class, this method cannot be overridden. * * @param cs The CommandSender using the command * @param cmd The Command being used * @param label The label of the command (alias) * @param args The arguments passed to the command * @return true to not display usage, false to display usage (essentially) */ @Override public final boolean onCommand(final CommandSender cs, final Command cmd, final String label, final String[] args) { if (!cmd.getName().equalsIgnoreCase(this.name)) return false; if (this.checkPermissions && !this.ah.isAuthorized(cs, cmd)) { RUtils.dispNoPerms(cs, new String[] { this.ah.getPermission(cmd) }); // ensure calling to varargs method return true; } final List<Flag> expectedFlags = this.getExpectedFlags(); if (expectedFlags.size() > 0) { for (final Flag f : new CommandArguments(args)) { if (!expectedFlags.contains(f)) { if (this.helpNames.containsAll(f.getNames())) { this.showFlagHelp(cs, cmd, label); return true; } cs.sendMessage(MessageColor.NEGATIVE + "Unexpected flag \"" + f.getFirstName() + ".\""); return true; } } } try { return this.runCommand(cs, cmd, label, args); } catch (final Throwable t) { this.handleException(cs, cmd, label, args, t); return true; } } /** * /** * Schedules a thread-safe implementation of {@link #hastebin(String)}. * * @param cs CommandSender to send the generated link to * @param paste Content to paste * @param messageBefore Message before the URL * @param urlMessage Message for the URL * @param messageAfter Message after the URL * @param urlTooltip Tooltip for the URL (can be null for none) */ protected void scheduleHastebin(final CommandSender cs, final String paste, final String messageBefore, final String urlMessage, final String messageAfter, final String urlTooltip) { this.plugin.getServer().getScheduler().runTaskAsynchronously(this.plugin, new Runnable() { @Override public void run() { String tempURL = null; if (Config.hastebinGeneral) { try { tempURL = BaseCommand.this.hastebin(paste); } catch (IOException ex) { ex.printStackTrace(); tempURL = null; } } final String url = tempURL; BaseCommand.this.plugin.getServer().getScheduler().runTask(BaseCommand.this.plugin, new Runnable() { @Override public void run() { if (url != null) { BaseCommand.this.plugin.getLogger().info("Paste: " + url); final FancyMessage fm = new FancyMessage(messageBefore).then(urlMessage); if (urlTooltip != null) fm.tooltip(urlTooltip); fm.link(url).then(messageAfter).send(cs); } else { new FancyMessage(Config.hastebinGeneral ? "An error occurred while trying to paste." : "Pasting is disabled.").color(MessageColor.NEGATIVE._()).send(cs); } } }); } }); } public static class Flag<T> { private final List<String> names = new ArrayList<>(); private final T value; private final Class<T> clazz; public Flag(final String... names) { Preconditions.checkNotNull(names, "names cannot be null"); if (names.length < 1) throw new IllegalArgumentException("names cannot be empty"); this.clazz = null; Collections.addAll(this.names, names); this.value = null; // used for template flags } public Flag(final Class<T> clazz, final String... names) { Preconditions.checkNotNull(names, "names cannot be null"); if (names.length < 1) throw new IllegalArgumentException("names cannot be empty"); this.clazz = clazz; Collections.addAll(this.names, names); this.value = null; // used for template flags } public Flag(final Class<T> clazz, final String[] names, final T value) { Preconditions.checkNotNull(names, "names cannot be null"); if (names.length < 1) throw new IllegalArgumentException("names cannot be empty"); this.clazz = clazz; Collections.addAll(this.names, names); this.value = value; } /** * A Flag is considered equal to another Flag if any of the names are the same and the value is the same. If the * value of this Flag is null, the values will not be checked when checking for equality. * * @param obj Object to check equality with * @return true if equal, false if otherwise */ @Override public boolean equals(final Object obj) { if (this == obj) return true; if (!(obj instanceof Flag)) return false; final Flag f = (Flag) obj; // Like, seriously. There is no way this can really be unchecked (f.getNames().containsAll()). //noinspection unchecked return !(this.clazz != null && f.getType() != null && !this.clazz.equals(f.getType())) && !(this.value != null && f.getValue() != null && !this.value.equals(f.getValue())) && (f.getNames().containsAll(this.getNames()) || this.getNames().containsAll(f.getNames())); } @Override public String toString() { return this.getNames() + " (" + this.getType() + "): " + this.getValue(); } public String getFirstName() { if (this.names.size() < 1) return null; return this.names.get(0); } public List<String> getNames() { return this.names; } public Class<T> getType() { return this.clazz; } public T getValue() { return this.value; } public T getValue(final T defaultValue) { return this.value == null ? defaultValue : this.value; } } private class HastebinData { @SuppressWarnings("UnusedDeclaration") private String key; public String getKey() { return this.key; } } /** * A class that contains flags and their parameters, along with extra parameters. */ protected class CommandArguments extends ArrayList<Flag> { private String[] extraParameters = new String[0]; CommandArguments(final String[] givenArguments) { this.processArguments(givenArguments); } CommandArguments(final String givenArguments) { this(givenArguments.split(" ")); } /** * Attempts to convert the given values to the given class. * * @param values Values to convert (will be joined if necessary) * @param clazz Class to convert to * @param <T> Type to return (will cast) * @return Converted value */ @SuppressWarnings("unchecked") private <T> T convertFlag(final String[] values, final Class<T> clazz) { final String joined = StringUtils.join(values, ' '); if (String[].class.isAssignableFrom(clazz)) { return (T) values; } else if (String.class.isAssignableFrom(clazz)) { return (T) joined; } else if (Integer.class.isAssignableFrom(clazz)) { return (T) (Integer) Integer.parseInt(joined); } else if (Short.class.isAssignableFrom(clazz)) { return (T) (Short) Short.parseShort(joined); } else if (Long.class.isAssignableFrom(clazz)) { return (T) (Long) Long.parseLong(joined); } else if (Float.class.isAssignableFrom(clazz)) { return (T) (Float) Float.parseFloat(joined); } return null; } /** * Creates a Flag based on expected flags, the given alias, and the given arguments. If there is an expected * flag with a matching alias, the given arguments will be converted to the expected type or to a String if no * expected type is given (by using {@link #convertFlag(String[], Class)}. * * @param alias Alias of the flag * @param args Arguments passed to the flag * @return Flag */ private Flag createFlag(final String alias, final String[] args) { Preconditions.checkNotNull(args, "args cannot be null"); final Flag templateFlag = new Flag(alias); final Flag realFlag; if (BaseCommand.this.expectedFlags.contains(templateFlag)) { realFlag = BaseCommand.this.expectedFlags.get(BaseCommand.this.expectedFlags.indexOf(templateFlag)); } else { realFlag = null; } Object o; try { o = args.length < 1 ? null : this.convertFlag(args, realFlag == null ? String.class : realFlag.getType()); } catch (final Exception ex) { o = null; } if (args.length < 1) { //noinspection unchecked return new Flag(null, new String[] { alias }, o); } else { //noinspection unchecked return new Flag<>(realFlag == null ? String.class : realFlag.getType(), new String[] { alias }, o); } } /** * Gets the name of the given flag. This strips the beginning hyphen or double-hyphen. * * @param s Flag * @return Flag name */ private String getFlagName(final String s) { if (!this.isFlag(s)) throw new IllegalArgumentException("Not a flag."); return s.substring(s.length() > 2 && s.substring(1).startsWith("-") ? 2 : 1); } /** * Gets if this argument is a flag. This will not match the flag terminator. * * @param s Argument * @return If the argument is a flag */ private boolean isFlag(final String s) { return s.startsWith("-") && !this.isFlagTerminator(s); } /** * Gets if the argument is the flag terminator. In this implementation, this can occur multiple times. * * @param s Argument * @return If the argument is the flag terminator */ private boolean isFlagTerminator(final String s) { return "--".equals(s); } /** * Gets any parameters that did not belong to a flag. * * @return Extra parameters */ public String[] getExtraParameters() { return this.extraParameters.clone(); } /** * Gets the corresponding Flag with values. * * @param flag Flag to get parameters for * @return Flag with values */ public <T> Flag<T> getFlag(final Flag<T> flag) { if (!this.contains(flag)) return null; //noinspection unchecked return this.get(this.indexOf(flag)); } /** * Checks to see if the given flag exists and has a value. * * @param flag Flag to check for * @return boolean */ public boolean hasContentFlag(final Flag flag) { final Flag storedFlag = this.getFlag(flag); return storedFlag != null && storedFlag.getValue() != null; } /** * Checks if the given Flag is set. Useful for boolean expectedFlags. * * @param flag Flag to check for * @return boolean */ public boolean hasFlag(final Flag flag) { return this.getFlag(flag) != null; } /** * Processes additional arguments for this instance. * * @param arguments Arguments to process */ public void processArguments(final String[] arguments) { String currentFlagName = null; final List<String> parameters = new ArrayList<>(); final List<String> extraParameters = new ArrayList<>(); for (String arg : arguments) { if (this.isFlag(arg) || this.isFlagTerminator(arg)) { if (currentFlagName != null) { final Flag f = this.createFlag(currentFlagName, parameters.toArray(new String[parameters.size()])); this.add(f); } parameters.clear(); currentFlagName = this.isFlagTerminator(arg) ? null : this.getFlagName(arg); continue; } arg = arg.replace("\\-", "-"); if (currentFlagName != null) parameters.add(arg); else extraParameters.add(arg); } if (currentFlagName != null) { final Flag f = this.createFlag(currentFlagName, parameters.toArray(new String[parameters.size()])); this.add(f); // last arg can't be neglected } this.extraParameters = (String[]) ArrayUtils.addAll(this.extraParameters, extraParameters.toArray(new String[extraParameters.size()])); } } }