diff --git a/pom.xml b/pom.xml index e7ec6a0d0..292df523e 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,6 @@ 4.0.0 com.gmail.nossr50.mcMMO mcMMO - 1.2.11 mcMMO https://github.com/TheYeti/mcMMO @@ -122,4 +121,5 @@ UTF-8 + 1.1.12-dev diff --git a/src/main/java/com/gmail/nossr50/Metrics.java b/src/main/java/com/gmail/nossr50/Metrics.java new file mode 100644 index 000000000..17a3963fa --- /dev/null +++ b/src/main/java/com/gmail/nossr50/Metrics.java @@ -0,0 +1,296 @@ +/* + * Copyright 2011 Tyler Blair. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * 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 java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.HashMap; +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 + */ + 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; + + /** + * The base url of the metrics domain + */ + private static final String BASE_URL = "http://metrics.griefcraft.com"; + + /** + * The url used to report a server's status + */ + private static final String REPORT_URL = "/report/%s"; + + /** + * The file where guid and opt out is stored in + */ + private static final String CONFIG_FILE = "plugins/PluginMetrics/config.yml"; + + /** + * Interval of time to ping in minutes + */ + private final static int PING_INTERVAL = 10; + + /** + * A map of the custom data plotters for plugins + */ + private Map> customData = Collections.synchronizedMap(new HashMap>()); + + /** + * The plugin configuration file + */ + private final YamlConfiguration configuration; + + /** + * Unique server id + */ + private String guid; + + public Metrics() throws IOException { + // load the config + File file = new File(CONFIG_FILE); + configuration = YamlConfiguration.loadConfiguration(file); + + // add some defaults + configuration.addDefault("opt-out", false); + configuration.addDefault("guid", UUID.randomUUID().toString()); + + // Do we need to create the file? + if (configuration.get("guid", null) == null) { + configuration.options().header("http://metrics.griefcraft.com").copyDefaults(true); + configuration.save(file); + } + + // Load the guid then + guid = configuration.getString("guid"); + } + + /** + * Adds a custom data plotter for a given plugin + * + * @param plugin + * @param plotter + */ + public void addCustomData(Plugin plugin, Plotter plotter) { + Set plotters = customData.get(plugin); + + if (plotters == null) { + plotters = Collections.synchronizedSet(new LinkedHashSet()); + customData.put(plugin, plotters); + } + + plotters.add(plotter); + } + + /** + * Begin measuring a plugin + * + * @param plugin + */ + public void beginMeasuringPlugin(final Plugin plugin) throws IOException { + // 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 + plugin.getServer().getScheduler().scheduleAsyncRepeatingTask(plugin, new Runnable() { + public void run() { + try { + postPlugin(plugin, true); + } catch (IOException e) { + System.out.println("[Metrics] " + e.getMessage()); + } + } + }, PING_INTERVAL * 1200, PING_INTERVAL * 1200); + } + + /** + * Generic method that posts a plugin to the metrics website + * + * @param plugin + */ + private void postPlugin(Plugin plugin, boolean isPing) throws IOException { + // 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 + ""); + + // If we're pinging, append it + if (isPing) { + data += '&' + encode("ping") + '=' + encode("true"); + } + + // Add any custom data (if applicable) + Set plotters = customData.get(plugin); + + if (plotters != null) { + for (Plotter plotter : plotters) { + data += "&" + encode("Custom" + plotter.getColumnName()) + + "=" + encode(Integer.toString(plotter.getValue())); + } + } + + // Create the url + URL url = new URL(BASE_URL + String.format(REPORT_URL, plugin.getDescription().getName())); + + // Connect to the website + URLConnection connection; + + // Mineshafter creates a socks proxy, so we can safely bypass it + if (isMineshafterPresent()) { + connection = url.openConnection(Proxy.NO_PROXY); + } else { + connection = url.openConnection(); + } + + connection.setDoOutput(true); + + // Write the data + OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); + writer.write(data); + writer.flush(); + + // Now read the response + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + response = reader.readLine(); + + // close resources + writer.close(); + reader.close(); + + if (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")) { + if (plotters != null) { + for (Plotter plotter : plotters) { + plotter.reset(); + } + } + } + } + //if (response.startsWith("OK")) - We should get "OK" followed by an optional description if everything goes right + } + + /** + * Check if mineshafter is present. If it is, we need to bypass it to send POST requests + * + * @return + */ + private boolean isMineshafterPresent() { + try { + Class.forName("mineshafter.MineServer"); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Encode text as UTF-8 + * + * @param text + * @return + */ + private static String encode(String text) throws UnsupportedEncodingException { + return URLEncoder.encode(text, "UTF-8"); + } + +} \ No newline at end of file diff --git a/src/main/java/com/gmail/nossr50/mcMMO.java b/src/main/java/com/gmail/nossr50/mcMMO.java index 695dca2e6..4163ec304 100644 --- a/src/main/java/com/gmail/nossr50/mcMMO.java +++ b/src/main/java/com/gmail/nossr50/mcMMO.java @@ -51,6 +51,7 @@ import java.util.HashMap; import java.util.logging.Level; import java.util.logging.Logger; +import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.PluginManager; @@ -93,6 +94,7 @@ public class mcMMO extends JavaPlugin public void onEnable() { + final Plugin thisPlugin = this; mcmmo = this.getFile(); new File(maindirectory).mkdir(); @@ -167,6 +169,21 @@ public class mcMMO extends JavaPlugin FileManager FM = SpoutManager.getFileManager(); FM.addToPreLoginCache(this, SpoutStuff.getFiles()); } + + //Plugin Metrics running in a new thread + new Thread(new Runnable() { + public void run() { + try { + // create a new metrics object + Metrics metrics = new Metrics(); + + // 'this' in this context is the Plugin object + metrics.beginMeasuringPlugin(thisPlugin); + } catch (IOException e) { + // Failed to submit the stats :-( + } + } + }).start(); } public PlayerProfile getPlayerProfile(Player player)