/*******************************************************************************
* Copyright (c) 2001-2005 Sasa Markovic and Ciaran Treanor.
* Copyright (c) 2011 The OpenNMS Group, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*******************************************************************************/
package org.jrobin.core.jrrd;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import org.jrobin.core.RrdException;
/**
* Instances of this class model
* <a href="http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/">Round Robin Database</a>
* (RRD) files.
*
* @author <a href="mailto:ciaran@codeloop.com">Ciaran Treanor</a>
* @version $Revision$
*/
public class RRDatabase {
RRDFile rrdFile;
// RRD file name
private String name;
Header header;
ArrayList<DataSource> dataSources;
ArrayList<Archive> archives;
Date lastUpdate;
/**
* Creates a database to read from.
*
* @param name the filename of the file to read from.
* @throws IOException if an I/O error occurs.
*/
public RRDatabase(String name) throws IOException,RrdException {
this(new File(name));
}
/**
* Creates a database to read from.
*
* @param file the file to read from.
* @throws IOException if an I/O error occurs.
*/
public RRDatabase(File file) throws IOException,RrdException {
name = file.getName();
rrdFile = new RRDFile(file);
header = new Header(rrdFile);
// Load the data sources
dataSources = new ArrayList<DataSource>();
for (int i = 0; i < header.dsCount; i++) {
DataSource ds = new DataSource(rrdFile);
dataSources.add(ds);
}
// Load the archives
archives = new ArrayList<Archive>();
for (int i = 0; i < header.rraCount; i++) {
Archive archive = new Archive(this);
archives.add(archive);
}
rrdFile.align();
long timestamp = (long)(rrdFile.readInt()) * 1000;
if(header.getIntVersion() >= 3) {
//Version 3 has an additional microsecond field
int microSeconds = rrdFile.readInt();
timestamp += (microSeconds/1000); //Date only does up to milliseconds
}
lastUpdate = new Date( timestamp );
// Load PDPStatus(s)
for (int i = 0; i < header.dsCount; i++) {
DataSource ds = dataSources.get(i);
ds.loadPDPStatusBlock(rrdFile);
}
// Load CDPStatus(s)
for (int i = 0; i < header.rraCount; i++) {
Archive archive = archives.get(i);
archive.loadCDPStatusBlocks(rrdFile, header.dsCount);
}
// Load current row information for each archive
for (int i = 0; i < header.rraCount; i++) {
Archive archive = archives.get(i);
archive.loadCurrentRow(rrdFile);
}
// Now load the data
for (int i = 0; i < header.rraCount; i++) {
Archive archive = archives.get(i);
archive.loadData(rrdFile, header.dsCount);
}
}
/**
* Returns the <code>Header</code> for this database.
*
* @return the <code>Header</code> for this database.
*/
public Header getHeader() {
return header;
}
/**
* Returns the date this database was last updated. To convert this date to
* the form returned by <code>rrdtool last</code> call Date.getTime() and
* divide the result by 1000.
*
* @return the date this database was last updated.
*/
public Date getLastUpdate() {
return lastUpdate;
}
/**
* Returns the <code>DataSource</code> at the specified position in this database.
*
* @param index index of <code>DataSource</code> to return.
* @return the <code>DataSource</code> at the specified position in this database
*/
public DataSource getDataSource(int index) {
return dataSources.get(index);
}
/**
* Returns an iterator over the data sources in this database in proper sequence.
*
* @return an iterator over the data sources in this database in proper sequence.
*/
public Iterator<DataSource> getDataSources() {
return dataSources.iterator();
}
/**
* Returns the <code>Archive</code> at the specified position in this database.
*
* @param index index of <code>Archive</code> to return.
* @return the <code>Archive</code> at the specified position in this database.
*/
public Archive getArchive(int index) {
return archives.get(index);
}
/**
* Returns an iterator over the archives in this database in proper sequence.
*
* @return an iterator over the archives in this database in proper sequence.
*/
public Iterator<Archive> getArchives() {
return archives.iterator();
}
/**
* Returns the number of archives in this database.
*
* @return the number of archives in this database.
*/
public int getNumArchives() {
return header.rraCount;
}
/**
* Returns an iterator over the archives in this database of the given type
* in proper sequence.
*
* @param type the consolidation function that should have been applied to
* the data.
* @return an iterator over the archives in this database of the given type
* in proper sequence.
*/
public Iterator<Archive> getArchives(ConsolidationFunctionType type) {
return getArchiveList(type).iterator();
}
ArrayList<Archive> getArchiveList(ConsolidationFunctionType type) {
ArrayList<Archive> subset = new ArrayList<Archive>();
for (int i = 0; i < archives.size(); i++) {
Archive archive = archives.get(i);
if (archive.getType().equals(type)) {
subset.add(archive);
}
}
return subset;
}
/**
* Closes this database stream and releases any associated system resources.
*
* @throws IOException if an I/O error occurs.
*/
public void close() throws IOException {
rrdFile.close();
}
/**
* Outputs the header information of the database to the given print stream
* using the default number format. The default format for <code>double</code>
* is 0.0000000000E0.
*
* @param s the PrintStream to print the header information to.
*/
public void printInfo(PrintStream s) {
NumberFormat numberFormat = new DecimalFormat("0.0000000000E0");
printInfo(s, numberFormat);
}
/**
* Returns data from the database corresponding to the given consolidation
* function and a step size of 1.
*
* @param type the consolidation function that should have been applied to
* the data.
* @return the raw data.
* @throws RrdException if there was a problem locating a data archive with
* the requested consolidation function.
* @throws IOException if there was a problem reading data from the database.
*/
public DataChunk getData(ConsolidationFunctionType type)
throws RrdException, IOException {
return getData(type, 1L);
}
/**
* Returns data from the database corresponding to the given consolidation
* function.
*
* @param type the consolidation function that should have been applied to
* the data.
* @param step the step size to use.
* @return the raw data.
* @throws RrdException if there was a problem locating a data archive with
* the requested consolidation function.
* @throws IOException if there was a problem reading data from the database.
*/
public DataChunk getData(ConsolidationFunctionType type, long step)
throws RrdException, IOException {
ArrayList<Archive> possibleArchives = getArchiveList(type);
if (possibleArchives.size() == 0) {
throw new RrdException("Database does not contain an Archive of consolidation function type "
+ type);
}
Calendar endCal = Calendar.getInstance();
endCal.set(Calendar.MILLISECOND, 0);
Calendar startCal = (Calendar) endCal.clone();
startCal.add(Calendar.DATE, -1);
long end = endCal.getTime().getTime() / 1000;
long start = startCal.getTime().getTime() / 1000;
Archive archive = findBestArchive(start, end, step, possibleArchives);
// Tune the parameters
step = header.pdpStep * archive.pdpCount;
start -= start % step;
if (end % step != 0) {
end += step - end % step;
}
int rows = (int) ((end - start) / step + 1);
//cat.debug("start " + start + " end " + end + " step " + step + " rows "
// + rows);
// Find start and end offsets
// This is terrible - some of this should be encapsulated in Archive - CT.
long lastUpdateLong = lastUpdate.getTime() / 1000;
long archiveEndTime = lastUpdateLong - (lastUpdateLong % step);
long archiveStartTime = archiveEndTime - (step * (archive.rowCount - 1));
int startOffset = (int) ((start - archiveStartTime) / step);
int endOffset = (int) ((archiveEndTime - end) / step);
//cat.debug("start " + archiveStartTime + " end " + archiveEndTime
// + " startOffset " + startOffset + " endOffset "
// + (archive.rowCount - endOffset));
DataChunk chunk = new DataChunk(start, startOffset, endOffset, step,
header.dsCount, rows);
archive.loadData(chunk);
return chunk;
}
/*
* This is almost a verbatim copy of the original C code by Tobias Oetiker.
* I need to put more of a Java style on it - CT
*/
private Archive findBestArchive(long start, long end, long step,
ArrayList<Archive> archives) {
Archive archive = null;
Archive bestFullArchive = null;
Archive bestPartialArchive = null;
long lastUpdateLong = lastUpdate.getTime() / 1000;
int firstPart = 1;
int firstFull = 1;
long bestMatch = 0;
//long bestPartRRA = 0;
long bestStepDiff = 0;
long tmpStepDiff = 0;
for (int i = 0; i < archives.size(); i++) {
archive = archives.get(i);
long calEnd = lastUpdateLong
- (lastUpdateLong
% (archive.pdpCount * header.pdpStep));
long calStart = calEnd
- (archive.pdpCount * archive.rowCount
* header.pdpStep);
long fullMatch = end - start;
if ((calEnd >= end) && (calStart < start)) { // Best full match
tmpStepDiff = Math.abs(step - (header.pdpStep * archive.pdpCount));
if ((firstFull != 0) || (tmpStepDiff < bestStepDiff)) {
firstFull = 0;
bestStepDiff = tmpStepDiff;
bestFullArchive = archive;
}
}
else { // Best partial match
long tmpMatch = fullMatch;
if (calStart > start) {
tmpMatch -= calStart - start;
}
if (calEnd < end) {
tmpMatch -= end - calEnd;
}
if ((firstPart != 0) || (bestMatch < tmpMatch)) {
firstPart = 0;
bestMatch = tmpMatch;
bestPartialArchive = archive;
}
}
}
// See how the matching went
// optimise this
if (firstFull == 0) {
archive = bestFullArchive;
}
else if (firstPart == 0) {
archive = bestPartialArchive;
}
return archive;
}
/**
* Outputs the header information of the database to the given print stream
* using the given number format. The format is almost identical to that
* produced by
* <a href="http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/manual/rrdinfo.html">rrdtool info</a>
*
* @param s the PrintStream to print the header information to.
* @param numberFormat the format to print <code>double</code>s as.
*/
public void printInfo(PrintStream s, NumberFormat numberFormat) {
s.print("filename = \"");
s.print(name);
s.println("\"");
s.print("rrd_version = \"");
s.print(header.version);
s.println("\"");
s.print("step = ");
s.println(header.pdpStep);
s.print("last_update = ");
s.println(lastUpdate.getTime() / 1000);
for (Iterator<DataSource> i = dataSources.iterator(); i.hasNext();) {
DataSource ds = i.next();
ds.printInfo(s, numberFormat);
}
int index = 0;
for (Iterator<Archive> i = archives.iterator(); i.hasNext();) {
Archive archive = i.next();
archive.printInfo(s, numberFormat, index++);
}
}
/**
* Outputs the content of the database to the given print stream
* as a stream of XML. The XML format is almost identical to that produced by
* <a href="http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/manual/rrddump.html">rrdtool dump</a>
*
* @param s the PrintStream to send the XML to.
*/
public void toXml(PrintStream s) throws RrdException {
s.println("<!--");
s.println(" -- Round Robin RRDatabase Dump ");
s.println(" -- Generated by jRRD <ciaran@codeloop.com>");
s.println(" -->");
s.println("<rrd>");
s.print("\t<version> ");
s.print(header.version);
s.println(" </version>");
s.print("\t<step> ");
s.print(header.pdpStep);
s.println(" </step> <!-- Seconds -->");
s.print("\t<lastupdate> ");
s.print(lastUpdate.getTime() / 1000);
s.print(" </lastupdate> <!-- ");
s.print(lastUpdate.toString());
s.println(" -->");
s.println();
for (int i = 0; i < header.dsCount; i++) {
DataSource ds = dataSources.get(i);
ds.toXml(s);
}
s.println("<!-- Round Robin Archives -->");
for (int i = 0; i < header.rraCount; i++) {
Archive archive = archives.get(i);
archive.toXml(s);
}
s.println("</rrd>");
}
/**
* Returns a summary the contents of this database.
*
* @return a summary of the information contained in this database.
*/
public String toString() {
StringBuffer sb = new StringBuffer("\n");
sb.append(header.toString());
for (Iterator<DataSource> i = dataSources.iterator(); i.hasNext();) {
DataSource ds = i.next();
sb.append("\n\t");
sb.append(ds.toString());
}
for (Iterator<Archive> i = archives.iterator(); i.hasNext();) {
Archive archive = i.next();
sb.append("\n\t");
sb.append(archive.toString());
}
return sb.toString();
}
}