diff --git a/src/main/java/com/gmail/nossr50/Metrics.java b/src/main/java/com/gmail/nossr50/Metrics.java index 17a3963fa..77a293028 100644 --- a/src/main/java/com/gmail/nossr50/Metrics.java +++ b/src/main/java/com/gmail/nossr50/Metrics.java @@ -25,11 +25,13 @@ * authors and contributors and should not be interpreted as representing official policies, * either expressed or implied, of anybody else. */ + package com.gmail.nossr50; import org.bukkit.Bukkit; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; import java.io.BufferedReader; import java.io.File; @@ -43,62 +45,19 @@ import java.net.URLConnection; import java.net.URLEncoder; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.UUID; -/** - * Tooling to post to metrics.griefcraft.com - */ public class Metrics { /** - * Interface used to collect custom data for a plugin + * The current revision number */ - public static abstract class Plotter { - - /** - * Get the column name for the plotted point - * - * @return the plotted point's column name - */ - public abstract String getColumnName(); - - /** - * Get the current value for the plotted point - * - * @return - */ - public abstract int getValue(); - - /** - * Called after the website graphs have been updated - */ - public void reset() { - } - - @Override - public int hashCode() { - return getColumnName().hashCode() + getValue(); - } - - @Override - public boolean equals(Object object) { - if (!(object instanceof Plotter)) { - return false; - } - - Plotter plotter = (Plotter) object; - return plotter.getColumnName().equals(getColumnName()) && plotter.getValue() == getValue(); - } - - } - - /** - * The metrics revision number - */ - private final static int REVISION = 4; + private final static int REVISION = 5; /** * The base url of the metrics domain @@ -116,14 +75,25 @@ public class Metrics { private static final String CONFIG_FILE = "plugins/PluginMetrics/config.yml"; /** - * Interval of time to ping in minutes + * 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; /** - * A map of the custom data plotters for plugins + * A map of all of the graphs for each plugin */ - private Map> customData = Collections.synchronizedMap(new HashMap>()); + private Map> graphs = Collections.synchronizedMap(new HashMap>()); + + /** + * A convenient map of the default Graph objects (used by addCustomData mainly) + */ + private Map defaultGraphs = Collections.synchronizedMap(new HashMap()); /** * The plugin configuration file @@ -154,6 +124,33 @@ public class Metrics { 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 plugin + * @param type + * @param name + * @return Graph object created. Will never return NULL under normal circumstances unless bad parameters are given + */ + public Graph createGraph(Plugin plugin, Graph.Type type, String name) { + if (plugin == null || type == null || name == null) { + throw new IllegalArgumentException("All arguments must not be null"); + } + + // Construct the graph object + Graph graph = new Graph(type, name); + + // Get the graphs for the plugin + Set graphs = getOrCreateGraphs(plugin); + + // Now we can add our graph + graphs.add(graph); + + // and return back + return graph; + } + /** * Adds a custom data plotter for a given plugin * @@ -161,14 +158,14 @@ public class Metrics { * @param plotter */ public void addCustomData(Plugin plugin, Plotter plotter) { - Set plotters = customData.get(plugin); + // The default graph for the plugin + Graph graph = getOrCreateDefaultGraph(plugin); - if (plotters == null) { - plotters = Collections.synchronizedSet(new LinkedHashSet()); - customData.put(plugin, plotters); - } + // Add the plotter to the graph o/ + graph.addPlotter(plotter); - plotters.add(plotter); + // Ensure the default graph is included in the submitted graphs + getOrCreateGraphs(plugin).add(graph); } /** @@ -176,25 +173,31 @@ public class Metrics { * * @param plugin */ - public void beginMeasuringPlugin(final Plugin plugin) throws IOException { + public void beginMeasuringPlugin(final Plugin plugin) { // Did we opt out? if (configuration.getBoolean("opt-out", false)) { return; } - // First tell the server about us - postPlugin(plugin, false); - - // Ping the server in intervals + // Begin hitting the server with glorious data plugin.getServer().getScheduler().scheduleAsyncRepeatingTask(plugin, new Runnable() { + private boolean firstPost = true; + public void run() { try { - postPlugin(plugin, true); + // We use the inverse of firstPost because if it is the first time we are posting, + // it is not a interval ping, so it evaluates to FALSE + // Each time thereafter it will evaluate to TRUE, i.e PING! + postPlugin(plugin, !firstPost); + + // After the first post we set firstPost to false + // Each post thereafter will be a ping + firstPost = false; } catch (IOException e) { System.out.println("[Metrics] " + e.getMessage()); } } - }, PING_INTERVAL * 1200, PING_INTERVAL * 1200); + }, 0, PING_INTERVAL * 1200); } /** @@ -203,26 +206,65 @@ public class Metrics { * @param plugin */ private void postPlugin(Plugin plugin, boolean isPing) throws IOException { + // The plugin's description file containg all of the plugin data such as name, version, author, etc + PluginDescriptionFile description = plugin.getDescription(); + + // The author string, created with description.getAuthors() + // Authors are separated by a comma + String authors = ""; + + // Add each author to the string + for (String author : description.getAuthors()) { + authors += author + ", "; + } + + // If there were any authors at all, we need to remove the last 2 characters + // the last 2 characters are the last comma and space + if (!authors.isEmpty()) { + authors = authors.substring(0, authors.length() - 2); + } + // Construct the post data - String response = "ERR No response"; String data = encode("guid") + '=' + encode(guid) - + '&' + encode("version") + '=' + encode(plugin.getDescription().getVersion()) - + '&' + encode("server") + '=' + encode(Bukkit.getVersion()) - + '&' + encode("players") + '=' + encode(String.valueOf(Bukkit.getServer().getOnlinePlayers().length)) - + '&' + encode("revision") + '=' + encode(REVISION + ""); + + encodeDataPair("authors", authors) + + encodeDataPair("version", description.getVersion()) + + encodeDataPair("server", Bukkit.getVersion()) + + encodeDataPair("players", Integer.toString(Bukkit.getServer().getOnlinePlayers().length)) + + encodeDataPair("revision", String.valueOf(REVISION)); // If we're pinging, append it if (isPing) { - data += '&' + encode("ping") + '=' + encode("true"); + data += encodeDataPair("ping", "true"); } - // Add any custom data (if applicable) - Set plotters = customData.get(plugin); + // Add any custom data available for the plugin + Set graphs = getOrCreateGraphs(plugin); - if (plotters != null) { - for (Plotter plotter : plotters) { - data += "&" + encode("Custom" + plotter.getColumnName()) - + "=" + encode(Integer.toString(plotter.getValue())); + // 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) { + Iterator iter = graphs.iterator(); + + while (iter.hasNext()) { + Graph graph = iter.next(); + + // Because we have a lock on the graphs set already, it is reasonable to assume + // that our lock transcends down to the individual plotters in the graphs also. + // Because our methods are private, no one but us can reasonably access this list + // without reflection so this is a safe assumption without adding more code. + 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 + 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() + String value = Integer.toString(plotter.getValue()); + + // Add it to the http post data :) + data += encodeDataPair(key, value); + } } } @@ -233,6 +275,7 @@ public class Metrics { URLConnection connection; // Mineshafter creates a socks proxy, so we can safely bypass it + // It does not reroute POST requests so we need to go around it if (isMineshafterPresent()) { connection = url.openConnection(Proxy.NO_PROXY); } else { @@ -248,7 +291,7 @@ public class Metrics { // Now read the response BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); - response = reader.readLine(); + String response = reader.readLine(); // close resources writer.close(); @@ -259,9 +302,15 @@ public class Metrics { } else { // Is this the first update this hour? if (response.contains("OK This is your first update this hour")) { - if (plotters != null) { - for (Plotter plotter : plotters) { - plotter.reset(); + synchronized (graphs) { + Iterator iter = graphs.iterator(); + + while (iter.hasNext()) { + Graph graph = iter.next(); + + for (Plotter plotter : graph.getPlotters()) { + plotter.reset(); + } } } } @@ -269,6 +318,42 @@ public class Metrics { //if (response.startsWith("OK")) - We should get "OK" followed by an optional description if everything goes right } + /** + * Get or create the Set of graphs for a specific plugin + * + * @param plugin + * @return + */ + private Set getOrCreateGraphs(Plugin plugin) { + Set theGraphs = graphs.get(plugin); + + // Create the Set if it does not already exist + if (theGraphs == null) { + theGraphs = Collections.synchronizedSet(new HashSet()); + graphs.put(plugin, theGraphs); + } + + return theGraphs; + } + + /** + * Get the default graph for a plugin and if it does not exist, create one + * + * @param plugin + * @return + */ + private Graph getOrCreateDefaultGraph(Plugin plugin) { + Graph graph = defaultGraphs.get(plugin); + + // Not yet created :( + if (graph == null) { + graph = new Graph(Graph.Type.Line, "Default"); + defaultGraphs.put(plugin, graph); + } + + return graph; + } + /** * Check if mineshafter is present. If it is, we need to bypass it to send POST requests * @@ -283,6 +368,21 @@ public class Metrics { } } + /** + * Encode a key/value data pair to be used in a HTTP post request. This INCLUDES a & so the first + * key/value pair MUST be included manually, e.g: + *

+ * String httpData = encode("guid") + "=" + encode("1234") + encodeDataPair("authors") + ".."; + *

+ * + * @param key + * @param value + * @return + */ + private static String encodeDataPair(String key, String value) throws UnsupportedEncodingException { + return "&" + encode(key) + "=" + encode(value); + } + /** * Encode text as UTF-8 * @@ -293,4 +393,177 @@ public class Metrics { return URLEncoder.encode(text, "UTF-8"); } + /** + * Represents a custom graph on the website + */ + public static class Graph { + + /** + * The graph's type that will be visible on the website + */ + public static enum Type { + + /** + * A simple line graph which also includes a scrollable timeline viewer to view + * as little or as much of the data as possible. + */ + Line, + + /** + * An area graph. This is the same as a line graph except the area under the curve is shaded + */ + Area, + + /** + * A column graph, which is a graph where the data is represented by columns on the vertical axis, + * i.e they go up and down. + */ + Column, + + /** + * A pie graph. The graph is generated by taking the data for the last hour and summing it + * together. Then the percentage for each plotter is calculated via round( (plot / total) * 100, 2 ) + */ + Pie + + } + + /** + * What the graph should be plotted as + */ + private final Type type; + + /** + * 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(Type type, String name) { + this.type = type; + this.name = name; + } + + /** + * Gets the graph's name + * + * @return + */ + public String getName() { + return name; + } + + /** + * Add a plotter to the graph, which will be used to plot entries + * + * @param plotter + */ + public void addPlotter(Plotter plotter) { + plotters.add(plotter); + } + + /** + * Remove a plotter from the graph + * + * @param plotter + */ + public void removePlotter(Plotter plotter) { + plotters.remove(plotter); + } + + /** + * Gets an unmodifiable set of the plotter objects in the graph + * @return + */ + public Set getPlotters() { + return Collections.unmodifiableSet(plotters); + } + + @Override + public int hashCode() { + return (type.hashCode() * 17) ^ name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof Graph)) { + return false; + } + + Graph graph = (Graph) object; + return graph.type == type && graph.name.equals(name); + } + + } + + /** + * 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 + */ + public Plotter(String name) { + this.name = name; + } + + /** + * Get the current value for the plotted point + * + * @return + */ + 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() + getValue(); + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof Plotter)) { + return false; + } + + Plotter plotter = (Plotter) object; + return plotter.name.equals(name) && plotter.getValue() == getValue(); + } + + } + } \ No newline at end of file