Java tutorial
/* * Copyright (C) 2015 Codelanx, All Rights Reserved * * This work is licensed under a Creative Commons * Attribution-NonCommercial-NoDerivs 3.0 Unported License. * * This program is protected software: You are free to distrubute your * own use of this software under the terms of the Creative Commons BY-NC-ND * license as published by Creative Commons in the year 2015 or as published * by a later date. You may not provide the source files or provide a means * of running the software outside of those licensed to use it. * * 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. * * You should have received a copy of the Creative Commons BY-NC-ND license * long with this program. If not, see <https://creativecommons.org/licenses/>. */ package com.codelanx.codelanxlib.logging; import com.codelanx.codelanxlib.util.exception.Exceptions; import com.codelanx.codelanxlib.CodelanxLib; import com.codelanx.codelanxlib.listener.ListenerManager; import com.codelanx.codelanxlib.util.Reflections; import com.codelanx.codelanxlib.util.Scheduler; import com.codelanx.codelanxlib.util.exception.IllegalPluginAccessException; import java.io.DataOutputStream; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import org.bukkit.Bukkit; import org.bukkit.Server; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.server.PluginEnableEvent; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; import org.json.simple.JSONObject; /** * Provides toggleable logging supporting for debug statements and error * reporting to a webservice for easy bugfixing. Will find unreported errors as * well and submit them. Note that this class is provided as a utility, it * should not be used as a substitute for general logging statements that are * <i>always</i> available. Use the {@link Logging} class instead. * * @since 0.1.0 * @author 1Rogue * @version 0.1.0 */ public final class Debugger { private final static Map<Plugin, DebugOpts> opts = new HashMap<>(); private final static Logger logger = Logger.getLogger(Debugger.class.getName()); private Debugger() { } private static DebugOpts getOpts() { Plugin p = Reflections.getCallingPlugin(1); return Debugger.getOpts(p); } private static DebugOpts getOpts(Plugin p) { DebugOpts back = Debugger.opts.get(p); if (back == null) { back = new DebugOpts(p); Debugger.opts.put(p, back); } return back; } /** * Hooks into Bukkit's plugin system and adds a handler to all plugin * loggers to allow catching unreported exceptions. If already hooked, will * do nothing. This method will continue to hook new plugins via a listener * * @since 0.1.0 * @version 0.1.0 * * @throws IllegalPluginAccessException If something other than * {@link CodelanxLib} calls this * method */ public static void hookBukkit() { //Check to make sure CodelanxLib is calling it Exceptions.illegalPluginAccess(Reflections.accessedFrom(CodelanxLib.class), "Debugger#hookBukkit may only be called by CodelanxLib"); Listener l = new BukkitPluginListener(); if (!ListenerManager.isRegisteredToBukkit(CodelanxLib.get(), l)) { Bukkit.getServer().getPluginManager().registerEvents(l, CodelanxLib.get()); } //Hook any current plugins without a handler for (Plugin p : Bukkit.getServer().getPluginManager().getPlugins()) { //boo arrays ExceptionHandler.apply(p); } } /** * Sets the URL to send a JSON payload of server information to as well as * any other relevant information for when a stack trace occurs. This allows * for a simple way of setting up error reporting. The default value for * this is {@code null}, and as a result will not send any information upon * errors occurring unless a target URL is set. * * @since 0.1.0 * @version 0.1.0 * * @param url The URL to send JSON payloads to */ public static void setReportingURL(String url) { DebugOpts opts = Debugger.getOpts(); if (opts != null) { opts.setUrl(url); } } /** * Sets whether or not to actually output any calls from your plugin to the * Debugger. This defaults to {@code false}. * * @since 0.1.0 * @version 0.1.0 * * @param output {@code true} if calls to the Debugger should print out */ public static void toggleOutput(boolean output) { DebugOpts opts = Debugger.getOpts(); if (opts != null) { opts.toggleOutput(output); } } /** * Sets whether or not calls to * {@link Debugger#error(Throwable, String, Object...)} will print errors. * This is distinct from {@link Debugger#toggleOutput(boolean)} and errors * will not depend on its value. This defaults to {@code false}. * * @since 0.1.0 * @version 0.1.0 * * @param hide {@code true} if errors should be printed */ public static void hideErrors(boolean hide) { DebugOpts opts = Debugger.getOpts(); if (opts != null) { opts.hideErrors(hide); } } /** * Prints to the Debugging {@link Logger} if * {@link Debugger#toggleOutput(boolean)} is set to {@code true} * * @since 0.1.0 * @version 0.1.0 * * @param level The {@link Level} to print at * @param format The formatting string * @param args The printf arguments */ public static void print(Level level, String format, Object... args) { DebugOpts opts = Debugger.getOpts(); if (opts == null || !opts.doOutput()) { return; } Debugger.logger.log(level, String.format("[%s]=> %s", opts.getPrefix(), String.format(format, args))); } /** * Prints to the Debugging {@link Logger} at {@link Level#INFO} if * {@link Debugger#toggleOutput(boolean)} is set to {@code true} * * @since 0.1.0 * @version 0.1.0 * * @param format The formatting string * @param args The printf arguments */ public static void print(String format, Object... args) { //Note, do not overload methods. getOpts() depends on stack location DebugOpts opts = Debugger.getOpts(); if (opts == null || !opts.doOutput()) { return; } Debugger.logger.log(Level.INFO, String.format("[%s]=> %s", opts.getPrefix(), String.format(format, args))); } /** * Prints to the Debugging {@link Logger} at {@link Level#SEVERE} if * {@link Debugger#toggleOutput(boolean)} is set to {@code true}. It will * also report the error with the URL set via * {@link Debugger#setReportingURL(String)}. If that is {@code null}, * nothing will be reported * * @since 0.1.0 * @version 0.1.0 * * @param error The {@link Throwable} to be printed * @param message The formatting string * @param args The formatting arguments */ public static void error(Throwable error, String message, Object... args) { DebugOpts opts = Debugger.getOpts(); if (opts == null) { return; } if (!opts.doHideErrors()) { Debugger.logger.log(Level.SEVERE, String.format(message, args), error); } //Send JSON payload Debugger.report(opts, error, String.format(message, args)); } /** * Reports an error to a specific reporting URL * * @since 0.1.0 * @version 0.1.0 * * @param opts The {@link DebugOpts} relevant to the current plugin context * @param error The {@link Throwable} to report * @param message The message relevant to the error */ private static void report(DebugOpts opts, Throwable error, String message) { if (opts == null || opts.getUrl() == null) { return; } Scheduler.runAsyncTask(() -> { JSONObject out = Debugger.getPayload(opts, error, message); try { Debugger.send(opts.getUrl(), out); } catch (IOException ex) { Debugger.logger.log(Level.WARNING, "Unable to report error"); //Logger-generated errors should not be re-reported, and //no ErrorManager is present for this instance } }, 0); } /** * Returns a JSON payload containing as much relevant server information as * possible (barring anything identifiable) and the error itself * * @since 0.1.0 * @version 0.1.0 * * @param opts The {@link DebugOpts} relevant to the current plugin context * @param error The {@link Throwable} to report * @param message The message relevant to the error * @return A new {@link JSONObject} payload */ private static JSONObject getPayload(DebugOpts opts, Throwable error, String message) { JSONObject back = new JSONObject(); back.put("project-type", "bukkit-plugin"); JSONObject plugin = new JSONObject(); PluginDescriptionFile pd = opts.getPlugin().getDescription(); plugin.put("name", pd.getName()); plugin.put("version", pd.getVersion()); plugin.put("main", pd.getMain()); plugin.put("prefix", opts.getPrefix()); back.put("plugin", plugin); JSONObject server = new JSONObject(); Server s = Bukkit.getServer(); server.put("allow-end", s.getAllowEnd()); server.put("allow-flight", s.getAllowFlight()); server.put("allow-nether", s.getAllowNether()); server.put("ambient-spawn-limit", s.getAmbientSpawnLimit()); server.put("animal-spawn-limit", s.getAnimalSpawnLimit()); server.put("binding-address", s.getIp()); server.put("bukkit-version", s.getBukkitVersion()); server.put("connection-throttle", s.getConnectionThrottle()); server.put("default-game-mode", s.getDefaultGameMode().name()); server.put("default-world-type", s.getWorldType()); server.put("generate-structures", s.getGenerateStructures()); server.put("idle-timeout", s.getIdleTimeout()); server.put("players-online", s.getOnlinePlayers().size()); server.put("max-players", s.getMaxPlayers()); server.put("monster-spawn-limit", s.getMonsterSpawnLimit()); server.put("motd", s.getMotd()); server.put("name", s.getName()); server.put("online-mode", s.getOnlineMode()); server.put("port", s.getPort()); server.put("server-id", s.getServerId()); server.put("server-name", s.getServerName()); server.put("spawn-radius", s.getSpawnRadius()); server.put("ticks-per-animal-spawns", s.getTicksPerAnimalSpawns()); server.put("ticks-per-monster-spawns", s.getTicksPerMonsterSpawns()); server.put("version", s.getVersion()); server.put("view-distance", s.getViewDistance()); server.put("warning-state", s.getWarningState()); server.put("water-animal-spawn-limit", s.getWaterAnimalSpawnLimit()); back.put("server", server); JSONObject system = new JSONObject(); system.put("name", System.getProperty("os.name")); system.put("version", System.getProperty("os.version")); system.put("arch", System.getProperty("os.arch")); back.put("system", system); JSONObject java = new JSONObject(); java.put("version", System.getProperty("java.version")); java.put("vendor", System.getProperty("java.vendor")); java.put("vendor-url", System.getProperty("java.vendor.url")); java.put("bit", System.getProperty("sun.arch.data.model")); back.put("java", java); back.put("message", message); back.put("error", Exceptions.readableStackTrace(error)); return back; } /** * Sends a JSON payload to a URL specified by the string parameter * * @since 0.1.0 * @version 0.1.0 * * @param url The URL to report to * @param payload The JSON payload to send via POST * @throws IOException If the sending failed */ private static void send(String url, JSONObject payload) throws IOException { URL loc = new URL(url); HttpURLConnection http = (HttpURLConnection) loc.openConnection(); http.setRequestMethod("POST"); http.setRequestProperty("Content-Type", "application/json"); http.setUseCaches(false); http.setDoOutput(true); try (DataOutputStream wr = new DataOutputStream(http.getOutputStream())) { wr.writeBytes(payload.toJSONString()); wr.flush(); } } /** * Represents internally stored debugging options for specific plugins * * @since 0.1.0 * @author 1Rogue * @version 0.1.0 */ private final static class DebugOpts { private final Plugin plugin; private final String prefix; private boolean output; private boolean hideErrors; private String url; /** * Constructor. Determines the logging prefix and initializes fields * * @since 0.1.0 * @version 0.1.0 * * @param plugin The {@link Plugin} relevant to this instance */ public DebugOpts(Plugin plugin) { this.plugin = plugin; this.prefix = plugin.getDescription().getPrefix() == null ? plugin.getName() : plugin.getDescription().getPrefix(); this.output = this.plugin.getClass() == CodelanxLib.class; //false except for CodelanxLib if (plugin.getDescription().getMain().startsWith("com.codelanx.")) { this.url = "http://blooper.codelanx.com/report"; //Hook specifically for codelanx plugins } else { this.url = null; } } /** * Returns {@code true} if output is printed to the debug {@link Logger} * * @since 0.1.0 * @version 0.1.0 * * @return {@code true} if output is printed */ public boolean doOutput() { return this.output; } /** * Toggles whether or not to print information to the debugger * * @since 0.1.0 * @version 0.1.0 * * @param output {@code true} to enable output */ public void toggleOutput(boolean output) { this.output = output; } /** * Toggles whether or not to print errors to the debugger * * @since 0.1.0 * @version 0.1.0 * * @param output */ public void hideErrors(boolean hide) { this.hideErrors = hide; } /** * Returns {@code true} if errors should not be printed to the debug * {@link Logger} * * @since 0.1.0 * @version 0.1.0 * * @return {@code true} if errors are hidden */ public boolean doHideErrors() { return this.hideErrors; } /** * Returns the URL that errors are reported to * * @since 0.1.0 * @version 0.1.0 * * @return The error reporting URL */ public String getUrl() { return this.url; } /** * Sets the URL to report errors to * * @since 0.1.0 * @version 0.1.0 * * @param url The error reporting URL */ public void setUrl(String url) { this.url = url; } /** * Returns the logging prefix used for debug output. This is typically * the plugin's name unless a prefix is specified in the plugin's * {@code plugin.yml} file. * * @since 0.1.0 * @version 0.1.0 * * @return The prefix used for debug output */ public String getPrefix() { return this.prefix; } /** * Returns the {@link Plugin} that this {@link DebugOpts} pertains to * * @since 0.1.0 * @version 0.1.0 * * @return The relevant {@link Plugin} to this instance */ public Plugin getPlugin() { return this.plugin; } } /** * Attachable {@link Handler} used to catch any exceptions that are logged * directly to a plugin's {@link Logger} * * @since 0.1.0 * @author 1Rogue * @version 0.1.0 */ public static class ExceptionHandler extends Handler { private final Plugin plugin; /** * Constructor. Sets the plugin to a field and sets the filter for this * {@link Handler} to {@link Level#SEVERE} * * @since 0.1.0 * @version 0.1.0 * * @param plugin The relevant {@link Plugin} to the {@link Logger} */ public ExceptionHandler(Plugin plugin) { this.plugin = plugin; super.setFilter((LogRecord record) -> record.getLevel() == Level.SEVERE); } /** * If {@link LogRecord#getThrown()} does not return {@code null}, then * this will call {@link Debugger#report(DebugOpts, Throwable, String)} * <br><br> {@inheritDoc} * * @since 0.1.0 * @version 0.1.0 * * @param record {@inheritDoc} */ @Override public void publish(LogRecord record) { if (record.getThrown() != null) { //Report exception Debugger.report(Debugger.getOpts(this.plugin), record.getThrown(), record.getMessage()); } } /** * Does nothing * * @since 0.1.0 * @version 0.1.0 */ @Override public void flush() { } //not buffered /** * Does nothing * * @since 0.1.0 * @version 0.1.0 * * @throws SecurityException Never happens */ @Override public void close() throws SecurityException { } //nothing to close /** * Applies a new {@link Handler} to the passed {@link Plugin}'s * {@link Logger} if it is not already attached to it * * @since 0.0.1 * @version 0.0.1 * * @param p The {@link Plugin} with the {@link Logger} to check */ public static void apply(Plugin p) { for (Handler h : p.getLogger().getHandlers()) { if (h instanceof ExceptionHandler) { return; } } p.getLogger().addHandler(new ExceptionHandler(p)); } } /** * A {@link Listener} for adding an {@link ExceptionHandler} to a * {@link Plugin}'s {@link Logger} upon it being enabled * * @since 0.1.0 * @author 1Rogue * @version 0.1.0 */ private final static class BukkitPluginListener implements Listener { /** * Appends an {@link ExceptionHandler} to a {@link Plugin}'s * {@link Logger} * * @since 0.1.0 * @version 0.1.0 * * @param event The relevant {@link PluginEnableEvent} from Bukkit */ @EventHandler public void onEnable(PluginEnableEvent event) { ExceptionHandler.apply(event.getPlugin()); } } }