package org.rrd4j.core.jrrd; import java.io.Closeable; 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.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * 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: 1.1 $ */ public class RRDatabase implements Closeable { final RRDFile rrdFile; // RRD file name private final String name; final Header header; private final ArrayList<DataSource> dataSources; private final ArrayList<Archive> archives; /** Timestamp of last data modification */ final Date lastUpdate; /** Data source name to index */ private final Map<String, Integer> nameindex; /** * Creates a database to read from. * * @param name the filename of the file to read from. * @throws java.io.IOException if an I/O error occurs. */ public RRDatabase(String name) throws IOException { this(new File(name)); } /** * Creates a database to read from. * * @param file the file to read from. * @throws java.io.IOException if an I/O error occurs. */ public RRDatabase(File file) throws IOException { /* * read the raw data according to the c-structure rrd_t (from rrd source * distribution file rrd_format.h) */ name = file.getName(); rrdFile = new RRDFile(file); header = new Header(rrdFile); nameindex = new HashMap<String, Integer>(header.dsCount); // Load the data sources dataSources = new ArrayList<DataSource>(header.dsCount); for (int i = 0; i < header.dsCount; i++) { DataSource ds = new DataSource(rrdFile); nameindex.put(ds.getName(), i); dataSources.add(ds); } // Load the archives archives = new ArrayList<Archive>(header.rraCount); for (int i = 0; i < header.rraCount; i++) { Archive archive = new Archive(this); archives.add(archive); } long last_up = (long) rrdFile.readLong() * 1000; /* rrd v >= 3 last_up with us */ if (header.getVersionAsInt() >= Constants.VERSION_WITH_LAST_UPDATE_SEC) { long last_up_usec = rrdFile.readLong(); last_up += last_up_usec / 1000; } lastUpdate = new Date(last_up); // 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; } /** * <p>getDataSourcesName.</p> * * @return a {@link java.util.Set} object. */ public Set<String> getDataSourcesName() { return nameindex.keySet(); } /** * 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); } /** * <p>getArchive.</p> * * @param name a {@link java.lang.String} object. * @return a {@link org.rrd4j.core.jrrd.Archive} object. */ public Archive getArchive(String name) { return archives.get(nameindex.get(name)); } /** * 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 (Archive archive : archives) { if (archive.getType().equals(type)) { subset.add(archive); } } return subset; } /** * Closes this database stream and releases any associated system resources. * * @throws java.io.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 java.lang.IllegalArgumentException if there was a problem locating a data archive with * the requested consolidation function. * @throws java.io.IOException if there was a problem reading data from the database. */ public DataChunk getData(ConsolidationFunctionType type) throws IOException { Calendar endCal = Calendar.getInstance(); endCal.set(Calendar.MILLISECOND, 0); Calendar startCal = (Calendar) endCal.clone(); startCal.add(Calendar.DATE, -1); return getData(type, startCal.getTime(), endCal.getTime(), 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 java.lang.IllegalArgumentException if there was a problem locating a data archive with * the requested consolidation function. * @throws java.io.IOException if there was a problem reading data from the database. * @param startDate a {@link java.util.Date} object. * @param endDate a {@link java.util.Date} object. */ public DataChunk getData(ConsolidationFunctionType type, Date startDate, Date endDate, long step) throws IOException { long end = endDate.getTime() / 1000; long start = startDate.getTime() / 1000; return getData(type, start, end, step); } /** * <p>getData.</p> * * @param type a {@link org.rrd4j.core.jrrd.ConsolidationFunctionType} object. * @param startTime seconds since epoch * @param endTime seconds since epoch * @param stepSeconds in seconds * @return a {@link org.rrd4j.core.jrrd.DataChunk} object. * @throws java.io.IOException if any. */ public DataChunk getData(ConsolidationFunctionType type, long startTime, long endTime, long stepSeconds) throws IOException { ArrayList<Archive> possibleArchives = getArchiveList(type); if (possibleArchives.size() == 0) { throw new IllegalArgumentException("Database does not contain an Archive of consolidation function type " + type); } Archive archive = findBestArchive(startTime, endTime, stepSeconds, possibleArchives); // Tune the parameters stepSeconds = header.pdpStep * archive.pdpCount; startTime -= startTime % stepSeconds; if (endTime % stepSeconds != 0) { endTime += stepSeconds - endTime % stepSeconds; } int rows = (int) ((endTime - startTime) / stepSeconds + 1); // 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 % stepSeconds); long archiveStartTime = archiveEndTime - (stepSeconds * (archive.rowCount - 1)); int startOffset = (int) ((startTime - archiveStartTime) / stepSeconds); int endOffset = (int) ((archiveEndTime - endTime) / stepSeconds); DataChunk chunk = new DataChunk(nameindex, startTime, startOffset, endOffset, stepSeconds, 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 bestStepDiff = 0; long tmpStepDiff; for (Archive archive1 : archives) { archive = archive1; 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 // optimize 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 (DataSource ds : dataSources) { ds.printInfo(s, numberFormat); } int index = 0; for (Archive archive : archives) { 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> * * A flush is issued at the end of the XML generation, so auto flush of the PrintStream can be set to false * * @param s the PrintStream to send the XML to. */ public void toXml(PrintStream s) { 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>"); s.flush(); } /** * Returns a summary the contents of this database. * * @return a summary of the information contained in this database. */ public String toString() { String endianness; if(rrdFile.isBigEndian()) endianness = "Big"; else endianness = "Little"; StringBuilder sb = new StringBuilder(endianness + " endian" + ", " + rrdFile.getBits() + " bits\n"); sb.append(header.toString()); sb.append(", lastupdate: "); sb.append(lastUpdate.getTime() / 1000); for (DataSource ds : dataSources) { sb.append("\n\t"); sb.append(ds.toString()); } for (Archive archive : archives) { sb.append("\n\t"); sb.append(archive.toString()); } return sb.toString(); } }