content: /*
   *  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 com.comphenix.protocol.ProtocolLibrary;
  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;
  
  import org.apache.commons.lang.builder.ToStringBuilder;
  import org.apache.commons.lang.builder.ToStringStyle;
  import org.bukkit.Bukkit;
  import org.bukkit.plugin.Plugin;
  
  /**
   * 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 final 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 occurred in " +
  					 methodName + " for " + pluginName, error);
  			return true;
  			
  		} else {
  			// Nope - only print the error count occasionally
  			if (isPowerOfTwo(errorCount)) {
  				logger.log(Level.SEVERE, "[" + pluginName + "] Unhandled exception number " + errorCount + " occurred 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 && ProtocolLibrary.getConfig().isChatWarnings()) {
  				Bukkit.getServer().broadcast(
  						String.format("Error %s (%s) occurred 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;
  	}
  }
  