diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml
index cd05e9d..726b2cc 100644
--- a/.github/workflows/sonarcloud.yml
+++ b/.github/workflows/sonarcloud.yml
@@ -1,4 +1,4 @@
-name: SonarCLoud Scanner
+name: SonarCloud Scanner
on:
push:
branches:
diff --git a/pom.xml b/pom.xml
index d44e4b4..f00de03 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,6 +13,8 @@
TheSilentPro_HeadDB
silent
https://sonarcloud.io
+ src/main/java/tsp/headdb/Metrics.java
+ *
84967
diff --git a/src/main/java/tsp/headdb/HeadDB.java b/src/main/java/tsp/headdb/HeadDB.java
index cc866b8..3d81696 100644
--- a/src/main/java/tsp/headdb/HeadDB.java
+++ b/src/main/java/tsp/headdb/HeadDB.java
@@ -67,7 +67,7 @@ public class HeadDB extends JavaPlugin {
initMetrics();
Utils.isLatestVersion(this, 84967, latest -> {
- if (!latest) {
+ if (!Boolean.TRUE.equals(latest)) {
Log.warning("There is a new update available for HeadDB on spigot!");
Log.warning("Download: https://www.spigotmc.org/resources/84967");
}
@@ -112,10 +112,6 @@ public class HeadDB extends JavaPlugin {
private void initMetrics() {
Metrics metrics = new Metrics(this, 9152);
- if (!metrics.isEnabled()) {
- Log.debug("Metrics are disabled.");
- return;
- }
metrics.addCustomChart(new Metrics.SimplePie("economy_provider", () -> {
if (this.getEconomyProvider() != null) {
diff --git a/src/main/java/tsp/headdb/Metrics.java b/src/main/java/tsp/headdb/Metrics.java
index e97f0d5..2016398 100644
--- a/src/main/java/tsp/headdb/Metrics.java
+++ b/src/main/java/tsp/headdb/Metrics.java
@@ -1,157 +1,113 @@
+/*
+ * This Metrics class was auto-generated and can be copied into your project if you are
+ * not using a build tool like Gradle or Maven for dependency management.
+ *
+ * IMPORTANT: You are not allowed to modify this class, except changing the package.
+ *
+ * Unallowed modifications include but are not limited to:
+ * - Remove the option for users to opt-out
+ * - Change the frequency for data submission
+ * - Obfuscate the code (every obfucator should allow you to make an exception for specific files)
+ * - Reformat the code (if you use a linter, add an exception)
+ *
+ * Violations will result in a ban of your plugin and account from bStats.
+ */
package tsp.headdb;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import com.google.gson.JsonPrimitive;
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+import java.util.zip.GZIPOutputStream;
+import javax.net.ssl.HttpsURLConnection;
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.bukkit.plugin.java.JavaPlugin;
-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;
+public class Metrics {
-/**
- * bStats collects some data for plugin authors.
- *
- * Check out https://bStats.org/ to learn more about bStats!
- */
-@SuppressWarnings({"WeakerAccess", "unused"})
-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;
- // The plugin id
- private final int pluginId;
-
- // A list with all custom charts
- private final List charts = new ArrayList<>();
+ private final MetricsBase metricsBase;
/**
- * Class constructor.
+ * Creates a new Metrics instance.
*
- * @param plugin The plugin which stats should be submitted.
- * @param pluginId The id of the plugin.
- * It can be found at What is my plugin id?
+ * @param plugin Your plugin instance.
+ * @param serviceId The id of the service. It can be found at What is my plugin id?
*/
- public Metrics(Plugin plugin, int pluginId) {
- if (plugin == null) {
- throw new IllegalArgumentException("Plugin cannot be null!");
- }
+ public Metrics(JavaPlugin plugin, int serviceId) {
this.plugin = plugin;
- this.pluginId = pluginId;
-
// 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 no effect on the server performance!\n" +
- "Check out https://bStats.org/ to learn more :)"
- ).copyDefaults(true);
+ config
+ .options()
+ .header(
+ "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n"
+ + "many people use their plugin and their total player count. It's recommended to keep bStats\n"
+ + "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n"
+ + "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n"
+ + "anonymous.")
+ .copyDefaults(true);
try {
config.save(configFile);
- } catch (IOException ignored) { }
+ } 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);
-
- if (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 our service
- Bukkit.getServicesManager().register(Metrics.class, this, plugin, ServicePriority.Normal);
- if (!found) {
- // We are the first!
- startSubmitting();
- }
- }
- }
-
- /**
- * Checks if bStats is enabled.
- *
- * @return Whether bStats is enabled or not.
- */
- public boolean isEnabled() {
- return enabled;
+ boolean enabled = config.getBoolean("enabled", true);
+ String serverUUID = config.getString("serverUuid");
+ boolean logErrors = config.getBoolean("logFailedRequests", false);
+ boolean logSentData = config.getBoolean("logSentData", false);
+ boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false);
+ metricsBase =
+ new MetricsBase(
+ "bukkit",
+ serverUUID,
+ serviceId,
+ enabled,
+ this::appendPlatformData,
+ this::appendServiceData,
+ submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask),
+ plugin::isEnabled,
+ (message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error),
+ (message) -> this.plugin.getLogger().log(Level.INFO, message),
+ logErrors,
+ logSentData,
+ logResponseStatusText);
}
/**
@@ -160,354 +116,265 @@ class Metrics {
* @param chart The chart to add.
*/
public void addCustomChart(CustomChart chart) {
- if (chart == null) {
- throw new IllegalArgumentException("Chart cannot be null!");
- }
- charts.add(chart);
+ metricsBase.addCustomChart(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 no effect but your plugin WILL be blocked/deleted!
- // WARNING: Just don't do it!
+ private void appendPlatformData(JsonObjectBuilder builder) {
+ builder.appendField("playerAmount", getPlayerAmount());
+ builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0);
+ builder.appendField("bukkitVersion", Bukkit.getVersion());
+ builder.appendField("bukkitName", Bukkit.getName());
+ builder.appendField("javaVersion", System.getProperty("java.version"));
+ builder.appendField("osName", System.getProperty("os.name"));
+ builder.appendField("osArch", System.getProperty("os.arch"));
+ builder.appendField("osVersion", System.getProperty("os.version"));
+ builder.appendField("coreCount", Runtime.getRuntime().availableProcessors());
}
- /**
- * 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.addProperty("pluginName", pluginName); // Append the name of the plugin
- data.addProperty("id", pluginId); // Append the id of the plugin
- data.addProperty("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.add("customCharts", customCharts);
-
- return data;
+ private void appendServiceData(JsonObjectBuilder builder) {
+ builder.appendField("pluginVersion", plugin.getDescription().getVersion());
}
- /**
- * Gets the server specific data.
- *
- * @return The server specific data.
- */
- private JsonObject getServerData() {
- // Minecraft specific data
- int playerAmount;
+ private int getPlayerAmount() {
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;
+ // Around MC 1.8 the return type was changed from an array to a collection,
+ // 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)
+ return 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
+ // Just use the new method if the reflection failed
+ return Bukkit.getOnlinePlayers().size();
}
- int onlineMode = Bukkit.getOnlineMode() ? 1 : 0;
- String bukkitVersion = Bukkit.getVersion();
- String bukkitName = Bukkit.getName();
-
- // 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.addProperty("serverUUID", serverUUID);
-
- data.addProperty("playerAmount", playerAmount);
- data.addProperty("onlineMode", onlineMode);
- data.addProperty("bukkitVersion", bukkitVersion);
- data.addProperty("bukkitName", bukkitName);
-
- data.addProperty("javaVersion", javaVersion);
- data.addProperty("osName", osName);
- data.addProperty("osArch", osArch);
- data.addProperty("osVersion", osVersion);
- data.addProperty("coreCount", coreCount);
-
- return data;
}
- /**
- * Collects the data and sends it afterwards.
- */
- private void submitData() {
- final JsonObject data = getServerData();
+ public static class MetricsBase {
- 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 :)
+ /** The version of the Metrics class. */
+ public static final String METRICS_VERSION = "3.0.0";
- for (RegisteredServiceProvider> provider : Bukkit.getServicesManager().getRegistrations(service)) {
- try {
- Object plugin = provider.getService().getMethod("getPluginData").invoke(provider.getProvider());
- if (plugin instanceof JsonObject) {
- pluginData.add((JsonObject) plugin);
- } else { // old bstats version compatibility
- try {
- Class> jsonObjectJsonSimple = Class.forName("org.json.simple.JSONObject");
- if (plugin.getClass().isAssignableFrom(jsonObjectJsonSimple)) {
- Method jsonStringGetter = jsonObjectJsonSimple.getDeclaredMethod("toJSONString");
- jsonStringGetter.setAccessible(true);
- String jsonString = (String) jsonStringGetter.invoke(plugin);
- JsonObject object = new JsonParser().parse(jsonString).getAsJsonObject();
- pluginData.add(object);
- }
- } catch (ClassNotFoundException e) {
- // minecraft version 1.14+
- if (logFailedRequests) {
- this.plugin.getLogger().log(Level.SEVERE, "Encountered unexpected exception", e);
- }
+ private static final ScheduledExecutorService scheduler =
+ Executors.newScheduledThreadPool(1, task -> new Thread(task, "bStats-Metrics"));
+
+ private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s";
+
+ private final String platform;
+
+ private final String serverUuid;
+
+ private final int serviceId;
+
+ private final Consumer appendPlatformDataConsumer;
+
+ private final Consumer appendServiceDataConsumer;
+
+ private final Consumer submitTaskConsumer;
+
+ private final Supplier checkServiceEnabledSupplier;
+
+ private final BiConsumer errorLogger;
+
+ private final Consumer infoLogger;
+
+ private final boolean logErrors;
+
+ private final boolean logSentData;
+
+ private final boolean logResponseStatusText;
+
+ private final Set customCharts = new HashSet<>();
+
+ private final boolean enabled;
+
+ /**
+ * Creates a new MetricsBase class instance.
+ *
+ * @param platform The platform of the service.
+ * @param serviceId The id of the service.
+ * @param serverUuid The server uuid.
+ * @param enabled Whether or not data sending is enabled.
+ * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and
+ * appends all platform-specific data.
+ * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and
+ * appends all service-specific data.
+ * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be
+ * used to delegate the data collection to a another thread to prevent errors caused by
+ * concurrency. Can be {@code null}.
+ * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled.
+ * @param errorLogger A consumer that accepts log message and an error.
+ * @param infoLogger A consumer that accepts info log messages.
+ * @param logErrors Whether or not errors should be logged.
+ * @param logSentData Whether or not the sent data should be logged.
+ * @param logResponseStatusText Whether or not the response status text should be logged.
+ */
+ public MetricsBase(
+ String platform,
+ String serverUuid,
+ int serviceId,
+ boolean enabled,
+ Consumer appendPlatformDataConsumer,
+ Consumer appendServiceDataConsumer,
+ Consumer submitTaskConsumer,
+ Supplier checkServiceEnabledSupplier,
+ BiConsumer errorLogger,
+ Consumer infoLogger,
+ boolean logErrors,
+ boolean logSentData,
+ boolean logResponseStatusText) {
+ this.platform = platform;
+ this.serverUuid = serverUuid;
+ this.serviceId = serviceId;
+ this.enabled = enabled;
+ this.appendPlatformDataConsumer = appendPlatformDataConsumer;
+ this.appendServiceDataConsumer = appendServiceDataConsumer;
+ this.submitTaskConsumer = submitTaskConsumer;
+ this.checkServiceEnabledSupplier = checkServiceEnabledSupplier;
+ this.errorLogger = errorLogger;
+ this.infoLogger = infoLogger;
+ this.logErrors = logErrors;
+ this.logSentData = logSentData;
+ this.logResponseStatusText = logResponseStatusText;
+ checkRelocation();
+ if (enabled) {
+ // WARNING: Removing the option to opt-out will get your plugin banned from bStats
+ startSubmitting();
+ }
+ }
+
+ public void addCustomChart(CustomChart chart) {
+ this.customCharts.add(chart);
+ }
+
+ private void startSubmitting() {
+ final Runnable submitTask =
+ () -> {
+ if (!enabled || !checkServiceEnabledSupplier.get()) {
+ // Submitting data or service is disabled
+ scheduler.shutdown();
+ return;
+ }
+ if (submitTaskConsumer != null) {
+ submitTaskConsumer.accept(this::submitData);
+ } else {
+ this.submitData();
+ }
+ };
+ // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution
+ // of requests on the
+ // bStats backend. To circumvent this problem, we introduce some randomness into the initial
+ // and second delay.
+ // WARNING: You must not modify and part of this Metrics class, including the submit delay or
+ // frequency!
+ // WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it!
+ long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3));
+ long secondDelay = (long) (1000 * 60 * (Math.random() * 30));
+ scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS);
+ scheduler.scheduleAtFixedRate(
+ submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS);
+ }
+
+ private void submitData() {
+ final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder();
+ appendPlatformDataConsumer.accept(baseJsonBuilder);
+ final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder();
+ appendServiceDataConsumer.accept(serviceJsonBuilder);
+ JsonObjectBuilder.JsonObject[] chartData =
+ customCharts.stream()
+ .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors))
+ .filter(Objects::nonNull)
+ .toArray(JsonObjectBuilder.JsonObject[]::new);
+ serviceJsonBuilder.appendField("id", serviceId);
+ serviceJsonBuilder.appendField("customCharts", chartData);
+ baseJsonBuilder.appendField("service", serviceJsonBuilder.build());
+ baseJsonBuilder.appendField("serverUUID", serverUuid);
+ baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION);
+ JsonObjectBuilder.JsonObject data = baseJsonBuilder.build();
+ scheduler.execute(
+ () -> {
+ try {
+ // Send the data
+ sendData(data);
+ } catch (Exception e) {
+ // Something went wrong! :(
+ if (logErrors) {
+ errorLogger.accept("Could not submit bStats metrics data", e);
}
}
- } catch (NullPointerException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) { }
- }
- } catch (NoSuchFieldException ignored) { }
+ });
}
- data.add("plugins", pluginData);
-
- // Create a new thread for the connection to the bStats server
- new Thread(() -> {
- 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);
+ private void sendData(JsonObjectBuilder.JsonObject data) throws Exception {
+ if (logSentData) {
+ infoLogger.accept("Sent bStats metrics data: " + data.toString());
+ }
+ String url = String.format(REPORT_URL, platform);
+ HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
+ // Compress the data to save bandwidth
+ byte[] compressedData = compress(data.toString());
+ connection.setRequestMethod("POST");
+ connection.addRequestProperty("Accept", "application/json");
+ connection.addRequestProperty("Connection", "close");
+ connection.addRequestProperty("Content-Encoding", "gzip");
+ connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length));
+ connection.setRequestProperty("Content-Type", "application/json");
+ connection.setRequestProperty("User-Agent", "Metrics-Service/1");
+ connection.setDoOutput(true);
+ try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
+ outputStream.write(compressedData);
+ }
+ StringBuilder builder = new StringBuilder();
+ try (BufferedReader bufferedReader =
+ new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
+ String line;
+ while ((line = bufferedReader.readLine()) != null) {
+ builder.append(line);
}
}
- }).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);
- }
- 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);
- try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
- outputStream.write(compressedData);
- }
-
- StringBuilder builder = new StringBuilder();
- try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
- String line;
- while ((line = bufferedReader.readLine()) != null) {
- builder.append(line);
+ if (logResponseStatusText) {
+ infoLogger.accept("Sent data to bStats and received response: " + builder);
}
}
- if (logResponseStatusText) {
- plugin.getLogger().info("Sent data to bStats and received response: " + builder);
+ /** Checks that the class was properly relocated. */
+ private void checkRelocation() {
+ // 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'});
+ final String examplePackage =
+ new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'});
+ // We want to make sure no one just copy & pastes the example and uses the wrong package
+ // names
+ if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage)
+ || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) {
+ throw new IllegalStateException("bStats Metrics class has not been relocated correctly!");
+ }
+ }
}
- }
-
- /**
- * 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();
- try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) {
- gzip.write(str.getBytes(StandardCharsets.UTF_8));
- }
- return outputStream.toByteArray();
- }
-
- /**
- * Represents a custom chart.
- */
- public static abstract class CustomChart {
-
- // The id of the chart
- final String chartId;
/**
- * Class constructor.
+ * Gzips the given string.
*
- * @param chartId The id of the chart.
+ * @param str The string to gzip.
+ * @return The gzipped string.
*/
- 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.addProperty("chartId", chartId);
- try {
- JsonObject data = getChartData();
- if (data == null) {
- // If the data is null we don't send the chart.
- return null;
- }
- chart.add("data", data);
- } catch (Throwable t) {
- if (logFailedRequests) {
- Bukkit.getLogger().log(Level.WARNING, "Failed to get data for custom chart with id " + chartId, t);
- }
+ private static byte[] compress(final String str) throws IOException {
+ if (str == null) {
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;
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) {
+ gzip.write(str.getBytes(StandardCharsets.UTF_8));
}
- data.addProperty("value", value);
- return data;
+ return outputStream.toByteArray();
}
}
- /**
- * Represents a custom advanced pie.
- */
- public static class AdvancedPie extends CustomChart {
-
- private final Callable