diff --git a/src/main/java/com/songoda/core/LocaleModule.java b/src/main/java/com/songoda/core/LocaleModule.java index 14a8bc37..9039554d 100644 --- a/src/main/java/com/songoda/core/LocaleModule.java +++ b/src/main/java/com/songoda/core/LocaleModule.java @@ -5,8 +5,6 @@ import org.json.simple.JSONArray; import org.json.simple.JSONObject; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; @@ -22,9 +20,7 @@ public class LocaleModule implements PluginInfoModule { JSONObject file = (JSONObject) o; if (file.get("type").equals("locale")) { - downloadLocale((String) file.get("link"), (String) file.get("name")); -// InputStream in = new URL((String) file.get("link")).openStream(); -// Locale.saveLocale(in, (String) file.get("name")); + downloadLocale(plugin, (String) file.get("link"), (String) file.get("name")); } } } catch (IOException e) { @@ -32,14 +28,14 @@ public class LocaleModule implements PluginInfoModule { } } - void downloadLocale(String link, String fileName) throws MalformedURLException, IOException { + void downloadLocale(PluginInfo plugin, String link, String fileName) throws MalformedURLException, IOException { URL url = new URL(link); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11"); urlConnection.setRequestProperty("Accept", "*/*"); urlConnection.setConnectTimeout(5000); - Locale.saveLocale(urlConnection.getInputStream(), fileName); + Locale.saveLocale(plugin.getJavaPlugin(), urlConnection.getInputStream(), fileName); urlConnection.disconnect(); } diff --git a/src/main/java/com/songoda/core/SongodaCore.java b/src/main/java/com/songoda/core/SongodaCore.java index e7bf5940..8ee4888e 100644 --- a/src/main/java/com/songoda/core/SongodaCore.java +++ b/src/main/java/com/songoda/core/SongodaCore.java @@ -13,7 +13,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; -import java.util.logging.Level; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; diff --git a/src/main/java/com/songoda/core/SongodaPlugin.java b/src/main/java/com/songoda/core/SongodaPlugin.java new file mode 100644 index 00000000..36d759cd --- /dev/null +++ b/src/main/java/com/songoda/core/SongodaPlugin.java @@ -0,0 +1,101 @@ +package com.songoda.core; + +import com.songoda.core.locale.Locale; +import com.songoda.core.utils.Metrics; +import java.util.logging.Level; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.plugin.java.JavaPlugin; + +/** + * REMINDER: When converting plugins to use this, REMOVE METRICS
+ * Must not have two instances of Metrics enabled! + * + * @author jascotty2 + */ +public abstract class SongodaPlugin extends JavaPlugin { + + protected Locale locale; + + protected ConsoleCommandSender console = Bukkit.getConsoleSender(); + private boolean emergencyStop = false; + + public abstract void onPluginLoad(); + + public abstract void onPluginEnable(); + + public abstract void onPluginDisable(); + + @Override + public final void onLoad() { + onPluginLoad(); + } + + @Override + public final void onEnable() { + console.sendMessage(ChatColor.GREEN + "============================="); + console.sendMessage(String.format("%s%s %s by %sSongoda <3!", ChatColor.GRAY.toString(), + getDescription().getName(), getDescription().getVersion(), ChatColor.DARK_PURPLE.toString())); + console.sendMessage(String.format("%sAction: %s%s%s...", ChatColor.GRAY.toString(), + ChatColor.GREEN.toString(), "Enabling", ChatColor.GRAY.toString())); + + try { + locale = Locale.loadDefaultLocale(this, "en_US"); + // Starting Metrics + Metrics.start(this); + onPluginEnable(); + } catch (Throwable t) { + getLogger().log(Level.SEVERE, "Unexpected error while loading " + getDescription().getName(), t); + console.sendMessage(ChatColor.RED + "Disabling plugin!"); + emergencyStop = true; + setEnabled(false); + } + + console.sendMessage(ChatColor.GREEN + "============================="); + } + + @Override + public final void onDisable() { + if (emergencyStop) { + return; + } + console.sendMessage(ChatColor.GREEN + "============================="); + console.sendMessage(String.format("%s%s %s by %sSongoda <3!", ChatColor.GRAY.toString(), + getDescription().getName(), getDescription().getVersion(), ChatColor.DARK_PURPLE.toString())); + console.sendMessage(String.format("%sAction: %s%s%s...", ChatColor.GRAY.toString(), + ChatColor.RED.toString(), "Disabling", ChatColor.GRAY.toString())); + onPluginDisable(); + console.sendMessage(ChatColor.GREEN + "============================="); + } + + public ConsoleCommandSender getConsole() { + return console; + } + + public Locale getLocale() { + return locale; + } + + /** + * Set the plugin's locale to a specific language + * + * @param localeName locale to use, eg "en_US" + * @param reload optionally reload the loaded locale if the locale didn't + * change + * @return true if the locale exists and was loaded successfully + */ + public boolean setLocale(String localeName, boolean reload) { + if (locale != null && locale.getName().equals(localeName)) { + return !reload || locale.reloadMessages(); + } else { + Locale l = Locale.loadLocale(this, localeName); + if (l != null) { + locale = l; + return true; + } else { + return false; + } + } + } +} diff --git a/src/main/java/com/songoda/core/locale/Locale.java b/src/main/java/com/songoda/core/locale/Locale.java index 2d9cfbca..2f9289bc 100644 --- a/src/main/java/com/songoda/core/locale/Locale.java +++ b/src/main/java/com/songoda/core/locale/Locale.java @@ -1,6 +1,7 @@ package com.songoda.core.locale; import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.plugin.Plugin; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -21,103 +22,139 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; /** - * Assists in the utilization of localization files. - * Created to be used by the Songoda Team. + * Assists in the utilization of localization files.
+ * Created to be used by the Songoda Team.
+ * NOTE: Using this class in multiple plugins requires shading! * * @author Brianna O'Keefe - Songoda */ public class Locale { - private static final List LOCALES = new ArrayList<>(); private static final Pattern NODE_PATTERN = Pattern.compile("(\\w+(?:\\.{1}\\w+)*)\\s*=\\s*\"(.*)\""); private static final String FILE_EXTENSION = ".lang"; - private static JavaPlugin plugin; - private static File localeFolder; private final Map nodes = new HashMap<>(); - - private static String defaultLocale; - - private File file; - private String name; + private final Plugin plugin; + private final File file; + private final String name; /** * Instantiate the Locale class for future use * - * @param name the name of the instantiated language + * @param plugin Owning Plugin + * @param file Location of the locale file + * @param name The locale name for the language */ - private Locale(String name) { - if (plugin == null) - return; - + public Locale(Plugin plugin, File file, String name) { + this.plugin = plugin; + this.file = file; this.name = name; - - String fileName = name + FILE_EXTENSION; - this.file = new File(localeFolder, fileName); - - if (!this.reloadMessages()) return; - - plugin.getLogger().info("Loaded locale \"" + fileName + "\""); } /** - * Initialize the class to load all existing language files and update them. - * This must be called before any other methods in this class as otherwise - * the methods will fail to invoke + * Load a default-included lang file from the plugin's jar file * - * @param plugin the plugin instance - * @param defaultLocale the default language + * @param plugin plugin to load from + * @param name name of the default locale, eg "en_US" + * @return returns the loaded Locale, or null if there was an error */ - public Locale(JavaPlugin plugin, String defaultLocale) { + public static Locale loadDefaultLocale(JavaPlugin plugin, String name) { + saveDefaultLocale(plugin, name, name); + return loadLocale(plugin, name); + } - Locale.plugin = plugin; - Locale.localeFolder = new File(plugin.getDataFolder(), "locales/"); + /** + * Load a locale from this plugin's locale directory + * + * @param plugin plugin to load from + * @param name name of the locale, eg "en_US" + * @return returns the loaded Locale, or null if there was an error + */ + public static Locale loadLocale(JavaPlugin plugin, String name) { + File localeFolder = new File(plugin.getDataFolder(), "locales/"); + if (!localeFolder.exists()) return null; + File localeFile = new File(localeFolder, name + FILE_EXTENSION); + if (!localeFolder.exists()) return null; + // found the lang file, now load it in! + Locale l = new Locale(plugin, localeFile, name); + if (!l.reloadMessages()) return null; + plugin.getLogger().info("Loaded locale \"" + name + "\""); + return l; + } - if (!localeFolder.exists()) localeFolder.mkdirs(); - - //Save the default locale file. - Locale.defaultLocale = defaultLocale; - saveLocale(defaultLocale); - - for (File file : localeFolder.listFiles()) { - String fileName = file.getName(); + /** + * Load all locales from this plugin's locale directory + * + * @param plugin plugin to load from + * @return returns the loaded Locales + */ + public static List loadAllLocales(JavaPlugin plugin) { + File localeFolder = new File(plugin.getDataFolder(), "locales/"); + List all = new ArrayList(); + for (File localeFile : localeFolder.listFiles()) { + String fileName = localeFile.getName(); if (!fileName.endsWith(FILE_EXTENSION)) continue; - - String name = fileName.substring(0, fileName.lastIndexOf('.')); - - if (name.split("_").length != 2) continue; - if (localeLoaded(name)) continue; - - LOCALES.add(new Locale(name)); + fileName = fileName.substring(0, fileName.lastIndexOf('.')); + if (fileName.split("_").length != 2) continue; + Locale l = new Locale(plugin, localeFile, fileName); + if (l.reloadMessages()) { + plugin.getLogger().info("Loaded locale \"" + fileName + "\""); + all.add(l); + } } + return all; } /** - * Save a locale file from the InputStream, to the locale folder + * Get a list of all locale files in this plugin's locale directory * - * @param fileName the name of the file to save - * @return true if the operation was successful, false otherwise + * @param plugin Plugin to check for */ - public static boolean saveLocale(String fileName) { - return saveLocale(plugin.getResource(defaultLocale + FILE_EXTENSION), fileName); + public static List getLocales(Plugin plugin) { + File localeFolder = new File(plugin.getDataFolder(), "locales/"); + List all = new ArrayList(); + for (File localeFile : localeFolder.listFiles()) { + String fileName = localeFile.getName(); + if (!fileName.endsWith(FILE_EXTENSION)) continue; + fileName = fileName.substring(0, fileName.lastIndexOf('.')); + if (fileName.split("_").length != 2) { + continue; + } + all.add(fileName); + } + return all; } /** - * Save a locale file from the InputStream, to the locale folder + * Save a locale file from the Plugin's Resources to the locale folder * - * @param in file to save + * @param plugin plugin owning the locale file + * @param locale the specific locale file to save + * @param fileName where to save the file + * @return true if the operation was successful, false otherwise + */ + public static boolean saveDefaultLocale(JavaPlugin plugin, String locale, String fileName) { + return saveLocale(plugin, plugin.getResource(locale + FILE_EXTENSION), fileName); + } + + /** + * Save a locale file from an InputStream to the locale folder + * + * @param plugin plugin owning the locale file + * @param in file to save * @param fileName the name of the file to save * @return true if the operation was successful, false otherwise */ - public static boolean saveLocale(InputStream in, String fileName) { + public static boolean saveLocale(Plugin plugin, InputStream in, String fileName) { + File localeFolder = new File(plugin.getDataFolder(), "locales/"); if (!localeFolder.exists()) localeFolder.mkdirs(); if (!fileName.endsWith(FILE_EXTENSION)) - fileName = (fileName.lastIndexOf(".") == -1 ? fileName : fileName.substring(0, fileName.lastIndexOf('.'))) + FILE_EXTENSION; + fileName = fileName + FILE_EXTENSION; File destinationFile = new File(localeFolder, fileName); if (destinationFile.exists()) - return compareFiles(in, destinationFile); + return updateFiles(plugin, in, destinationFile); try (OutputStream outputStream = new FileOutputStream(destinationFile)) { copy(in, outputStream); @@ -126,8 +163,6 @@ public class Locale { if (fileName.split("_").length != 2) return false; - LOCALES.add(new Locale(fileName)); - if (defaultLocale == null) defaultLocale = fileName; return true; } catch (IOException e) { return false; @@ -135,10 +170,8 @@ public class Locale { } // Write new changes to existing files, if any at all - private static boolean compareFiles(InputStream in, File existingFile) { - InputStream defaultFile = - in == null ? plugin.getResource((defaultLocale != null ? defaultLocale : "en_US") + FILE_EXTENSION) : in; - + // TODO: implement auto-update lang files with missing translations (load from en_us) + private static boolean updateFiles(Plugin plugin, InputStream defaultFile, File existingFile) { boolean changed = false; List defaultLines, existingLines; @@ -161,7 +194,7 @@ public class Locale { writer.write("# New messages for " + plugin.getName() + " v" + plugin.getDescription().getVersion() + "."); // If changes were found outside of the default file leave a note explaining that. - if (in == null) { + if (defaultFile == null) { writer.newLine(); writer.write("# These translations were found untranslated, join"); writer.newLine(); @@ -177,7 +210,6 @@ public class Locale { changed = true; } } - if (in != null && !changed) compareFiles(null, existingFile); } catch (IOException e) { return false; } @@ -185,32 +217,6 @@ public class Locale { return changed; } - - /** - * Check whether a locale exists and is registered or not - * - * @param name the whole language tag (i.e. "en_US") - * @return true if it exists - */ - public static boolean localeLoaded(String name) { - for (Locale locale : LOCALES) - if (locale.getName().equals(name)) return true; - return false; - } - - - /** - * Get a locale by its entire proper name (i.e. "en_US") - * - * @param name the full name of the locale - * @return locale of the specified name - */ - public static Locale getLocale(String name) { - for (Locale locale : LOCALES) - if (locale.getName().equalsIgnoreCase(name)) return locale; - return null; - } - /** * Clear the previous message cache and load new messages directly from file * diff --git a/src/main/java/com/songoda/core/utils/Metrics.java b/src/main/java/com/songoda/core/utils/Metrics.java new file mode 100644 index 00000000..65f33e24 --- /dev/null +++ b/src/main/java/com/songoda/core/utils/Metrics.java @@ -0,0 +1,698 @@ +package com.songoda.core.utils; + +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.ServicePriority; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +import javax.net.ssl.HttpsURLConnection; +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.logging.Level; +import java.util.zip.GZIPOutputStream; + +/** + * bStats collects some data for plugin authors. + *

+ * Check out https://bStats.org/ to learn more about bStats! + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public class Metrics { + + static { + // You can use the property to disable the check in your test environment + if (System.getProperty("bstats.relocatecheck") == null || !System.getProperty("bstats.relocatecheck").equals("false")) { + // Maven's Relocate is clever and changes strings, too. So we have to use this little "trick" ... :D + final String defaultPackage = new String( + new byte[]{'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's', '.', 'b', 'u', 'k', 'k', 'i', 't'}); + final String examplePackage = new String(new byte[]{'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); + // We want to make sure nobody just copy & pastes the example and use the wrong package names + if (Metrics.class.getPackage().getName().equals(defaultPackage) || Metrics.class.getPackage().getName().equals(examplePackage)) { + throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); + } + } + } + + // The version of this bStats class + public static final int B_STATS_VERSION = 1; + + // The url to which the data is sent + private static final String URL = "https://bStats.org/submitData/bukkit"; + + // Is bStats enabled on this server? + private boolean enabled; + + // Should failed requests be logged? + private static boolean logFailedRequests; + + // Should the sent data be logged? + private static boolean logSentData; + + // Should the response text be logged? + private static boolean logResponseStatusText; + + // The uuid of the server + private static String serverUUID; + + // The plugin + private final Plugin plugin; + + // A list with all custom charts + private final List charts = new ArrayList<>(); + + /** + * Class constructor. + * + * @param plugin The plugin which stats should be submitted. + */ + public static void start(Plugin plugin) { + Metrics m = new Metrics(plugin); + if (m.enabled) { + boolean found = false; + // Search for all other bStats Metrics classes to see if we are the first one + for (Class service : Bukkit.getServicesManager().getKnownServices()) { + try { + service.getField("B_STATS_VERSION"); // Our identifier :) + found = true; // We aren't the first + break; + } catch (NoSuchFieldException ignored) { } + } + // Register this specific instance as a service + Bukkit.getServicesManager().register(Metrics.class, m, plugin, ServicePriority.Normal); + if (!found) { + // We are the first! + m.startSubmitting(); + } + } + } + + private Metrics(Plugin plugin) { + if (plugin == null) { + throw new IllegalArgumentException("Plugin cannot be null!"); + } + this.plugin = plugin; + + // Get the config file + File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); + File configFile = new File(bStatsFolder, "config.yml"); + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + + // Check if the config file exists + if (!config.isSet("serverUuid")) { + + // Add default values + config.addDefault("enabled", true); + // Every server gets it's unique random id. + config.addDefault("serverUuid", UUID.randomUUID().toString()); + // Should failed request be logged? + config.addDefault("logFailedRequests", false); + // Should the sent data be logged? + config.addDefault("logSentData", false); + // Should the response text be logged? + config.addDefault("logResponseStatusText", false); + + // Inform the server owners about bStats + config.options().header( + "bStats collects some data for plugin authors like how many servers are using their plugins.\n" + + "To honor their work, you should not disable it.\n" + + "This has nearly blacklist effect on the server performance!\n" + + "Check out https://bStats.org/ to learn more :)" + ).copyDefaults(true); + try { + config.save(configFile); + } catch (IOException ignored) { } + } + + // Load the data + enabled = config.getBoolean("enabled", true); + serverUUID = config.getString("serverUuid"); + logFailedRequests = config.getBoolean("logFailedRequests", false); + logSentData = config.getBoolean("logSentData", false); + logResponseStatusText = config.getBoolean("logResponseStatusText", false); + } + + /** + * Checks if bStats is enabled. + * + * @return Whether bStats is enabled or not. + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Adds a custom chart. + * + * @param chart The chart to add. + */ + public void addCustomChart(CustomChart chart) { + if (chart == null) { + throw new IllegalArgumentException("Chart cannot be null!"); + } + charts.add(chart); + } + + /** + * Starts the Scheduler which submits our data every 30 minutes. + */ + private void startSubmitting() { + final Timer timer = new Timer(true); // We use a timer cause the Bukkit scheduler is affected by server lags + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + if (!plugin.isEnabled()) { // Plugin was disabled + timer.cancel(); + return; + } + // Nevertheless we want our code to run in the Bukkit main thread, so we have to use the Bukkit scheduler + // Don't be afraid! The connection to the bStats server is still async, only the stats collection is sync ;) + Bukkit.getScheduler().runTask(plugin, () -> submitData()); + } + }, 1000 * 60 * 5, 1000 * 60 * 30); + // Submit the data every 30 minutes, first time after 5 minutes to give other plugins enough time to start + // WARNING: Changing the frequency has blacklist effect but your plugin WILL be blocked/deleted! + // WARNING: Just don't do it! + } + + /** + * Gets the plugin specific data. + * This method is called using Reflection. + * + * @return The plugin specific data. + */ + public JSONObject getPluginData() { + JSONObject data = new JSONObject(); + + String pluginName = plugin.getDescription().getName(); + String pluginVersion = plugin.getDescription().getVersion(); + + data.put("pluginName", pluginName); // Append the name of the plugin + data.put("pluginVersion", pluginVersion); // Append the version of the plugin + JSONArray customCharts = new JSONArray(); + for (CustomChart customChart : charts) { + // Add the data of the custom charts + JSONObject chart = customChart.getRequestJsonObject(); + if (chart == null) { // If the chart is null, we skip it + continue; + } + customCharts.add(chart); + } + data.put("customCharts", customCharts); + + return data; + } + + /** + * Gets the server specific data. + * + * @return The server specific data. + */ + private JSONObject getServerData() { + // Minecraft specific data + int playerAmount; + try { + // Around MC 1.8 the return type was changed to a collection from an array, + // This fixes java.lang.NoSuchMethodError: org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; + Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); + playerAmount = onlinePlayersMethod.getReturnType().equals(Collection.class) + ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() + : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; + } catch (Exception e) { + playerAmount = Bukkit.getOnlinePlayers().size(); // Just use the new method if the Reflection failed + } + int onlineMode = Bukkit.getOnlineMode() ? 1 : 0; + String bukkitVersion = Bukkit.getVersion(); + + // OS/Java specific data + String javaVersion = System.getProperty("java.version"); + String osName = System.getProperty("os.name"); + String osArch = System.getProperty("os.arch"); + String osVersion = System.getProperty("os.version"); + int coreCount = Runtime.getRuntime().availableProcessors(); + + JSONObject data = new JSONObject(); + + data.put("serverUUID", serverUUID); + + data.put("playerAmount", playerAmount); + data.put("onlineMode", onlineMode); + data.put("bukkitVersion", bukkitVersion); + + data.put("javaVersion", javaVersion); + data.put("osName", osName); + data.put("osArch", osArch); + data.put("osVersion", osVersion); + data.put("coreCount", coreCount); + + return data; + } + + /** + * Collects the data and sends it afterwards. + */ + private void submitData() { + final JSONObject data = getServerData(); + + JSONArray pluginData = new JSONArray(); + // Search for all other bStats Metrics classes to get their plugin data + for (Class service : Bukkit.getServicesManager().getKnownServices()) { + try { + service.getField("B_STATS_VERSION"); // Our identifier :) + + for (RegisteredServiceProvider provider : Bukkit.getServicesManager().getRegistrations(service)) { + try { + pluginData.add(provider.getService().getMethod("getPluginData").invoke(provider.getProvider())); + } catch (NullPointerException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) { } + } + } catch (NoSuchFieldException ignored) { } + } + + data.put("plugins", pluginData); + + // Create a new thread for the connection to the bStats server + new Thread(new Runnable() { + @Override + public void run() { + try { + // Send the data + sendData(plugin, data); + } catch (Exception e) { + // Something went wrong! :( + if (logFailedRequests) { + plugin.getLogger().log(Level.WARNING, "Could not submit plugin stats of " + plugin.getName(), e); + } + } + } + }).start(); + } + + /** + * Sends the data to the bStats server. + * + * @param plugin Any plugin. It's just used to get a logger instance. + * @param data The data to send. + * @throws Exception If the request failed. + */ + private static void sendData(Plugin plugin, JSONObject data) throws Exception { + if (data == null) { + throw new IllegalArgumentException("Data cannot be null!"); + } + if (Bukkit.isPrimaryThread()) { + throw new IllegalAccessException("This method must not be called from the main thread!"); + } + if (logSentData) { + plugin.getLogger().info("Sending data to bStats: " + data.toString()); + } + HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection(); + + // Compress the data to save bandwidth + byte[] compressedData = compress(data.toString()); + + // Add headers + connection.setRequestMethod("POST"); + connection.addRequestProperty("Accept", "application/json"); + connection.addRequestProperty("Connection", "close"); + connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request + connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); + connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format + connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION); + + // Send data + connection.setDoOutput(true); + DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream()); + outputStream.write(compressedData); + outputStream.flush(); + outputStream.close(); + + InputStream inputStream = connection.getInputStream(); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + + StringBuilder builder = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + builder.append(line); + } + bufferedReader.close(); + if (logResponseStatusText) { + plugin.getLogger().info("Sent data to bStats and received response: " + builder.toString()); + } + } + + /** + * Gzips the given String. + * + * @param str The string to gzip. + * @return The gzipped String. + * @throws IOException If the compression failed. + */ + private static byte[] compress(final String str) throws IOException { + if (str == null) { + return null; + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(outputStream); + gzip.write(str.getBytes(StandardCharsets.UTF_8)); + gzip.close(); + return outputStream.toByteArray(); + } + + /** + * Represents a custom chart. + */ + public static abstract class CustomChart { + + // The id of the chart + final String chartId; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + */ + CustomChart(String chartId) { + if (chartId == null || chartId.isEmpty()) { + throw new IllegalArgumentException("ChartId cannot be null or empty!"); + } + this.chartId = chartId; + } + + private JSONObject getRequestJsonObject() { + JSONObject chart = new JSONObject(); + chart.put("chartId", chartId); + try { + JSONObject data = getChartData(); + if (data == null) { + // If the data is null we don't send the chart. + return null; + } + chart.put("data", data); + } catch (Throwable t) { + if (logFailedRequests) { + Bukkit.getLogger().log(Level.WARNING, "Failed to get data for custom chart with id " + chartId, t); + } + return null; + } + return chart; + } + + protected abstract JSONObject getChartData() throws Exception; + + } + + /** + * Represents a custom simple pie. + */ + public static class SimplePie extends CustomChart { + + private final Callable callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SimplePie(String chartId, Callable callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JSONObject getChartData() throws Exception { + JSONObject data = new JSONObject(); + String value = callable.call(); + if (value == null || value.isEmpty()) { + // Null = skip the chart + return null; + } + data.put("value", value); + return data; + } + } + + /** + * Represents a custom advanced pie. + */ + public static class AdvancedPie extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public AdvancedPie(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JSONObject getChartData() throws Exception { + JSONObject data = new JSONObject(); + JSONObject values = new JSONObject(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + continue; // Skip this invalid + } + allSkipped = false; + values.put(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + data.put("values", values); + return data; + } + } + + /** + * Represents a custom drilldown pie. + */ + public static class DrilldownPie extends CustomChart { + + private final Callable>> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public DrilldownPie(String chartId, Callable>> callable) { + super(chartId); + this.callable = callable; + } + + @Override + public JSONObject getChartData() throws Exception { + JSONObject data = new JSONObject(); + JSONObject values = new JSONObject(); + Map> map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean reallyAllSkipped = true; + for (Map.Entry> entryValues : map.entrySet()) { + JSONObject value = new JSONObject(); + boolean allSkipped = true; + for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { + value.put(valueEntry.getKey(), valueEntry.getValue()); + allSkipped = false; + } + if (!allSkipped) { + reallyAllSkipped = false; + values.put(entryValues.getKey(), value); + } + } + if (reallyAllSkipped) { + // Null = skip the chart + return null; + } + data.put("values", values); + return data; + } + } + + /** + * Represents a custom single line chart. + */ + public static class SingleLineChart extends CustomChart { + + private final Callable callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SingleLineChart(String chartId, Callable callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JSONObject getChartData() throws Exception { + JSONObject data = new JSONObject(); + int value = callable.call(); + if (value == 0) { + // Null = skip the chart + return null; + } + data.put("value", value); + return data; + } + + } + + /** + * Represents a custom multi line chart. + */ + public static class MultiLineChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public MultiLineChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JSONObject getChartData() throws Exception { + JSONObject data = new JSONObject(); + JSONObject values = new JSONObject(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + continue; // Skip this invalid + } + allSkipped = false; + values.put(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + data.put("values", values); + return data; + } + + } + + /** + * Represents a custom simple bar chart. + */ + public static class SimpleBarChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SimpleBarChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JSONObject getChartData() throws Exception { + JSONObject data = new JSONObject(); + JSONObject values = new JSONObject(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + for (Map.Entry entry : map.entrySet()) { + JSONArray categoryValues = new JSONArray(); + categoryValues.add(entry.getValue()); + values.put(entry.getKey(), categoryValues); + } + data.put("values", values); + return data; + } + + } + + /** + * Represents a custom advanced bar chart. + */ + public static class AdvancedBarChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public AdvancedBarChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JSONObject getChartData() throws Exception { + JSONObject data = new JSONObject(); + JSONObject values = new JSONObject(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue().length == 0) { + continue; // Skip this invalid + } + allSkipped = false; + JSONArray categoryValues = new JSONArray(); + for (int categoryValue : entry.getValue()) { + categoryValues.add(categoryValue); + } + values.put(entry.getKey(), categoryValues); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + data.put("values", values); + return data; + } + } + +} \ No newline at end of file