diff --git a/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java index 43dfd99d..12cb5b24 100644 --- a/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java @@ -4,14 +4,12 @@ import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; -import javax.print.attribute.standard.Severity; - import org.bukkit.Server; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import com.comphenix.protocol.injector.PacketFilterManager; -import com.comphenix.protocol.metrics.MetricsLite; +import com.comphenix.protocol.metrics.Metrics; public class ProtocolLibrary extends JavaPlugin { @@ -22,7 +20,7 @@ public class ProtocolLibrary extends JavaPlugin { private Logger logger; // Metrics - private MetricsLite metrics; + private Metrics metrics; @Override public void onLoad() { @@ -30,7 +28,7 @@ public class ProtocolLibrary extends JavaPlugin { protocolManager = new PacketFilterManager(getClassLoader(), logger); try { - metrics = new MetricsLite(this); + metrics = new Metrics(this); metrics.start(); } catch (IOException e) { logger.log(Level.SEVERE, "Unable to enable metrics.", e); diff --git a/ProtocolLib/src/com/comphenix/protocol/metrics/MetricsLite.java b/ProtocolLib/src/com/comphenix/protocol/metrics/Metrics.java similarity index 55% rename from ProtocolLib/src/com/comphenix/protocol/metrics/MetricsLite.java rename to ProtocolLib/src/com/comphenix/protocol/metrics/Metrics.java index 33fdd595..86cd3ad6 100644 --- a/ProtocolLib/src/com/comphenix/protocol/metrics/MetricsLite.java +++ b/ProtocolLib/src/com/comphenix/protocol/metrics/Metrics.java @@ -29,8 +29,8 @@ package com.comphenix.protocol.metrics; */ import org.bukkit.Bukkit; -import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; @@ -44,10 +44,28 @@ import java.net.Proxy; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; import java.util.UUID; import java.util.logging.Level; -public class MetricsLite { +/** + *

+ * The metrics class obtains data about a plugin and submits statistics about it to the metrics backend. + *

+ *

+ * Public methods provided by this class: + *

+ * + * Graph createGraph(String name);
+ * void addCustomData(Metrics.Plotter plotter);
+ * void start();
+ *
+ */ +public class Metrics { /** * The current revision number @@ -64,21 +82,37 @@ public class MetricsLite { */ private static final String REPORT_URL = "/report/%s"; + /** + * The separator to use for custom data. This MUST NOT change unless you are hosting your own + * version of metrics and want to change it. + */ + private static final String CUSTOM_DATA_SEPARATOR = "~~"; + /** * Interval of time to ping (in minutes) */ - private final static int PING_INTERVAL = 10; + private static final int PING_INTERVAL = 10; /** * The plugin this metrics submits for */ private final Plugin plugin; + /** + * All of the custom graphs to submit to metrics + */ + private final Set graphs = Collections.synchronizedSet(new HashSet()); + + /** + * The default graph, used for addCustomData when you don't want a specific graph + */ + private final Graph defaultGraph = new Graph("Default"); + /** * The plugin configuration file */ private final YamlConfiguration configuration; - + /** * The plugin configuration file */ @@ -99,7 +133,7 @@ public class MetricsLite { */ private volatile int taskId = -1; - public MetricsLite(Plugin plugin) throws IOException { + public Metrics(final Plugin plugin) throws IOException { if (plugin == null) { throw new IllegalArgumentException("Plugin cannot be null"); } @@ -124,6 +158,58 @@ public class MetricsLite { guid = configuration.getString("guid"); } + /** + * Construct and create a Graph that can be used to separate specific plotters to their own graphs + * on the metrics website. Plotters can be added to the graph object returned. + * + * @param name The name of the graph + * @return Graph object created. Will never return NULL under normal circumstances unless bad parameters are given + */ + public Graph createGraph(final String name) { + if (name == null) { + throw new IllegalArgumentException("Graph name cannot be null"); + } + + // Construct the graph object + final Graph graph = new Graph(name); + + // Now we can add our graph + graphs.add(graph); + + // and return back + return graph; + } + + /** + * Add a Graph object to Metrics that represents data for the plugin that should be sent to the backend + * + * @param graph The name of the graph + */ + public void addGraph(final Graph graph) { + if (graph == null) { + throw new IllegalArgumentException("Graph cannot be null"); + } + + graphs.add(graph); + } + + /** + * Adds a custom data plotter to the default graph + * + * @param plotter The plotter to use to plot custom data + */ + public void addCustomData(final Plotter plotter) { + if (plotter == null) { + throw new IllegalArgumentException("Plotter cannot be null"); + } + + // Add the plotter to the graph o/ + defaultGraph.addPlotter(plotter); + + // Ensure the default graph is included in the submitted graphs + graphs.add(defaultGraph); + } + /** * Start measuring statistics. This will immediately create an async repeating task as the plugin and send * the initial data to the metrics backend, and then after that it will post in increments of @@ -156,6 +242,10 @@ public class MetricsLite { if (isOptOut() && taskId > 0) { plugin.getServer().getScheduler().cancelTask(taskId); taskId = -1; + // Tell all plotters to stop gathering information. + for (Graph graph : graphs){ + graph.onOptOut(); + } } } @@ -199,23 +289,23 @@ public class MetricsLite { } /** - * Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task. - * - * @throws IOException - */ + * Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task. + * + * @throws IOException + */ public void enable() throws IOException { // This has to be synchronized or it can collide with the check in the task. synchronized (optOutLock) { - // Check if the server owner has already set opt-out, if not, set it. - if (isOptOut()) { - configuration.set("opt-out", false); - configuration.save(configurationFile); - } + // Check if the server owner has already set opt-out, if not, set it. + if (isOptOut()) { + configuration.set("opt-out", false); + configuration.save(configurationFile); + } - // Enable Task, if it is not running - if (taskId < 0) { - start(); - } + // Enable Task, if it is not running + if (taskId < 0) { + start(); + } } } @@ -261,7 +351,7 @@ public class MetricsLite { /** * Generic method that posts a plugin to the metrics website */ - private void postPlugin(boolean isPing) throws IOException { + private void postPlugin(final boolean isPing) throws IOException { // The plugin's description file containg all of the plugin data such as name, version, author, etc final PluginDescriptionFile description = plugin.getDescription(); @@ -278,6 +368,30 @@ public class MetricsLite { encodeDataPair(data, "ping", "true"); } + // Acquire a lock on the graphs, which lets us make the assumption we also lock everything + // inside of the graph (e.g plotters) + synchronized (graphs) { + final Iterator iter = graphs.iterator(); + + while (iter.hasNext()) { + final Graph graph = iter.next(); + + for (Plotter plotter : graph.getPlotters()) { + // The key name to send to the metrics server + // The format is C-GRAPHNAME-PLOTTERNAME where separator - is defined at the top + // Legacy (R4) submitters use the format Custom%s, or CustomPLOTTERNAME + final String key = String.format("C%s%s%s%s", CUSTOM_DATA_SEPARATOR, graph.getName(), CUSTOM_DATA_SEPARATOR, plotter.getColumnName()); + + // The value to send, which for the foreseeable future is just the string + // value of plotter.getValue() + final String value = Integer.toString(plotter.getValue()); + + // Add it to the http post data :) + encodeDataPair(data, key, value); + } + } + } + // Create the url URL url = new URL(BASE_URL + String.format(REPORT_URL, encode(plugin.getDescription().getName()))); @@ -309,8 +423,22 @@ public class MetricsLite { if (response == null || response.startsWith("ERR")) { throw new IOException(response); //Throw the exception + } else { + // Is this the first update this hour? + if (response.contains("OK This is your first update this hour")) { + synchronized (graphs) { + final Iterator iter = graphs.iterator(); + + while (iter.hasNext()) { + final Graph graph = iter.next(); + + for (Plotter plotter : graph.getPlotters()) { + plotter.reset(); + } + } + } + } } - //if (response.startsWith("OK")) - We should get "OK" followed by an optional description if everything goes right } /** @@ -354,4 +482,150 @@ public class MetricsLite { return URLEncoder.encode(text, "UTF-8"); } -} + /** + * Represents a custom graph on the website + */ + public static class Graph { + + /** + * The graph's name, alphanumeric and spaces only :) + * If it does not comply to the above when submitted, it is rejected + */ + private final String name; + + /** + * The set of plotters that are contained within this graph + */ + private final Set plotters = new LinkedHashSet(); + + private Graph(final String name) { + this.name = name; + } + + /** + * Gets the graph's name + * + * @return the Graph's name + */ + public String getName() { + return name; + } + + /** + * Add a plotter to the graph, which will be used to plot entries + * + * @param plotter the plotter to add to the graph + */ + public void addPlotter(final Plotter plotter) { + plotters.add(plotter); + } + + /** + * Remove a plotter from the graph + * + * @param plotter the plotter to remove from the graph + */ + public void removePlotter(final Plotter plotter) { + plotters.remove(plotter); + } + + /** + * Gets an unmodifiable set of the plotter objects in the graph + * + * @return an unmodifiable {@link Set} of the plotter objects + */ + public Set getPlotters() { + return Collections.unmodifiableSet(plotters); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(final Object object) { + if (!(object instanceof Graph)) { + return false; + } + + final Graph graph = (Graph) object; + return graph.name.equals(name); + } + + /** + * Called when the server owner decides to opt-out of Metrics while the server is running. + */ + protected void onOptOut() { + } + + } + + /** + * Interface used to collect custom data for a plugin + */ + public static abstract class Plotter { + + /** + * The plot's name + */ + private final String name; + + /** + * Construct a plotter with the default plot name + */ + public Plotter() { + this("Default"); + } + + /** + * Construct a plotter with a specific plot name + * + * @param name the name of the plotter to use, which will show up on the website + */ + public Plotter(final String name) { + this.name = name; + } + + /** + * Get the current value for the plotted point. Since this function defers to an external function + * it may or may not return immediately thus cannot be guaranteed to be thread friendly or safe. + * This function can be called from any thread so care should be taken when accessing resources + * that need to be synchronized. + * + * @return the current value for the point to be plotted. + */ + public abstract int getValue(); + + /** + * Get the column name for the plotted point + * + * @return the plotted point's column name + */ + public String getColumnName() { + return name; + } + + /** + * Called after the website graphs have been updated + */ + public void reset() { + } + + @Override + public int hashCode() { + return getColumnName().hashCode(); + } + + @Override + public boolean equals(final Object object) { + if (!(object instanceof Plotter)) { + return false; + } + + final Plotter plotter = (Plotter) object; + return plotter.name.equals(name) && plotter.getValue() == getValue(); + } + + } +} \ No newline at end of file