/*******************************************************************************
* Copyright (c) 2013, 2014 Lectorius, Inc.
* Authors:
* Vijay Pandurangan (vijayp@mitro.co)
* Evan Jones (ej@mitro.co)
* Adam Hilss (ahilss@mitro.co)
*
*
* 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 3 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/>.
*
* You can contact the authors at inbound@mitro.co.
*******************************************************************************/
package co.mitro.analysis;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import co.mitro.core.exceptions.DoEmailVerificationException;
import co.mitro.core.exceptions.MitroServletException;
import co.mitro.core.server.Manager;
import co.mitro.core.server.Templates;
import co.mitro.core.server.data.DBAcl;
import co.mitro.core.server.data.DBGroup;
import co.mitro.core.server.data.DBHistoricalOrgState;
import co.mitro.core.server.data.DBHistoricalUserState;
import co.mitro.core.server.data.DBIdentity;
import co.mitro.core.server.data.RPC.GetOrganizationStateResponse;
import co.mitro.core.server.data.RPC.ListMySecretsAndGroupKeysResponse;
import co.mitro.core.server.data.RPC.ListMySecretsAndGroupKeysResponse.GroupInfo;
import co.mitro.core.servlets.GetOrganizationState;
import co.mitro.core.servlets.ListMySecretsAndGroupKeys;
import co.mitro.core.servlets.MitroServlet.MitroRequestContext;
import com.github.mustachejava.Mustache;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.collect.TreeMultimap;
import com.google.common.io.Files;
public class StatsGenerator {
private static final Mustache userStateTemplate = Templates.compile("user_state.mustache");
private static final Mustache orgStateTemplate = Templates.compile("org_state.mustache");
private static final Mustache indexTemplate = Templates.compile("index_state.mustache");
private static final Logger logger = LoggerFactory.getLogger(StatsGenerator.class);
public static final class Link implements Comparable<Object> {
public String text;
public String url;
public int count;
public Link(String t, String u, int count) {
text = t;
url = u;
this.count = count;
}
@Override
public int compareTo(Object o) {
Link l = (Link) o;
return (text + url).compareTo(l.text + l.url);
}
}
public static class Snapshot {
public final ArrayList<DBHistoricalOrgState> orgStateObjects = new ArrayList<>();
public final ArrayList<DBHistoricalUserState> userStateObjects = new ArrayList<>();
}
/**
* Generate statistics and return newly created objects that have not been committed.
* @param outDir directory in which to write summary files. Subdirectories outDir/users
* and outDir/orgs must exist. Supply null for no output.
*/
public static Snapshot generateStatistics(String outDir, Manager manager)
throws SQLException, IOException, MitroServletException {
final long runTimestampMs = System.currentTimeMillis();
Snapshot output = new Snapshot();
// TODO: don't do this in one gigantic transaction.
Multimap<Integer, Link> countToFile = TreeMultimap.create(Ordering.natural().reverse(), Ordering.natural());
// get all orgs.
Map<Integer, GroupInfo> orgIdToOrg = Maps.newHashMap();
for (DBGroup o : DBGroup.getAllOrganizations(manager)) {
GroupInfo newGi = new GroupInfo();
newGi.autoDelete = o.isAutoDelete();
newGi.groupId = o.getId();
newGi.isTopLevelOrg = true;
newGi.name = o.getName();
Set<String> users = Sets.newHashSet();
for (DBGroup orgGroup : o.getAllOrgGroups(manager)) {
users.add(orgGroup.getName());
}
newGi.users = Lists.newArrayList(users);
orgIdToOrg.put(newGi.groupId, newGi);
}
int numPeople = 0;
for (DBIdentity id : manager.identityDao.queryForAll()) {
++numPeople;
try {
logger.info(id.getName() + ": " + id.getGuidCookie());
DBHistoricalUserState userState = getHistoricalUserState(manager,
runTimestampMs, orgIdToOrg, id);
output.userStateObjects.add(userState);
String filename = id.getName() + ".html";
renderIfOutputEnabled(outDir, "/users/" + filename, userStateTemplate, userState);
countToFile.put(userState.numSecrets, new Link(id.getName(), filename, userState.numSecrets));
} catch (MitroServletException e) {
logger.error("UNKNOWN ERROR", e);
}
}
renderIfOutputEnabled(outDir, "/users/index.html", indexTemplate, countToFile.values());
countToFile.clear();
int numOrgs = 0;
// now do the orgs
for (DBGroup org : DBGroup.getAllOrganizations(manager)) {
++numOrgs;
// hack to make this work
Set<Integer> admins = Sets.newHashSet();
org.putDirectUsersIntoSet(admins, DBAcl.adminAccess());
int userId = admins.iterator().next();
DBIdentity dbi = manager.identityDao.queryForId(userId);
MitroRequestContext context = new MitroRequestContext(dbi, null, manager, null);
GetOrganizationStateResponse resp = GetOrganizationState.doOperation(context, org.getId());
DBHistoricalOrgState orgState = new DBHistoricalOrgState(resp, org.getId(), runTimestampMs);
output.orgStateObjects.add(orgState);
String filename = org.getId() + ".html";
renderIfOutputEnabled(outDir, "/orgs/" + filename, orgStateTemplate, orgState);
countToFile.put(orgState.numMembers + orgState.numAdmins,
new Link(org.getName() + org.getId(), org.getId() + ".html", orgState.numAdmins + orgState.numMembers));
}
renderIfOutputEnabled(outDir, "/orgs/index.html", indexTemplate, countToFile.values());
renderIfOutputEnabled(outDir, "/index.html", indexTemplate, ImmutableList.of(
new Link("organizations", "orgs/index.html", numOrgs),
new Link("users", "users/index.html", numPeople)));
return output;
}
public static DBHistoricalUserState getHistoricalUserState(Manager manager,
final long runTimestampMs, Map<Integer, GroupInfo> orgIdToOrg,
DBIdentity id) throws SQLException, MitroServletException,
DoEmailVerificationException {
MitroRequestContext context = new MitroRequestContext(id, null, manager, null);
ListMySecretsAndGroupKeysResponse out = ListMySecretsAndGroupKeys.executeWithoutAuditLog(context);
DBHistoricalUserState userState = new DBHistoricalUserState(out, orgIdToOrg, runTimestampMs);
userState.referrerDomain = id.getReferrer();
return userState;
}
/**
* Renders template with scope to a file located at outDir + suffix.
*
* @param outDir must not end with /
* @param suffix must start with /
* @param template
* @param scope
*/
private static void renderIfOutputEnabled(
String outDir, String suffix, Mustache template, Object scope) throws IOException {
assert suffix.charAt(0) == '/';
if (outDir != null) {
assert !outDir.endsWith("/");
String path = outDir + suffix;
try (BufferedWriter writer = Files.newWriter(new File(path), Charsets.UTF_8)) {
template.execute(writer, scope);
}
}
}
}