Laloth

LalothCMD METRICS 7/27/12 1:00 AM

Jul 27th, 2012
32
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. package me.Laloth.Main;
  2.  
  3. import org.bukkit.Bukkit;
  4. import org.bukkit.configuration.file.YamlConfiguration;
  5. import org.bukkit.configuration.InvalidConfigurationException;
  6. import org.bukkit.plugin.Plugin;
  7. import org.bukkit.plugin.PluginDescriptionFile;
  8.  
  9. import java.io.BufferedReader;
  10. import java.io.File;
  11. import java.io.IOException;
  12. import java.io.InputStreamReader;
  13. import java.io.OutputStreamWriter;
  14. import java.io.UnsupportedEncodingException;
  15. import java.net.Proxy;
  16. import java.net.URL;
  17. import java.net.URLConnection;
  18. import java.net.URLEncoder;
  19. import java.util.Collections;
  20. import java.util.HashSet;
  21. import java.util.Iterator;
  22. import java.util.LinkedHashSet;
  23. import java.util.Set;
  24. import java.util.UUID;
  25. import java.util.logging.Level;
  26.  
  27. /**
  28.  * <p>
  29.  * The metrics class obtains data about a plugin and submits statistics about it to the metrics backend.
  30.  * </p>
  31.  * <p>
  32.  * Public methods provided by this class:
  33.  * </p>
  34.  * <code>
  35.  * Graph createGraph(String name); <br/>
  36.  * void addCustomData(Metrics.Plotter plotter); <br/>
  37.  * void start(); <br/>
  38.  * </code>
  39.  */
  40. public class Metrics {
  41.  
  42.     /**
  43.      * The current revision number
  44.      */
  45.     private final static int REVISION = 5;
  46.  
  47.     /**
  48.      * The base url of the metrics domain
  49.      */
  50.     private static final String BASE_URL = "http://mcstats.org";
  51.  
  52.     /**
  53.      * The url used to report a server's status
  54.      */
  55.     private static final String REPORT_URL = "/report/%s";
  56.  
  57.     /**
  58.      * The file where guid and opt out is stored in
  59.      */
  60.     private static final String CONFIG_FILE = "plugins/PluginMetrics/config.yml";
  61.  
  62.     /**
  63.      * The separator to use for custom data. This MUST NOT change unless you are hosting your own
  64.      * version of metrics and want to change it.
  65.      */
  66.     private static final String CUSTOM_DATA_SEPARATOR = "~~";
  67.  
  68.     /**
  69.      * Interval of time to ping (in minutes)
  70.      */
  71.     private static final int PING_INTERVAL = 10;
  72.  
  73.     /**
  74.      * The plugin this metrics submits for
  75.      */
  76.     private final Plugin plugin;
  77.  
  78.     /**
  79.      * All of the custom graphs to submit to metrics
  80.      */
  81.     private final Set<Graph> graphs = Collections.synchronizedSet(new HashSet<Graph>());
  82.  
  83.     /**
  84.      * The default graph, used for addCustomData when you don't want a specific graph
  85.      */
  86.     private final Graph defaultGraph = new Graph("Default");
  87.  
  88.     /**
  89.      * The plugin configuration file
  90.      */
  91.     private final YamlConfiguration configuration;
  92.      
  93.     /**
  94.      * The plugin configuration file
  95.      */
  96.     private final File configurationFile;
  97.  
  98.     /**
  99.      * Unique server id
  100.      */
  101.     private final String guid;
  102.  
  103.     /**
  104.      * Lock for synchronization
  105.      */
  106.     private final Object optOutLock = new Object();
  107.  
  108.     /**
  109.      * Id of the scheduled task
  110.      */
  111.     private volatile int taskId = -1;
  112.  
  113.     public Metrics(final Plugin plugin) throws IOException {
  114.         if (plugin == null) {
  115.             throw new IllegalArgumentException("Plugin cannot be null");
  116.         }
  117.  
  118.         this.plugin = plugin;
  119.  
  120.         // load the config
  121.         configurationFile = new File(CONFIG_FILE);
  122.         configuration = YamlConfiguration.loadConfiguration(configurationFile);
  123.  
  124.         // add some defaults
  125.         configuration.addDefault("opt-out", false);
  126.         configuration.addDefault("guid", UUID.randomUUID().toString());
  127.  
  128.         // Do we need to create the file?
  129.         if (configuration.get("guid", null) == null) {
  130.             configuration.options().header("http://mcstats.org").copyDefaults(true);
  131.             configuration.save(configurationFile);
  132.         }
  133.  
  134.         // Load the guid then
  135.         guid = configuration.getString("guid");
  136.     }
  137.  
  138.     /**
  139.      * Construct and create a Graph that can be used to separate specific plotters to their own graphs
  140.      * on the metrics website. Plotters can be added to the graph object returned.
  141.      *
  142.      * @param name
  143.      * @return Graph object created. Will never return NULL under normal circumstances unless bad parameters are given
  144.      */
  145.     public Graph createGraph(final String name) {
  146.         if (name == null) {
  147.             throw new IllegalArgumentException("Graph name cannot be null");
  148.         }
  149.  
  150.         // Construct the graph object
  151.         final Graph graph = new Graph(name);
  152.  
  153.         // Now we can add our graph
  154.         graphs.add(graph);
  155.  
  156.         // and return back
  157.         return graph;
  158.     }
  159.  
  160.     /**
  161.      * Adds a custom data plotter to the default graph
  162.      *
  163.      * @param plotter
  164.      */
  165.     public void addCustomData(final Plotter plotter) {
  166.         if (plotter == null) {
  167.             throw new IllegalArgumentException("Plotter cannot be null");
  168.         }
  169.  
  170.         // Add the plotter to the graph o/
  171.         defaultGraph.addPlotter(plotter);
  172.  
  173.         // Ensure the default graph is included in the submitted graphs
  174.         graphs.add(defaultGraph);
  175.     }
  176.  
  177.     /**
  178.      * Start measuring statistics. This will immediately create an async repeating task as the plugin and send
  179.      * the initial data to the metrics backend, and then after that it will post in increments of
  180.      * PING_INTERVAL * 1200 ticks.
  181.      *
  182.      * @return True if statistics measuring is running, otherwise false.
  183.      */
  184.     public boolean start() {
  185.         synchronized (optOutLock) {
  186.             // Did we opt out?
  187.             if (isOptOut()) {
  188.                 return false;
  189.             }
  190.  
  191.             // Is metrics already running?
  192.             if (taskId >= 0) {
  193.                 return true;
  194.             }
  195.  
  196.             // Begin hitting the server with glorious data
  197.             taskId = plugin.getServer().getScheduler().scheduleAsyncRepeatingTask(plugin, new Runnable() {
  198.  
  199.                 private boolean firstPost = true;
  200.  
  201.                 public void run() {
  202.                     try {
  203.                         // This has to be synchronized or it can collide with the disable method.
  204.                         synchronized (optOutLock) {
  205.                             // Disable Task, if it is running and the server owner decided to opt-out
  206.                             if (isOptOut() && taskId > 0) {
  207.                                 plugin.getServer().getScheduler().cancelTask(taskId);
  208.                                 taskId = -1;
  209.                             }
  210.                         }
  211.  
  212.                         // We use the inverse of firstPost because if it is the first time we are posting,
  213.                         // it is not a interval ping, so it evaluates to FALSE
  214.                         // Each time thereafter it will evaluate to TRUE, i.e PING!
  215.                         postPlugin(!firstPost);
  216.  
  217.                         // After the first post we set firstPost to false
  218.                         // Each post thereafter will be a ping
  219.                         firstPost = false;
  220.                     } catch (IOException e) {
  221.                         Bukkit.getLogger().log(Level.INFO, "[Metrics] " + e.getMessage());
  222.                     }
  223.                 }
  224.             }, 0, PING_INTERVAL * 1200);
  225.  
  226.             return true;
  227.         }
  228.     }
  229.  
  230.     /**
  231.      * Has the server owner denied plugin metrics?
  232.      *
  233.      * @return
  234.      */
  235.     public boolean isOptOut() {
  236.         synchronized(optOutLock) {
  237.             try {
  238.                 // Reload the metrics file
  239.                 configuration.load(CONFIG_FILE);
  240.             } catch (IOException ex) {
  241.                 Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage());
  242.                 return true;
  243.             } catch (InvalidConfigurationException ex) {
  244.                 Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage());
  245.                 return true;
  246.             }
  247.             return configuration.getBoolean("opt-out", false);
  248.         }
  249.     }
  250.  
  251.     /**
  252.     * Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task.
  253.     *
  254.     * @throws IOException
  255.     */
  256.     public void enable() throws IOException {
  257.         // This has to be synchronized or it can collide with the check in the task.
  258.         synchronized (optOutLock) {
  259.             // Check if the server owner has already set opt-out, if not, set it.
  260.             if (isOptOut()) {
  261.                 configuration.set("opt-out", false);
  262.                 configuration.save(configurationFile);
  263.             }
  264.  
  265.             // Enable Task, if it is not running
  266.             if (taskId < 0) {
  267.                 start();
  268.             }
  269.         }
  270.     }
  271.  
  272.     /**
  273.      * Disables metrics for the server by setting "opt-out" to true in the config file and canceling the metrics task.
  274.      *
  275.      * @throws IOException
  276.      */
  277.     public void disable() throws IOException {
  278.         // This has to be synchronized or it can collide with the check in the task.
  279.         synchronized (optOutLock) {
  280.             // Check if the server owner has already set opt-out, if not, set it.
  281.             if (!isOptOut()) {
  282.                 configuration.set("opt-out", true);
  283.                 configuration.save(configurationFile);
  284.             }
  285.  
  286.             // Disable Task, if it is running
  287.             if (taskId > 0) {
  288.                 this.plugin.getServer().getScheduler().cancelTask(taskId);
  289.                 taskId = -1;
  290.             }
  291.         }
  292.     }
  293.  
  294.     /**
  295.      * Generic method that posts a plugin to the metrics website
  296.      */
  297.     private void postPlugin(final boolean isPing) throws IOException {
  298.         // The plugin's description file containg all of the plugin data such as name, version, author, etc
  299.         final PluginDescriptionFile description = plugin.getDescription();
  300.  
  301.         // Construct the post data
  302.         final StringBuilder data = new StringBuilder();
  303.         data.append(encode("guid")).append('=').append(encode(guid));
  304.         encodeDataPair(data, "version", description.getVersion());
  305.         encodeDataPair(data, "server", Bukkit.getVersion());
  306.         encodeDataPair(data, "players", Integer.toString(Bukkit.getServer().getOnlinePlayers().length));
  307.         encodeDataPair(data, "revision", String.valueOf(REVISION));
  308.  
  309.         // If we're pinging, append it
  310.         if (isPing) {
  311.             encodeDataPair(data, "ping", "true");
  312.         }
  313.  
  314.         // Acquire a lock on the graphs, which lets us make the assumption we also lock everything
  315.         // inside of the graph (e.g plotters)
  316.         synchronized (graphs) {
  317.             final Iterator<Graph> iter = graphs.iterator();
  318.  
  319.             while (iter.hasNext()) {
  320.                 final Graph graph = iter.next();
  321.  
  322.                 for (Plotter plotter : graph.getPlotters()) {
  323.                     // The key name to send to the metrics server
  324.                     // The format is C-GRAPHNAME-PLOTTERNAME where separator - is defined at the top
  325.                     // Legacy (R4) submitters use the format Custom%s, or CustomPLOTTERNAME
  326.                     final String key = String.format("C%s%s%s%s", CUSTOM_DATA_SEPARATOR, graph.getName(), CUSTOM_DATA_SEPARATOR, plotter.getColumnName());
  327.  
  328.                     // The value to send, which for the foreseeable future is just the string
  329.                     // value of plotter.getValue()
  330.                     final String value = Integer.toString(plotter.getValue());
  331.  
  332.                     // Add it to the http post data :)
  333.                     encodeDataPair(data, key, value);
  334.                 }
  335.             }
  336.         }
  337.  
  338.         // Create the url
  339.         URL url = new URL(BASE_URL + String.format(REPORT_URL, encode(plugin.getDescription().getName())));
  340.  
  341.         // Connect to the website
  342.         URLConnection connection;
  343.  
  344.         // Mineshafter creates a socks proxy, so we can safely bypass it
  345.         // It does not reroute POST requests so we need to go around it
  346.         if (isMineshafterPresent()) {
  347.             connection = url.openConnection(Proxy.NO_PROXY);
  348.         } else {
  349.             connection = url.openConnection();
  350.         }
  351.  
  352.         connection.setDoOutput(true);
  353.  
  354.         // Write the data
  355.         final OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
  356.         writer.write(data.toString());
  357.         writer.flush();
  358.  
  359.         // Now read the response
  360.         final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
  361.         final String response = reader.readLine();
  362.  
  363.         // close resources
  364.         writer.close();
  365.         reader.close();
  366.  
  367.         if (response == null || response.startsWith("ERR")) {
  368.             throw new IOException(response); //Throw the exception
  369.         } else {
  370.             // Is this the first update this hour?
  371.             if (response.contains("OK This is your first update this hour")) {
  372.                 synchronized (graphs) {
  373.                     final Iterator<Graph> iter = graphs.iterator();
  374.  
  375.                     while (iter.hasNext()) {
  376.                         final Graph graph = iter.next();
  377.  
  378.                         for (Plotter plotter : graph.getPlotters()) {
  379.                             plotter.reset();
  380.                         }
  381.                     }
  382.                 }
  383.             }
  384.         }
  385.     }
  386.  
  387.     /**
  388.      * Check if mineshafter is present. If it is, we need to bypass it to send POST requests
  389.      *
  390.      * @return
  391.      */
  392.     private boolean isMineshafterPresent() {
  393.         try {
  394.             Class.forName("mineshafter.MineServer");
  395.             return true;
  396.         } catch (Exception e) {
  397.             return false;
  398.         }
  399.     }
  400.  
  401.     /**
  402.      * <p>Encode a key/value data pair to be used in a HTTP post request. This INCLUDES a & so the first
  403.      * key/value pair MUST be included manually, e.g:</p>
  404.      * <code>
  405.      * StringBuffer data = new StringBuffer();
  406.      * data.append(encode("guid")).append('=').append(encode(guid));
  407.      * encodeDataPair(data, "version", description.getVersion());
  408.      * </code>
  409.      *
  410.      * @param buffer
  411.      * @param key
  412.      * @param value
  413.      * @return
  414.      */
  415.     private static void encodeDataPair(final StringBuilder buffer, final String key, final String value) throws UnsupportedEncodingException {
  416.         buffer.append('&').append(encode(key)).append('=').append(encode(value));
  417.     }
  418.  
  419.     /**
  420.      * Encode text as UTF-8
  421.      *
  422.      * @param text
  423.      * @return
  424.      */
  425.     private static String encode(final String text) throws UnsupportedEncodingException {
  426.         return URLEncoder.encode(text, "UTF-8");
  427.     }
  428.  
  429.     /**
  430.      * Represents a custom graph on the website
  431.      */
  432.     public static class Graph {
  433.  
  434.         /**
  435.          * The graph's name, alphanumeric and spaces only :)
  436.          * If it does not comply to the above when submitted, it is rejected
  437.          */
  438.         private final String name;
  439.  
  440.         /**
  441.          * The set of plotters that are contained within this graph
  442.          */
  443.         private final Set<Plotter> plotters = new LinkedHashSet<Plotter>();
  444.  
  445.         private Graph(final String name) {
  446.             this.name = name;
  447.         }
  448.  
  449.         /**
  450.          * Gets the graph's name
  451.          *
  452.          * @return
  453.          */
  454.         public String getName() {
  455.             return name;
  456.         }
  457.  
  458.         /**
  459.          * Add a plotter to the graph, which will be used to plot entries
  460.          *
  461.          * @param plotter
  462.          */
  463.         public void addPlotter(final Plotter plotter) {
  464.             plotters.add(plotter);
  465.         }
  466.  
  467.         /**
  468.          * Remove a plotter from the graph
  469.          *
  470.          * @param plotter
  471.          */
  472.         public void removePlotter(final Plotter plotter) {
  473.             plotters.remove(plotter);
  474.         }
  475.  
  476.         /**
  477.          * Gets an <b>unmodifiable</b> set of the plotter objects in the graph
  478.          *
  479.          * @return
  480.          */
  481.         public Set<Plotter> getPlotters() {
  482.             return Collections.unmodifiableSet(plotters);
  483.         }
  484.  
  485.         @Override
  486.         public int hashCode() {
  487.             return name.hashCode();
  488.         }
  489.  
  490.         @Override
  491.         public boolean equals(final Object object) {
  492.             if (!(object instanceof Graph)) {
  493.                 return false;
  494.             }
  495.  
  496.             final Graph graph = (Graph) object;
  497.             return graph.name.equals(name);
  498.         }
  499.  
  500.     }
  501.  
  502.     /**
  503.      * Interface used to collect custom data for a plugin
  504.      */
  505.     public static abstract class Plotter {
  506.  
  507.         /**
  508.          * The plot's name
  509.          */
  510.         private final String name;
  511.  
  512.         /**
  513.          * Construct a plotter with the default plot name
  514.          */
  515.         public Plotter() {
  516.             this("Default");
  517.         }
  518.  
  519.         /**
  520.          * Construct a plotter with a specific plot name
  521.          *
  522.          * @param name
  523.          */
  524.         public Plotter(final String name) {
  525.             this.name = name;
  526.         }
  527.  
  528.         /**
  529.          * Get the current value for the plotted point
  530.          *
  531.          * @return
  532.          */
  533.         public abstract int getValue();
  534.  
  535.         /**
  536.          * Get the column name for the plotted point
  537.          *
  538.          * @return the plotted point's column name
  539.          */
  540.         public String getColumnName() {
  541.             return name;
  542.         }
  543.  
  544.         /**
  545.          * Called after the website graphs have been updated
  546.          */
  547.         public void reset() {
  548.         }
  549.  
  550.         @Override
  551.         public int hashCode() {
  552.             return getColumnName().hashCode() + getValue();
  553.         }
  554.  
  555.         @Override
  556.         public boolean equals(final Object object) {
  557.             if (!(object instanceof Plotter)) {
  558.                 return false;
  559.             }
  560.  
  561.             final Plotter plotter = (Plotter) object;
  562.             return plotter.name.equals(name) && plotter.getValue() == getValue();
  563.         }
  564.  
  565.     }
  566.  
  567. }
RAW Paste Data