package org.rrd4j.core;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import org.rrd4j.ConsolFun;
import org.rrd4j.DsType;
/**
* <p>Class to represent definition of new Round Robin Database (RRD).
* Object of this class is used to create
* new RRD from scratch - pass its reference as a <code>RrdDb</code> constructor
* argument (see documentation for {@link org.rrd4j.core.RrdDb RrdDb} class). <code>RrdDef</code>
* object <b>does not</b> actually create new RRD. It just holds all necessary
* information which will be used during the actual creation process.</p>
*
* <p>RRD definition (RrdDef object) consists of the following elements:</p>
*
* <ul>
* <li> path to RRD that will be created
* <li> starting timestamp
* <li> step
* <li> version, 1 for linear disposition of archives, 2 for matrix disposition
* <li> one or more datasource definitions
* <li> one or more archive definitions
* </ul>
* <p>RrdDef provides API to set all these elements. For the complete explanation of all
* RRD definition parameters, see RRDTool's
* <a href="../../../../man/rrdcreate.html" target="man">rrdcreate man page</a>.</p>
*
* @author Sasa Markovic
*/
public class RrdDef {
/**
* Default RRD step to be used if not specified in constructor (300 seconds).
*/
public static final long DEFAULT_STEP = 300L;
/**
* If not specified in constructor, starting timestamp will be set to the
* current timestamp plus DEFAULT_INITIAL_SHIFT seconds (-10).
*/
public static final long DEFAULT_INITIAL_SHIFT = -10L;
/** Constant <code>DEFAULTVERSION=2</code> */
public static final int DEFAULTVERSION = 2;
private URI uri;
private long startTime = Util.getTime() + DEFAULT_INITIAL_SHIFT;
private long step = DEFAULT_STEP;
private int version = DEFAULTVERSION;
private List<DsDef> dsDefs = new ArrayList<DsDef>();
private List<ArcDef> arcDefs = new ArrayList<ArcDef>();
/**
* <p>Creates new RRD definition object with the given path.
* When this object is passed to
* <code>RrdDb</code> constructor, new RRD will be created using the
* specified path.<p>
* <p>The will be transformed internally to an URI using the default backend factory.</p>
*
* @param rrdpath Path to new RRD.
*/
public RrdDef(String rrdpath) {
if (rrdpath == null || rrdpath.length() == 0) {
throw new IllegalArgumentException("No path specified");
}
this.uri = RrdBackendFactory.buildGenericUri(rrdpath);
}
/**
* Creates new RRD definition object with the given path.
* When this object is passed to
* <code>RrdDb</code> constructor, new RRD will be created using the
* specified path.
*
* @param uri URI to the new RRD.
*/
public RrdDef(URI uri) {
this.uri = uri;
}
/**
* <p>Creates new RRD definition object with the given path and step.</p>
* <p>The will be transformed internally to an URI using the default backend factory.</p>
*
* @param path URI to new RRD.
* @param step RRD step.
*/
public RrdDef(String path, long step) {
this(path);
if (step <= 0) {
throw new IllegalArgumentException("Invalid RRD step specified: " + step);
}
this.step = step;
}
/**
* Creates new RRD definition object with the given path and step.
*
* @param uri URI to new RRD.
* @param step RRD step.
*/
public RrdDef(URI uri, long step) {
this(uri);
if (step <= 0) {
throw new IllegalArgumentException("Invalid RRD step specified: " + step);
}
this.step = step;
}
/**
* <p>Creates new RRD definition object with the given path, starting timestamp
* and step.</p>
* <p>The will be transformed internally to an URI using the default backend factory.</p>
*
* @param path Path to new RRD.
* @param startTime RRD starting timestamp.
* @param step RRD step.
*/
public RrdDef(String path, long startTime, long step) {
this(path, step);
if (startTime < 0) {
throw new IllegalArgumentException("Invalid RRD start time specified: " + startTime);
}
this.startTime = startTime;
}
/**
* Creates new RRD definition object with the given path, starting timestamp
* and step.
*
* @param uri URI to new RRD.
* @param startTime RRD starting timestamp.
* @param step RRD step.
*/
public RrdDef(URI uri, long startTime, long step) {
this(uri, step);
if (startTime < 0) {
throw new IllegalArgumentException("Invalid RRD start time specified: " + startTime);
}
this.startTime = startTime;
}
/**
* <p>Creates new RRD definition object with the given path, starting timestamp,
* step and version.</p>
* <p>The will be transformed internally to an URI using the default backend factory.</p>
*
* @param path Path to new RRD.
* @param startTime RRD starting timestamp.
* @param step RRD step.
* @param version RRD's file version.
*/
public RrdDef(String path, long startTime, long step, int version) {
this(path, startTime, step);
if(startTime < 0) {
throw new IllegalArgumentException("Invalid RRD start time specified: " + startTime);
}
this.version = version;
}
/**
* Creates new RRD definition object with the given path, starting timestamp,
* step and version.
*
* @param uri URI to new RRD.
* @param startTime RRD starting timestamp.
* @param step RRD step.
* @param version RRD's file version.
*/
public RrdDef(URI uri, long startTime, long step, int version) {
this(uri, startTime, step);
if(startTime < 0) {
throw new IllegalArgumentException("Invalid RRD start time specified: " + startTime);
}
this.version = version;
}
/**
* Returns path for the new RRD. It's extracted from the URI. If it's an opaque URI, it return the scheme specific part.
*
* @return path to the new RRD which should be created
*/
public String getPath() {
if (uri.isOpaque()) {
return uri.getSchemeSpecificPart();
} else {
return uri.getPath();
}
}
/**
* Returns URI for the new RRD
*
* @return URI to the new RRD which should be created
*/
public URI getUri() {
return uri;
}
/**
* Returns starting time stamp for the RRD that should be created.
*
* @return RRD starting time stamp
*/
public long getStartTime() {
return startTime;
}
/**
* Returns time step for the RRD that will be created.
*
* @return RRD step
*/
public long getStep() {
return step;
}
/**
* Returns the RRD file version
*
* @return the version
*/
public int getVersion() {
return version;
}
/**
* <p>Sets path to RRD.</p>
* <p>The will be transformed internally to an URI using the default backend factory.</p>
*
* @param path path to new RRD.
*/
public void setPath(String path) {
this.uri = RrdBackendFactory.getDefaultFactory().getUri(path);
}
/**
* Sets URI to RRD.
*
* @param uri URI to new RRD.
*/
public void setPath(URI uri) {
this.uri = uri;
}
/**
* Sets RRD's starting timestamp.
*
* @param startTime Starting timestamp.
*/
public void setStartTime(long startTime) {
this.startTime = startTime;
}
/**
* Sets RRD's starting timestamp.
*
* @param date starting date
*/
public void setStartTime(Date date) {
this.startTime = Util.getTimestamp(date);
}
/**
* Sets RRD's starting timestamp.
*
* @param gc starting date
*/
public void setStartTime(Calendar gc) {
this.startTime = Util.getTimestamp(gc);
}
/**
* Sets RRD's time step.
*
* @param step RRD time step.
*/
public void setStep(long step) {
this.step = step;
}
/**
* Sets RRD's file version.
*
* @param version the version to set
*/
public void setVersion(int version) {
this.version = version;
}
/**
* Adds single datasource definition represented with object of class <code>DsDef</code>.
*
* @param dsDef Datasource definition.
*/
public void addDatasource(DsDef dsDef) {
if (dsDefs.contains(dsDef)) {
throw new IllegalArgumentException("Datasource already defined: " + dsDef.dump());
}
dsDefs.add(dsDef);
}
/**
* <p>Adds single datasource to RRD definition by specifying its data source name, source type,
* heartbeat, minimal and maximal value. For the complete explanation of all data
* source definition parameters see RRDTool's
* <a href="../../../../man/rrdcreate.html" target="man">rrdcreate man page</a>.</p>
* <p><b>IMPORTANT NOTE:</b> If datasource name ends with '!', corresponding archives will never
* store NaNs as datasource values. In that case, NaN datasource values will be silently
* replaced with zeros by the framework.</p>
*
* @param dsName Data source name.
* @param dsType Data source type. Valid types are "COUNTER",
* "GAUGE", "DERIVE" and "ABSOLUTE" (these string constants are conveniently defined in
* the {@link org.rrd4j.DsType} class).
* @param heartbeat Data source heartbeat.
* @param minValue Minimal acceptable value. Use <code>Double.NaN</code> if unknown.
* @param maxValue Maximal acceptable value. Use <code>Double.NaN</code> if unknown.
* @throws java.lang.IllegalArgumentException Thrown if new datasource definition uses already used data
* source name.
*/
public void addDatasource(String dsName, DsType dsType, long heartbeat, double minValue, double maxValue) {
addDatasource(new DsDef(dsName, dsType, heartbeat, minValue, maxValue));
}
/**
* <p>Adds single datasource to RRD definition from a RRDTool-like
* datasource definition string. The string must have six elements separated with colons
* (:) in the following order:</p>
* <pre>
* DS:name:type:heartbeat:minValue:maxValue
* </pre>
* <p>For example:</p>
* <pre>
* DS:input:COUNTER:600:0:U
* </pre>
* <p>For more information on datasource definition parameters see <code>rrdcreate</code>
* man page.</p>
*
* @param rrdToolDsDef Datasource definition string with the syntax borrowed from RRDTool.
* @throws java.lang.IllegalArgumentException Thrown if invalid string is supplied.
*/
public void addDatasource(String rrdToolDsDef) {
IllegalArgumentException illArgException = new IllegalArgumentException(
"Wrong rrdtool-like datasource definition: " + rrdToolDsDef);
if (rrdToolDsDef == null) throw illArgException;
StringTokenizer tokenizer = new StringTokenizer(rrdToolDsDef, ":");
if (tokenizer.countTokens() != 6) {
throw illArgException;
}
String[] tokens = new String[6];
for (int curTok = 0; tokenizer.hasMoreTokens(); curTok++) {
tokens[curTok] = tokenizer.nextToken();
}
if (!"DS".equalsIgnoreCase(tokens[0])) {
throw illArgException;
}
String dsName = tokens[1];
DsType dsType = DsType.valueOf(tokens[2]);
long dsHeartbeat;
try {
dsHeartbeat = Long.parseLong(tokens[3]);
}
catch (NumberFormatException nfe) {
throw illArgException;
}
double minValue = Double.NaN;
if (!"U".equalsIgnoreCase(tokens[4])) {
try {
minValue = Double.parseDouble(tokens[4]);
}
catch (NumberFormatException nfe) {
throw illArgException;
}
}
double maxValue = Double.NaN;
if (!"U".equalsIgnoreCase(tokens[5])) {
try {
maxValue = Double.parseDouble(tokens[5]);
}
catch (NumberFormatException nfe) {
throw illArgException;
}
}
addDatasource(new DsDef(dsName, dsType, dsHeartbeat, minValue, maxValue));
}
/**
* Adds data source definitions to RRD definition in bulk.
*
* @param dsDefs Array of data source definition objects.
*/
public void addDatasource(DsDef... dsDefs) {
for (DsDef dsDef : dsDefs) {
addDatasource(dsDef);
}
}
/**
* Adds single archive definition represented with object of class <code>ArcDef</code>.
*
* @param arcDef Archive definition.
* @throws java.lang.IllegalArgumentException Thrown if archive with the same consolidation function
* and the same number of steps is already added.
*/
public void addArchive(ArcDef arcDef) {
if (arcDefs.contains(arcDef)) {
throw new IllegalArgumentException("Archive already defined: " + arcDef.dump());
}
arcDefs.add(arcDef);
}
/**
* Adds archive definitions to RRD definition in bulk.
*
* @param arcDefs Array of archive definition objects
* @throws java.lang.IllegalArgumentException Thrown if RRD definition already contains archive with
* the same consolidation function and the same number of steps.
*/
public void addArchive(ArcDef... arcDefs) {
for (ArcDef arcDef : arcDefs) {
addArchive(arcDef);
}
}
/**
* Adds single archive definition by specifying its consolidation function, X-files factor,
* number of steps and rows. For the complete explanation of all archive
* definition parameters see RRDTool's
* <a href="../../../../man/rrdcreate.html" target="man">rrdcreate man page</a>.
*
* @param consolFun Consolidation function.
* @param xff X-files factor. Valid values are between 0 and 1.
* @param steps Number of archive steps
* @param rows Number of archive rows
* @throws java.lang.IllegalArgumentException Thrown if archive with the same consolidation function
* and the same number of steps is already added.
*/
public void addArchive(ConsolFun consolFun, double xff, int steps, int rows) {
addArchive(new ArcDef(consolFun, xff, steps, rows));
}
/**
* <p>Adds single archive to RRD definition from a RRDTool-like
* archive definition string. The string must have five elements separated with colons
* (:) in the following order:</p>
* <pre>
* RRA:consolidationFunction:XFilesFactor:steps:rows
* </pre>
* <p>For example:</p>
* <pre>
* RRA:AVERAGE:0.5:10:1000
* </pre>
* <p>For more information on archive definition parameters see <code>rrdcreate</code>
* man page.</p>
*
* @param rrdToolArcDef Archive definition string with the syntax borrowed from RRDTool.
* @throws java.lang.IllegalArgumentException Thrown if invalid string is supplied.
*/
public void addArchive(String rrdToolArcDef) {
IllegalArgumentException illArgException = new IllegalArgumentException(
"Wrong rrdtool-like archive definition: " + rrdToolArcDef);
StringTokenizer tokenizer = new StringTokenizer(rrdToolArcDef, ":");
if (tokenizer.countTokens() != 5) {
throw illArgException;
}
String[] tokens = new String[5];
for (int curTok = 0; tokenizer.hasMoreTokens(); curTok++) {
tokens[curTok] = tokenizer.nextToken();
}
if (!"RRA".equalsIgnoreCase(tokens[0])) {
throw illArgException;
}
ConsolFun consolFun = ConsolFun.valueOf(tokens[1]);
double xff;
try {
xff = Double.parseDouble(tokens[2]);
}
catch (NumberFormatException nfe) {
throw illArgException;
}
int steps;
try {
steps = Integer.parseInt(tokens[3]);
}
catch (NumberFormatException nfe) {
throw illArgException;
}
int rows;
try {
rows = Integer.parseInt(tokens[4]);
}
catch (NumberFormatException nfe) {
throw illArgException;
}
addArchive(new ArcDef(consolFun, xff, steps, rows));
}
/**
* Returns all data source definition objects specified so far.
*
* @return Array of data source definition objects
*/
public DsDef[] getDsDefs() {
return dsDefs.toArray(new DsDef[dsDefs.size()]);
}
/**
* Returns all archive definition objects specified so far.
*
* @return Array of archive definition objects.
*/
public ArcDef[] getArcDefs() {
return arcDefs.toArray(new ArcDef[0]);
}
/**
* Returns number of defined datasources.
*
* @return Number of defined datasources.
*/
public int getDsCount() {
return dsDefs.size();
}
/**
* Returns number of defined archives.
*
* @return Number of defined archives.
*/
public int getArcCount() {
return arcDefs.size();
}
/**
* Returns string that represents all specified RRD creation parameters. Returned string
* has the syntax of RRDTool's <code>create</code> command.
*
* @return Dumped content of <code>RrdDb</code> object.
*/
public String dump() {
StringBuilder sb = new StringBuilder("create \"");
sb.append(uri)
.append("\"")
.append(" --version ").append(getVersion())
.append(" --start ").append(getStartTime())
.append(" --step ").append(getStep()).append(" ");
for (DsDef dsDef : dsDefs) {
sb.append(dsDef.dump()).append(" ");
}
for (ArcDef arcDef : arcDefs) {
sb.append(arcDef.dump()).append(" ");
}
return sb.toString().trim();
}
String getRrdToolCommand() {
return dump();
}
void removeDatasource(String dsName) {
for (int i = 0; i < dsDefs.size(); i++) {
DsDef dsDef = dsDefs.get(i);
if (dsDef.getDsName().equals(dsName)) {
dsDefs.remove(i);
return;
}
}
throw new IllegalArgumentException("Could not find datasource named '" + dsName + "'");
}
void saveSingleDatasource(String dsName) {
Iterator<DsDef> it = dsDefs.iterator();
while (it.hasNext()) {
DsDef dsDef = it.next();
if (!dsDef.getDsName().equals(dsName)) {
it.remove();
}
}
}
void removeArchive(ConsolFun consolFun, int steps) {
ArcDef arcDef = findArchive(consolFun, steps);
if (!arcDefs.remove(arcDef)) {
throw new IllegalArgumentException("Could not remove archive " + consolFun + "/" + steps);
}
}
ArcDef findArchive(ConsolFun consolFun, int steps) {
for (ArcDef arcDef : arcDefs) {
if (arcDef.getConsolFun() == consolFun && arcDef.getSteps() == steps) {
return arcDef;
}
}
throw new IllegalArgumentException("Could not find archive " + consolFun + "/" + steps);
}
/**
* <p>Exports RrdDef object to output stream in XML format. Generated XML code can be parsed
* with {@link org.rrd4j.core.RrdDefTemplate} class.</p>
* <p>It use a format compatible with previous RRD4J's version, using
* a path, instead of an URI.</p>
*
* @param out Output stream
*/
public void exportXmlTemplate(OutputStream out) {
exportXmlTemplate(out, true);
}
/**
* Exports RrdDef object to output stream in XML format. Generated XML code can be parsed
* with {@link org.rrd4j.core.RrdDefTemplate} class.
* <p>If <code>compatible</code> is set to true, it returns an XML compatible with previous RRD4J's versions, using
* a path, instead of an URI.</p>
*
* @param out Output stream
* @param compatible Compatible with previous versions.
*/
public void exportXmlTemplate(OutputStream out, boolean compatible) {
XmlWriter xml = new XmlWriter(out);
xml.startTag("rrd_def");
if (compatible) {
xml.writeTag("path", getPath());
} else {
xml.writeTag("uri", getUri());
}
xml.writeTag("step", getStep());
xml.writeTag("start", getStartTime());
// datasources
DsDef[] dsDefs = getDsDefs();
for (DsDef dsDef : dsDefs) {
xml.startTag("datasource");
xml.writeTag("name", dsDef.getDsName());
xml.writeTag("type", dsDef.getDsType());
xml.writeTag("heartbeat", dsDef.getHeartbeat());
xml.writeTag("min", dsDef.getMinValue(), "U");
xml.writeTag("max", dsDef.getMaxValue(), "U");
xml.closeTag(); // datasource
}
ArcDef[] arcDefs = getArcDefs();
for (ArcDef arcDef : arcDefs) {
xml.startTag("archive");
xml.writeTag("cf", arcDef.getConsolFun());
xml.writeTag("xff", arcDef.getXff());
xml.writeTag("steps", arcDef.getSteps());
xml.writeTag("rows", arcDef.getRows());
xml.closeTag(); // archive
}
xml.closeTag(); // rrd_def
xml.flush();
}
/**
* <p>Exports RrdDef object to string in XML format. Generated XML string can be parsed
* with {@link org.rrd4j.core.RrdDefTemplate} class.</p>
* <p>If <code>compatible</code> is set to true, it returns an XML compatible with previous RRD4J's versions, using
* a path, instead of an URI.</p>
*
*
* @param compatible Compatible with previous versions.
* @return XML formatted string representing this RrdDef object
*/
public String exportXmlTemplate(boolean compatible) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
exportXmlTemplate(out, compatible);
return out.toString();
}
/**
* <p>Exports RrdDef object to string in XML format. Generated XML string can be parsed
* with {@link org.rrd4j.core.RrdDefTemplate} class.</p>
* <p>It use a format compatible with previous RRD4J's version, using
* a path, instead of an URI.</p>
*
* @return XML formatted string representing this RrdDef object
*/
public String exportXmlTemplate() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
exportXmlTemplate(out);
return out.toString();
}
/**
* <p>Exports RrdDef object to a file in XML format. Generated XML code can be parsed
* with {@link org.rrd4j.core.RrdDefTemplate} class.</p>
* <p>It use a format compatible with previous RRD4J's version, using
* a path, instead of an URI.</p>
*
* @param filePath Path to the file
* @throws java.io.IOException if any.
*/
public void exportXmlTemplate(String filePath) throws IOException {
exportXmlTemplate(filePath, true);
}
/**
* <p>Exports RrdDef object to a file in XML format. Generated XML code can be parsed
* with {@link org.rrd4j.core.RrdDefTemplate} class.</p>
* <p>If <code>compatible</code> is set to true, it returns an XML compatible with previous RRD4J versions, using
* a path, instead of an URI.</p>
*
* @param filePath Path to the file
* @param compatible Compatible with previous versions.
* @throws java.io.IOException if any.
*/
public void exportXmlTemplate(String filePath, boolean compatible) throws IOException {
FileOutputStream out = new FileOutputStream(filePath, false);
exportXmlTemplate(out, compatible);
out.close();
}
/**
* Returns the number of storage bytes required to create RRD from this
* RrdDef object.
*
* @return Estimated byte count of the underlying RRD storage.
*/
public long getEstimatedSize() {
int dsCount = dsDefs.size();
int arcCount = arcDefs.size();
int rowsCount = 0;
for (ArcDef arcDef : arcDefs) {
rowsCount += arcDef.getRows();
}
String[] dsNames = new String[dsCount];
for (int i = 0; i < dsNames.length ; i++) {
dsNames[i] = dsDefs.get(i).getDsName();
}
return calculateSize(dsCount, arcCount, rowsCount, dsNames);
}
static long calculateSize(int dsCount, int arcCount, int rowsCount, String[] dsNames) {
int postStorePayload = 0;
for(String n: dsNames) {
if (n.length() > RrdPrimitive.STRING_LENGTH) {
postStorePayload += n.length() * 2 + Short.SIZE / 8;
}
}
return (24L + 48L * dsCount + 16L * arcCount +
20L * dsCount * arcCount + 8L * dsCount * rowsCount) +
(1L + 2L * dsCount + arcCount) * 2L * RrdPrimitive.STRING_LENGTH +
postStorePayload;
}
/**
* {@inheritDoc}
*
* <p>Compares the current RrdDef with another. RrdDefs are considered equal if:</p>
* <ul>
* <li>RRD steps match
* <li>all datasources have exactly the same definition in both RrdDef objects (datasource names,
* types, heartbeat, min and max values must match)
* <li>all archives have exactly the same definition in both RrdDef objects (archive consolidation
* functions, X-file factors, step and row counts must match)
* </ul>
*/
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof RrdDef)) {
return false;
}
RrdDef rrdDef2 = (RrdDef) obj;
// check primary RRD step
if (step != rrdDef2.step) {
return false;
}
// check datasources
DsDef[] dsDefs = getDsDefs(), dsDefs2 = rrdDef2.getDsDefs();
if (dsDefs.length != dsDefs2.length) {
return false;
}
for (DsDef dsDef : dsDefs) {
boolean matched = false;
for (DsDef aDsDefs2 : dsDefs2) {
if (dsDef.exactlyEqual(aDsDefs2)) {
matched = true;
break;
}
}
// this datasource could not be matched
if (!matched) {
return false;
}
}
// check archives
ArcDef[] arcDefs = getArcDefs(), arcDefs2 = rrdDef2.getArcDefs();
if (arcDefs.length != arcDefs2.length) {
return false;
}
for (ArcDef arcDef : arcDefs) {
boolean matched = false;
for (ArcDef anArcDefs2 : arcDefs2) {
if (arcDef.exactlyEqual(anArcDefs2)) {
matched = true;
break;
}
}
// this archive could not be matched
if (!matched) {
return false;
}
}
// everything matches
return true;
}
/**
* <p>hasDatasources.</p>
*
* @return a boolean.
*/
public boolean hasDatasources() {
return !dsDefs.isEmpty();
}
/**
* <p>hasArchives.</p>
*
* @return a boolean.
*/
public boolean hasArchives() {
return !arcDefs.isEmpty();
}
/**
* Removes all datasource definitions.
*/
public void removeDatasources() {
dsDefs.clear();
}
/**
* Removes all RRA archive definitions.
*/
public void removeArchives() {
arcDefs.clear();
}
}