Java tutorial
/* Copyright (c) 2005 Robin Vobruba <hoijui.quaero@gmail.com> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.springrts.springls.statistics; import com.springrts.springls.Battle; import com.springrts.springls.Clients; import com.springrts.springls.Context; import com.springrts.springls.ContextReceiver; import com.springrts.springls.ServerConfiguration; import com.springrts.springls.Updateable; import com.springrts.springls.accounts.AccountsService; import com.springrts.springls.util.Misc; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import org.apache.commons.configuration.Configuration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Statistics file format: * <time> <#active-clients> <#active-battles> <#accounts> <#active-accounts> <list-of-mods> * where <time> is of form: "hhmmss" * and "active battles" are battles that are in-game and have 2 or more players * in it and <list-of-mods> is a list of first k mods (where k is 0 or greater) * with frequencies of active battles using these mods. Example: XTA 0.66 15. * Note that delimiter in <list-of-mods> is TAB and not SPACE! See code for more * info. * * Aggregated statistics file format: * <date> <time> <#active-clients> <#active-battles> <#accounts> <#active-accounts> <list-of-mods> * where <date> is of form: "ddMMyy" * and all other fields are of same format as those from normal statistics file. * * @author Betalord * @author hoijui */ public class Statistics implements ContextReceiver, Updateable { private static final Logger LOG = LoggerFactory.getLogger(Statistics.class); /** in milliseconds */ private final long saveStatisticsInterval = 1000 * 60 * 20; /** * See the <a href="http://ploticus.sourceforge.net/">Ploticus page</a> * for more info. */ private static final String PLOTICUS_FULLPATH = "./ploticus/bin/pl"; private static final String STATISTICS_FOLDER = "./stats/"; /** * Time when we last updated statistics. * @see java.lang.System#currentTimeMillis() */ private long lastStatisticsUpdate; private Context context = null; public Statistics() { lastStatisticsUpdate = System.currentTimeMillis(); } @Override public void receiveContext(Context context) { this.context = context; } @Override public void update() { Configuration conf = context.getService(Configuration.class); boolean recording = conf.getBoolean(ServerConfiguration.STATISTICS_STORE); if (recording && ((System.currentTimeMillis() - lastStatisticsUpdate) > saveStatisticsInterval)) { saveStatisticsToDisk(); } } private void ensureStatsDirExists() { // create statistics folder if it does not exist yet File file = new File(STATISTICS_FOLDER); if (!file.exists()) { boolean success = (file.mkdir()); if (!success) { LOG.error("Unable to create folder: {}", STATISTICS_FOLDER); } else { LOG.info("Created missing folder: {}", STATISTICS_FOLDER); } } } /** * Saves Statistics to permanent storage. * @return -1 on error; otherwise: time (in milliseconds) it took * to save the statistics file */ public int saveStatisticsToDisk() { long taken; try { ensureStatsDirExists(); lastStatisticsUpdate = System.currentTimeMillis(); taken = autoUpdateStatisticsFile(); if (taken != -1) { createAggregateFile(); // to simplify parsing generatePloticusImages(); LOG.info("*** Statistics saved to disk. Time taken: {} ms.", taken); } } catch (Exception ex) { LOG.error("*** Failed saving statistics... Stack trace:", ex); taken = -1; } return (int) taken; } /** * Will append statistics file (or create one if it doesn't exist) * and add latest statistics to it. * @return milliseconds taken to calculate statistics, or -1 if it fails */ private long autoUpdateStatisticsFile() { String fileName = STATISTICS_FOLDER + now("ddMMyy") + ".dat"; long startTime = System.currentTimeMillis(); int activeBattlesCount = 0; for (int i = 0; i < context.getBattles().getBattlesSize(); i++) { Battle battle = context.getBattles().getBattleByIndex(i); // at least 1 client + founder == 2 players if ((battle.getClientsSize() > 0) && battle.inGame()) { activeBattlesCount++; } } String topMods = currentlyPopularModsList(); Writer outF = null; Writer out = null; try { outF = new FileWriter(fileName, true); out = new BufferedWriter(outF); Clients clients = context.getClients(); AccountsService accounts = context.getAccountsService(); out.write(now("HHmmss")); out.write(" "); out.write("" + clients.getClientsSize()); out.write(" "); out.write("" + activeBattlesCount); out.write(" "); out.write("" + accounts.getAccountsSize()); out.write(" "); out.write("" + accounts.getActiveAccountsSize()); out.write(" "); out.write(topMods); out.write(Misc.EOL); } catch (IOException ex) { LOG.error("Unable to access file <" + fileName + ">. Skipping ...", ex); return -1; } finally { try { if (out != null) { out.close(); } else if (outF != null) { outF.close(); } } catch (IOException ex) { LOG.trace("Failed closing statistics file-writer for file: " + fileName, ex); } } long updateTime = System.currentTimeMillis() - startTime; LOG.info("Statistics have been updated and stored to disk in {} ms", updateTime); return updateTime; } /** * This will create "statistics.dat" file which will contain all records * from the last 7 days. */ private boolean createAggregateFile() { String fileName = STATISTICS_FOLDER + "statistics.dat"; Writer outF = null; Writer out = null; try { // overwrite if it exists, or create new one outF = new FileWriter(fileName, false); out = new BufferedWriter(outF); String line; SimpleDateFormat formatter = new SimpleDateFormat("ddMMyy"); Date today = today(); long msPerDay = 1000 * 60 * 60 * 24; // get file names for last 7 days (that is today + previous 6 days) for (int i = 7; i > 0; i--) { Date day = new Date(); day.setTime(today.getTime() - (((long) i - 1) * msPerDay)); String dayStr = formatter.format(day); File fileDay = new File(STATISTICS_FOLDER + formatter.format(dayStr) + ".dat"); Reader inF = null; BufferedReader in = null; try { inF = new FileReader(fileDay); in = new BufferedReader(inF); LOG.trace("Found stats: <{}>", fileDay.getAbsolutePath()); while ((line = in.readLine()) != null) { out.write(dayStr); out.write(' '); out.write(line); out.write(Misc.EOL); } } catch (IOException ex) { LOG.trace("Skipped stats: <" + fileDay.getAbsolutePath() + ">", ex); } finally { if (in != null) { in.close(); } else if (inF != null) { inF.close(); } } } } catch (IOException ex) { LOG.error("Unable to access file <" + fileName + ">. Skipping ...", ex); return false; } finally { try { if (out != null) { out.close(); } else if (outF != null) { outF.close(); } } catch (IOException ex) { LOG.trace("Failed closing aggregate statistics file-writer for" + " file: " + fileName, ex); } } return true; } private static final long DAY = 1000L * 60L * 60L * 24L; private boolean generatePloticusImages() { boolean ret = false; try { StringBuilder cmd; String[] cmds; SimpleDateFormat dayFormatter = new SimpleDateFormat("ddMMyy"); // from (today_00:00 - 6 days) till today_00:00 Date endDate = today(); // today_00:00 Date startDate = new Date(endDate.getTime() - (6L * DAY)); String startDateString = dayFormatter.format(startDate); String endDateString = dayFormatter.format(endDate); SimpleDateFormat lastUpdateFormatter = new SimpleDateFormat("dd/MM/yyyy, HH:mm:ss (z)"); long upTime = System.currentTimeMillis() - context.getServer().getStartTime(); // generate "server stats diagram": cmds = new String[8]; cmds[0] = PLOTICUS_FULLPATH; cmds[1] = "-png"; cmds[2] = STATISTICS_FOLDER + "info.pl"; cmds[3] = "-o"; cmds[4] = STATISTICS_FOLDER + "info.png"; cmds[5] = "lastupdate=" + lastUpdateFormatter.format(new Date()); cmds[6] = "uptime=" + Misc.timeToDHM(upTime); cmds[7] = "clients=" + context.getClients().getClientsSize(); Runtime.getRuntime().exec(cmds).waitFor(); // generate "online clients diagram": cmd = new StringBuilder(PLOTICUS_FULLPATH).append(" -png ").append(STATISTICS_FOLDER) .append("clients.pl -o ").append(STATISTICS_FOLDER).append("clients.png startdate=") .append(startDateString).append(" enddate=").append(endDateString).append(" datafile=") .append(STATISTICS_FOLDER).append("statistics.dat"); Runtime.getRuntime().exec(cmd.toString()).waitFor(); // generate "active battles diagram": cmd = new StringBuilder(PLOTICUS_FULLPATH).append(" -png ").append(STATISTICS_FOLDER) .append("battles.pl -o ").append(STATISTICS_FOLDER).append("battles.png startdate=") .append(startDateString).append(" enddate=").append(endDateString).append(" datafile=") .append(STATISTICS_FOLDER).append("statistics.dat"); Runtime.getRuntime().exec(cmd.toString()).waitFor(); // generate "accounts diagram": cmd = new StringBuilder(PLOTICUS_FULLPATH).append(" -png ").append(STATISTICS_FOLDER) .append("accounts.pl -o ").append(STATISTICS_FOLDER).append("accounts.png startdate=") .append(startDateString).append(" enddate=").append(endDateString).append(" datafile=") .append(STATISTICS_FOLDER).append("statistics.dat"); Runtime.getRuntime().exec(cmd.toString()).waitFor(); // generate "popular mods chart": String[] params = getPopularModsList(now("ddMMyy")).split("\t"); cmd = new StringBuilder(PLOTICUS_FULLPATH).append(" -png ").append(STATISTICS_FOLDER) .append("mods.pl -o ").append(STATISTICS_FOLDER).append("mods.png count=") .append(Integer.parseInt(params[0])).append(" enddate=").append(endDateString) .append(" datafile=").append(STATISTICS_FOLDER).append("statistics.dat"); for (int i = 1; i < params.length; i++) { if ((i % 2) != 0) { // odd index cmd.append("mod").append((i + 1) / 2).append("=").append(params[i]); } else { // even index cmd.append("modfreq").append(i / 2).append("=").append(params[i]); } } Runtime.getRuntime().exec(cmd.toString()).waitFor(); ret = true; } catch (InterruptedException ex) { LOG.error("Failed generating ploticus charts!", ex); ret = false; } catch (IOException ex) { LOG.error("Failed generating ploticus charts!", ex); ret = false; } return ret; } /** * Returns the list of mods being played right now (top 5 mods only) * with frequencies (number of battles). * @return [list-len] "modname1" [numBattles1] "modname2" [numBattles2]" ... * Where delimiter is TAB (not SPACE). * An empty list is denoted by 0 value for list-len. */ private String currentlyPopularModsList() { List<ModBattles> modBattles = new ArrayList<ModBattles>(); for (int i = 0; i < context.getBattles().getBattlesSize(); i++) { Battle battle = context.getBattles().getBattleByIndex(i); if (battle.inGame() && (battle.getClientsSize() >= 1)) { // add to list or update in list: int modIndex = modBattles.indexOf(battle.getModName()); if (modIndex == -1) { modBattles.add(new ModBattles(battle.getModName(), 1)); } else { modBattles.get(modIndex).addBattles(1); } } } return createModPopularityString(modBattles); } private static class ModBattles implements Comparable<ModBattles> { public static final Comparator<ModBattles> BATTLES_COMPARATOR = new Comparator<ModBattles>() { @Override public int compare(ModBattles modBattles1, ModBattles modBattles2) { return modBattles1.getBattles() - modBattles2.getBattles(); } }; private final String name; private int battles = 0; ModBattles(String name, int battles) { this.name = name; this.battles = battles; } public String getName() { return name; } public int getBattles() { return battles; } public void addBattles(int additionalBattles) { this.battles += additionalBattles; } @Override public int compareTo(ModBattles other) { return getName().compareTo(other.getName()); } @Override public boolean equals(Object other) { if (other instanceof String) { return getName().equals((String) other); } else if (other instanceof ModBattles) { return getName().equals(((ModBattles) other).getName()); } else { return false; } } @Override public int hashCode() { int hash = 7; hash = 23 * hash + (this.name != null ? this.name.hashCode() : 0); return hash; } } /** * Returns a list of the top 5 popular mods for a certain date. * The date must be in the format "ddMMyy". It will take the first entry for * every new hour and add it to the list. Other entries for the same hour * will be ignored. * @return [list-len] "modname1" [numBattles1] "modname2" [numBattles2]" ... * Where delimiter is TAB (not SPACE). * An empty list is denoted by 0 value for list-len. * @see #currentlyPopularModList() */ private String getPopularModsList(String date) { String popularModsList; File file = new File(STATISTICS_FOLDER + date + ".dat"); Reader inF = null; BufferedReader in = null; try { byte lastHour = -1; String line; inF = new FileReader(file); in = new BufferedReader(inF); List<ModBattles> modBattles = new ArrayList<ModBattles>(); while ((line = in.readLine()) != null) { byte sHour = Byte.parseByte(line.substring(0, 2)); // 00 .. 23 if (lastHour == sHour) { continue; // skip this input line } lastHour = sHour; String modFrequencyStr = Misc.makeSentence(line.split(" "), 5); String[] modFrequenciesRaw = modFrequencyStr.split("\t"); if ((modFrequenciesRaw.length % 2) != 1) { // the number of arguments must be odd // -> numMods + (numMods * (modName + modFrequency)) throw new Exception("Bad mod list format"); } int numMods = Integer.parseInt(modFrequenciesRaw[0]); if (modFrequenciesRaw.length != (1 + (numMods * 2))) { throw new Exception("Bad mod list format"); } for (int i = 0; i < numMods; i++) { int i2 = i * 2; String name = modFrequenciesRaw[i2 + 1]; int battles = Integer.parseInt(modFrequenciesRaw[i2 + 2]); int modIndex = modBattles.indexOf(name); if (modIndex == -1) { modBattles.add(new ModBattles(name, battles)); } else { modBattles.get(modIndex).addBattles(battles); } } } popularModsList = createModPopularityString(modBattles); } catch (Exception ex) { LOG.error("Error in getPopularModsList(). Skipping ...", ex); popularModsList = "0"; } finally { try { if (in != null) { in.close(); } else if (inF != null) { inF.close(); } } catch (IOException ex) { LOG.trace("Failed closing statistics file-reader for file: " + file.getAbsolutePath(), ex); } } return popularModsList; } private static String createModPopularityString(List<ModBattles> modBattles) { // now generate a list of top 5 mods with frequencies: StringBuilder result = new StringBuilder(512); int numMods = Math.min(5, modBattles.size()); // return 5 or less mods result.append(numMods); // Note: do not cut the array by numMods, // or sorting will not have any effect! Collections.sort(modBattles, ModBattles.BATTLES_COMPARATOR); for (ModBattles mod : modBattles) { result.append("\t").append(mod.getName()); result.append("\t").append(mod.getBattles()); } return result.toString(); } /** * Usage (some examples): * * @param format examples: * "dd MMMMM yyyy" * "yyyyMMdd" * "dd.MM.yy" * "MM/dd/yy" * "yyyy.MM.dd G 'at' hh:mm:ss z" * "EEE, MMM d, ''yy" * "h:mm a" * "H:mm:ss:SSS" * "K:mm a,z" * "yyyy.MMMMM.dd GGG hh:mm aaa" * * Taken from <a href="http://www.rgagnon.com/javadetails/java-0106.html"> * here</a>. * * Also see * <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/text/SimpleDateFormat.html"> * SimpleDateFormat JavaDoc</a> for more info. */ private static String now(String format) { Date now = new Date(); SimpleDateFormat formatter = new SimpleDateFormat(format); String current = formatter.format(now); return current; } private static final String TODAY_FILTER_FORMAT = "ddMMyy"; private static Date today() { Date today = null; try { today = new SimpleDateFormat(TODAY_FILTER_FORMAT).parse(now(TODAY_FILTER_FORMAT)); } catch (ParseException ex) { // this could not possible ever happen! LOG.error("Failed creating date string for 'today'", ex); } return today; } }