org.royaldev.royalcommands.rcommands.BaseCommand.java Source code

Java tutorial

Introduction

Here is the source code for org.royaldev.royalcommands.rcommands.BaseCommand.java

Source

/*
 * 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()]));
        }
    }
}