com.comphenix.protocol.error.DetailedErrorReporter.java Source code

Java tutorial

Introduction

Here is the source code for com.comphenix.protocol.error.DetailedErrorReporter.java

Source

/*
 *  ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
 *  Copyright (C) 2012 Kristian S. Stangeland
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the
 *  GNU General Public License as published by the Free Software Foundation; either version 2 of
 *  the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *  See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with this program;
 *  if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
 *  02111-1307 USA
 */

package com.comphenix.protocol.error;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;

import com.comphenix.protocol.ProtocolLogger;
import com.comphenix.protocol.collections.ExpireHashMap;
import com.comphenix.protocol.error.Report.ReportBuilder;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.reflect.PrettyPrinter;
import com.google.common.base.Preconditions;
import com.google.common.primitives.Primitives;

/**
 * Internal class used to handle exceptions.
 * 
 * @author Kristian
 */
public class DetailedErrorReporter implements ErrorReporter {
    /**
     * Report format for printing the current exception count.
     */
    public static final ReportType REPORT_EXCEPTION_COUNT = new ReportType("Internal exception count: %s!");

    public static final String SECOND_LEVEL_PREFIX = "  ";
    public static final String DEFAULT_PREFIX = "  ";
    public static final String DEFAULT_SUPPORT_URL = "https://github.com/dmulloy2/ProtocolLib/issues";

    // Users that are informed about errors in the chat
    public static final String ERROR_PERMISSION = "protocol.info";

    // We don't want to spam the server
    public static final int DEFAULT_MAX_ERROR_COUNT = 20;

    // Prevent spam per plugin too
    private ConcurrentMap<String, AtomicInteger> warningCount = new ConcurrentHashMap<String, AtomicInteger>();

    protected String prefix;
    protected String supportURL;

    protected AtomicInteger internalErrorCount = new AtomicInteger();

    protected int maxErrorCount;
    protected Logger logger;

    protected WeakReference<Plugin> pluginReference;
    protected String pluginName;

    // Whether or not Apache Commons is not present
    protected static boolean apacheCommonsMissing;

    // Whether or not detailed errror reporting is enabled
    protected boolean detailedReporting;

    // Map of global objects
    protected Map<String, Object> globalParameters = new HashMap<String, Object>();

    // Reports to ignore
    private ExpireHashMap<Report, Boolean> rateLimited = new ExpireHashMap<Report, Boolean>();
    private Object rateLock = new Object();

    /**
     * Create a default error reporting system.
     * @param plugin - the plugin owner.
     */
    public DetailedErrorReporter(Plugin plugin) {
        this(plugin, DEFAULT_PREFIX, DEFAULT_SUPPORT_URL);
    }

    /**
     * Create a central error reporting system.
     * @param plugin - the plugin owner.
     * @param prefix - default line prefix.
     * @param supportURL - URL to report the error.
     */
    public DetailedErrorReporter(Plugin plugin, String prefix, String supportURL) {
        this(plugin, prefix, supportURL, DEFAULT_MAX_ERROR_COUNT, getBukkitLogger());
    }

    /**
     * Create a central error reporting system.
     * @param plugin - the plugin owner.
     * @param prefix - default line prefix.
     * @param supportURL - URL to report the error.
     * @param maxErrorCount - number of errors to print before giving up.
     * @param logger - current logger.
     */
    public DetailedErrorReporter(Plugin plugin, String prefix, String supportURL, int maxErrorCount,
            Logger logger) {
        if (plugin == null)
            throw new IllegalArgumentException("Plugin cannot be NULL.");

        this.pluginReference = new WeakReference<Plugin>(plugin);
        this.pluginName = getNameSafely(plugin);
        this.prefix = prefix;
        this.supportURL = supportURL;
        this.maxErrorCount = maxErrorCount;
        this.logger = logger;
    }

    private String getNameSafely(Plugin plugin) {
        try {
            return plugin.getName();
        } catch (LinkageError e) {
            return "ProtocolLib";
        }
    }

    // Attempt to get the logger.
    private static Logger getBukkitLogger() {
        try {
            return Bukkit.getLogger();
        } catch (LinkageError e) {
            return Logger.getLogger("Minecraft");
        }
    }

    /**
     * Determine if we're using detailed error reporting.
     * @return TRUE if we are, FALSE otherwise.
     */
    public boolean isDetailedReporting() {
        return detailedReporting;
    }

    /**
     * Set whether or not to use detailed error reporting.
     * @param detailedReporting - TRUE to enable it, FALSE otherwise.
     */
    public void setDetailedReporting(boolean detailedReporting) {
        this.detailedReporting = detailedReporting;
    }

    @Override
    public void reportMinimal(Plugin sender, String methodName, Throwable error, Object... parameters) {
        if (reportMinimalNoSpam(sender, methodName, error)) {
            // Print parameters, if they are given
            if (parameters != null && parameters.length > 0) {
                logger.log(Level.SEVERE, printParameters(parameters));
            }
        }
    }

    @Override
    public void reportMinimal(Plugin sender, String methodName, Throwable error) {
        reportMinimalNoSpam(sender, methodName, error);
    }

    /**
     * Report a problem with a given method and plugin, ensuring that we don't exceed the maximum number of error reports.
     * @param sender - the component that observed this exception.
     * @param methodName - the method name.
     * @param error - the error itself.
     * @return TRUE if the error was printed, FALSE if it was suppressed.
     */
    public boolean reportMinimalNoSpam(Plugin sender, String methodName, Throwable error) {
        String pluginName = PacketAdapter.getPluginName(sender);
        AtomicInteger counter = warningCount.get(pluginName);

        // Thread safe pattern
        if (counter == null) {
            AtomicInteger created = new AtomicInteger();
            counter = warningCount.putIfAbsent(pluginName, created);

            if (counter == null) {
                counter = created;
            }
        }

        final int errorCount = counter.incrementAndGet();

        // See if we should print the full error
        if (errorCount < getMaxErrorCount()) {
            logger.log(Level.SEVERE,
                    "[" + pluginName + "] Unhandled exception occured in " + methodName + " for " + pluginName,
                    error);
            return true;

        } else {
            // Nope - only print the error count occationally
            if (isPowerOfTwo(errorCount)) {
                logger.log(Level.SEVERE, "[" + pluginName + "] Unhandled exception number " + errorCount
                        + " occured in " + methodName + " for " + pluginName, error);
            }
            return false;
        }
    }

    /**
     * Determine if a given number is a power of two.
     * <p>
     * That is, if there exists an N such that 2^N = number.
     * @param number - the number to check.
     * @return TRUE if the given number is a power of two, FALSE otherwise.
     */
    private boolean isPowerOfTwo(int number) {
        return (number & (number - 1)) == 0;
    }

    @Override
    public void reportDebug(Object sender, ReportBuilder builder) {
        reportDebug(sender, Preconditions.checkNotNull(builder, "builder cannot be NULL").build());
    }

    @Override
    public void reportDebug(Object sender, Report report) {
        if (logger.isLoggable(Level.FINE) && canReport(report)) {
            reportLevel(Level.FINE, sender, report);
        }
    }

    @Override
    public void reportWarning(Object sender, ReportBuilder reportBuilder) {
        if (reportBuilder == null)
            throw new IllegalArgumentException("reportBuilder cannot be NULL.");

        reportWarning(sender, reportBuilder.build());
    }

    @Override
    public void reportWarning(Object sender, Report report) {
        if (logger.isLoggable(Level.WARNING) && canReport(report)) {
            reportLevel(Level.WARNING, sender, report);
        }
    }

    /**
     * Determine if we should print the given report.
     * <p>
     * The default implementation will check for rate limits.
     * @param report - the report to check.
     * @return TRUE if we should print it, FALSE otherwise.
     */
    protected boolean canReport(Report report) {
        long rateLimit = report.getRateLimit();

        // Check for rate limit
        if (rateLimit > 0) {
            synchronized (rateLock) {
                if (rateLimited.containsKey(report)) {
                    return false;
                }
                rateLimited.put(report, true, rateLimit, TimeUnit.NANOSECONDS);
            }
        }
        return true;
    }

    private void reportLevel(Level level, Object sender, Report report) {
        String message = "[" + pluginName + "] [" + getSenderName(sender) + "] " + report.getReportMessage();

        // Print the main warning
        if (report.getException() != null) {
            logger.log(level, message, report.getException());
        } else {
            logger.log(level, message);

            // Remember the call stack
            if (detailedReporting) {
                printCallStack(level, logger);
            }
        }

        // Parameters?
        if (report.hasCallerParameters()) {
            // Write it
            logger.log(level, printParameters(report.getCallerParameters()));
        }
    }

    /**
     * Retrieve the name of a sender class.
     * @param sender - sender object.
     * @return The name of the sender's class.
     */
    private String getSenderName(Object sender) {
        if (sender != null)
            return ReportType.getSenderClass(sender).getSimpleName();
        else
            return "NULL";
    }

    @Override
    public void reportDetailed(Object sender, ReportBuilder reportBuilder) {
        reportDetailed(sender, reportBuilder.build());
    }

    @Override
    public void reportDetailed(Object sender, Report report) {
        final Plugin plugin = pluginReference.get();
        final int errorCount = internalErrorCount.incrementAndGet();

        // Do not overtly spam the server!
        if (errorCount > getMaxErrorCount()) {
            // Only allow the error count at rare occations
            if (isPowerOfTwo(errorCount)) {
                // Permit it - but print the number of exceptions first
                reportWarning(this, Report.newBuilder(REPORT_EXCEPTION_COUNT).messageParam(errorCount).build());
            } else {
                // NEVER SPAM THE CONSOLE
                return;
            }
        }

        // Secondary rate limit
        if (!canReport(report)) {
            return;
        }

        StringWriter text = new StringWriter();
        PrintWriter writer = new PrintWriter(text);

        // Helpful message
        writer.println("[" + pluginName + "] INTERNAL ERROR: " + report.getReportMessage());
        writer.println("If this problem hasn't already been reported, please open a ticket");
        writer.println("at " + supportURL + " with the following data:");

        // Now, let us print important exception information
        writer.println("Stack Trace:");

        if (report.getException() != null) {
            report.getException().printStackTrace(writer);

        } else if (detailedReporting) {
            printCallStack(writer);
        }

        // Data dump!
        writer.println("Dump:");

        // Relevant parameters
        if (report.hasCallerParameters()) {
            printParameters(writer, report.getCallerParameters());
        }

        // Global parameters
        for (String param : globalParameters()) {
            writer.println(SECOND_LEVEL_PREFIX + param + ":");
            writer.println(addPrefix(getStringDescription(getGlobalParameter(param)),
                    SECOND_LEVEL_PREFIX + SECOND_LEVEL_PREFIX));
        }

        // Now, for the sender itself
        writer.println("Sender:");
        writer.println(addPrefix(getStringDescription(sender), SECOND_LEVEL_PREFIX));

        // And plugin
        if (plugin != null) {
            writer.println("Version:");
            writer.println(addPrefix(plugin.toString(), SECOND_LEVEL_PREFIX));
        }

        // And java version
        writer.println("Java Version:");
        writer.println(addPrefix(System.getProperty("java.version"), SECOND_LEVEL_PREFIX));

        // Add the server version too
        if (Bukkit.getServer() != null) {
            writer.println("Server:");
            writer.println(addPrefix(Bukkit.getServer().getVersion(), SECOND_LEVEL_PREFIX));

            // Inform of this occurrence
            if (ERROR_PERMISSION != null) {
                Bukkit.getServer().broadcast(String.format("Error %s (%s) occured in %s.",
                        report.getReportMessage(), report.getException(), sender), ERROR_PERMISSION);
            }
        }

        // Make sure it is reported
        logger.severe(addPrefix(text.toString(), prefix));
    }

    /**
     * Print the call stack to the given logger.
     * @param logger - the logger.
     */
    private void printCallStack(Level level, Logger logger) {
        StringWriter text = new StringWriter();
        printCallStack(new PrintWriter(text));

        // Print the exception
        logger.log(level, text.toString());
    }

    /**
     * Print the current call stack.
     * @param writer - the writer.
     */
    private void printCallStack(PrintWriter writer) {
        Exception current = new Exception("Not an error! This is the call stack.");
        current.printStackTrace(writer);
    }

    private String printParameters(Object... parameters) {
        StringWriter writer = new StringWriter();

        // Print and retrieve the string buffer
        printParameters(new PrintWriter(writer), parameters);
        return writer.toString();
    }

    private void printParameters(PrintWriter writer, Object[] parameters) {
        writer.println("Parameters: ");

        // We *really* want to get as much information as possible
        for (Object param : parameters) {
            writer.println(addPrefix(getStringDescription(param), SECOND_LEVEL_PREFIX));
        }
    }

    /**
     * Adds the given prefix to every line in the text.
     * @param text - text to modify.
     * @param prefix - prefix added to every line in the text.
     * @return The modified text.
     */
    protected String addPrefix(String text, String prefix) {
        return text.replaceAll("(?m)^", prefix);
    }

    /**
     * Retrieve a string representation of the given object.
     * @param value - object to convert.
     * @return String representation.
     */
    public static String getStringDescription(Object value) {
        // We can't only rely on toString.
        if (value == null) {
            return "[NULL]";
        }
        if (isSimpleType(value) || value instanceof Class<?>) {
            return value.toString();
        } else {
            try {
                if (!apacheCommonsMissing)
                    return ToStringBuilder.reflectionToString(value, ToStringStyle.MULTI_LINE_STYLE, false, null);
            } catch (LinkageError ex) {
                // Apache is probably missing
                apacheCommonsMissing = true;
            } catch (ThreadDeath | OutOfMemoryError e) {
                throw e;
            } catch (Throwable ex) {
                // Don't use the error logger to log errors in error logging (that could lead to infinite loops)
                ProtocolLogger.log(Level.WARNING, "Cannot convert to a String with Apache: " + ex.getMessage());
            }

            // Use our custom object printer instead
            try {
                return PrettyPrinter.printObject(value, value.getClass(), Object.class);
            } catch (IllegalAccessException e) {
                return "[Error: " + e.getMessage() + "]";
            }
        }
    }

    /**
     * Determine if the given object is a wrapper for a primitive/simple type or not.
     * @param test - the object to test.
     * @return TRUE if this object is simple enough to simply be printed, FALSE othewise.
     */
    protected static boolean isSimpleType(Object test) {
        return test instanceof String || Primitives.isWrapperType(test.getClass());
    }

    /**
     * Retrieve the current number of errors printed through {@link #reportDetailed(Object, Report)}.
     * @return Number of errors printed.
     */
    public int getErrorCount() {
        return internalErrorCount.get();
    }

    /**
     * Set the number of errors printed.
     * @param errorCount - new number of errors printed.
     */
    public void setErrorCount(int errorCount) {
        internalErrorCount.set(errorCount);
    }

    /**
     * Retrieve the maximum number of errors we can print before we begin suppressing errors.
     * @return Maximum number of errors.
     */
    public int getMaxErrorCount() {
        return maxErrorCount;
    }

    /**
     * Set the maximum number of errors we can print before we begin suppressing errors.
     * @param maxErrorCount - new max count.
     */
    public void setMaxErrorCount(int maxErrorCount) {
        this.maxErrorCount = maxErrorCount;
    }

    /**
     * Adds the given global parameter. It will be included in every error report.
     * <p>
     * Both key and value must be non-null.
     * @param key - name of parameter.
     * @param value - the global parameter itself.
     */
    public void addGlobalParameter(String key, Object value) {
        if (key == null)
            throw new IllegalArgumentException("key cannot be NULL.");
        if (value == null)
            throw new IllegalArgumentException("value cannot be NULL.");

        globalParameters.put(key, value);
    }

    /**
     * Retrieve a global parameter by its key.
     * @param key - key of the parameter to retrieve.
     * @return The value of the global parameter, or NULL if not found.
     */
    public Object getGlobalParameter(String key) {
        if (key == null)
            throw new IllegalArgumentException("key cannot be NULL.");

        return globalParameters.get(key);
    }

    /**
     * Reset all global parameters.
     */
    public void clearGlobalParameters() {
        globalParameters.clear();
    }

    /**
     * Retrieve a set of every registered global parameter.
     * @return Set of all registered global parameters.
     */
    public Set<String> globalParameters() {
        return globalParameters.keySet();
    }

    /**
     * Retrieve the support URL that will be added to all detailed reports.
     * @return Support URL.
     */
    public String getSupportURL() {
        return supportURL;
    }

    /**
     * Set the support URL that will be added to all detailed reports.
     * @param supportURL - the new support URL.
     */
    public void setSupportURL(String supportURL) {
        this.supportURL = supportURL;
    }

    /**
     * Retrieve the prefix to apply to every line in the error reports.
     * @return Error report prefix.
     */
    public String getPrefix() {
        return prefix;
    }

    /**
     * Set the prefix to apply to every line in the error reports.
     * @param prefix - new prefix.
     */
    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    /**
     * Retrieve the current logger that is used to print all reports.
     * @return The current logger.
     */
    public Logger getLogger() {
        return logger;
    }

    /**
     * Set the current logger that is used to print all reports.
     * @param logger - new logger.
     */
    public void setLogger(Logger logger) {
        this.logger = logger;
    }
}