/*
* StatsRepository.java - Copyright(c) 2013 Joe Pasqua
* Provided under the MIT License. See the LICENSE file for details.
* Created: Aug 25, 2013
*/
package org.noroomattheinn.visibletesla.stats;
import com.google.common.collect.Range;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import static org.noroomattheinn.tesla.Tesla.logger;
/**
* StatsRepository
*
* @author Joe Pasqua <joe at NoRoomAtTheInn dot org>
*/
public class StatsRepository {
/*------------------------------------------------------------------------------
*
* Internal State
*
*----------------------------------------------------------------------------*/
private final File statsFile;
private final List<Stat> entriesSinceLastFlush = new ArrayList<>();
private PrintStream statsWriter;
//private FileLock repoLock = null;
/*==============================================================================
* ------- -------
* ------- Public Interface To This Class -------
* ------- -------
*============================================================================*/
public StatsRepository(File statsFile) throws IOException {
this.statsFile = statsFile;
if (!prepRepository()) {
throw new IOException("Unable to access repository: " + statsFile);
}
}
public interface Recorder {
void recordElement(long time, String type, double val);
}
public synchronized void loadExistingData(Recorder r) {
BufferedReader rdr = getReaderForFile(statsFile);
if (rdr == null) return;
String line;
Map<String,String> lastEntries = new HashMap<>();
while ((line = getLineFromReader(rdr)) != null) {
String tokens[] = line.split("\\s");
if ((tokens.length-1)%2 != 0) { // Malformed line
logger.log(
Level.INFO, "Malformed stats entry: Improper number of tokens: {0}", line);
continue;
}
Map<String,String> merged = mergeEntries(tokens, lastEntries);
try {
long time = Long.valueOf(tokens[0]);
for (Map.Entry<String,String> entry : merged.entrySet()) {
String type = entry.getKey();
double val = Double.valueOf(entry.getValue());
r.recordElement(time, type, val);
}
lastEntries = merged;
} catch (NumberFormatException ex) {
logger.log(Level.INFO, "Malformed stats entry", ex);
}
}
}
public synchronized void loadExistingData(final Recorder r, final Range<Long> period) {
loadExistingData(new Recorder() {
@Override public void recordElement(long time, String type, double val) {
if (period.contains(time)) r.recordElement(time, type, val);
}
});
}
public synchronized void storeElement(String type, long time, double value) {
if (statsWriter == null) return;
if (Double.isNaN(value) || Double.isInfinite(value)) value = 0.0;
entriesSinceLastFlush.add(new Stat(time, type, value));
}
public synchronized void close() {
if (statsWriter != null) statsWriter.close();
}
private final Map<String,Double> lastValForType = new HashMap<>();
private Set<String> columnsInLastRow = new HashSet<>();
private boolean sameColumnsAsLastTime(Map<String,Double> thisRow) {
if (thisRow.size() != columnsInLastRow.size()) return false;
for (String columnName : columnsInLastRow) {
if (thisRow.get(columnName) == null) return false;
}
return true;
}
public synchronized void flushElements() {
Map<Long,Map<String,Double>> rows = new TreeMap<>();
// It's possible that there are entries for multiple points in time
// Create a map with a key for each unique time where the value
// is a list of Entries with that time
for (Stat entry : entriesSinceLastFlush) {
Map<String,Double> row = rows.get(entry.sample.timestamp);
if (row == null) {
row = new HashMap<>();
rows.put(entry.sample.timestamp, row);
}
row.put(entry.type, entry.sample.value);
}
for (Map.Entry<Long,Map<String,Double>> row : rows.entrySet()) {
long time = row.getKey();
Map<String,Double> thisRow = row.getValue();
if (!sameColumnsAsLastTime(thisRow)) { lastValForType.clear(); }
StringBuilder newValues = new StringBuilder();
boolean hasDupes = false;
Set<String> columnsInThisRow = new HashSet<>();
for (Map.Entry<String,Double> column : thisRow.entrySet()) {
String type = column.getKey();
double value = column.getValue();
columnsInThisRow.add(type);
Double lastValue = lastValForType.get(type);
if (lastValue == null || lastValue != value) {
lastValForType.put(type, value);
newValues.append(" ").append(type).append(" ").append(value);
} else {
hasDupes = true;
}
}
statsWriter.print(time);
if (hasDupes) statsWriter.print(" * *");
statsWriter.println(newValues.toString());
columnsInLastRow = columnsInThisRow;
}
statsWriter.flush();
entriesSinceLastFlush.clear();
}
/*------------------------------------------------------------------------------
*
* PRIVATE - Utility methods that aid in the reading and writing of data
* to the external file in a space efficient manner
*
*----------------------------------------------------------------------------*/
private Map<String,String> mergeEntries(String[] newTokens, Map<String,String>lastEntries) {
// If the newTokens contains a "*" column, then start by copying the last
// set of values. If not, start with an empty Map.
Map<String,String> vals = null;
for (String token : newTokens) {
if (token.equals("*")) {
vals = new HashMap<>(lastEntries);
break;
}
}
if (vals == null) vals = new HashMap<>();
// Go through the new tokens and overwrite any previous values for the same type
for (int i = 1; i < newTokens.length; ) {
String type = newTokens[i++];
String value = newTokens[i++];
if (!type.equals("*"))
vals.put(type, value);
}
return vals;
}
/*------------------------------------------------------------------------------
*
* Private Utility Methods and Classes
*
*----------------------------------------------------------------------------*/
private boolean prepRepository() {
releaseWriter();
FileOutputStream fos = obtainStream();
if (fos == null) return false;
statsWriter = new PrintStream(fos);
return (statsWriter != null);
}
private FileOutputStream obtainStream() {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(statsFile, true);
} catch (IOException e) {
logger.warning("Unable to obtain lock on StatsRepository: " + e.toString());
}
return fos;
}
private void releaseWriter() {
if (statsWriter != null) statsWriter.close();
}
private BufferedReader getReaderForFile(File file) {
try {
return new BufferedReader(new FileReader(file));
} catch (FileNotFoundException ex) {
logger.log(Level.INFO, "Could not open file", ex);
}
return null;
}
private String getLineFromReader(BufferedReader rdr) {
try {
return rdr.readLine();
} catch (IOException ex) {
logger.log(Level.INFO, "Failed reading line", ex);
}
return null;
}
}