All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.shynixn.petblocks.sponge.logic.business.dependency.Metrics2 Maven / Gradle / Ivy

Go to download

PetBlocks is a spigot and also a sponge plugin to use blocks and custom heads as pets in Minecraft.

There is a newer version: 8.29.0
Show newest version
package com.github.shynixn.petblocks.sponge.logic.business.dependency;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.inject.Inject;
import ninja.leaping.configurate.commented.CommentedConfigurationNode;
import ninja.leaping.configurate.hocon.HoconConfigurationLoader;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.spongepowered.api.Platform;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.config.ConfigDir;
import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.game.state.GamePreInitializationEvent;
import org.spongepowered.api.plugin.PluginContainer;
import org.spongepowered.api.scheduler.Scheduler;
import org.spongepowered.api.scheduler.Task;

import javax.net.ssl.HttpsURLConnection;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.zip.GZIPOutputStream;

public class Metrics2 implements Metrics {
    /**
     * Internal class for storing information about old bStats instances.
     */
    private static class OutdatedInstance implements Metrics {
        private Object instance;
        private Method method;
        private PluginContainer plugin;

        private OutdatedInstance(Object instance, Method method, PluginContainer plugin) {
            this.instance = instance;
            this.method = method;
            this.plugin = plugin;
        }

        @Override
        public void cancel() {
            // Do nothing, handled once elsewhere
        }

        @Override
        public List getKnownMetricsInstances() {
            return new ArrayList<>();
        }

        @Override
        public JsonObject getPluginData() {
            try {
                return (JsonObject) method.invoke(instance);
            } catch (ClassCastException | IllegalAccessException | InvocationTargetException ignored) { }
            return null;
        }

        @Override
        public PluginContainer getPluginContainer() {
            return plugin;
        }

        @Override
        public int getRevision() {
            return 0;
        }

        @Override
        public void linkMetrics(Metrics metrics) {
            // Do nothing
        }
    }

    /**
     * A factory to create new Metrics classes.
     */
    public static class Factory {

        private final PluginContainer plugin;
        private final Logger logger;
        private final Path configDir;

        // The constructor is not meant to be called by the user.
        // The instance is created using Dependency Injection (https://docs.spongepowered.org/master/en/plugin/injection.html)
        @Inject
        private Factory(PluginContainer plugin, Logger logger, @ConfigDir(sharedRoot = true) Path configDir) {
            this.plugin = plugin;
            this.logger = logger;
            this.configDir = configDir;
        }

        /**
         * Creates a new Metrics2 class.
         *
         * @param pluginId The id of the plugin.
         *                 It can be found at What is my plugin id?
         *                 

Not to be confused with Sponge's {@link PluginContainer#getId()} method! * @return A Metrics2 instance that can be used to register custom charts. *

The return value can be ignored, when you do not want to register custom charts. */ public Metrics2 make(int pluginId) { return new Metrics2(plugin, logger, configDir, pluginId); } } // The version of bStats info being sent public static final int B_STATS_VERSION = 1; // The version of this bStats class public static final int B_STATS_CLASS_REVISION = 2; // The url to which the data is sent private static final String URL = "https://bStats.org/submitData/sponge"; // The logger private Logger logger; // The plugin private final PluginContainer plugin; // The plugin id private final int pluginId; // The uuid of the server private String serverUUID; // Should failed requests be logged? private boolean logFailedRequests = false; // Should the sent data be logged? private static boolean logSentData; // Should the response text be logged? private static boolean logResponseStatusText; // A list with all known metrics class objects including this one private final List knownMetricsInstances = new CopyOnWriteArrayList<>(); // A list with all custom charts private final List charts = new ArrayList<>(); // The config path private Path configDir; // The list of instances from the bStats 1 instance's that started first private List oldInstances = new ArrayList<>(); // The timer task private TimerTask timerTask; // The constructor is not meant to be called by the user, but by using the Factory private Metrics2(PluginContainer plugin, Logger logger, Path configDir, int pluginId) { this.plugin = plugin; this.logger = logger; this.configDir = configDir; this.pluginId = pluginId; Sponge.getEventManager().registerListeners(plugin, this); } @Listener public void startup(GamePreInitializationEvent event) { try { loadConfig(); } catch (IOException e) { // Failed to load configuration logger.warn("Failed to load bStats config!", e); return; } if (Sponge.getServiceManager().isRegistered(Metrics.class)) { Metrics provider = Sponge.getServiceManager().provideUnchecked(Metrics.class); provider.linkMetrics(this); } else { Sponge.getServiceManager().setProvider(plugin.getInstance().get(), Metrics.class, this); this.linkMetrics(this); startSubmitting(); } } @Override public void cancel() { if (timerTask != null) { timerTask.cancel(); } } @Override public List getKnownMetricsInstances() { return knownMetricsInstances; } @Override public PluginContainer getPluginContainer() { return plugin; } @Override public int getRevision() { return B_STATS_CLASS_REVISION; } /** * Links a bStats 1 metrics class with this instance. * * @param metrics An object of the metrics class to link. */ private void linkOldMetrics(Object metrics) { try { Field field = metrics.getClass().getDeclaredField("plugin"); field.setAccessible(true); PluginContainer plugin = (PluginContainer) field.get(metrics); Method method = metrics.getClass().getMethod("getPluginData"); linkMetrics(new OutdatedInstance(metrics, method, plugin)); } catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException e) { // Move on, this bStats is broken } } /** * Links an other metrics class with this class. * This method is called using Reflection. * * @param metrics An object of the metrics class to link. */ @Override public void linkMetrics(Metrics metrics) { knownMetricsInstances.add(metrics); } /** * Adds a custom chart. * * @param chart The chart to add. */ public void addCustomChart(CustomChart chart) { Validate.notNull(chart, "Chart cannot be null"); charts.add(chart); } @Override public JsonObject getPluginData() { JsonObject data = new JsonObject(); String pluginName = plugin.getName(); String pluginVersion = plugin.getVersion().orElse("unknown"); data.addProperty("pluginName", pluginName); data.addProperty("id", pluginId); data.addProperty("pluginVersion", pluginVersion); data.addProperty("metricsRevision", B_STATS_CLASS_REVISION); JsonArray customCharts = new JsonArray(); for (CustomChart customChart : charts) { // Add the data of the custom charts JsonObject chart = customChart.getRequestJsonObject(logger, logFailedRequests); if (chart == null) { // If the chart is null, we skip it continue; } customCharts.add(chart); } data.add("customCharts", customCharts); return data; } private void startSubmitting() { // bStats 1 cleanup. Runs once. try { File configPath = configDir.resolve("bStats").toFile(); configPath.mkdirs(); String className = readFile(new File(configPath, "temp.txt")); if (className != null) { try { // Let's check if a class with the given name exists. Class clazz = Class.forName(className); // Time to eat it up! Field instancesField = clazz.getDeclaredField("knownMetricsInstances"); instancesField.setAccessible(true); oldInstances = (List) instancesField.get(null); for (Object instance : oldInstances) { linkOldMetrics(instance); // Om nom nom } oldInstances.clear(); // Look at me. I'm the bStats now. // Cancel its timer task // bStats for Sponge version 1 did not expose its timer task - gotta go find it! Map threadSet = Thread.getAllStackTraces(); for (Map.Entry entry : threadSet.entrySet()) { try { if (entry.getKey().getName().startsWith("Timer")) { Field timerThreadField = entry.getKey().getClass().getDeclaredField("queue"); timerThreadField.setAccessible(true); Object taskQueue = timerThreadField.get(entry.getKey()); Field taskQueueField = taskQueue.getClass().getDeclaredField("queue"); taskQueueField.setAccessible(true); Object[] tasks = (Object[]) taskQueueField.get(taskQueue); for (Object task : tasks) { if (task == null) { continue; } if (task.getClass().getName().startsWith(clazz.getName())) { ((TimerTask) task).cancel(); } } } } catch (Exception ignored) { } } } catch (ReflectiveOperationException ignored) { } } } catch (IOException ignored) { } // We use a timer cause want to be independent from the server tps final Timer timer = new Timer(true); timerTask = new TimerTask() { @Override public void run() { // Catch any stragglers from inexplicable post-server-load plugin loading of outdated bStats for (Object instance : oldInstances) { linkOldMetrics(instance); // Om nom nom } oldInstances.clear(); // Look at me. I'm the bStats now. // The data collection (e.g. for custom graphs) is done sync // Don't be afraid! The connection to the bStats server is still async, only the stats collection is sync ;) Scheduler scheduler = Sponge.getScheduler(); Task.Builder taskBuilder = scheduler.createTaskBuilder(); taskBuilder.execute(() -> submitData()).submit(plugin); } }; timer.scheduleAtFixedRate(timerTask, 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! // Let's log if things are enabled or not, once at startup: List enabled = new ArrayList<>(); List disabled = new ArrayList<>(); for (Metrics metrics : knownMetricsInstances) { if (Sponge.getMetricsConfigManager().areMetricsEnabled(metrics.getPluginContainer())) { enabled.add(metrics.getPluginContainer().getName()); } else { disabled.add(metrics.getPluginContainer().getName()); } } StringBuilder builder = new StringBuilder().append(System.lineSeparator()); builder.append("bStats metrics is present in ").append((enabled.size() + disabled.size())).append(" plugins on this server."); builder.append(System.lineSeparator()); if (enabled.isEmpty()) { builder.append("Presently, none of them are allowed to send data.").append(System.lineSeparator()); } else { builder.append("Presently, the following ").append(enabled.size()).append(" plugins are allowed to send data:").append(System.lineSeparator()); builder.append(enabled).append(System.lineSeparator()); } if (disabled.isEmpty()) { builder.append("None of them have data sending disabled."); builder.append(System.lineSeparator()); } else { builder.append("Presently, the following ").append(disabled.size()).append(" plugins are not allowed to send data:").append(System.lineSeparator()); builder.append(disabled).append(System.lineSeparator()); } builder.append("To change the enabled/disabled state of any bStats use in a plugin, visit the Sponge config!"); logger.info(builder.toString()); } /** * Gets the server specific data. * * @return The server specific data. */ private JsonObject getServerData() { // Minecraft specific data int playerAmount = Math.min(Sponge.getServer().getOnlinePlayers().size(), 200); int onlineMode = Sponge.getServer().getOnlineMode() ? 1 : 0; String minecraftVersion = Sponge.getGame().getPlatform().getMinecraftVersion().getName(); String spongeImplementation = Sponge.getPlatform().getContainer(Platform.Component.IMPLEMENTATION).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("minecraftVersion", minecraftVersion); data.addProperty("spongeImplementation", spongeImplementation); 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(); JsonArray pluginData = new JsonArray(); // Search for all other bStats Metrics classes to get their plugin data for (Metrics metrics : knownMetricsInstances) { if (!Sponge.getMetricsConfigManager().areMetricsEnabled(metrics.getPluginContainer())) { continue; } JsonObject plugin = metrics.getPluginData(); if (plugin != null) { pluginData.add(plugin); } } if (pluginData.size() == 0) { return; // All plugins disabled, so we don't send anything } data.add("plugins", pluginData); // Create a new thread for the connection to the bStats server new Thread(() -> { try { // Send the data sendData(logger, data); } catch (Exception e) { // Something went wrong! :( if (logFailedRequests) { logger.warn("Could not submit plugin stats!", e); } } }).start(); } /** * Loads the bStats configuration. * * @throws IOException If something did not work :( */ private void loadConfig() throws IOException { File configPath = configDir.resolve("bStats").toFile(); configPath.mkdirs(); File configFile = new File(configPath, "config.conf"); HoconConfigurationLoader configurationLoader = HoconConfigurationLoader.builder().setFile(configFile).build(); CommentedConfigurationNode node; if (!configFile.exists()) { configFile.createNewFile(); node = configurationLoader.load(); // Add default values node.getNode("enabled").setValue(false); // Every server gets it's unique random id. node.getNode("serverUuid").setValue(UUID.randomUUID().toString()); // Should failed request be logged? node.getNode("logFailedRequests").setValue(false); // Should the sent data be logged? node.getNode("logSentData").setValue(false); // Should the response text be logged? node.getNode("logResponseStatusText").setValue(false); node.getNode("enabled").setComment( "Enabling bStats in this file is deprecated. At least one of your plugins now uses the\n" + "Sponge config to control bStats. Leave this value as you want it to be for outdated plugins,\n" + "but look there for further control"); // Add information about bStats node.getNode("serverUuid").setComment( "bStats collects some data for plugin authors like how many servers are using their plugins.\n" + "To control whether this is enabled or disabled, see the Sponge configuration file.\n" + "Check out https://bStats.org/ to learn more :)" ); node.getNode("configVersion").setValue(2); configurationLoader.save(node); } else { node = configurationLoader.load(); if (!node.getNode("configVersion").isVirtual()) { node.getNode("configVersion").setValue(2); node.getNode("enabled").setComment( "Enabling bStats in this file is deprecated. At least one of your plugins now uses the\n" + "Sponge config to control bStats. Leave this value as you want it to be for outdated plugins,\n" + "but look there for further control"); node.getNode("serverUuid").setComment( "bStats collects some data for plugin authors like how many servers are using their plugins.\n" + "To control whether this is enabled or disabled, see the Sponge configuration file.\n" + "Check out https://bStats.org/ to learn more :)" ); configurationLoader.save(node); } } // Load configuration serverUUID = node.getNode("serverUuid").getString(); logFailedRequests = node.getNode("logFailedRequests").getBoolean(false); logSentData = node.getNode("logSentData").getBoolean(false); logResponseStatusText = node.getNode("logResponseStatusText").getBoolean(false); } /** * Reads the first line of the file. * * @param file The file to read. Cannot be null. * @return The first line of the file or {@code null} if the file does not exist or is empty. * @throws IOException If something did not work :( */ private String readFile(File file) throws IOException { if (!file.exists()) { return null; } try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) { return bufferedReader.readLine(); } } /** * Sends the data to the bStats server. * * @param logger The used logger. * @param data The data to send. * @throws Exception If the request failed. */ private static void sendData(Logger logger, JsonObject data) throws Exception { Validate.notNull(data, "Data cannot be null"); if (logSentData) { logger.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) { logger.info("Sent data to bStats and received response: {}", builder); } } /** * 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 private 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(Logger logger, boolean logFailedRequests) { 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) { logger.warn("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.addProperty("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.addProperty(entry.getKey(), entry.getValue()); } if (allSkipped) { // Null = skip the chart return null; } data.add("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.addProperty(valueEntry.getKey(), valueEntry.getValue()); allSkipped = false; } if (!allSkipped) { reallyAllSkipped = false; values.add(entryValues.getKey(), value); } } if (reallyAllSkipped) { // Null = skip the chart return null; } data.add("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.addProperty("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.addProperty(entry.getKey(), entry.getValue()); } if (allSkipped) { // Null = skip the chart return null; } data.add("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(new JsonPrimitive(entry.getValue())); values.add(entry.getKey(), categoryValues); } data.add("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(new JsonPrimitive(categoryValue)); } values.add(entry.getKey(), categoryValues); } if (allSkipped) { // Null = skip the chart return null; } data.add("values", values); return data; } } }