Java tutorial
/* * The MIT License (MIT) * * Copyright (c) 2016. Diorite (by Bartomiej Mazur (aka GotoFinal)) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package org.diorite.impl.metrics; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.zip.GZIPOutputStream; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.diorite.impl.CoreMain; import org.diorite.impl.DioriteCore; import org.diorite.cfg.DioriteConfig.OnlineMode; import org.diorite.entity.EntityType; import org.diorite.utils.SpammyError; import it.unimi.dsi.fastutil.shorts.Short2IntMap; import it.unimi.dsi.fastutil.shorts.Short2IntMap.Entry; import it.unimi.dsi.fastutil.shorts.Short2IntOpenHashMap; public class Metrics { /** * Separator used by metric dount graphs. */ public static final String GRAPH_SEPARATOR = "~=~"; /** * The current revision number */ private static final int REVISION = 7; /** * The base url of the metrics domain */ private static final String BASE_URL = "http://report.mcstats.org/plugin/Diorite"; /** * Interval of time to ping (in minutes) */ private static final int PING_INTERVAL = 15; /** * All of the custom graphs to submit to metrics */ private final Set<DynamicMetricsGraph> graphs = Collections.synchronizedSet(new HashSet<>(5)); /** * Server instance */ protected final DioriteCore core; /** * The thread submission is running on */ private Thread thread = null; public static Metrics start(final DioriteCore core) { /** * Not all graphs will be visible on metrics, and some of them may be removed in future */ final Metrics m = new Metrics(core); { final MetricsGraph graph = m.createGraph("RealAuthMode"); switch (core.getOnlineMode()) { case TRUE: graph.addPlotter(new BooleanMetricsPlotter("Online")); break; case FALSE: graph.addPlotter(new BooleanMetricsPlotter("Offline")); break; case AUTO: graph.addPlotter(new BooleanMetricsPlotter("Auto")); break; default: graph.addPlotter(new BooleanMetricsPlotter("Unknown")); } } { final MetricsGraph graph = m.createGraph("UsedPlugins"); graph.addPlotter(new MetricsPlotter("Plugins") { @Override public int getValue() { return core.getPluginManager().getPlugins().size(); } }); } { final MetricsGraph graph = m.createGraph("EntitiesV1"); graph.addPlotter(new MetricsPlotter("Entities") { @Override public int getValue() { return core.getWorldsManager().getWorlds().stream().mapToInt(w -> w.getEntityTrackers().size()) .sum(); } }); } { final DynamicMetricsGraph graph = new DynamicMetricsGraph("EntitiesV2") { @Override public Set<MetricsPlotter> getPlotters() { final Set<MetricsPlotter> result = new HashSet<>(20); final Short2IntMap map = new Short2IntOpenHashMap(20, .1f); core.getWorldsManager().getWorlds() .forEach(w -> w.getEntityTrackers().getStats().short2IntEntrySet() .forEach((entry) -> map.put(entry.getShortKey(), (map.get(entry.getShortKey()) + entry.getIntValue())))); for (final Entry entry : map.short2IntEntrySet()) { final EntityType type = EntityType.getByEnumOrdinal(entry.getShortKey()); if (type != null) { result.add(new SimpleMetricsPlotter(type.getName(), entry.getIntValue())); } } return result; } }; m.addGraph(graph); } /** * Testing, how that will look, may be removed in the future. * Seems to be stupid idea, but, whatever. */ { m.addGraph(new PluginsMetricsGraph("UsedPluginsV2", core, false)); m.addGraph(new PluginsMetricsGraph("UsedPluginsV3", core, true)); } m.start(); return m; } Metrics(final DioriteCore core) { if (core == null) { throw new IllegalArgumentException("Server cannot be null"); } this.core = core; } /** * Get the full server version * * @return full server version */ public String getFullServerVersion() { return DioriteCore.getInstance().getServerModName(); } /** * Get the amount of players online * * @return amount of players online */ public int getPlayersOnline() { return this.core.getPlayersManager().getRawPlayers().size(); } /** * 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 MetricsGraph createGraph(final String name) { if (name == null) { throw new IllegalArgumentException("Graph name cannot be null"); } // Construct the graph object final MetricsGraph graph = new MetricsGraph(name); // Now we can add our graph this.graphs.add(graph); // and return back return graph; } /** * Add a Graph object to SpoutMetrics that represents data for the plugin that should be sent to the backend * * @param graph The name of the graph */ public void addGraph(final DynamicMetricsGraph graph) { if (graph == null) { throw new IllegalArgumentException("Graph cannot be null"); } this.graphs.add(graph); } public void stop() { final Thread t = this.thread; this.thread = null; t.interrupt(); } /** * 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 PING_INTERVAL * 1200 * ticks. * * @return True if statistics measuring is running, otherwise false. */ public boolean start() { // Did we opt out? if (this.isOptOut()) { return false; } // Is metrics already running? if (this.thread != null) { return true; } this.thread = new Thread(new Runnable() { private boolean firstPost = true; private long nextPost = 0L; @Override public void run() { while (Metrics.this.core.isRunning() && (Metrics.this.thread != null)) { if ((this.nextPost == 0L) || (System.currentTimeMillis() > this.nextPost)) { try { // 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! Metrics.this.postPlugin(!this.firstPost); // After the first post we set firstPost to false // Each post thereafter will be a ping this.firstPost = false; this.nextPost = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(PING_INTERVAL); } catch (final IOException e) { if (CoreMain.isEnabledDebug()) { SpammyError.out("[Metrics] " + e.getMessage(), (int) TimeUnit.MINUTES.toSeconds(5), this.hashCode() + 2); } } } try { Thread.sleep(Math.min(Math.max(this.nextPost - System.currentTimeMillis(), 1), TimeUnit.MINUTES.toMillis(PING_INTERVAL))); } catch (final InterruptedException e) { if (Metrics.this.thread != null) { e.printStackTrace(); } } } } }, "MCStats / Plugin Metrics"); this.thread.start(); return true; } /** * Has the server owner denied plugin metrics? * * @return true if metrics should be opted out of it */ public boolean isOptOut() { return this.getUUID().isEmpty(); } public String getUUID() { return this.core.getConfig().getMetricsUuid(); } /** * Generic method that posts a plugin to the metrics website */ private void postPlugin(final boolean isPing) throws IOException { final String serverVersion = this.getFullServerVersion(); final int playersOnline = this.getPlayersOnline(); // END server software specific section -- all code below does not use any code outside of this class / Java // Construct the post data final StringBuilder json = new StringBuilder(1024); json.append('{'); // The plugin's description file containg all of the plugin data such as name, version, author, etc appendJSONPair(json, "guid", this.getUUID()); appendJSONPair(json, "plugin_version", this.core.getVersion()); appendJSONPair(json, "server_version", serverVersion); appendJSONPair(json, "players_online", Integer.toString(playersOnline)); // New data as of R6 final String osname = System.getProperty("os.name"); String osarch = System.getProperty("os.arch"); final String osversion = System.getProperty("os.version"); final String java_version = System.getProperty("java.version"); final int coreCount = Runtime.getRuntime().availableProcessors(); // normalize os arch .. amd64 -> x86_64 if (osarch.equals("amd64")) { osarch = "x86_64"; } appendJSONPair(json, "osname", osname); appendJSONPair(json, "osarch", osarch); appendJSONPair(json, "osversion", osversion); appendJSONPair(json, "cores", Integer.toString(coreCount)); appendJSONPair(json, "auth_mode", (this.core.getOnlineMode() == OnlineMode.FALSE) ? "0" : "1"); appendJSONPair(json, "java_version", java_version); // If we're pinging, append it if (isPing) { appendJSONPair(json, "ping", "1"); } if (!this.graphs.isEmpty()) { synchronized (this.graphs) { json.append(','); json.append('"'); json.append("graphs"); json.append('"'); json.append(':'); json.append('{'); boolean firstGraph = true; for (final DynamicMetricsGraph graph : this.graphs) { final StringBuilder graphJson = new StringBuilder(); graphJson.append('{'); for (final MetricsPlotter plotter : graph.getPlotters()) { appendJSONPair(graphJson, plotter.getColumnName(), Integer.toString(plotter.getValue())); } graphJson.append('}'); if (!firstGraph) { json.append(','); } json.append(escapeJSON(graph.getName())); json.append(':'); json.append(graphJson); firstGraph = false; } json.append('}'); } } // close json json.append('}'); // Create the url final URL url = new URL(BASE_URL); // Connect to the website final URLConnection connection; connection = url.openConnection(); final byte[] uncompressed = json.toString().getBytes(); final byte[] compressed = gzip(json.toString()); // Headers connection.addRequestProperty("User-Agent", "MCStats/" + REVISION); connection.addRequestProperty("Content-Type", "application/json"); connection.addRequestProperty("Content-Encoding", "gzip"); connection.addRequestProperty("Content-Length", Integer.toString(compressed.length)); connection.addRequestProperty("Accept", "application/json"); connection.addRequestProperty("Connection", "close"); connection.setDoOutput(true); if (CoreMain.isEnabledDebug()) { SpammyError.out("[Metrics] Prepared request for Diorite uncompressed=" + uncompressed.length + " compressed=" + compressed.length, (int) TimeUnit.MINUTES.toSeconds(5), this.hashCode() + 1); } // Write the data try (final OutputStream os = connection.getOutputStream()) { os.write(compressed); os.flush(); } String response; try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { response = reader.readLine(); } if ((response == null) || response.startsWith("ERR") || response.startsWith("7")) { if (response == null) { response = "null"; } else if (response.startsWith("7")) { response = response.substring(response.startsWith("7,") ? 2 : 1); } throw new IOException(response); } else { // Is this the first update this hour? if (response.equals("1") || response.contains("This is your first update this hour")) { synchronized (this.graphs) { this.graphs.forEach(DynamicMetricsGraph::resetPlotters); } } } } /** * GZip compress a string of bytes. * * @param input string to compress. * * @return compressed a string of bytes. */ public static byte[] gzip(final String input) { try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { try (GZIPOutputStream gzos = new GZIPOutputStream(baos)) { gzos.write(input.getBytes("UTF-8")); } catch (final IOException e) { e.printStackTrace(); } return baos.toByteArray(); } catch (final IOException e) { throw new RuntimeException(e); } } private static void appendJSONPair(final StringBuilder json, final String key, final String value) { boolean isValueNumeric = false; try { if (value.equals("0") || !value.endsWith("0")) { Double.parseDouble(value); isValueNumeric = true; } } catch (final NumberFormatException e) { isValueNumeric = false; } if (json.charAt(json.length() - 1) != '{') { json.append(','); } json.append(escapeJSON(key)); json.append(':'); if (isValueNumeric) { json.append(value); } else { json.append(escapeJSON(value)); } } /** * Escape a string to create a valid JSON string * * @param text * * @return */ private static String escapeJSON(final String text) { final StringBuilder builder = new StringBuilder(); builder.append('"'); for (int index = 0; index < text.length(); index++) { final char chr = text.charAt(index); switch (chr) { case '"': case '\\': builder.append('\\'); builder.append(chr); break; case '\b': builder.append("\\b"); break; case '\t': builder.append("\\t"); break; case '\n': builder.append("\\n"); break; case '\r': builder.append("\\r"); break; default: if (chr < ' ') { final String t = "000" + Integer.toHexString(chr); builder.append("\\u").append(t.substring(t.length() - 4)); } else { builder.append(chr); } break; } } builder.append('"'); return builder.toString(); } /** * Encode text as UTF-8 * * @param text the text to encode * * @return the encoded text, as UTF-8 */ private static String urlEncode(final String text) throws UnsupportedEncodingException { return URLEncoder.encode(text, "UTF-8"); } @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).appendSuper(super.toString()) .append("graphs", this.graphs).append("server", this.core).append("thread", this.thread).toString(); } }