/**
* Copyright (c) 2007, Markus Jevring <markus@jevring.net>
*
* 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.
* 3. The names of the contributors may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``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 REGENTS 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.
*
*/
package cu.ftpd.user.statistics.local;
import cu.ftpd.logging.Logging;
import cu.ftpd.user.User;
import cu.ftpd.user.statistics.StatisticsEntry;
import cu.ftpd.user.statistics.UserStatistics;
import cu.ftpd.user.userbases.NoSuchUserException;
import cu.ftpd.Server;
import cu.ftpd.ServiceManager;
import java.io.*;
import java.util.*;
/**
* @author Markus Jevring <markus@jevring.net>
* @since 2007-maj-28 : 21:36:29
* @version $Id: LocalUserStatistics.java 258 2008-10-26 12:47:23Z jevring $
*/
public class LocalUserStatistics implements UserStatistics {
private final Map<String, StatisticsEntry> statistics = Collections.synchronizedMap(new HashMap<String, StatisticsEntry>());
private final File userStatisticsDir;
public LocalUserStatistics(File statisticsDir) {
this.userStatisticsDir = statisticsDir;
userStatisticsDir.mkdirs();
if (!userStatisticsDir.isDirectory()) {
throw new IllegalArgumentException("Could not find dir for user statistics: " + userStatisticsDir.getAbsolutePath() + " (or it is not a directory)");
} else {
for (String filename : userStatisticsDir.list()) {
StatisticsEntry se = loadUserStatistics(filename);
if (se != null) { // since we can return null, we can do a simple .isFile() check in the method
statistics.put(filename, se);
}
}
}
}
public void upload(String username, String section, long bytes, long time) {
StatisticsEntry us = getUserStatistics(username, section);
us.upload(bytes, time);
store(us);
if (!"default".equals(section)) {
// always add the data to the default section as well, since that keeps track of ALL the statistics
us = getUserStatistics(username, "default");
us.upload(bytes, time);
store(us);
}
}
public void download(String username, String section, long bytes, long time) {
StatisticsEntry us = getUserStatistics(username, section);
us.download(bytes, time);
store(us);
if (!"default".equals(section)) {
// always add the data to the default section as well, since that keeps track of ALL the statistics
us = getUserStatistics(username, "default");
us.download(bytes, time);
store(us);
}
}
public void store(StatisticsEntry statisticsEntry) {
// _todo: write lastUpdated to the file so we have sane data when we restart the server
// used when a user logs off
if (statisticsEntry != null) { // don't store things that do no exist
PrintWriter out = null;
try {
//long start = System.currentTimeMillis();
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(userStatisticsDir, statisticsEntry.getName() + '.' + statisticsEntry.getSection())))));
out.write(statisticsEntry.getLastUpdate() + "\r\n");
out.write(statisticsEntry.toString());
out.flush();
//System.out.println((System.currentTimeMillis() - start) + " milliseconds taken to store userstatistics " + statisticsEntry.getName());
} catch (IOException e) {
Logging.getErrorLog().reportError("Failed to write user statistics to file: " + e.getMessage());
} finally {
if (out != null) {
out.close();
}
}
}
}
private StatisticsEntry loadUserStatistics(String filename){
final File statfile = new File(userStatisticsDir, filename);
if (!statfile.isFile()) {
// this is ok, since we have other reasons for returning null
return null;
}
String[] s = filename.split("\\.");
if (s.length != 2) {
// some weird file, skip it (this also means that usernames or sections cannon contain periods)
return null;
// _todo: let it be possible for sections to contain periods, so this check must be done in a different manner
// sections can no longer contain periods
}
StatisticsEntry us = new StatisticsEntry(s[0], s[1]);
//long start = System.currentTimeMillis();
BufferedReader in = null;
try {
in = new BufferedReader(new InputStreamReader(new FileInputStream(statfile)));
String line = in.readLine();
try {
us.setLastUpdate(Long.parseLong(line));
line = in.readLine();
} catch (NumberFormatException e) {
// do nothing
// we get this error if we convert from old-style files that did not have this date at the top
}
line = line.substring(1, line.length() - 1); // remove the wrapping "[" and "]"
String[] statline = line.split(",\\s+");
for (int i = 0; i < statline.length; i++) {
try {
us.set(i, Long.parseLong(statline[i]));
} catch (NumberFormatException e) {
System.err.println("Found bad data in user statistics file: " + statline[i] + " for user: " + filename);
}
}
statistics.put(filename, us);
} catch (IOException e) {
Logging.getErrorLog().reportError("Failed to read user statistics from file: " + e.getMessage());
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//System.out.println((System.currentTimeMillis() - start) + " milliseconds taken to read userstatistics " + username);
return us;
}
public StatisticsEntry getUserStatistics(String username, String section) {
// _todo: handle the case of the default section
// no problem, the default section is named "default", so there will always be a name
// _todo: we must handle this for extracting statistics as well, also, when we add stuff to a section, we also always need to add it to the default section
// done (see download() and upload())
//System.out.println("requesting statistics entry: " + username + "." + section);
StatisticsEntry us = statistics.get(username + '.' + section);
if (us == null) {
//System.out.println("entry not found, creating...");
us = new StatisticsEntry(username, section);
statistics.put(username + '.' + section, us);
}
return us;
}
public TreeMap<Long, StatisticsEntry> get(int statistics, String section) {
TreeMap<Long, StatisticsEntry> map = new TreeMap<Long, StatisticsEntry>();
for (StatisticsEntry se : this.statistics.values()) {
if (se.getSection().equals(section)) {
// we're doing this as an easy way of flipping the ordering of the TreeMap, as I didn't want to write an inverse comparator for such a simple thing
map.put(Long.MAX_VALUE - se.get(statistics), se);
}
}
return map;
}
/**
* Returns the statistics for the groups. If a groupname is supplied, the statistics represent only members of that group.
*
* @param groupname the group for which to view user statistics. <code>null</code> creates a list for all groups.
* @param statistics the statistics we are after.
* @param section the name of the section
* @return if a group name was specified; a TreeMap with statistics inside the named group. If no name was provided; a TreeMap with statistics of all the groups, collected and sorted per group.
*/
public TreeMap<Long, StatisticsEntry> getGroupStatistics(String groupname, int statistics, String section) {
// _todo: support using sections here (maybe in the future)
// not yet, too much work, and nobody will use if. if the users want it, I will add it.
TreeMap<Long, StatisticsEntry> map = new TreeMap<Long, StatisticsEntry>();
if (groupname == null || "default".equals(groupname)) {
// we are doing a ranking for all groupS
HashMap<String, StatisticsEntry> groupStats = createGroupStats(section);
for (StatisticsEntry se : groupStats.values()) {
map.put(Long.MAX_VALUE - se.get(UserStatistics.ALLUP_BYTES), se);
}
} else {
// we are doing a ranking WITHIN A group
for (StatisticsEntry us : this.statistics.values()) {
try {
if (ServiceManager.getServices().getUserbase().getUser(us.getName()).isMemberOfGroup(groupname) && us.getSection().equals(section)) {
// we're doing this as an easy way of flipping the ordering of the TreeMap, as I didn't want to write an inverse comparator for such a simple thing
map.put(Long.MAX_VALUE - us.get(UserStatistics.ALLUP_BYTES), us);
}
} catch (NoSuchUserException e) {
System.err.println("Found some user statistics that belongs to an unknown user: " + e.getMessage()); // this can happen if a user has recently been removed from the userbase, but his statistics are still here.
}
}
}
return map;
}
private HashMap<String, StatisticsEntry> createGroupStats(String section) {
// NOTE: for now, it broke when passing a section, so we will just disregard the setting section
// we will come back to it later
if (section == null || "".equals(section)) {
section = "default";
}
HashMap<String, StatisticsEntry> groupStats = new HashMap<String, StatisticsEntry>();
for (User user : ServiceManager.getServices().getUserbase().getUsers().values()) {
// note: this only lists groups that have members in them. otherwise they won't have any stats anyway
for (String group : user.getGroups()) {
StatisticsEntry stat = groupStats.get(group);
if (stat == null) {
stat = new StatisticsEntry(group, section);
groupStats.put(group, stat);
}
// _todo: support using sections here (maybe in the future)
// not yet, too much work, and nobody will use if. if the users want it, I will add it.
stat.set(UserStatistics.ALLUP_BYTES, (stat.get(UserStatistics.ALLUP_BYTES) + getUserStatistics(user.getUsername(), section).get(UserStatistics.ALLUP_BYTES)));
stat.set(UserStatistics.ALLDN_BYTES, (stat.get(UserStatistics.ALLDN_BYTES) + getUserStatistics(user.getUsername(), section).get(UserStatistics.ALLDN_BYTES)));
stat.set(UserStatistics.MNUP_BYTES, (stat.get(UserStatistics.MNUP_BYTES) + getUserStatistics(user.getUsername(), section).get(UserStatistics.MNUP_BYTES)));
stat.set(UserStatistics.MNDN_BYTES, (stat.get(UserStatistics.MNDN_BYTES) + getUserStatistics(user.getUsername(), section).get(UserStatistics.MNDN_BYTES)));
stat.set(UserStatistics.WKUP_BYTES, (stat.get(UserStatistics.WKUP_BYTES) + getUserStatistics(user.getUsername(), section).get(UserStatistics.WKUP_BYTES)));
stat.set(UserStatistics.WKDN_BYTES, (stat.get(UserStatistics.WKDN_BYTES) + getUserStatistics(user.getUsername(), section).get(UserStatistics.WKDN_BYTES)));
stat.set(UserStatistics.DAYUP_BYTES, (stat.get(UserStatistics.DAYUP_BYTES) + getUserStatistics(user.getUsername(), section).get(UserStatistics.DAYUP_BYTES)));
stat.set(UserStatistics.DAYDN_BYTES, (stat.get(UserStatistics.DAYDN_BYTES) + getUserStatistics(user.getUsername(), section).get(UserStatistics.DAYDN_BYTES)));
}
}
return groupStats;
}
public void deleteUser(String username) {
synchronized(statistics) {
Iterator<Map.Entry<String,StatisticsEntry>> i = statistics.entrySet().iterator();
while (i.hasNext()) {
Map.Entry<String,StatisticsEntry> entry = i.next();
if (entry.getValue().getName().equals(username)) {
i.remove();
File statfile = new File(userStatisticsDir, entry.getKey());
statfile.delete();
}
}
}
}
}