Add missing parts of rrd4j 3.5 omitted from previous checkin (ticket #2684)

Apache 2.0 and LGPLv2.1
This commit is contained in:
zzz
2020-02-25 15:15:23 +00:00
parent 88a4261b03
commit ab28ee960e
76 changed files with 15137 additions and 2 deletions

View File

@ -0,0 +1,79 @@
/*
* Copyright 2010 Tom Gibara
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.tomgibara.crinch.hashing;
import java.math.BigInteger;
/**
* <p>
* Implementations of this interface can generate one hash value for a given
* object. Depending upon the implementation, null values may be supported.
* </p>
*
* @author tomgibara
*
* @param <T>
* the type of objects for which hashes may be generated
*/
public interface Hash<T> {
HashRange getRange();
/**
* The hash value as a {@link BigInteger}. This method may be useful in
* circumstances where the generated hash is too large to be accomodated in
* a single primitive value, eg. if cryptographic hashes are being used.
*
* @param value
* the object to be hashed
* @return the object's hash code, never null
* @throws IllegalArgumentException
* if the value cannot be hashed
*/
BigInteger hashAsBigInt(T value) throws IllegalArgumentException;
/**
* The hash value as an int. This method should provide better performance
* for integer-ranged hashes. This value is not guaranteed to lie within the
* indicated {@link com.tomgibara.crinch.hashing.HashRange}.
*
* @param value
* the object to be hashed
* @return the object's hash code
* @throws IllegalArgumentException
* if the value cannot be hashed
*/
int hashAsInt(T value) throws IllegalArgumentException;
/**
* The hash value as a long. This method should provide better performance
* for long-ranged hashes. This value is not guaranteed to lie within the
* indicated {@link com.tomgibara.crinch.hashing.HashRange}.
*
* @param value
* the object to be hashed
* @return the object's hash code
* @throws IllegalArgumentException
* if the value cannot be hashed
*/
long hashAsLong(T value) throws IllegalArgumentException;
}

View File

@ -0,0 +1,136 @@
/*
* Copyright 2010 Tom Gibara
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.tomgibara.crinch.hashing;
import java.math.BigInteger;
/**
* Records the range of values that a hash value may take. Both range values are inclusive.
*
* @author tomgibara
*
*/
//TODO really need to revisit the inclusivity of maximum bound
public class HashRange {
// statics
private static final BigInteger INT_MINIMUM = BigInteger.valueOf(Integer.MIN_VALUE);
private static final BigInteger INT_MAXIMUM = BigInteger.valueOf(Integer.MAX_VALUE);
private static final BigInteger LONG_MINIMUM = BigInteger.valueOf(Long.MIN_VALUE);
private static final BigInteger LONG_MAXIMUM = BigInteger.valueOf(Long.MAX_VALUE);
public static final HashRange FULL_INT_RANGE = new HashRange(INT_MINIMUM, INT_MAXIMUM);
public static final HashRange POSITIVE_INT_RANGE = new HashRange(BigInteger.ONE, INT_MAXIMUM);
public static final HashRange FULL_LONG_RANGE = new HashRange(LONG_MINIMUM, LONG_MAXIMUM);
public static final HashRange POSITIVE_LONG_RANGE = new HashRange(BigInteger.ONE, LONG_MAXIMUM);
// fields
private final BigInteger minimum;
private final BigInteger maximum;
private final boolean intBounded;
private final boolean longBounded;
private BigInteger size = null;
private Boolean intSized = null;
private Boolean longSized = null;
// constructors
public HashRange(BigInteger minimum, BigInteger maximum) {
if (minimum == null) throw new IllegalArgumentException();
if (maximum == null) throw new IllegalArgumentException();
if (minimum.compareTo(maximum) > 0) throw new IllegalArgumentException();
this.minimum = minimum;
this.maximum = maximum;
intBounded = minimum.compareTo(INT_MINIMUM) >= 0 && maximum.compareTo(INT_MAXIMUM) <= 0;
longBounded = minimum.compareTo(LONG_MINIMUM) >= 0 && maximum.compareTo(LONG_MAXIMUM) <= 0;
// defer size related work - don't want to mem alloc in constructor
}
public HashRange(int minimum, int maximum) {
this(BigInteger.valueOf(minimum), BigInteger.valueOf(maximum));
}
public HashRange(long minimum, long maximum) {
this(BigInteger.valueOf(minimum), BigInteger.valueOf(maximum));
}
// accessors
public boolean isZeroBased() {
return minimum.signum() == 0;
}
public boolean isIntBounded() {
return intBounded;
}
public boolean isLongBounded() {
return longBounded;
}
public BigInteger getMinimum() {
return minimum;
}
public BigInteger getMaximum() {
return maximum;
}
public BigInteger getSize() {
return size == null ? size = maximum.subtract(minimum).add(BigInteger.ONE) : size;
}
public boolean isIntSized() {
if (intSized == null) intSized = getSize().compareTo(INT_MAXIMUM) <= 0;
return intSized;
}
public boolean isLongSized() {
if (longSized == null) longSized = getSize().compareTo(LONG_MAXIMUM) <= 0;
return longSized;
}
// methods
public HashRange zeroBased() {
return isZeroBased() ? this : new HashRange(BigInteger.ZERO, maximum.subtract(minimum));
}
// object methods
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (!(obj instanceof HashRange)) return false;
HashRange that = (HashRange) obj;
return this.minimum.equals(that.minimum) && this.maximum.equals(that.maximum);
}
@Override
public int hashCode() {
return minimum.hashCode() ^ 7 * maximum.hashCode();
}
@Override
public String toString() {
return "[" + minimum + ", " + maximum + "]";
}
}

View File

@ -0,0 +1,317 @@
/*
* Copyright 2010 Tom Gibara
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.tomgibara.crinch.hashing;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Comparator;
/**
* <p>
* A "minimal perfect hash" for Strings. After construction with an array of
* <em>n</em> unique non-null strings, an instance of this class will return a
* unique hash value <em>h</em> (0 &lt;= h &lt; n) for any string <em>s</em> in the
* array. A negative has value will typically be returned for a string that is
* not in the array.
* </p>
*
* <p>
* However, the supplied array is <em>not</em> retained. This means that the
* implementation cannot necessarily confirm that a string is not in the
* supplied array. Where this implementation cannot distinguish that a string is
* not in the array, a 'valid' hash value may be returned. Under no
* circumstances will a hash value be returned that is greater than or equal to
* <em>n</em>.
* </p>
*
* <p>
* <strong>IMPORTANT NOTE:</strong> The array of strings supplied to the
* constructor will be mutated: it is re-ordered so that
* <code>hash(a[i]) == i</code>. Application code must generally use this
* information to map hash values back onto the appropriate string value.
* </p>
*
* <p>
* <strong>NOTE:</strong> Good performance of this algorithm is predicated on
* string hash values being cached by the <code>String</code> class. Experience
* indicates that is is a good assumption.
* </p>
*
*
* @author Tom Gibara
*/
public class PerfectStringHash implements Hash<String> {
// statics
/**
* Comparator used to order the supplied string array. Hashcodes take
* priority, we will do a binary search on those. Otherwise, lengths take
* priority over character ordering because the hash algorithm prefers to
* compare lengths, it's faster.
*/
private static final Comparator<String> comparator = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
final int h1 = s1.hashCode();
final int h2 = s2.hashCode();
if (h1 == h2) {
final int d = s1.length() - s2.length();
return d == 0 ? s1.compareTo(s2) : d;
}
return h1 < h2 ? -1 : 1;
}
};
/**
* Builds a (typically v. small) decision tree for distinguishing strings
* that share the same hash value.
*
* @param values
* the string values to distinguish
* @param start
* the index from which the values should be read
* @param length
* the number of string values that need to be distinguished
* @param pivots
* the array that will hold our decision nodes
* @param pivotIndex
* the index at which the tree should be written
*/
private static void generatePivots(String[] values, int start, int length, int[] pivots, int pivotIndex) {
final int capacity = Integer.highestOneBit(length - 1) << 1;
final int depth = Integer.numberOfTrailingZeros(capacity);
pivots[ pivotIndex << 1 ] = depth;
pivots[(pivotIndex << 1) + 1] = length;
pivotIndex++;
//build the array
for (int i = 0; i < depth; i++) {
int step = capacity >> i;
for (int j = (1 << (depth-i-1)) - 1; j < capacity; j += step) {
final int part;
final int comp;
if (j >= length - 1) {
part = Integer.MIN_VALUE;
comp = 0;
} else {
final String v1 = values[start + j];
final String v2 = values[start + j + 1];
final int l1 = v1.length();
final int l2 = v2.length();
if (l1 == l2) {
int tPart = -1;
int tComp = -1;
for (int k = 0; k < l1; k++) {
final char c1 = v1.charAt(k);
final char c2 = v2.charAt(k);
if (c1 == c2) continue;
if (c1 < c2) { //must occur at some point because we have already checked that the two strings are unequal
tPart = k;
tComp = c1;
} else {
//shouldn't be possible - we've sorted the strings to avoid this case
throw new IllegalStateException();
}
break;
}
//check if we've been passed a duplicated value
if (tPart == -1) throw new IllegalArgumentException("duplicate value: " + v1);
part = tPart;
comp = tComp;
} else {
part = -1;
comp = l1;
}
}
pivots[ pivotIndex<<1 ] = part;
pivots[(pivotIndex<<1) + 1] = comp;
pivotIndex++;
}
}
}
// fields
/**
* The hashcodes of the supplied strings.
*/
private final int[] hashes;
/**
* Stores two ints for every string, an offset into the pivot array (-1 if
* not necessary) and the depth of the decision tree that is rooted there.
*/
private final int[] offsets;
/**
* Stores two ints for every decision, the index at which a character
* comparison needs to be made, followed by the character value to be
* compared against; or -1 to indicate a length comparison, followed by the
* length to be compared against.
*/
private final int[] pivots;
/**
* Cache a range object which indicates the range of hash values generated.
*/
private final HashRange range;
/**
* Constructs a minimal perfect string hashing over the supplied strings.
*
* @param values
* an array of unique non-null strings that will be reordered
* such that <code>hash(values[i]) == i</code>.
*/
public PerfectStringHash(final String values[]) {
final int length = values.length;
if (length == 0) throw new IllegalArgumentException("No values supplied");
final int[] hashes = new int[length];
final int[] offsets = new int[2 * length];
final int[] runLengths = new int[length];
//sort values so that we can assume ordering by hashcode, length and char[]
Arrays.sort(values, comparator);
//pull the hashcodes into an array for analysis
for (int i = 0; i < length; i++) hashes[i] = values[i].hashCode();
//test for unique hashes
int offset = 0;
if (length > 1) {
int previousHash = hashes[0];
int runLength = 1;
for (int i = 1; i <= length; i++) {
int currentHash = i == length ? ~previousHash : hashes[i];
if (currentHash == previousHash) {
runLength++;
} else {
if (runLength > 1) {
final int firstIndex = i - runLength;
for (int j = i - 1; j >= firstIndex; j--) {
runLengths[j] = runLength;
//offset points to the first node in decision tree
offsets[ j<<1 ] = offset;
//adjustment is number of indices to first duplicate
offsets[(j<<1) + 1] = j - firstIndex;
}
//extra one for recording depth
offset += (Integer.highestOneBit(runLength - 1) << 1);
runLength = 1;
} else {
runLengths[i-1] = 1;
offsets[(i-1)<<1] = -1;
}
}
previousHash = currentHash;
}
}
//shortcut for when all hashes are unique
if (offset == 0) {
this.hashes = hashes;
this.offsets = null;
this.pivots = null;
this.range = new HashRange(0, length - 1);
return;
}
//build the decision trees
final int[] pivots = new int[offset * 2];
for (int i = 0; i < length;) {
final int runLength = runLengths[i];
if (runLength > 1) generatePivots(values, i, runLength, pivots, (int) offsets[i << 1]);
i += runLength;
}
//setup our state
this.pivots = pivots;
this.offsets = offsets;
this.hashes = hashes;
this.range = new HashRange(0, length - 1);
}
// hash generator methods
@Override
public HashRange getRange() {
return range;
}
@Override
public BigInteger hashAsBigInt(String value) {
return BigInteger.valueOf(hash(value));
}
//TODO decide whether to throw an IAE if -1 is returned from hash
@Override
public int hashAsInt(String value) {
return hash(value);
}
@Override
public long hashAsLong(String value) {
return hash(value);
}
/**
* Generates a hashcode for the supplied string.
*
* @param value
* any string, not null
* @return a minimal hashcode for the supplied string, or -1
*/
private int hash(String value) {
final int h = value.hashCode();
final int index = Arrays.binarySearch(hashes, h);
final int[] pivots = this.pivots;
if (pivots == null || index < 0) return index;
final int offset = offsets[index << 1];
if (offset == -1) return index;
final int depth = pivots[(offset << 1) ];
final int count = pivots[(offset << 1) + 1];
int i = 0;
for (int d = 0; d < depth; d++) {
final int t = (offset + (1 << d) + i) << 1;
final int part = pivots[t ];
final int comp = pivots[t + 1];
final boolean right;
if (part == Integer.MIN_VALUE) { //easy case - no right value
right = false;
} else if (part == -1) { //compare length
right = value.length() > comp;
} else { //lengths are equal, compare character
right = value.charAt(part) > (char) comp;
}
i <<= 1;
if (right) i++;
}
return i >= count ? -1 : index + i - offsets[(index << 1) + 1];
}
}

View File

@ -0,0 +1,133 @@
package org.rrd4j.core;
import org.rrd4j.ConsolFun;
/**
* Class to represent single archive definition within the RRD.
* Archive definition consists of the following four elements:
*
* <ul>
* <li>consolidation function
* <li>X-files factor
* <li>number of steps
* <li>number of rows.
* </ul>
* For the complete explanation of all archive definition parameters, see RRDTool's
* <a href="http://oss.oetiker.ch/rrdtool/doc/rrdcreate.en.html" target="man">rrdcreate man page</a>
*
*
* @author Sasa Markovic
*/
public class ArcDef {
private final ConsolFun consolFun;
private final double xff;
private final int steps;
private int rows;
/**
* Creates new archive definition object. This object should be passed as argument to
* {@link org.rrd4j.core.RrdDef#addArchive(ArcDef) addArchive()} method of
* {@link RrdDb RrdDb} object.
* <p>For the complete explanation of all archive definition parameters, see RRDTool's
* <a href="http://oss.oetiker.ch/rrdtool/doc/rrdcreate.en.html" target="man">rrdcreate man page</a></p>
*
* @param consolFun Consolidation function. Allowed values are "AVERAGE", "MIN",
* "MAX", "LAST" and "TOTAL" (these string constants are conveniently defined in the
* {@link org.rrd4j.ConsolFun} class).
* @param xff X-files factor, between 0 and 1.
* @param steps Number of archive steps.
* @param rows Number of archive rows.
*/
public ArcDef(ConsolFun consolFun, double xff, int steps, int rows) {
if (consolFun == null) {
throw new IllegalArgumentException("Null consolidation function specified");
}
if (Double.isNaN(xff) || xff < 0.0 || xff >= 1.0) {
throw new IllegalArgumentException("Invalid xff, must be >= 0 and < 1: " + xff);
}
if (steps < 1 || rows < 2) {
throw new IllegalArgumentException("Invalid steps/rows settings: " + steps + "/" + rows +
". Minimal values allowed are steps=1, rows=2");
}
this.consolFun = consolFun;
this.xff = xff;
this.steps = steps;
this.rows = rows;
}
/**
* Returns consolidation function.
*
* @return Consolidation function.
*/
public ConsolFun getConsolFun() {
return consolFun;
}
/**
* Returns the X-files factor.
*
* @return X-files factor value.
*/
public double getXff() {
return xff;
}
/**
* Returns the number of primary RRD steps which complete a single archive step.
*
* @return Number of steps.
*/
public int getSteps() {
return steps;
}
/**
* Returns the number of rows (aggregated values) stored in the archive.
*
* @return Number of rows.
*/
public int getRows() {
return rows;
}
/**
* Returns string representing archive definition (RRDTool format).
*
* @return String containing all archive definition parameters.
*/
public String dump() {
return "RRA:" + consolFun + ":" + xff + ":" + steps + ":" + rows;
}
/**
* {@inheritDoc}
*
* Checks if two archive definitions are equal.
* Archive definitions are considered equal if they have the same number of steps
* and the same consolidation function. It is not possible to create RRD with two
* equal archive definitions.
*/
public boolean equals(Object obj) {
if (obj instanceof ArcDef) {
ArcDef arcObj = (ArcDef) obj;
return consolFun == arcObj.consolFun && steps == arcObj.steps;
}
return false;
}
@Override
public int hashCode() {
return consolFun.hashCode() + steps * 19;
}
void setRows(int rows) {
this.rows = rows;
}
boolean exactlyEqual(ArcDef def) {
return consolFun == def.consolFun && xff == def.xff && steps == def.steps && rows == def.rows;
}
}

View File

@ -0,0 +1,109 @@
package org.rrd4j.core;
import java.io.IOException;
/**
* Class to represent internal RRD archive state for a single datasource. Objects of this
* class are never manipulated directly, it's up to Rrd4j to manage internal archive states.
*
* @author Sasa Markovic
*/
public class ArcState implements RrdUpdater<ArcState> {
private Archive parentArc;
private RrdDouble<ArcState> accumValue;
private RrdLong<ArcState> nanSteps;
ArcState(Archive parentArc, boolean shouldInitialize) throws IOException {
this.parentArc = parentArc;
accumValue = new RrdDouble<>(this);
nanSteps = new RrdLong<>(this);
if (shouldInitialize) {
Header header = parentArc.getParentDb().getHeader();
long step = header.getStep();
long lastUpdateTime = header.getLastUpdateTime();
long arcStep = parentArc.getArcStep();
long initNanSteps = (Util.normalize(lastUpdateTime, step) -
Util.normalize(lastUpdateTime, arcStep)) / step;
accumValue.set(Double.NaN);
nanSteps.set(initNanSteps);
}
}
String dump() throws IOException {
return "accumValue:" + accumValue.get() + " nanSteps:" + nanSteps.get() + "\n";
}
void setNanSteps(long value) throws IOException {
nanSteps.set(value);
}
/**
* Returns the number of currently accumulated NaN steps.
*
* @return Number of currently accumulated NaN steps.
* @throws java.io.IOException Thrown in case of I/O error
*/
public long getNanSteps() throws IOException {
return nanSteps.get();
}
void setAccumValue(double value) throws IOException {
accumValue.set(value);
}
/**
* Returns the value accumulated so far.
*
* @return Accumulated value
* @throws java.io.IOException Thrown in case of I/O error
*/
public double getAccumValue() throws IOException {
return accumValue.get();
}
/**
* Returns the Archive object to which this ArcState object belongs.
*
* @return Parent Archive object.
*/
public Archive getParent() {
return parentArc;
}
void appendXml(XmlWriter writer) throws IOException {
writer.startTag("ds");
writer.writeTag("value", accumValue.get());
writer.writeTag("unknown_datapoints", nanSteps.get());
writer.closeTag(); // ds
}
/**
* {@inheritDoc}
*
* Copies object's internal state to another ArcState object.
*/
public void copyStateTo(ArcState arcState) throws IOException {
arcState.accumValue.set(accumValue.get());
arcState.nanSteps.set(nanSteps.get());
}
/**
* Returns the underlying storage (backend) object which actually performs all
* I/O operations.
*
* @return I/O backend object
*/
public RrdBackend getRrdBackend() {
return parentArc.getRrdBackend();
}
/**
* Required to implement RrdUpdater interface. You should never call this method directly.
*
* @return Allocator object
*/
public RrdAllocator getRrdAllocator() {
return parentArc.getRrdAllocator();
}
}

View File

@ -0,0 +1,419 @@
package org.rrd4j.core;
import org.rrd4j.ConsolFun;
import java.io.IOException;
/**
* Class to represent single RRD archive in a RRD with its internal state.
* Normally, you don't need methods to manipulate archive objects directly
* because Rrd4j framework does it automatically for you.
* <p>
* Each archive object consists of three parts: archive definition, archive state objects
* (one state object for each datasource) and round robin archives (one round robin for
* each datasource). API (read-only) is provided to access each of these parts.
*
* @author Sasa Markovic
*/
public class Archive implements RrdUpdater<Archive> {
private final RrdDb parentDb;
// definition
private final RrdEnum<Archive, ConsolFun> consolFun;
protected final RrdDouble<Archive> xff;
protected final RrdInt<Archive> steps;
protected final RrdInt<Archive> rows;
// state
private final Robin[] robins;
private final ArcState[] states;
Archive(RrdDb parentDb, ArcDef arcDef) throws IOException {
this.parentDb = parentDb;
consolFun = new RrdEnum<>(this, false, ConsolFun.class); // Don't cache, as the enum type should be used instead
xff = new RrdDouble<>(this);
steps = new RrdInt<>(this, true); // constant, may be cached
rows = new RrdInt<>(this, true); // constant, may be cached
boolean shouldInitialize = arcDef != null;
if (shouldInitialize) {
consolFun.set(arcDef.getConsolFun());
xff.set(arcDef.getXff());
steps.set(arcDef.getSteps());
rows.set(arcDef.getRows());
}
int n = parentDb.getHeader().getDsCount();
int numRows = rows.get();
states = new ArcState[n];
int version = parentDb.getHeader().getVersion();
if (version == 1) {
robins = new RobinArray[n];
for (int i = 0; i < n; i++) {
states[i] = new ArcState(this, shouldInitialize);
robins[i] = new RobinArray(this, numRows, shouldInitialize);
}
} else {
@SuppressWarnings("unchecked")
RrdInt<Archive>[] pointers = new RrdInt[n];
robins = new RobinMatrix[n];
for (int i = 0; i < n; i++) {
pointers[i] = new RrdInt<>(this);
//Purge old pointers content, avoid problems with file reuse
if(shouldInitialize) {
pointers[i].set(0);
}
states[i] = new ArcState(this, shouldInitialize);
}
RrdDoubleMatrix<Archive> values = new RrdDoubleMatrix<>(this, numRows, n, shouldInitialize);
for (int i = 0; i < n; i++) {
robins[i] = new RobinMatrix(this, values, pointers[i], i);
}
}
}
// read from XML
Archive(RrdDb parentDb, DataImporter reader, int arcIndex) throws IOException {
this(parentDb, new ArcDef(
reader.getConsolFun(arcIndex), reader.getXff(arcIndex),
reader.getSteps(arcIndex), reader.getRows(arcIndex)));
int n = parentDb.getHeader().getDsCount();
for (int i = 0; i < n; i++) {
// restore state
states[i].setAccumValue(reader.getStateAccumValue(arcIndex, i));
states[i].setNanSteps(reader.getStateNanSteps(arcIndex, i));
// restore robins
double[] values = reader.getValues(arcIndex, i);
robins[i].update(values);
}
}
/**
* Returns archive time step in seconds. Archive step is equal to RRD step
* multiplied with the number of archive steps.
*
* @return Archive time step in seconds
* @throws java.io.IOException Thrown in case of I/O error.
*/
public long getArcStep() throws IOException {
return parentDb.getHeader().getStep() * steps.get();
}
String dump() throws IOException {
StringBuilder sb = new StringBuilder("== ARCHIVE ==\n");
sb.append("RRA:")
.append(consolFun.name())
.append(":")
.append(xff.get())
.append(":")
.append(steps.get())
.append(":")
.append(rows.get())
.append("\n")
.append("interval [")
.append(getStartTime())
.append(", ")
.append(getEndTime())
.append("]" + "\n");
for (int i = 0; i < robins.length; i++) {
sb.append(states[i].dump());
sb.append(robins[i].dump());
}
return sb.toString();
}
RrdDb getParentDb() {
return parentDb;
}
void archive(int dsIndex, double value, long numUpdates) throws IOException {
Robin robin = robins[dsIndex];
ArcState state = states[dsIndex];
long step = parentDb.getHeader().getStep();
long lastUpdateTime = parentDb.getHeader().getLastUpdateTime();
long updateTime = Util.normalize(lastUpdateTime, step) + step;
long arcStep = getArcStep();
// finish current step
while (numUpdates > 0) {
accumulate(state, value);
numUpdates--;
if (updateTime % arcStep == 0) {
finalizeStep(state, robin);
break;
} else {
updateTime += step;
}
}
// update robin in bulk
int bulkUpdateCount = (int) Math.min(numUpdates / steps.get(), (long) rows.get());
robin.bulkStore(value, bulkUpdateCount);
// update remaining steps
long remainingUpdates = numUpdates % steps.get();
for (long i = 0; i < remainingUpdates; i++) {
accumulate(state, value);
}
}
private void accumulate(ArcState state, double value) throws IOException {
if (Double.isNaN(value)) {
state.setNanSteps(state.getNanSteps() + 1);
} else {
switch (consolFun.get()) {
case MIN:
state.setAccumValue(Util.min(state.getAccumValue(), value));
break;
case MAX:
state.setAccumValue(Util.max(state.getAccumValue(), value));
break;
case FIRST:
if (Double.isNaN(state.getAccumValue())) {
state.setAccumValue(value);
}
break;
case LAST:
state.setAccumValue(value);
break;
case AVERAGE:
case TOTAL:
state.setAccumValue(Util.sum(state.getAccumValue(), value));
break;
}
}
}
private void finalizeStep(ArcState state, Robin robin) throws IOException {
// should store
long arcSteps = steps.get();
double arcXff = xff.get();
long nanSteps = state.getNanSteps();
double accumValue = state.getAccumValue();
if (nanSteps <= arcXff * arcSteps && !Double.isNaN(accumValue)) {
if (consolFun.get() == ConsolFun.AVERAGE) {
accumValue /= (arcSteps - nanSteps);
}
robin.store(accumValue);
} else {
robin.store(Double.NaN);
}
state.setAccumValue(Double.NaN);
state.setNanSteps(0);
}
/**
* Returns archive consolidation function ("AVERAGE", "MIN", "MAX", "FIRST", "LAST" or "TOTAL").
*
* @return Archive consolidation function.
* @throws java.io.IOException Thrown in case of I/O error.
*/
public ConsolFun getConsolFun() throws IOException {
return consolFun.get();
}
/**
* Returns archive X-files factor.
*
* @return Archive X-files factor (between 0 and 1).
* @throws java.io.IOException Thrown in case of I/O error.
*/
public double getXff() throws IOException {
return xff.get();
}
/**
* Returns the number of archive steps.
*
* @return Number of archive steps.
* @throws java.io.IOException Thrown in case of I/O error.
*/
public int getSteps() throws IOException {
return steps.get();
}
/**
* Returns the number of archive rows.
*
* @return Number of archive rows.
* @throws java.io.IOException Thrown in case of I/O error.
*/
public int getRows() throws IOException {
return rows.get();
}
/**
* Returns current starting timestamp. This value is not constant.
*
* @return Timestamp corresponding to the first archive row
* @throws java.io.IOException Thrown in case of I/O error.
*/
public long getStartTime() throws IOException {
long endTime = getEndTime();
long arcStep = getArcStep();
long numRows = rows.get();
return endTime - (numRows - 1) * arcStep;
}
/**
* Returns current ending timestamp. This value is not constant.
*
* @return Timestamp corresponding to the last archive row
* @throws java.io.IOException Thrown in case of I/O error.
*/
public long getEndTime() throws IOException {
long arcStep = getArcStep();
long lastUpdateTime = parentDb.getHeader().getLastUpdateTime();
return Util.normalize(lastUpdateTime, arcStep);
}
/**
* Returns the underlying archive state object. Each datasource has its
* corresponding ArcState object (archive states are managed independently
* for each RRD datasource).
*
* @param dsIndex Datasource index
* @return Underlying archive state object
*/
public ArcState getArcState(int dsIndex) {
return states[dsIndex];
}
/**
* Returns the underlying round robin archive. Robins are used to store actual
* archive values on a per-datasource basis.
*
* @param dsIndex Index of the datasource in the RRD.
* @return Underlying round robin archive for the given datasource.
*/
public Robin getRobin(int dsIndex) {
return robins[dsIndex];
}
FetchData fetchData(FetchRequest request) throws IOException {
long arcStep = getArcStep();
long fetchStart = Util.normalize(request.getFetchStart(), arcStep);
long fetchEnd = Util.normalize(request.getFetchEnd(), arcStep);
if (fetchEnd < request.getFetchEnd()) {
fetchEnd += arcStep;
}
long startTime = getStartTime();
long endTime = getEndTime();
String[] dsToFetch = request.getFilter();
if (dsToFetch == null) {
dsToFetch = parentDb.getDsNames();
}
int dsCount = dsToFetch.length;
int ptsCount = (int) ((fetchEnd - fetchStart) / arcStep + 1);
long[] timestamps = new long[ptsCount];
double[][] values = new double[dsCount][ptsCount];
long matchStartTime = Math.max(fetchStart, startTime);
long matchEndTime = Math.min(fetchEnd, endTime);
double[][] robinValues = null;
if (matchStartTime <= matchEndTime) {
// preload robin values
int matchCount = (int) ((matchEndTime - matchStartTime) / arcStep + 1);
int matchStartIndex = (int) ((matchStartTime - startTime) / arcStep);
robinValues = new double[dsCount][];
for (int i = 0; i < dsCount; i++) {
int dsIndex = parentDb.getDsIndex(dsToFetch[i]);
robinValues[i] = robins[dsIndex].getValues(matchStartIndex, matchCount);
}
}
for (int ptIndex = 0; ptIndex < ptsCount; ptIndex++) {
long time = fetchStart + ptIndex * arcStep;
timestamps[ptIndex] = time;
for (int i = 0; i < dsCount; i++) {
double value = Double.NaN;
if (time >= matchStartTime && time <= matchEndTime) {
// inbound time
int robinValueIndex = (int) ((time - matchStartTime) / arcStep);
assert robinValues != null;
value = robinValues[i][robinValueIndex];
}
values[i][ptIndex] = value;
}
}
FetchData fetchData = new FetchData(this, request);
fetchData.setTimestamps(timestamps);
fetchData.setValues(values);
return fetchData;
}
void appendXml(XmlWriter writer) throws IOException {
writer.startTag("rra");
writer.writeTag("cf", consolFun.name());
writer.writeComment(getArcStep() + " seconds");
writer.writeTag("pdp_per_row", steps.get());
writer.startTag("params");
writer.writeTag("xff", xff.get());
writer.closeTag(); // params
writer.startTag("cdp_prep");
for (ArcState state : states) {
state.appendXml(writer);
}
writer.closeTag(); // cdp_prep
writer.startTag("database");
long startTime = getStartTime();
for (int i = 0; i < rows.get(); i++) {
long time = startTime + i * getArcStep();
writer.writeComment(Util.getDate(time) + " / " + time);
writer.startTag("row");
for (Robin robin : robins) {
writer.writeTag("v", robin.getValue(i));
}
writer.closeTag(); // row
}
writer.closeTag(); // database
writer.closeTag(); // rra
}
/**
* {@inheritDoc}
*
* Copies object's internal state to another Archive object.
*/
public void copyStateTo(Archive arc) throws IOException {
if (arc.consolFun.get() != consolFun.get()) {
throw new IllegalArgumentException("Incompatible consolidation functions");
}
if (arc.steps.get() != steps.get()) {
throw new IllegalArgumentException("Incompatible number of steps");
}
int count = parentDb.getHeader().getDsCount();
for (int i = 0; i < count; i++) {
int j = Util.getMatchingDatasourceIndex(parentDb, i, arc.parentDb);
if (j >= 0) {
states[i].copyStateTo(arc.states[j]);
robins[i].copyStateTo(arc.robins[j]);
}
}
}
/**
* Sets X-files factor to a new value.
*
* @param xff New X-files factor value. Must be &gt;= 0 and &lt; 1.
* @throws java.io.IOException Thrown in case of I/O error
*/
public void setXff(double xff) throws IOException {
if (xff < 0D || xff >= 1D) {
throw new IllegalArgumentException("Invalid xff supplied (" + xff + "), must be >= 0 and < 1");
}
this.xff.set(xff);
}
/**
* Returns the underlying storage (backend) object which actually performs all
* I/O operations.
*
* @return I/O backend object
*/
public RrdBackend getRrdBackend() {
return parentDb.getRrdBackend();
}
/**
* Required to implement RrdUpdater interface. You should never call this method directly.
*
* @return Allocator object
*/
public RrdAllocator getRrdAllocator() {
return parentDb.getRrdAllocator();
}
}

View File

@ -0,0 +1,191 @@
package org.rrd4j.core;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.Arrays;
/**
* A backend that store and provides access to data using a {@link java.nio.ByteBuffer}, using java internal methods for
* long, integer and others types.
*
* @author Fabrice Bacchella
* @since 3.4
*
*/
public abstract class ByteBufferBackend extends RrdBackend {
private volatile boolean dirty = false;
private ByteBuffer byteBuffer;
protected ByteBufferBackend(String path) {
super(path);
}
protected void setByteBuffer(ByteBuffer byteBuffer) {
this.byteBuffer = byteBuffer;
byteBuffer.order(BYTEORDER);
}
/**
* Writes bytes to the underlying RRD file on the disk
*
* @param offset Starting file offset
* @param b Bytes to be written.
* @throws java.io.IOException if any.
* @throws java.lang.IllegalArgumentException if offset is bigger that the possible mapping position (2GiB).
*/
protected synchronized void write(long offset, byte[] b) throws IOException {
checkOffsetAndByteBuffer(offset);
byteBuffer.put(b, (int) offset, b.length);
dirty = true;
}
@Override
protected void writeShort(long offset, short value) throws IOException {
checkOffsetAndByteBuffer(offset);
byteBuffer.putShort((int)offset, value);
dirty = true;
}
@Override
protected void writeInt(long offset, int value) throws IOException {
checkOffsetAndByteBuffer(offset);
byteBuffer.putInt((int)offset, value);
dirty = true;
}
@Override
protected void writeLong(long offset, long value) throws IOException {
checkOffsetAndByteBuffer(offset);
byteBuffer.putLong((int)offset, value);
dirty = true;
}
@Override
protected void writeDouble(long offset, double value) throws IOException {
checkOffsetAndByteBuffer(offset);
byteBuffer.putDouble((int)offset, value);
dirty = true;
}
@Override
protected void writeDouble(long offset, double value, int count)
throws IOException {
checkOffsetAndByteBuffer(offset);
double[] values = new double[count];
Arrays.fill(values, value);
// position must be set in the original ByteByffer, as DoubleBuffer uses a "double" offset
byteBuffer.position((int)offset);
byteBuffer.asDoubleBuffer().put(values, 0, count);
dirty = true;
}
@Override
protected void writeDouble(long offset, double[] values) throws IOException {
checkOffsetAndByteBuffer(offset);
// position must be set in the original ByteByffer, as DoubleBuffer uses a "double" offset
byteBuffer.position((int)offset);
byteBuffer.asDoubleBuffer().put(values, 0, values.length);
dirty = true;
}
@Override
protected void writeString(long offset, String value, int length) throws IOException {
checkOffsetAndByteBuffer(offset);
byteBuffer.position((int)offset);
CharBuffer cbuff = byteBuffer.asCharBuffer();
cbuff.limit(length);
cbuff.put(value);
while (cbuff.position() < cbuff.limit()) {
cbuff.put(' ');
}
dirty = true;
}
/**
* Reads a number of bytes from the RRD file on the disk
*
* @param offset Starting file offset
* @param b Buffer which receives bytes read from the file.
* @throws java.io.IOException Thrown in case of I/O error.
* @throws java.lang.IllegalArgumentException if offset is bigger that the possible mapping position (2GiB).
*/
protected synchronized void read(long offset, byte[] b) throws IOException {
checkOffsetAndByteBuffer(offset);
byteBuffer.get(b, (int) offset, b.length);
}
@Override
protected short readShort(long offset) throws IOException {
checkOffsetAndByteBuffer(offset);
return byteBuffer.getShort((int)offset);
}
@Override
protected int readInt(long offset) throws IOException {
checkOffsetAndByteBuffer(offset);
return byteBuffer.getInt((int)offset);
}
@Override
protected long readLong(long offset) throws IOException {
checkOffsetAndByteBuffer(offset);
return byteBuffer.getLong((int)offset);
}
@Override
public double readDouble(long offset) throws IOException {
checkOffsetAndByteBuffer(offset);
return byteBuffer.getDouble((int)offset);
}
@Override
public double[] readDouble(long offset, int count) throws IOException {
checkOffsetAndByteBuffer(offset);
double[] values = new double[count];
// position must be set in the original ByteByffer, as DoubleBuffer is a "double" offset
byteBuffer.position((int)offset);
byteBuffer.asDoubleBuffer().get(values, 0, count);
return values;
}
@Override
protected CharBuffer getCharBuffer(long offset, int size) throws RrdException {
checkOffsetAndByteBuffer(offset);
byteBuffer.position((int)offset);
CharBuffer cbuffer = byteBuffer.asCharBuffer();
cbuffer.limit(size);
return cbuffer;
}
protected void close() throws IOException {
byteBuffer = null;
}
/**
* Ensure that the conversion from long offset to integer offset will not overflow
* @param offset
* @throws RrdException
*/
private void checkOffsetAndByteBuffer(long offset) throws RrdException {
if (offset < 0 || offset > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Illegal offset: " + offset);
}
if (byteBuffer == null) {
throw new RrdException("Empty rrd");
}
}
protected boolean isDirty() {
return dirty;
}
@Override
protected void rrdClose() throws IOException {
super.rrdClose();
dirty = false;
}
}

View File

@ -0,0 +1,84 @@
package org.rrd4j.core;
import java.io.Closeable;
import java.io.IOException;
import org.rrd4j.ConsolFun;
import org.rrd4j.DsType;
/**
* <p>An abstract class to import data from external source.</p>
* @author Fabrice Bacchella
* @since 3.5
*/
public abstract class DataImporter implements Closeable {
// header
public abstract String getVersion() throws IOException;
public abstract long getLastUpdateTime() throws IOException;
public abstract long getStep() throws IOException;
public abstract int getDsCount() throws IOException;
public abstract int getArcCount() throws IOException;
// datasource
public abstract String getDsName(int dsIndex) throws IOException;
public abstract DsType getDsType(int dsIndex) throws IOException;
public abstract long getHeartbeat(int dsIndex) throws IOException;
public abstract double getMinValue(int dsIndex) throws IOException;
public abstract double getMaxValue(int dsIndex) throws IOException;
// datasource state
public abstract double getLastValue(int dsIndex) throws IOException;
public abstract double getAccumValue(int dsIndex) throws IOException;
public abstract long getNanSeconds(int dsIndex) throws IOException;
// archive
public abstract ConsolFun getConsolFun(int arcIndex) throws IOException;
public abstract double getXff(int arcIndex) throws IOException;
public abstract int getSteps(int arcIndex) throws IOException;
public abstract int getRows(int arcIndex) throws IOException;
// archive state
public abstract double getStateAccumValue(int arcIndex, int dsIndex) throws IOException;
public abstract int getStateNanSteps(int arcIndex, int dsIndex) throws IOException;
public abstract double[] getValues(int arcIndex, int dsIndex) throws IOException;
protected long getEstimatedSize() throws IOException {
int dsCount = getDsCount();
int arcCount = getArcCount();
int rowCount = 0;
for (int i = 0; i < arcCount; i++) {
rowCount += getRows(i);
}
String[] dsNames = new String[getDsCount()];
for (int i = 0 ; i < dsNames.length; i++) {
dsNames[i] = getDsName(i);
}
return RrdDef.calculateSize(dsCount, arcCount, rowCount, dsNames);
}
void release() throws IOException {
// NOP
}
@Override
public void close() throws IOException {
release();
}
}

View File

@ -0,0 +1,479 @@
package org.rrd4j.core;
import java.io.IOException;
import org.rrd4j.DsType;
/**
* <p>Class to represent single datasource within RRD. Each datasource object holds the
* following information: datasource definition (once set, never changed) and
* datasource state variables (changed whenever RRD gets updated).</p>
* <p>Normally, you don't need to manipulate Datasource objects directly, it's up to
* Rrd4j framework to do it for you.</p>
*
* @author Sasa Markovic
*/
public class Datasource implements RrdUpdater<Datasource> {
private static final double MAX_32_BIT = Math.pow(2, 32);
private static final double MAX_64_BIT = Math.pow(2, 64);
private static final String INVALID_MIN_MAX_VALUES = "Invalid min/max values: ";
private double accumLastValue;
private final RrdDb parentDb;
// definition
private final RrdString<Datasource> dsName;
private final RrdEnum<Datasource, DsType> dsType;
private final RrdLong<Datasource> heartbeat;
private final RrdDouble<Datasource> minValue, maxValue;
// state variables
private RrdDouble<Datasource> lastValue;
private RrdLong<Datasource> nanSeconds;
private RrdDouble<Datasource> accumValue;
Datasource(RrdDb parentDb, DsDef dsDef) throws IOException {
boolean shouldInitialize = dsDef != null;
this.parentDb = parentDb;
dsName = new RrdString<>(this);
dsType = new RrdEnum<>(this, DsType.class);
heartbeat = new RrdLong<>(this);
minValue = new RrdDouble<>(this);
maxValue = new RrdDouble<>(this);
lastValue = new RrdDouble<>(this);
accumValue = new RrdDouble<>(this);
nanSeconds = new RrdLong<>(this);
accumLastValue = Double.NaN;
if (shouldInitialize) {
dsName.set(dsDef.getDsName());
dsType.set(dsDef.getDsType());
heartbeat.set(dsDef.getHeartbeat());
minValue.set(dsDef.getMinValue());
maxValue.set(dsDef.getMaxValue());
lastValue.set(Double.NaN);
accumValue.set(0.0);
Header header = parentDb.getHeader();
nanSeconds.set(header.getLastUpdateTime() % header.getStep());
}
}
Datasource(RrdDb parentDb, DataImporter reader, int dsIndex) throws IOException {
this(parentDb, null);
dsName.set(reader.getDsName(dsIndex));
dsType.set(reader.getDsType(dsIndex));
heartbeat.set(reader.getHeartbeat(dsIndex));
minValue.set(reader.getMinValue(dsIndex));
maxValue.set(reader.getMaxValue(dsIndex));
lastValue.set(reader.getLastValue(dsIndex));
accumValue.set(reader.getAccumValue(dsIndex));
nanSeconds.set(reader.getNanSeconds(dsIndex));
}
String dump() throws IOException {
return "== DATASOURCE ==\n" +
"DS:" + dsName.get() + ":" + dsType.name() + ":" +
heartbeat.get() + ":" + minValue.get() + ":" +
maxValue.get() + "\nlastValue:" + lastValue.get() +
" nanSeconds:" + nanSeconds.get() +
" accumValue:" + accumValue.get() + "\n";
}
/**
* Returns datasource name.
*
* @return Datasource name
* @throws java.io.IOException Thrown in case of I/O error
*/
public String getName() throws IOException {
return dsName.get();
}
/**
* Returns datasource type (GAUGE, COUNTER, DERIVE, ABSOLUTE).
*
* @return Datasource type.
* @throws java.io.IOException Thrown in case of I/O error
*/
public DsType getType() throws IOException {
return dsType.get();
}
/**
* Returns datasource heartbeat
*
* @return Datasource heartbeat
* @throws java.io.IOException Thrown in case of I/O error
*/
public long getHeartbeat() throws IOException {
return heartbeat.get();
}
/**
* Returns minimal allowed value for this datasource.
*
* @return Minimal value allowed.
* @throws java.io.IOException Thrown in case of I/O error
*/
public double getMinValue() throws IOException {
return minValue.get();
}
/**
* Returns maximal allowed value for this datasource.
*
* @return Maximal value allowed.
* @throws java.io.IOException Thrown in case of I/O error
*/
public double getMaxValue() throws IOException {
return maxValue.get();
}
/**
* Returns last known value of the datasource.
*
* @return Last datasource value.
* @throws java.io.IOException Thrown in case of I/O error
*/
public double getLastValue() throws IOException {
return lastValue.get();
}
/**
* Returns value this datasource accumulated so far.
*
* @return Accumulated datasource value.
* @throws java.io.IOException Thrown in case of I/O error
*/
public double getAccumValue() throws IOException {
return accumValue.get();
}
/**
* Returns the number of accumulated NaN seconds.
*
* @return Accumulated NaN seconds.
* @throws java.io.IOException Thrown in case of I/O error
*/
public long getNanSeconds() throws IOException {
return nanSeconds.get();
}
final void process(long newTime, double newValue) throws IOException {
Header header = parentDb.getHeader();
long step = header.getStep();
long oldTime = header.getLastUpdateTime();
long startTime = Util.normalize(oldTime, step);
long endTime = startTime + step;
double oldValue = lastValue.get();
double updateValue = calculateUpdateValue(oldTime, oldValue, newTime, newValue);
if (newTime < endTime) {
accumulate(oldTime, newTime, updateValue);
}
else {
// should store something
long boundaryTime = Util.normalize(newTime, step);
accumulate(oldTime, boundaryTime, updateValue);
double value = calculateTotal(startTime, boundaryTime);
double lastCalculateValue = calculateLastTotal(startTime, boundaryTime);
// how many updates?
long numSteps = (boundaryTime - endTime) / step + 1L;
// ACTION!
parentDb.archive(this, value, lastCalculateValue, numSteps);
// cleanup
nanSeconds.set(0);
accumValue.set(0.0);
accumLastValue = Double.NaN;
accumulate(boundaryTime, newTime, updateValue);
}
}
private double calculateUpdateValue(long oldTime, double oldValue,
long newTime, double newValue) throws IOException {
double updateValue = Double.NaN;
if (newTime - oldTime <= heartbeat.get()) {
switch (dsType.get()) {
case GAUGE:
updateValue = newValue;
break;
case COUNTER:
if (!Double.isNaN(newValue) && !Double.isNaN(oldValue)) {
double diff = newValue - oldValue;
if (diff < 0) {
diff += MAX_32_BIT;
}
if (diff < 0) {
diff += MAX_64_BIT - MAX_32_BIT;
}
if (diff >= 0) {
updateValue = diff / (newTime - oldTime);
}
}
break;
case ABSOLUTE:
if (!Double.isNaN(newValue)) {
updateValue = newValue / (newTime - oldTime);
}
break;
case DERIVE:
if (!Double.isNaN(newValue) && !Double.isNaN(oldValue)) {
updateValue = (newValue - oldValue) / (newTime - oldTime);
}
break;
}
if (!Double.isNaN(updateValue)) {
double minVal = minValue.get();
double maxVal = maxValue.get();
if (!Double.isNaN(minVal) && updateValue < minVal) {
updateValue = Double.NaN;
}
if (!Double.isNaN(maxVal) && updateValue > maxVal) {
updateValue = Double.NaN;
}
}
}
lastValue.set(newValue);
return updateValue;
}
private void accumulate(long oldTime, long newTime, double updateValue) throws IOException {
if (Double.isNaN(updateValue)) {
nanSeconds.set(nanSeconds.get() + (newTime - oldTime));
}
else {
accumValue.set(accumValue.get() + updateValue * (newTime - oldTime));
accumLastValue = updateValue;
}
}
private double calculateTotal(long startTime, long boundaryTime) throws IOException {
double totalValue = Double.NaN;
long validSeconds = boundaryTime - startTime - nanSeconds.get();
if (nanSeconds.get() <= heartbeat.get() && validSeconds > 0) {
totalValue = accumValue.get() / validSeconds;
}
// IMPORTANT:
// if datasource name ends with "!", we'll send zeros instead of NaNs
// this might be handy from time to time
if (Double.isNaN(totalValue) && dsName.get().endsWith(DsDef.FORCE_ZEROS_FOR_NANS_SUFFIX)) {
totalValue = 0D;
}
return totalValue;
}
private double calculateLastTotal(long startTime, long boundaryTime) throws IOException {
double totalValue = Double.NaN;
long validSeconds = boundaryTime - startTime - nanSeconds.get();
if (nanSeconds.get() <= heartbeat.get() && validSeconds > 0) {
totalValue = accumLastValue;
}
if (Double.isNaN(totalValue) && dsName.get().endsWith(DsDef.FORCE_ZEROS_FOR_NANS_SUFFIX)) {
totalValue = 0D;
}
return totalValue;
}
void appendXml(XmlWriter writer) throws IOException {
writer.startTag("ds");
writer.writeTag("name", dsName.get());
writer.writeTag("type", dsType.name());
writer.writeTag("minimal_heartbeat", heartbeat.get());
writer.writeTag("min", minValue.get());
writer.writeTag("max", maxValue.get());
writer.writeComment("PDP Status");
writer.writeTag("last_ds", lastValue.get(), "UNKN");
writer.writeTag("value", accumValue.get());
writer.writeTag("unknown_sec", nanSeconds.get());
writer.closeTag(); // ds
}
/**
* {@inheritDoc}
*
* Copies object's internal state to another Datasource object.
*/
public void copyStateTo(Datasource datasource) throws IOException {
if (!datasource.dsName.get().equals(dsName.get())) {
throw new IllegalArgumentException("Incompatible datasource names");
}
if (datasource.dsType.get() != dsType.get()) {
throw new IllegalArgumentException("Incompatible datasource types");
}
datasource.lastValue.set(lastValue.get());
datasource.nanSeconds.set(nanSeconds.get());
datasource.accumValue.set(accumValue.get());
}
/**
* Returns index of this Datasource object in the RRD.
*
* @return Datasource index in the RRD.
* @throws java.io.IOException Thrown in case of I/O error
*/
public int getDsIndex() throws IOException {
try {
return parentDb.getDsIndex(dsName.get());
}
catch (IllegalArgumentException e) {
return -1;
}
}
/**
* Sets datasource heartbeat to a new value.
*
* @param heartbeat New heartbeat value
* @throws java.io.IOException Thrown in case of I/O error
* @throws java.lang.IllegalArgumentException Thrown if invalid (non-positive) heartbeat value is specified.
*/
public void setHeartbeat(long heartbeat) throws IOException {
if (heartbeat < 1L) {
throw new IllegalArgumentException("Invalid heartbeat specified: " + heartbeat);
}
this.heartbeat.set(heartbeat);
}
/**
* Sets datasource name to a new value
*
* @param newDsName New datasource name
* @throws java.io.IOException Thrown in case of I/O error
*/
public void setDsName(String newDsName) throws IOException {
if (parentDb.containsDs(newDsName)) {
throw new IllegalArgumentException("Datasource already defined in this RRD: " + newDsName);
}
this.dsName.set(newDsName);
}
/**
* <p>Setter for the field <code>dsType</code>.</p>
*
* @param newDsType a {@link org.rrd4j.DsType} object.
* @throws java.io.IOException if any.
*/
public void setDsType(DsType newDsType) throws IOException {
// set datasource type
dsType.set(newDsType);
// reset datasource status
lastValue.set(Double.NaN);
accumValue.set(0.0);
// reset archive status
int dsIndex = parentDb.getDsIndex(dsName.get());
Archive[] archives = parentDb.getArchives();
for (Archive archive : archives) {
archive.getArcState(dsIndex).setAccumValue(Double.NaN);
}
}
/**
* Sets minimum allowed value for this datasource. If <code>filterArchivedValues</code>
* argument is set to true, all archived values less then <code>minValue</code> will
* be fixed to NaN.
*
* @param minValue New minimal value. Specify <code>Double.NaN</code> if no minimal
* value should be set
* @param filterArchivedValues true, if archived datasource values should be fixed;
* false, otherwise.
* @throws java.io.IOException Thrown in case of I/O error
* @throws java.lang.IllegalArgumentException Thrown if invalid minValue was supplied (not less then maxValue)
*/
public void setMinValue(double minValue, boolean filterArchivedValues) throws IOException {
double maxValue = this.maxValue.get();
if (!Double.isNaN(minValue) && !Double.isNaN(maxValue) && minValue >= maxValue) {
throw new IllegalArgumentException(INVALID_MIN_MAX_VALUES + minValue + "/" + maxValue);
}
this.minValue.set(minValue);
if (!Double.isNaN(minValue) && filterArchivedValues) {
int dsIndex = getDsIndex();
Archive[] archives = parentDb.getArchives();
for (Archive archive : archives) {
archive.getRobin(dsIndex).filterValues(minValue, Double.NaN);
}
}
}
/**
* Sets maximum allowed value for this datasource. If <code>filterArchivedValues</code>
* argument is set to true, all archived values greater then <code>maxValue</code> will
* be fixed to NaN.
*
* @param maxValue New maximal value. Specify <code>Double.NaN</code> if no max
* value should be set.
* @param filterArchivedValues true, if archived datasource values should be fixed;
* false, otherwise.
* @throws java.io.IOException Thrown in case of I/O error
* @throws java.lang.IllegalArgumentException Thrown if invalid maxValue was supplied (not greater then minValue)
*/
public void setMaxValue(double maxValue, boolean filterArchivedValues) throws IOException {
double minValue = this.minValue.get();
if (!Double.isNaN(minValue) && !Double.isNaN(maxValue) && minValue >= maxValue) {
throw new IllegalArgumentException(INVALID_MIN_MAX_VALUES + minValue + "/" + maxValue);
}
this.maxValue.set(maxValue);
if (!Double.isNaN(maxValue) && filterArchivedValues) {
int dsIndex = getDsIndex();
Archive[] archives = parentDb.getArchives();
for (Archive archive : archives) {
archive.getRobin(dsIndex).filterValues(Double.NaN, maxValue);
}
}
}
/**
* Sets min/max values allowed for this datasource. If <code>filterArchivedValues</code>
* argument is set to true, all archived values less then <code>minValue</code> or
* greater then <code>maxValue</code> will be fixed to NaN.
*
* @param minValue New minimal value. Specify <code>Double.NaN</code> if no min
* value should be set.
* @param maxValue New maximal value. Specify <code>Double.NaN</code> if no max
* value should be set.
* @param filterArchivedValues true, if archived datasource values should be fixed;
* false, otherwise.
* @throws java.io.IOException Thrown in case of I/O error
* @throws java.lang.IllegalArgumentException Thrown if invalid min/max values were supplied
*/
public void setMinMaxValue(double minValue, double maxValue, boolean filterArchivedValues) throws IOException {
if (!Double.isNaN(minValue) && !Double.isNaN(maxValue) && minValue >= maxValue) {
throw new IllegalArgumentException(INVALID_MIN_MAX_VALUES + minValue + "/" + maxValue);
}
this.minValue.set(minValue);
this.maxValue.set(maxValue);
if (!(Double.isNaN(minValue) && Double.isNaN(maxValue)) && filterArchivedValues) {
int dsIndex = getDsIndex();
Archive[] archives = parentDb.getArchives();
for (Archive archive : archives) {
archive.getRobin(dsIndex).filterValues(minValue, maxValue);
}
}
}
/**
* Returns the underlying storage (backend) object which actually performs all
* I/O operations.
*
* @return I/O backend object
*/
public RrdBackend getRrdBackend() {
return parentDb.getRrdBackend();
}
/**
* Required to implement RrdUpdater interface. You should never call this method directly.
*
* @return Allocator object
*/
public RrdAllocator getRrdAllocator() {
return parentDb.getRrdAllocator();
}
}

View File

@ -0,0 +1,154 @@
package org.rrd4j.core;
import org.rrd4j.DsType;
/**
* <p>Class to represent single data source definition within the RRD.
* Datasource definition consists of the following five elements:</p>
* <ul>
* <li>data source name
* <li>data source type
* <li>heartbeat
* <li>minimal value
* <li>maximal value
* </ul>
* <p>For the complete explanation of all source definition parameters, see RRDTool's
* <a href="http://oss.oetiker.ch/rrdtool/doc/rrdcreate.en.html" target="man">rrdcreate man page</a>.</p>
*
* @author Sasa Markovic
*/
public class DsDef {
static final String FORCE_ZEROS_FOR_NANS_SUFFIX = "!";
private final String dsName;
private final DsType dsType;
private final long heartbeat;
private final double minValue, maxValue;
/**
* Creates new data source definition object. This object should be passed as argument
* to {@link org.rrd4j.core.RrdDef#addDatasource(DsDef) addDatasource()}
* method of {@link RrdDb RrdDb} object.
* <p>
* For the complete explanation of all source definition parameters, see RRDTool's
* <a href="http://oss.oetiker.ch/rrdtool/doc/rrdcreate.en.html" target="man">rrdcreate man page</a>
* <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.
*
* @param dsName Data source name.
* @param dsType Data source type. Valid values are "COUNTER", "GAUGE", "DERIVE"
* and "ABSOLUTE" (these string constants are conveniently defined in the
* {@link org.rrd4j.DsType} class).
* @param heartbeat Hearbeat
* @param minValue Minimal value. Use <code>Double.NaN</code> if unknown.
* @param maxValue Maximal value. Use <code>Double.NaN</code> if unknown.
*/
public DsDef(String dsName, DsType dsType, long heartbeat, double minValue, double maxValue) {
if (dsName == null) {
throw new IllegalArgumentException("Null datasource name specified");
}
if (dsName.length() == 0) {
throw new IllegalArgumentException("Datasource name length equal to zero");
}
if (dsType == null) {
throw new IllegalArgumentException("Null datasource type specified");
}
if (heartbeat <= 0) {
throw new IllegalArgumentException("Invalid heartbeat, must be positive: " + heartbeat);
}
if (!Double.isNaN(minValue) && !Double.isNaN(maxValue) && minValue >= maxValue) {
throw new IllegalArgumentException("Invalid min/max values specified: " +
minValue + "/" + maxValue);
}
this.dsName = dsName;
this.dsType = dsType;
this.heartbeat = heartbeat;
this.minValue = minValue;
this.maxValue = maxValue;
}
/**
* Returns data source name.
*
* @return Data source name.
*/
public String getDsName() {
return dsName;
}
/**
* Returns source type.
*
* @return Source type ("COUNTER", "GAUGE", "DERIVE" or "ABSOLUTE").
*/
public DsType getDsType() {
return dsType;
}
/**
* Returns source heartbeat.
*
* @return Source heartbeat.
*/
public long getHeartbeat() {
return heartbeat;
}
/**
* Returns minimal calculated source value.
*
* @return Minimal value.
*/
public double getMinValue() {
return minValue;
}
/**
* Returns maximal calculated source value.
*
* @return Maximal value.
*/
public double getMaxValue() {
return maxValue;
}
/**
* Returns string representing source definition (RRDTool format).
*
* @return String containing all data source definition parameters.
*/
public String dump() {
return "DS:" + dsName + ":" + dsType + ":" + heartbeat +
":" + Util.formatDouble(minValue, "U", false) +
":" + Util.formatDouble(maxValue, "U", false);
}
/**
* {@inheritDoc}
*
* Checks if two datasource definitions are equal.
* Source definitions are treated as equal if they have the same source name.
* It is not possible to create RRD with two equal archive definitions.
*/
public boolean equals(Object obj) {
if (obj instanceof DsDef) {
DsDef dsObj = (DsDef) obj;
return dsName.equals(dsObj.dsName);
}
return false;
}
@Override
public int hashCode() {
return dsName.hashCode();
}
boolean exactlyEqual(DsDef def) {
return dsName.equals(def.dsName) && dsType == def.dsType &&
heartbeat == def.heartbeat && Util.equal(minValue, def.minValue) &&
Util.equal(maxValue, def.maxValue);
}
}

View File

@ -0,0 +1,500 @@
package org.rrd4j.core;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import org.rrd4j.ConsolFun;
import org.rrd4j.data.Aggregates;
import org.rrd4j.data.DataProcessor;
/**
* Class used to represent data fetched from the RRD.
* Object of this class is created when the method
* {@link org.rrd4j.core.FetchRequest#fetchData() fetchData()} is
* called on a {@link org.rrd4j.core.FetchRequest FetchRequest} object.
* <p>
*
* Data returned from the RRD is, simply, just one big table filled with
* timestamps and corresponding datasource values.
* Use {@link #getRowCount() getRowCount()} method to count the number
* of returned timestamps (table rows).
* <p>
*
* The first table column is filled with timestamps. Time intervals
* between consecutive timestamps are guaranteed to be equal. Use
* {@link #getTimestamps() getTimestamps()} method to get an array of
* timestamps returned.
* <p>
*
* Remaining columns are filled with datasource values for the whole timestamp range,
* on a column-per-datasource basis. Use {@link #getColumnCount() getColumnCount()} to find
* the number of datasources and {@link #getValues(int) getValues(i)} method to obtain
* all values for the i-th datasource. Returned datasource values correspond to
* the values returned with {@link #getTimestamps() getTimestamps()} method.
* <p>
*
* @author Sasa Markovic
*/
@SuppressWarnings("deprecation")
public class FetchData {
// anything funny will do
private static final String RPN_SOURCE_NAME = "WHERE THE SPEECHLES UNITE IN A SILENT ACCORD";
private FetchRequest request;
private String[] dsNames;
private long[] timestamps;
private double[][] values;
private Archive matchingArchive;
private long arcStep;
private long arcEndTime;
FetchData(Archive matchingArchive, FetchRequest request) throws IOException {
this.matchingArchive = matchingArchive;
this.arcStep = matchingArchive.getArcStep();
this.arcEndTime = matchingArchive.getEndTime();
this.dsNames = request.getFilter();
if (this.dsNames == null) {
this.dsNames = matchingArchive.getParentDb().getDsNames();
}
this.request = request;
}
void setTimestamps(long[] timestamps) {
this.timestamps = timestamps;
}
void setValues(double[][] values) {
this.values = values;
}
/**
* Returns the number of rows fetched from the corresponding RRD.
* Each row represents datasource values for the specific timestamp.
*
* @return Number of rows.
*/
public int getRowCount() {
return timestamps.length;
}
/**
* Returns the number of columns fetched from the corresponding RRD.
* This number is always equal to the number of datasources defined
* in the RRD. Each column represents values of a single datasource.
*
* @return Number of columns (datasources).
*/
public int getColumnCount() {
return dsNames.length;
}
/**
* Returns an array of timestamps covering the whole range specified in the
* {@link FetchRequest FetchReguest} object.
*
* @return Array of equidistant timestamps.
*/
public long[] getTimestamps() {
return timestamps;
}
/**
* Returns the step with which this data was fetched.
*
* @return Step as long.
*/
public long getStep() {
return timestamps[1] - timestamps[0];
}
/**
* Returns all archived values for a single datasource.
* Returned values correspond to timestamps
* returned with {@link #getTimestamps() getTimestamps()} method.
*
* @param dsIndex Datasource index.
* @return Array of single datasource values.
*/
public double[] getValues(int dsIndex) {
return values[dsIndex];
}
/**
* Returns all archived values for all datasources.
* Returned values correspond to timestamps
* returned with {@link #getTimestamps() getTimestamps()} method.
*
* @return Two-dimensional aray of all datasource values.
*/
public double[][] getValues() {
return values;
}
/**
* Returns all archived values for a single datasource.
* Returned values correspond to timestamps
* returned with {@link #getTimestamps() getTimestamps()} method.
*
* @param dsName Datasource name.
* @return Array of single datasource values.
*/
public double[] getValues(String dsName) {
for (int dsIndex = 0; dsIndex < getColumnCount(); dsIndex++) {
if (dsName.equals(dsNames[dsIndex])) {
return getValues(dsIndex);
}
}
throw new IllegalArgumentException("Datasource [" + dsName + "] not found");
}
/**
* Returns a set of values created by applying RPN expression to the fetched data.
* For example, if you have two datasources named <code>x</code> and <code>y</code>
* in this FetchData and you want to calculate values for <code>(x+y)/2</code> use something like:
* <p>
* <code>getRpnValues("x,y,+,2,/");</code>
*
* @param rpnExpression RRDTool-like RPN expression
* @return Calculated values
* @throws java.lang.IllegalArgumentException Thrown if invalid RPN expression is supplied
*/
public double[] getRpnValues(String rpnExpression) {
DataProcessor dataProcessor = createDataProcessor(rpnExpression);
return dataProcessor.getValues(RPN_SOURCE_NAME);
}
/**
* Returns {@link FetchRequest FetchRequest} object used to create this FetchData object.
*
* @return Fetch request object.
*/
public FetchRequest getRequest() {
return request;
}
/**
* Returns the first timestamp in this FetchData object.
*
* @return The smallest timestamp.
*/
public long getFirstTimestamp() {
return timestamps[0];
}
/**
* Returns the last timestamp in this FecthData object.
*
* @return The biggest timestamp.
*/
public long getLastTimestamp() {
return timestamps[timestamps.length - 1];
}
/**
* Returns Archive object which is determined to be the best match for the
* timestamps specified in the fetch request. All datasource values are obtained
* from round robin archives belonging to this archive.
*
* @return Matching archive.
*/
public Archive getMatchingArchive() {
return matchingArchive;
}
/**
* Returns array of datasource names found in the corresponding RRD. If the request
* was filtered (data was fetched only for selected datasources), only datasources selected
* for fetching are returned.
*
* @return Array of datasource names.
*/
public String[] getDsNames() {
return dsNames;
}
/**
* Retrieve the table index number of a datasource by name. Names are case sensitive.
*
* @param dsName Name of the datasource for which to find the index.
* @return Index number of the datasources in the value table.
*/
public int getDsIndex(String dsName) {
// Let's assume the table of dsNames is always small, so it is not necessary to use a hashmap for lookups
for (int i = 0; i < dsNames.length; i++) {
if (dsNames[i].equals(dsName)) {
return i;
}
}
return -1; // Datasource not found !
}
/**
* Dumps the content of the whole FetchData object. Useful for debugging.
*
* @return a {@link java.lang.String} containing the contents of this object, for debugging.
*/
public String dump() {
StringBuilder buffer = new StringBuilder();
for (int row = 0; row < getRowCount(); row++) {
buffer.append(timestamps[row]);
buffer.append(": ");
for (int dsIndex = 0; dsIndex < getColumnCount(); dsIndex++) {
buffer.append(Util.formatDouble(values[dsIndex][row], true));
buffer.append(" ");
}
buffer.append("\n");
}
return buffer.toString();
}
/**
* Returns string representing fetched data in a RRDTool-like form.
*
* @return Fetched data as a string in a rrdfetch-like output form.
*/
public String toString() {
// print header row
StringBuilder buff = new StringBuilder();
buff.append(padWithBlanks("", 10))
.append(" ");
for (String dsName : dsNames) {
buff.append(padWithBlanks(dsName, 18));
}
buff.append("\n \n");
for (int i = 0; i < timestamps.length; i++) {
buff.append(padWithBlanks(Long.toString(timestamps[i]), 10));
buff.append(":");
for (int j = 0; j < dsNames.length; j++) {
double value = values[j][i];
String valueStr = Double.isNaN(value) ? "nan" : Util.formatDouble(value);
buff.append(padWithBlanks(valueStr, 18));
}
buff.append("\n");
}
return buff.toString();
}
private static String padWithBlanks(String input, int width) {
StringBuilder buff = new StringBuilder("");
int diff = width - input.length();
while (diff-- > 0) {
buff.append(' ');
}
buff.append(input);
return buff.toString();
}
/**
* Returns single aggregated value from the fetched data for a single datasource.
*
* @param dsName Datasource name
* @param consolFun Consolidation function to be applied to fetched datasource values.
* Valid consolidation functions are "MIN", "MAX", "LAST", "FIRST", "AVERAGE" and "TOTAL"
* (these string constants are conveniently defined in the {@link org.rrd4j.ConsolFun} class)
* @throws java.lang.IllegalArgumentException Thrown if the given datasource name cannot be found in fetched data.
* @return a double.
* @deprecated This method is deprecated. Uses instance of {@link org.rrd4j.data.Variable}, used with {@link org.rrd4j.data.DataProcessor#addDatasource(String, String, Variable)}
*/
@Deprecated
public double getAggregate(String dsName, ConsolFun consolFun) {
DataProcessor dp = createDataProcessor(null);
return dp.getAggregate(dsName, consolFun);
}
/**
* Returns aggregated value for a set of values calculated by applying an RPN expression to the
* fetched data. For example, if you have two datasources named <code>x</code> and <code>y</code>
* in this FetchData and you want to calculate MAX value of <code>(x+y)/2</code> use something like:
* <p>
* <code>getRpnAggregate("x,y,+,2,/", "MAX");</code>
*
* @param rpnExpression RRDTool-like RPN expression
* @param consolFun Consolidation function (MIN, MAX, LAST, FIRST, AVERAGE or TOTAL)
* @return Aggregated value
* @throws java.lang.IllegalArgumentException Thrown if invalid RPN expression is supplied
* @deprecated This method is deprecated. Uses instance of {@link org.rrd4j.data.Variable}, used with {@link org.rrd4j.data.DataProcessor#addDatasource(String, String, Variable)}
*/
@Deprecated
public double getRpnAggregate(String rpnExpression, ConsolFun consolFun) {
DataProcessor dataProcessor = createDataProcessor(rpnExpression);
return dataProcessor.getAggregate(RPN_SOURCE_NAME, consolFun);
}
/**
* Returns all aggregated values (MIN, MAX, LAST, FIRST, AVERAGE or TOTAL) calculated from the fetched data
* for a single datasource.
*
* @param dsName Datasource name.
* @return Simple object containing all aggregated values.
* @throws java.lang.IllegalArgumentException Thrown if the given datasource name cannot be found in the fetched data.
* @deprecated This method is deprecated. Uses instance of {@link org.rrd4j.data.Variable}, used with {@link org.rrd4j.data.DataProcessor#addDatasource(String, String, Variable)}
*/
@Deprecated
public Aggregates getAggregates(String dsName) {
DataProcessor dataProcessor = createDataProcessor(null);
return dataProcessor.getAggregates(dsName);
}
/**
* Returns all aggregated values for a set of values calculated by applying an RPN expression to the
* fetched data. For example, if you have two datasources named <code>x</code> and <code>y</code>
* in this FetchData and you want to calculate MIN, MAX, LAST, FIRST, AVERAGE and TOTAL value
* of <code>(x+y)/2</code> use something like:
* <p>
* <code>getRpnAggregates("x,y,+,2,/");</code>
*
* @param rpnExpression RRDTool-like RPN expression
* @return Object containing all aggregated values
* @throws java.lang.IllegalArgumentException Thrown if invalid RPN expression is supplied
* @throws java.io.IOException if any.
* @deprecated This method is deprecated. Uses instance of {@link org.rrd4j.data.Variable}, used with {@link org.rrd4j.data.DataProcessor#addDatasource(String, String, Variable)}
*/
@Deprecated
public Aggregates getRpnAggregates(String rpnExpression) throws IOException {
DataProcessor dataProcessor = createDataProcessor(rpnExpression);
return dataProcessor.getAggregates(RPN_SOURCE_NAME);
}
/**
* Used by ISPs which charge for bandwidth utilization on a "95th percentile" basis.
* <p>
*
* The 95th percentile is the highest source value left when the top 5% of a numerically sorted set
* of source data is discarded. It is used as a measure of the peak value used when one discounts
* a fair amount for transitory spikes. This makes it markedly different from the average.
* <p>
*
* Read more about this topic at:<p>
* <a href="http://www.red.net/support/resourcecentre/leasedline/percentile.php">Rednet</a> or<br>
* <a href="http://www.bytemark.co.uk/support/tech/95thpercentile.html">Bytemark</a>.
*
* @param dsName Datasource name
* @return 95th percentile of fetched source values
* @throws java.lang.IllegalArgumentException Thrown if invalid source name is supplied
* @deprecated This method is deprecated. Uses instance of {@link org.rrd4j.data.Variable.PERCENTILE}, used with {@link org.rrd4j.data.DataProcessor#addDatasource(String, String, Variable)}
*/
@Deprecated
public double get95Percentile(String dsName) {
DataProcessor dataProcessor = createDataProcessor(null);
return dataProcessor.get95Percentile(dsName);
}
/**
* Same as {@link #get95Percentile(String)}, but for a set of values calculated with the given
* RPN expression.
*
* @param rpnExpression RRDTool-like RPN expression
* @return 95-percentile
* @throws java.lang.IllegalArgumentException Thrown if invalid RPN expression is supplied
* @deprecated This method is deprecated. Uses instance of {@link org.rrd4j.data.Variable.PERCENTILE}, used with {@link org.rrd4j.data.DataProcessor#addDatasource(String, String, Variable)}
*/
@Deprecated
public double getRpn95Percentile(String rpnExpression) {
DataProcessor dataProcessor = createDataProcessor(rpnExpression);
return dataProcessor.get95Percentile(RPN_SOURCE_NAME);
}
/**
* Dumps fetch data to output stream in XML format. A flush is issued at the end of the xml generation.
*
* @param outputStream Output stream to dump fetch data to
* @throws java.io.IOException Thrown in case of I/O error
*/
public void exportXml(OutputStream outputStream) throws IOException {
//No auto flush for XmlWriter, it will be flushed once, when export is finished
XmlWriter writer = new XmlWriter(outputStream, false);
writer.startTag("fetch_data");
writer.startTag("request");
writer.writeTag("file", request.getParentDb().getPath());
writer.writeComment(Util.getDate(request.getFetchStart()));
writer.writeTag("start", request.getFetchStart());
writer.writeComment(Util.getDate(request.getFetchEnd()));
writer.writeTag("end", request.getFetchEnd());
writer.writeTag("resolution", request.getResolution());
writer.writeTag("cf", request.getConsolFun());
writer.closeTag(); // request
writer.startTag("datasources");
for (String dsName : dsNames) {
writer.writeTag("name", dsName);
}
writer.closeTag(); // datasources
writer.startTag("data");
for (int i = 0; i < timestamps.length; i++) {
writer.startTag("row");
writer.writeComment(Util.getDate(timestamps[i]));
writer.writeTag("timestamp", timestamps[i]);
writer.startTag("values");
for (int j = 0; j < dsNames.length; j++) {
writer.writeTag("v", values[j][i]);
}
writer.closeTag(); // values
writer.closeTag(); // row
}
writer.closeTag(); // data
writer.closeTag(); // fetch_data
writer.flush();
}
/**
* Dumps fetch data to file in XML format.
*
* @param filepath Path to destination file
* @throws java.io.IOException Thrown in case of I/O error
*/
public void exportXml(String filepath) throws IOException {
try(OutputStream outputStream = new FileOutputStream(filepath)) {
exportXml(outputStream);
}
}
/**
* Dumps fetch data in XML format.
*
* @return String containing XML formatted fetch data
* @throws java.io.IOException Thrown in case of I/O error
*/
public String exportXml() throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
exportXml(outputStream);
return outputStream.toString();
}
/**
* Returns the step of the corresponding RRA archive
*
* @return Archive step in seconds
*/
public long getArcStep() {
return arcStep;
}
/**
* Returns the timestamp of the last populated slot in the corresponding RRA archive
*
* @return Timestamp in seconds
*/
public long getArcEndTime() {
return arcEndTime;
}
private DataProcessor createDataProcessor(String rpnExpression) {
DataProcessor dataProcessor = new DataProcessor(request.getFetchStart(), request.getFetchEnd());
for (String dsName : dsNames) {
dataProcessor.addDatasource(dsName, this);
}
if (rpnExpression != null) {
dataProcessor.addDatasource(RPN_SOURCE_NAME, rpnExpression);
}
try {
dataProcessor.processData();
}
catch (IOException ioe) {
// highly unlikely, since all datasources have already calculated values
throw new RuntimeException("Impossible error: " + ioe);
}
return dataProcessor;
}
}

View File

@ -0,0 +1,175 @@
package org.rrd4j.core;
import org.rrd4j.ConsolFun;
import java.io.IOException;
import java.util.Set;
/**
* Class to represent fetch request. For the complete explanation of all
* fetch parameters consult RRDTool's
* <a href="http://oss.oetiker.ch/rrdtool/doc/rrdfetch.en.html" target="man">rrdfetch man page</a>.
* <p>
* You cannot create <code>FetchRequest</code> directly (no public constructor
* is provided). Use {@link org.rrd4j.core.RrdDb#createFetchRequest(ConsolFun, long, long, long)
* createFetchRequest()} method of your {@link org.rrd4j.core.RrdDb RrdDb} object.
*
* @author Sasa Markovic
*/
public class FetchRequest {
private RrdDb parentDb;
private ConsolFun consolFun;
private long fetchStart;
private long fetchEnd;
private long resolution;
private String[] filter;
FetchRequest(RrdDb parentDb, ConsolFun consolFun, long fetchStart, long fetchEnd, long resolution) {
if (consolFun == null) {
throw new IllegalArgumentException("Null consolidation function in fetch request");
}
if (fetchStart < 0) {
throw new IllegalArgumentException("Invalid start time in fetch request: " + fetchStart);
}
if (fetchEnd < 0) {
throw new IllegalArgumentException("Invalid end time in fetch request: " + fetchEnd);
}
if (fetchStart > fetchEnd) {
throw new IllegalArgumentException("Invalid start/end time in fetch request: " + fetchStart +
" > " + fetchEnd);
}
if (resolution <= 0) {
throw new IllegalArgumentException("Invalid resolution in fetch request: " + resolution);
}
this.parentDb = parentDb;
this.consolFun = consolFun;
this.fetchStart = fetchStart;
this.fetchEnd = fetchEnd;
this.resolution = resolution;
}
/**
* Sets request filter in order to fetch data only for
* the specified array of datasources (datasource names).
* If not set (or set to null), fetched data will
* contain values of all datasources defined in the corresponding RRD.
* To fetch data only from selected
* datasources, specify an array of datasource names as method argument.
*
* @param filter Array of datasources (datasource names) to fetch data from.
*/
public void setFilter(String... filter) {
this.filter = filter;
}
/**
* Sets request filter in order to fetch data only for
* the specified set of datasources (datasource names).
* If the filter is not set (or set to null), fetched data will
* contain values of all datasources defined in the corresponding RRD.
* To fetch data only from selected
* datasources, specify a set of datasource names as method argument.
*
* @param filter Set of datasource names to fetch data for.
*/
public void setFilter(Set<String> filter) {
this.filter = filter.toArray(new String[filter.size()]);
}
/**
* Sets request filter in order to fetch data only for
* a single datasource (datasource name).
* If not set (or set to null), fetched data will
* contain values of all datasources defined in the corresponding RRD.
* To fetch data for a single datasource only,
* specify an array of datasource names as method argument.
*
* @param filter A single datasource (datasource name) to fetch data from.
*/
public void setFilter(String filter) {
this.filter = (filter == null) ? null : (new String[]{filter});
}
/**
* Returns request filter. See {@link #setFilter(String...) setFilter()} for
* complete explanation.
*
* @return Request filter (array of datasource names), null if not set.
*/
public String[] getFilter() {
return filter;
}
/**
* Returns consolidation function to be used during the fetch process.
*
* @return Consolidation function.
*/
public ConsolFun getConsolFun() {
return consolFun;
}
/**
* Returns starting timestamp to be used for the fetch request.
*
* @return Starting timestamp in seconds.
*/
public long getFetchStart() {
return fetchStart;
}
/**
* Returns ending timestamp to be used for the fetch request.
*
* @return Ending timestamp in seconds.
*/
public long getFetchEnd() {
return fetchEnd;
}
/**
* Returns fetch resolution to be used for the fetch request.
*
* @return Fetch resolution in seconds.
*/
public long getResolution() {
return resolution;
}
/**
* Dumps the content of fetch request using the syntax of RRDTool's fetch command.
*
* @return Fetch request dump.
*/
public String dump() {
return "fetch \"" + parentDb.getRrdBackend().getPath() +
"\" " + consolFun + " --start " + fetchStart + " --end " + fetchEnd +
(resolution > 1 ? " --resolution " + resolution : "");
}
String getRrdToolCommand() {
return dump();
}
/**
* Returns data from the underlying RRD and puts it in a single
* {@link org.rrd4j.core.FetchData FetchData} object.
*
* @return FetchData object filled with timestamps and datasource values.
* @throws java.io.IOException Thrown in case of I/O error.
*/
public FetchData fetchData() throws IOException {
return parentDb.fetchData(this);
}
/**
* Returns the underlying RrdDb object.
*
* @return RrdDb object used to create this FetchRequest object.
*/
public RrdDb getParentDb() {
return parentDb;
}
}

View File

@ -0,0 +1,239 @@
package org.rrd4j.core;
import java.io.IOException;
/**
* Class to represent RRD header. Header information is mainly static (once set, it
* cannot be changed), with the exception of last update time (this value is changed whenever
* RRD gets updated).
* <p>
*
* Normally, you don't need to manipulate the Header object directly - Rrd4j framework
* does it for you.
* <p>
*
* @author Sasa Markovic*
*/
public class Header implements RrdUpdater<Header> {
static final int SIGNATURE_LENGTH = 5;
static final String SIGNATURE = "RRD4J";
static final String DEFAULT_SIGNATURE = "RRD4J, version 0.1";
static final String RRDTOOL_VERSION1 = "0001";
static final String RRDTOOL_VERSION3 = "0003";
private static final String[] VERSIONS = {"version 0.1", "version 0.2"};
private RrdDb parentDb;
private int version = -1;
private RrdString<Header> signature;
private RrdLong<Header> step;
private RrdInt<Header> dsCount, arcCount;
private RrdLong<Header> lastUpdateTime;
Header(RrdDb parentDb, RrdDef rrdDef) throws IOException {
this.parentDb = parentDb;
String initSignature = null;
if(rrdDef != null) {
version = rrdDef.getVersion();
initSignature = SIGNATURE + ", " + VERSIONS[version - 1];
}
else {
initSignature = DEFAULT_SIGNATURE;
}
signature = new RrdString<>(this); // NOT constant, may be cached
step = new RrdLong<>(this, true); // constant, may be cached
dsCount = new RrdInt<>(this, true); // constant, may be cached
arcCount = new RrdInt<>(this, true); // constant, may be cached
lastUpdateTime = new RrdLong<>(this);
if (rrdDef != null) {
signature.set(initSignature);
step.set(rrdDef.getStep());
dsCount.set(rrdDef.getDsCount());
arcCount.set(rrdDef.getArcCount());
lastUpdateTime.set(rrdDef.getStartTime());
}
}
Header(RrdDb parentDb, DataImporter reader) throws IOException {
this(parentDb, (RrdDef) null);
String importVersion = reader.getVersion();
switch(importVersion) {
case RRDTOOL_VERSION1:
version = 1;
break;
case RRDTOOL_VERSION3:
version = 2;
break;
default:
throw new IllegalArgumentException("Could not get version " + version);
}
signature.set(SIGNATURE + ", " + VERSIONS[version - 1]);
step.set(reader.getStep());
dsCount.set(reader.getDsCount());
arcCount.set(reader.getArcCount());
lastUpdateTime.set(reader.getLastUpdateTime());
}
/**
* Returns RRD signature. Initially, the returned string will be
* of the form <b><i>Rrd4j, version x.x</i></b>.
*
* @return RRD signature
* @throws java.io.IOException Thrown in case of I/O error
*/
public String getSignature() throws IOException {
return signature.get();
}
/**
* <p>getInfo.</p>
*
* @return a {@link java.lang.String} object.
* @throws java.io.IOException if any.
*/
public String getInfo() throws IOException {
return getSignature().substring(SIGNATURE_LENGTH);
}
/**
* <p>setInfo.</p>
*
* @param info a {@link java.lang.String} object.
* @throws java.io.IOException if any.
*/
public void setInfo(String info) throws IOException {
if (info != null && info.length() > 0) {
signature.set(SIGNATURE + info);
}
else {
signature.set(SIGNATURE);
}
}
/**
* Returns the last update time of the RRD.
*
* @return Timestamp (Unix epoch, no milliseconds) corresponding to the last update time.
* @throws java.io.IOException Thrown in case of I/O error
*/
public long getLastUpdateTime() throws IOException {
return lastUpdateTime.get();
}
/**
* Returns primary RRD time step.
*
* @return Primary time step in seconds
* @throws java.io.IOException Thrown in case of I/O error
*/
public long getStep() throws IOException {
return step.get();
}
/**
* Returns the number of datasources defined in the RRD.
*
* @return Number of datasources defined
* @throws java.io.IOException Thrown in case of I/O error
*/
public int getDsCount() throws IOException {
return dsCount.get();
}
/**
* Returns the number of archives defined in the RRD.
*
* @return Number of archives defined
* @throws java.io.IOException Thrown in case of I/O error
*/
public int getArcCount() throws IOException {
return arcCount.get();
}
void setLastUpdateTime(long lastUpdateTime) throws IOException {
this.lastUpdateTime.set(lastUpdateTime);
}
String dump() throws IOException {
return "== HEADER ==\n" +
"signature:" + getSignature() +
" lastUpdateTime:" + getLastUpdateTime() +
" step:" + getStep() +
" dsCount:" + getDsCount() +
" arcCount:" + getArcCount() + "\n";
}
void appendXml(XmlWriter writer) throws IOException {
writer.writeComment(signature.get());
writer.writeTag("version", RRDTOOL_VERSION3);
writer.writeComment("Seconds");
writer.writeTag("step", step.get());
writer.writeComment(Util.getDate(lastUpdateTime.get()));
writer.writeTag("lastupdate", lastUpdateTime.get());
}
/**
* {@inheritDoc}
*
* Copies object's internal state to another Header object.
*/
public void copyStateTo(Header header) throws IOException {
header.lastUpdateTime.set(lastUpdateTime.get());
}
/**
* Returns the underlying storage (backend) object which actually performs all
* I/O operations.
*
* @return I/O backend object
*/
public RrdBackend getRrdBackend() {
return parentDb.getRrdBackend();
}
/**
* Return the RRD version.
*
* @return RRD version
* @throws java.io.IOException if any.
*/
public int getVersion() throws IOException {
if(version < 0) {
for(int i=0; i < VERSIONS.length; i++) {
if(signature.get().endsWith(VERSIONS[i])) {
version = i + 1;
break;
}
}
}
return version;
}
boolean isRrd4jHeader() {
try {
return signature.get().startsWith(SIGNATURE) || signature.get().startsWith("JR"); // backwards compatible with JRobin
} catch (IOException ioe) {
return false;
}
}
void validateHeader() throws IOException {
if (!isRrd4jHeader()) {
throw new InvalidRrdException("Invalid file header. File [" + parentDb.getCanonicalPath() + "] is not a RRD4J RRD file");
}
}
/**
* Required to implement RrdUpdater interface. You should never call this method directly.
*
* @return Allocator object
*/
public RrdAllocator getRrdAllocator() {
return parentDb.getRrdAllocator();
}
}

View File

@ -0,0 +1,20 @@
package org.rrd4j.core;
/**
* An exception indicating a corrupted RRD.
*
* @since 3.4
*/
public class InvalidRrdException extends RrdException {
private static final long serialVersionUID = 1L;
public InvalidRrdException(String message) {
super(message);
}
public InvalidRrdException(String message, Exception cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,152 @@
package org.rrd4j.core;
import java.io.IOException;
/**
* Class to represent archive values for a single datasource. Robin class is the heart of
* the so-called "round robin database" concept. Basically, each Robin object is a
* fixed length array of double values. Each double value represents consolidated, archived
* value for the specific timestamp. When the underlying array of double values gets completely
* filled, new values will replace the oldest ones.
* <p>
* Robin object does not hold values in memory - such object could be quite large.
* Instead of it, Robin reads them from the backend I/O only when necessary.
*
* @author Sasa Markovic
*/
public interface Robin extends RrdUpdater<Robin> {
/**
* Fetches all archived values.
*
* @return Array of double archive values, starting from the oldest one.
* @throws java.io.IOException Thrown in case of I/O specific error.
*/
double[] getValues() throws IOException;
/**
* Updates archived values in bulk.
*
* @param newValues Array of double values to be stored in the archive
* @throws java.io.IOException Thrown in case of I/O error
* @throws java.lang.IllegalArgumentException Thrown if the length of the input array is different from the length of
* this archive
*/
void setValues(double... newValues) throws IOException;
/**
* (Re)sets all values in this archive to the same value.
*
* @param newValue New value
* @throws java.io.IOException Thrown in case of I/O error
*/
void setValues(double newValue) throws IOException;
/**
* Returns the i-th value from the Robin archive.
*
* @param index Value index
* @return Value stored in the i-th position (the oldest value has zero index)
* @throws java.io.IOException Thrown in case of I/O specific error.
*/
double getValue(int index) throws IOException;
/**
* Sets the i-th value in the Robin archive.
*
* @param index index in the archive (the oldest value has zero index)
* @param value value to be stored
* @throws java.io.IOException Thrown in case of I/O specific error.
*/
void setValue(int index, double value) throws IOException;
/**
* Returns the Archive object to which this Robin object belongs.
*
* @return Parent Archive object
*/
Archive getParent();
/**
* Returns the size of the underlying array of archived values.
*
* @return Number of stored values
*/
int getSize();
/**
* {@inheritDoc}
*
* Copies object's internal state to another Robin object.
*/
void copyStateTo(Robin other) throws IOException;
/**
* Filters values stored in this archive based on the given boundary.
* Archived values found to be outside of <code>[minValue, maxValue]</code> interval (inclusive)
* will be silently replaced with <code>NaN</code>.
*
* @param minValue lower boundary
* @param maxValue upper boundary
* @throws java.io.IOException Thrown in case of I/O error
*/
void filterValues(double minValue, double maxValue) throws IOException;
/**
* Returns the underlying storage (backend) object which actually performs all
* I/O operations.
*
* @return I/O backend object
*/
RrdBackend getRrdBackend();
/**
* Required to implement RrdUpdater interface. You should never call this method directly.
*
* @return Allocator object
*/
RrdAllocator getRrdAllocator();
/**
* <p>update.</p>
*
* @param newValues an array of double.
* @throws java.io.IOException if any.
*/
void update(double[] newValues) throws IOException;
/**
* <p>dump.</p>
*
* @return a {@link java.lang.String} object.
* @throws java.io.IOException if any.
*/
String dump() throws IOException;
/**
* <p>store.</p>
*
* @param newValue a double.
* @throws java.io.IOException if any.
*/
void store(double newValue) throws IOException;
/**
* <p>bulkStore.</p>
*
* @param newValue a double.
* @param bulkCount a int.
* @throws java.io.IOException if any.
*/
void bulkStore(double newValue, int bulkCount) throws IOException;
/**
* <p>getValues.</p>
*
* @param index a int.
* @param count a int.
* @return an array of double.
* @throws java.io.IOException if any.
*/
double[] getValues(int index, int count) throws IOException;
}

View File

@ -0,0 +1,247 @@
package org.rrd4j.core;
import java.io.IOException;
/**
* Class to represent archive values for a single datasource. Robin class is the heart of
* the so-called "round robin database" concept. Basically, each Robin object is a
* fixed length array of double values. Each double value represents consolidated, archived
* value for the specific timestamp. When the underlying array of double values gets completely
* filled, new values will replace the oldest ones.<p>
* <p/>
* Robin object does not hold values in memory - such object could be quite large.
* Instead of it, Robin reads them from the backend I/O only when necessary.
*
* @author Sasa Markovic
*/
class RobinArray implements Robin {
private final Archive parentArc;
private final RrdInt<Robin> pointer;
private final RrdDoubleArray<Robin> values;
private int rows;
RobinArray(Archive parentArc, int rows, boolean shouldInitialize) throws IOException {
this.parentArc = parentArc;
this.pointer = new RrdInt<>(this);
this.values = new RrdDoubleArray<>(this, rows);
this.rows = rows;
if (shouldInitialize) {
pointer.set(0);
values.set(0, Double.NaN, rows);
}
}
/* (non-Javadoc)
* @see org.rrd4j.core.Robin#getValues()
*/
/**
* <p>Getter for the field <code>values</code>.</p>
*
* @return an array of double.
* @throws java.io.IOException if any.
*/
public double[] getValues() throws IOException {
return getValues(0, rows);
}
// stores single value
/** {@inheritDoc} */
public void store(double newValue) throws IOException {
int position = pointer.get();
values.set(position, newValue);
pointer.set((position + 1) % rows);
}
// stores the same value several times
/** {@inheritDoc} */
public void bulkStore(double newValue, int bulkCount) throws IOException {
assert bulkCount <= rows: "Invalid number of bulk updates: " + bulkCount + " rows=" + rows;
int position = pointer.get();
// update tail
int tailUpdateCount = Math.min(rows - position, bulkCount);
values.set(position, newValue, tailUpdateCount);
pointer.set((position + tailUpdateCount) % rows);
// do we need to update from the start?
int headUpdateCount = bulkCount - tailUpdateCount;
if (headUpdateCount > 0) {
values.set(0, newValue, headUpdateCount);
pointer.set(headUpdateCount);
}
}
/**
* <p>update.</p>
*
* @param newValues an array of double.
* @throws java.io.IOException if any.
*/
public void update(double[] newValues) throws IOException {
assert rows == newValues.length: "Invalid number of robin values supplied (" + newValues.length +
"), exactly " + rows + " needed";
pointer.set(0);
values.writeDouble(0, newValues);
}
/* (non-Javadoc)
* @see org.rrd4j.core.Robin#setValues(double)
*/
/** {@inheritDoc} */
public void setValues(double... newValues) throws IOException {
if (rows != newValues.length) {
throw new IllegalArgumentException("Invalid number of robin values supplied (" + newValues.length +
"), exactly " + rows + " needed");
}
update(newValues);
}
/* (non-Javadoc)
* @see org.rrd4j.core.Robin#setValues(double)
*/
/** {@inheritDoc} */
public void setValues(double newValue) throws IOException {
double[] values = new double[rows];
for (int i = 0; i < values.length; i++) {
values[i] = newValue;
}
update(values);
}
/**
* <p>dump.</p>
*
* @return a {@link java.lang.String} object.
* @throws java.io.IOException if any.
*/
public String dump() throws IOException {
StringBuilder buffer = new StringBuilder("Robin " + pointer.get() + "/" + rows + ": ");
double[] values = getValues();
for (double value : values) {
buffer.append(Util.formatDouble(value, true)).append(" ");
}
buffer.append("\n");
return buffer.toString();
}
/* (non-Javadoc)
* @see org.rrd4j.core.Robin#getValue(int)
*/
/** {@inheritDoc} */
public double getValue(int index) throws IOException {
int arrayIndex = (pointer.get() + index) % rows;
return values.get(arrayIndex);
}
/* (non-Javadoc)
* @see org.rrd4j.core.Robin#setValue(int, double)
*/
/** {@inheritDoc} */
public void setValue(int index, double value) throws IOException {
int arrayIndex = (pointer.get() + index) % rows;
values.set(arrayIndex, value);
}
/** {@inheritDoc} */
public double[] getValues(int index, int count) throws IOException {
assert count <= rows: "Too many values requested: " + count + " rows=" + rows;
int startIndex = (pointer.get() + index) % rows;
int tailReadCount = Math.min(rows - startIndex, count);
double[] tailValues = values.get(startIndex, tailReadCount);
if (tailReadCount < count) {
int headReadCount = count - tailReadCount;
double[] headValues = values.get(0, headReadCount);
double[] values = new double[count];
int k = 0;
for (double tailValue : tailValues) {
values[k++] = tailValue;
}
for (double headValue : headValues) {
values[k++] = headValue;
}
return values;
}
else {
return tailValues;
}
}
/* (non-Javadoc)
* @see org.rrd4j.core.Robin#getParent()
*/
/**
* <p>getParent.</p>
*
* @return a {@link org.rrd4j.core.Archive} object.
*/
public Archive getParent() {
return parentArc;
}
/* (non-Javadoc)
* @see org.rrd4j.core.Robin#getSize()
*/
/**
* <p>getSize.</p>
*
* @return a int.
*/
public int getSize() {
return rows;
}
/* (non-Javadoc)
* @see org.rrd4j.core.Robin#copyStateTo(org.rrd4j.core.RrdUpdater)
*/
/** {@inheritDoc} */
public void copyStateTo(Robin robin) throws IOException {
int rowsDiff = rows - robin.getSize();
for (int i = 0; i < robin.getSize(); i++) {
int j = i + rowsDiff;
robin.store(j >= 0 ? getValue(j) : Double.NaN);
}
}
/* (non-Javadoc)
* @see org.rrd4j.core.Robin#filterValues(double, double)
*/
/** {@inheritDoc} */
public void filterValues(double minValue, double maxValue) throws IOException {
for (int i = 0; i < rows; i++) {
double value = values.get(i);
if (!Double.isNaN(minValue) && !Double.isNaN(value) && minValue > value) {
values.set(i, Double.NaN);
}
if (!Double.isNaN(maxValue) && !Double.isNaN(value) && maxValue < value) {
values.set(i, Double.NaN);
}
}
}
/* (non-Javadoc)
* @see org.rrd4j.core.Robin#getRrdBackend()
*/
/**
* <p>getRrdBackend.</p>
*
* @return a {@link org.rrd4j.core.RrdBackend} object.
*/
public RrdBackend getRrdBackend() {
return parentArc.getRrdBackend();
}
/* (non-Javadoc)
* @see org.rrd4j.core.Robin#getRrdAllocator()
*/
/**
* <p>getRrdAllocator.</p>
*
* @return a {@link org.rrd4j.core.RrdAllocator} object.
*/
public RrdAllocator getRrdAllocator() {
return parentArc.getRrdAllocator();
}
}

View File

@ -0,0 +1,239 @@
package org.rrd4j.core;
import java.io.IOException;
/**
* Class to represent archive values for a single datasource. Robin class is the heart of
* the so-called "round robin database" concept. Basically, each Robin object is a
* fixed length array of double values. Each double value reperesents consolidated, archived
* value for the specific timestamp. When the underlying array of double values gets completely
* filled, new values will replace the oldest ones.<p>
* <p/>
* Robin object does not hold values in memory - such object could be quite large.
* Instead of it, Robin reads them from the backend I/O only when necessary.
*
* @author Fabrice Bacchella
*/
class RobinMatrix implements Robin {
private final Archive parentArc;
private final RrdInt<Archive> pointer;
private final RrdDoubleMatrix<Archive> values;
private int rows;
private int column;
RobinMatrix(Archive parentArc, RrdDoubleMatrix<Archive> values, RrdInt<Archive> pointer, int column) throws IOException {
this.parentArc = parentArc;
this.pointer = pointer;
this.values = values;
this.rows = values.getRows();
this.column = column;
}
/**
* Fetches all archived values.
*
* @return Array of double archive values, starting from the oldest one.
* @throws java.io.IOException Thrown in case of I/O specific error.
*/
public double[] getValues() throws IOException {
return getValues(0, rows);
}
// stores single value
/** {@inheritDoc} */
public void store(double newValue) throws IOException {
int position = pointer.get();
values.set(column, position, newValue);
pointer.set((position + 1) % rows);
}
// stores the same value several times
/** {@inheritDoc} */
public void bulkStore(double newValue, int bulkCount) throws IOException {
assert bulkCount <= rows: "Invalid number of bulk updates: " + bulkCount + " rows=" + rows;
int position = pointer.get();
// update tail
int tailUpdateCount = Math.min(rows - position, bulkCount);
values.set(column, position, newValue, tailUpdateCount);
pointer.set((position + tailUpdateCount) % rows);
// do we need to update from the start?
int headUpdateCount = bulkCount - tailUpdateCount;
if (headUpdateCount > 0) {
values.set(column, 0, newValue, headUpdateCount);
pointer.set(headUpdateCount);
}
}
/**
* <p>update.</p>
*
* @param newValues an array of double.
* @throws java.io.IOException if any.
*/
public void update(double[] newValues) throws IOException {
assert rows == newValues.length: "Invalid number of robin values supplied (" + newValues.length +
"), exactly " + rows + " needed";
pointer.set(0);
values.set(column, 0, newValues);
}
/**
* {@inheritDoc}
*
* Updates archived values in bulk.
*/
public void setValues(double... newValues) throws IOException {
if (rows != newValues.length) {
throw new IllegalArgumentException("Invalid number of robin values supplied (" + newValues.length +
"), exactly " + rows + " needed");
}
update(newValues);
}
/**
* {@inheritDoc}
*
* (Re)sets all values in this archive to the same value.
*/
public void setValues(double newValue) throws IOException {
double[] values = new double[rows];
for (int i = 0; i < values.length; i++) {
values[i] = newValue;
}
update(values);
}
/**
* <p>dump.</p>
*
* @return a {@link java.lang.String} object.
* @throws java.io.IOException if any.
*/
public String dump() throws IOException {
StringBuilder buffer = new StringBuilder("Robin " + pointer.get() + "/" + rows + ": ");
double[] values = getValues();
for (double value : values) {
buffer.append(Util.formatDouble(value, true)).append(" ");
}
buffer.append("\n");
return buffer.toString();
}
/**
* {@inheritDoc}
*
* Returns the i-th value from the Robin archive.
*/
public double getValue(int index) throws IOException {
int arrayIndex = (pointer.get() + index) % rows;
return values.get(column, arrayIndex);
}
/**
* {@inheritDoc}
*
* Sets the i-th value in the Robin archive.
*/
public void setValue(int index, double value) throws IOException {
int arrayIndex = (pointer.get() + index) % rows;
values.set(column, arrayIndex, value);
}
/** {@inheritDoc} */
public double[] getValues(int index, int count) throws IOException {
assert count <= rows: "Too many values requested: " + count + " rows=" + rows;
int startIndex = (pointer.get() + index) % rows;
int tailReadCount = Math.min(rows - startIndex, count);
double[] tailValues = values.get(column, startIndex, tailReadCount);
if (tailReadCount < count) {
int headReadCount = count - tailReadCount;
double[] headValues = values.get(column, 0, headReadCount);
double[] values = new double[count];
int k = 0;
for (double tailValue : tailValues) {
values[k++] = tailValue;
}
for (double headValue : headValues) {
values[k++] = headValue;
}
return values;
}
else {
return tailValues;
}
}
/**
* Returns the Archive object to which this Robin object belongs.
*
* @return Parent Archive object
*/
public Archive getParent() {
return parentArc;
}
/**
* Returns the size of the underlying array of archived values.
*
* @return Number of stored values
*/
public int getSize() {
return rows;
}
/**
* {@inheritDoc}
*
* Copies object's internal state to another Robin object.
*/
public void copyStateTo(Robin robin) throws IOException {
int rowsDiff = rows - robin.getSize();
for (int i = 0; i < robin.getSize(); i++) {
int j = i + rowsDiff;
robin.store(j >= 0 ? getValue(j) : Double.NaN);
}
}
/**
* {@inheritDoc}
*
* Filters values stored in this archive based on the given boundary.
* Archived values found to be outside of <code>[minValue, maxValue]</code> interval (inclusive)
* will be silently replaced with <code>NaN</code>.
*/
public void filterValues(double minValue, double maxValue) throws IOException {
for (int i = 0; i < rows; i++) {
double value = values.get(column, i);
if (!Double.isNaN(minValue) && !Double.isNaN(value) && minValue > value) {
values.set(column, i, Double.NaN);
}
if (!Double.isNaN(maxValue) && !Double.isNaN(value) && maxValue < value) {
values.set(column, i, Double.NaN);
}
}
}
/**
* Returns the underlying storage (backend) object which actually performs all
* I/O operations.
*
* @return I/O backend object
*/
public RrdBackend getRrdBackend() {
return parentArc.getRrdBackend();
}
/**
* Required to implement RrdUpdater interface. You should never call this method directly.
*
* @return Allocator object
*/
public RrdAllocator getRrdAllocator() {
return parentArc.getRrdAllocator();
}
}

View File

@ -0,0 +1,22 @@
package org.rrd4j.core;
import java.io.IOException;
/**
* An internal usage class.
*
* @author Sasa Markovic
*/
public class RrdAllocator {
private long allocationPointer = 0L;
RrdAllocator() {
super();
}
long allocate(long byteCount) throws IOException {
long pointer = allocationPointer;
allocationPointer += byteCount;
return pointer;
}
}

View File

@ -0,0 +1,431 @@
package org.rrd4j.core;
import java.io.IOException;
import java.lang.ref.PhantomReference;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
/**
* <p>Base implementation class for all backend classes. Each Round Robin Database object
* ({@link org.rrd4j.core.RrdDb} object) is backed with a single RrdBackend object which performs
* actual I/O operations on the underlying storage. Rrd4j supports
* multiple backends out of the box. E.g.:</p>
* <ul>
* <li>{@link org.rrd4j.core.RrdRandomAccessFileBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdRandomAccessFileBackendFactory} class. This was the default backend used in all
* Rrd4j releases prior to 1.4.0. It uses java.io.* package and
* RandomAccessFile class to store RRD data in files on the disk.
*
* <li>{@link org.rrd4j.core.RrdNioBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdNioBackendFactory} class. The backend uses java.io.* and java.nio.*
* classes (mapped ByteBuffer) to store RRD data in files on the disk. This backend is fast, very fast,
* but consumes a lot of memory (borrowed not from the JVM but from the underlying operating system
* directly). <b>This is the default backend used in Rrd4j since 1.4.0 release.</b>
*
* <li>{@link org.rrd4j.core.RrdMemoryBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdMemoryBackendFactory} class. This backend stores all data in memory. Once
* JVM exits, all data gets lost. The backend is extremely fast and memory hungry.
* </ul>
*
* <p>To create your own backend in order to provide some custom type of RRD storage,
* you should do the following:</p>
*
* <ul>
* <li>Create your custom RrdBackend class (RrdCustomBackend, for example)
* by extending RrdBackend class. You have to implement all abstract methods defined
* in the base class.
*
* <li>Create your custom RrdBackendFactory class (RrdCustomBackendFactory,
* for example) by extending RrdBackendFactory class. You have to implement all
* abstract methods defined in the base class. Your custom factory class will actually
* create custom backend objects when necessary.
*
* <li>Create instance of your custom RrdBackendFactory and register it as a regular
* factory available to Rrd4j framework. See javadoc for {@link org.rrd4j.core.RrdBackendFactory} to
* find out how to do this.
* </ul>
*
* @author Sasa Markovic
*/
public abstract class RrdBackend {
/**
* All {@link java.nio.ByteBuffer} usage should use this standard order.
*/
protected static final ByteOrder BYTEORDER = ByteOrder.BIG_ENDIAN;
private static final char STARTPRIVATEAREA = '\ue000';
private static final char ENDPRIVATEAREA = '\uf8ff';
private static final int STARTPRIVATEAREACODEPOINT = Character.codePointAt(new char[]{STARTPRIVATEAREA}, 0);
private static final int ENDPRIVATEAREACODEPOINT = Character.codePointAt(new char[]{ENDPRIVATEAREA}, 0);
private static final int PRIVATEAREASIZE = ENDPRIVATEAREACODEPOINT - STARTPRIVATEAREACODEPOINT + 1;
private static final int MAXUNSIGNEDSHORT = Short.MAX_VALUE - Short.MIN_VALUE;
private static volatile boolean instanceCreated = false;
private final String path;
private RrdBackendFactory factory;
private long nextBigStringOffset = -1;
private PhantomReference<RrdDb> ref;
/**
* Creates backend for a RRD storage with the given path.
*
* @param path String identifying RRD storage. For files on the disk, this
* argument should represent file path. Other storage types might interpret
* this argument differently.
*/
protected RrdBackend(String path) {
this.path = path;
instanceCreated = true;
}
/**
*
* @param factory the factory to set
*/
void done(RrdBackendFactory factory, PhantomReference<RrdDb> ref) {
this.factory = factory;
this.ref = ref;
}
/**
* @return the factory
*/
public RrdBackendFactory getFactory() {
return factory;
}
/**
* Returns path to the storage.
*
* @return Storage path
*/
public String getPath() {
return path;
}
/**
* Return the URI associated to this backend, using the factory to generate it from the path.
*
* @return URI to this backend's rrd.
*/
public URI getUri() {
return factory.getUri(path);
}
/**
* Writes an array of bytes to the underlying storage starting from the given
* storage offset.
*
* @param offset Storage offset.
* @param b Array of bytes that should be copied to the underlying storage
* @throws java.io.IOException Thrown in case of I/O error
*/
protected abstract void write(long offset, byte[] b) throws IOException;
/**
* Reads an array of bytes from the underlying storage starting from the given
* storage offset.
*
* @param offset Storage offset.
* @param b Array which receives bytes from the underlying storage
* @throws java.io.IOException Thrown in case of I/O error
*/
protected abstract void read(long offset, byte[] b) throws IOException;
/**
* Returns the number of RRD bytes in the underlying storage.
*
* @return Number of RRD bytes in the storage.
* @throws java.io.IOException Thrown in case of I/O error.
*/
public abstract long getLength() throws IOException;
/**
* Sets the number of bytes in the underlying RRD storage.
* This method is called only once, immediately after a new RRD storage gets created.
*
* @param length Length of the underlying RRD storage in bytes.
* @throws java.io.IOException Thrown in case of I/O error.
*/
protected abstract void setLength(long length) throws IOException;
/**
* Closes the underlying backend. Used internally, should not be called from external code.
*
* @throws java.io.IOException Thrown in case of I/O error
*/
protected abstract void close() throws IOException;
/**
* Closes the underlying backend. Call by {@code RrdDb#close()} when it's closed. All subclass must keep calling it.
*
* @throws java.io.IOException Thrown in case of I/O error
*/
protected void rrdClose() throws IOException {
try {
close();
} finally {
if (ref != null) {
ref.clear();
}
}
}
/**
* This method suggests the caching policy to the Rrd4j frontend (high-level) classes. If <code>true</code>
* is returned, frontend classes will cache frequently used parts of a RRD file in memory to improve
* performance. If <code>false</code> is returned, high level classes will never cache RRD file sections
* in memory.
*
* @return <code>true</code> if file caching is enabled, <code>false</code> otherwise. By default, the
* method returns <code>true</code> but it can be overridden in subclasses.
*/
protected boolean isCachingAllowed() {
return factory.cachingAllowed;
}
/**
* Reads all RRD bytes from the underlying storage.
*
* @return RRD bytes
* @throws java.io.IOException Thrown in case of I/O error
*/
public final byte[] readAll() throws IOException {
byte[] b = new byte[(int) getLength()];
read(0, b);
return b;
}
protected void writeShort(long offset, short value) throws IOException {
byte[] b = new byte[2];
b[0] = (byte) ((value >>> 8) & 0xFF);
b[1] = (byte) ((value >>> 0) & 0xFF);
write(offset, b);
}
protected void writeInt(long offset, int value) throws IOException {
write(offset, getIntBytes(value));
}
protected void writeLong(long offset, long value) throws IOException {
write(offset, getLongBytes(value));
}
protected void writeDouble(long offset, double value) throws IOException {
write(offset, getDoubleBytes(value));
}
protected void writeDouble(long offset, double value, int count) throws IOException {
byte[] b = getDoubleBytes(value);
byte[] image = new byte[8 * count];
int k = 0;
for (int i = 0; i < count; i++) {
image[k++] = b[0];
image[k++] = b[1];
image[k++] = b[2];
image[k++] = b[3];
image[k++] = b[4];
image[k++] = b[5];
image[k++] = b[6];
image[k++] = b[7];
}
write(offset, image);
}
protected void writeDouble(long offset, double[] values) throws IOException {
int count = values.length;
byte[] image = new byte[8 * count];
int k = 0;
for (int i = 0; i < count; i++) {
byte[] b = getDoubleBytes(values[i]);
image[k++] = b[0];
image[k++] = b[1];
image[k++] = b[2];
image[k++] = b[3];
image[k++] = b[4];
image[k++] = b[5];
image[k++] = b[6];
image[k++] = b[7];
}
write(offset, image);
}
protected final void writeString(long offset, String value) throws IOException {
if (nextBigStringOffset < 0) {
nextBigStringOffset = getLength() - (Short.SIZE / 8);
}
value = value.trim();
// Over-sized string are appended at the end of the RRD
// The real position is encoded in the "short" ds name, using the private use area from Unicode
// This area span the range E000-F8FF, that' a 6400 char area,
if (value.length() > RrdPrimitive.STRING_LENGTH) {
String bigString = value;
int byteStringLength = Math.min(MAXUNSIGNEDSHORT, bigString.length());
long bigStringOffset = nextBigStringOffset;
nextBigStringOffset -= byteStringLength * 2 + (Short.SIZE / 8);
writeShort(bigStringOffset, (short)byteStringLength);
writeString(bigStringOffset - bigString.length() * 2, bigString, byteStringLength);
// Now we generate the new string that encode the position
long reminder = bigStringOffset;
StringBuilder newValue = new StringBuilder(value.substring(0, RrdPrimitive.STRING_LENGTH));
int i = RrdPrimitive.STRING_LENGTH;
// Read in inverse order, so write in inverse order
while (reminder > 0) {
// Only the first char is kept, as it will never byte a multi-char code point
newValue.setCharAt(--i, Character.toChars((int)(reminder % PRIVATEAREASIZE + STARTPRIVATEAREACODEPOINT))[0]);
reminder = (long) Math.floor( ((float)reminder) / (float)PRIVATEAREASIZE);
}
value = newValue.toString();
}
writeString(offset, value, RrdPrimitive.STRING_LENGTH);
}
protected void writeString(long offset, String value, int length) throws IOException {
ByteBuffer bbuf = ByteBuffer.allocate(length * 2);
bbuf.order(BYTEORDER);
bbuf.position(0);
bbuf.limit(length * 2);
CharBuffer cbuf = bbuf.asCharBuffer();
cbuf.put(value);
while (cbuf.position() < cbuf.limit()) {
cbuf.put(' ');
}
write(offset, bbuf.array());
}
protected short readShort(long offset) throws IOException {
byte[] b = new byte[2];
read(offset, b);
return (short) (((b[0] << 8) & 0x0000FF00) + (b[1] & 0x000000FF));
}
protected int readInt(long offset) throws IOException {
byte[] b = new byte[4];
read(offset, b);
return getInt(b);
}
protected long readLong(long offset) throws IOException {
byte[] b = new byte[8];
read(offset, b);
return getLong(b);
}
protected double readDouble(long offset) throws IOException {
byte[] b = new byte[8];
read(offset, b);
return getDouble(b);
}
protected double[] readDouble(long offset, int count) throws IOException {
int byteCount = 8 * count;
byte[] image = new byte[byteCount];
read(offset, image);
double[] values = new double[count];
int k = -1;
for (int i = 0; i < count; i++) {
byte[] b = new byte[]{
image[++k], image[++k], image[++k], image[++k],
image[++k], image[++k], image[++k], image[++k]
};
values[i] = getDouble(b);
}
return values;
}
/**
* Extract a CharBuffer from the backend, used by readString
*
* @param offset
* @param size
* @return
* @throws IOException
*/
protected CharBuffer getCharBuffer(long offset, int size) throws IOException {
ByteBuffer bbuf = ByteBuffer.allocate(size * 2);
bbuf.order(BYTEORDER);
read(offset, bbuf.array());
bbuf.position(0);
bbuf.limit(size * 2);
return bbuf.asCharBuffer();
}
protected final String readString(long offset) throws IOException {
CharBuffer cbuf = getCharBuffer(offset, RrdPrimitive.STRING_LENGTH);
long realStringOffset = 0;
int i = -1;
while (++i < RrdPrimitive.STRING_LENGTH) {
char c = cbuf.charAt(RrdPrimitive.STRING_LENGTH - i - 1);
if (c >= STARTPRIVATEAREA && c <= ENDPRIVATEAREA) {
realStringOffset += ((long) c - STARTPRIVATEAREACODEPOINT) * Math.pow(PRIVATEAREASIZE, i);
cbuf.limit(RrdPrimitive.STRING_LENGTH - i - 1);
} else {
break;
}
}
if (realStringOffset > 0) {
int bigStringSize = readShort(realStringOffset);
// Signed to unsigned arithmetic
if (bigStringSize < 0) {
bigStringSize += MAXUNSIGNEDSHORT + 1;
}
return getCharBuffer(realStringOffset - bigStringSize * 2, bigStringSize).toString();
} else {
return cbuf.slice().toString().trim();
}
}
// static helper methods
private static byte[] getIntBytes(int value) {
byte[] b = new byte[4];
b[0] = (byte) ((value >>> 24) & 0xFF);
b[1] = (byte) ((value >>> 16) & 0xFF);
b[2] = (byte) ((value >>> 8) & 0xFF);
b[3] = (byte) ((value >>> 0) & 0xFF);
return b;
}
private static byte[] getLongBytes(long value) {
byte[] b = new byte[8];
b[0] = (byte) ((int) (value >>> 56) & 0xFF);
b[1] = (byte) ((int) (value >>> 48) & 0xFF);
b[2] = (byte) ((int) (value >>> 40) & 0xFF);
b[3] = (byte) ((int) (value >>> 32) & 0xFF);
b[4] = (byte) ((int) (value >>> 24) & 0xFF);
b[5] = (byte) ((int) (value >>> 16) & 0xFF);
b[6] = (byte) ((int) (value >>> 8) & 0xFF);
b[7] = (byte) ((int) (value >>> 0) & 0xFF);
return b;
}
private static byte[] getDoubleBytes(double value) {
return getLongBytes(Double.doubleToLongBits(value));
}
private static int getInt(byte[] b) {
assert b.length == 4 : "Invalid number of bytes for integer conversion";
return ((b[0] << 24) & 0xFF000000) + ((b[1] << 16) & 0x00FF0000) +
((b[2] << 8) & 0x0000FF00) + (b[3] & 0x000000FF);
}
private static long getLong(byte[] b) {
assert b.length == 8 : "Invalid number of bytes for long conversion";
int high = getInt(new byte[]{b[0], b[1], b[2], b[3]});
int low = getInt(new byte[]{b[4], b[5], b[6], b[7]});
return ((long) (high) << 32) + (low & 0xFFFFFFFFL);
}
private static double getDouble(byte[] b) {
assert b.length == 8 : "Invalid number of bytes for double conversion";
return Double.longBitsToDouble(getLong(b));
}
static boolean isInstanceCreated() {
return instanceCreated;
}
}

View File

@ -0,0 +1,26 @@
package org.rrd4j.core;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
* Description of a {@link RrdBackendFactory}
*
* @author Fabrice Bacchella
* @since 3.4
*
*/
@Documented
@Retention(RUNTIME)
@Target(TYPE)
public @interface RrdBackendAnnotation {
public static boolean DEFAULT_CACHING_ALLOWED = true;
String name();
boolean cachingAllowed() default DEFAULT_CACHING_ALLOWED;
String scheme() default "";
boolean shouldValidateHeader();
}

View File

@ -0,0 +1,20 @@
package org.rrd4j.core;
/**
* Wrap a exception generated by the backend store
*
* @author Fabrice Bacchella
* @since 3.4
*
*/
public class RrdBackendException extends RrdException {
public RrdBackendException(String message) {
super(message);
}
public RrdBackendException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,576 @@
package org.rrd4j.core;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Base (abstract) backend factory class which holds references to all concrete
* backend factories and defines abstract methods which must be implemented in
* all concrete factory implementations.
* <p>
*
* Factory classes are used to create concrete {@link org.rrd4j.core.RrdBackend} implementations.
* Each factory creates unlimited number of specific backend objects.
*
* Rrd4j supports six different backend types (backend factories) out of the box:
* <ul>
* <li>{@link org.rrd4j.core.RrdRandomAccessFileBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdRandomAccessFileBackendFactory} class. This was the default backend used in all
* Rrd4j releases before 1.4.0 release. It uses java.io.* package and RandomAccessFile class to store
* RRD data in files on the disk.
*
* <li>{@link org.rrd4j.core.RrdSafeFileBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdSafeFileBackendFactory} class. It uses java.io.* package and RandomAccessFile class to store
* RRD data in files on the disk. This backend is SAFE:
* it locks the underlying RRD file during update/fetch operations, and caches only static
* parts of a RRD file in memory. Therefore, this backend is safe to be used when RRD files should
* be shared <b>between several JVMs</b> at the same time. However, this backend is *slow* since it does
* not use fast java.nio.* package (it's still based on the RandomAccessFile class).
*
* <li>{@link org.rrd4j.core.RrdNioBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdNioBackendFactory} class. The backend uses java.io.* and java.nio.*
* classes (mapped ByteBuffer) to store RRD data in files on the disk. This is the default backend
* since 1.4.0 release.
*
* <li>{@link org.rrd4j.core.RrdMemoryBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdMemoryBackendFactory} class. This backend stores all data in memory. Once
* JVM exits, all data gets lost. The backend is extremely fast and memory hungry.
*
* <li>{@link org.rrd4j.core.RrdBerkeleyDbBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdBerkeleyDbBackendFactory} class. It stores RRD data to ordinary disk files
* using <a href="http://www.oracle.com/technetwork/database/berkeleydb/overview/index-093405.html">Oracle Berkeley DB</a> Java Edition.
*
* <li>{@link org.rrd4j.core.RrdMongoDBBackend}: objects of this class are created from the {@link org.rrd4j.core.RrdMongoDBBackendFactory} class.
* It stores data in a {@link com.mongodb.DBCollection} from <a href="http://www.mongodb.org/">MongoDB</a>.
* </ul>
* <p>
* Each backend factory used to be identified by its {@link #getName() name}. Constructors
* are provided in the {@link org.rrd4j.core.RrdDb} class to create RrdDb objects (RRD databases)
* backed with a specific backend.
* <p>
* A more generic management was added in version 3.2 that allows multiple instances of a backend to be used. Each backend can
* manage custom URL. They are tried in the declared order by the {@link #setActiveFactories(RrdBackendFactory...)} or
* {@link #addFactories(RrdBackendFactory...)} and the method {@link #canStore(URI)} return true when it can manage the given
* URI. Using {@link #setActiveFactories(RrdBackendFactory...)} with new created instance is the preferred way to manage factories, as
* it provides a much precise control of creation and end of life of factories.
* <p>
* Since 3.4, using only {@link #setActiveFactories(RrdBackendFactory...)} and {@link #addActiveFactories(RrdBackendFactory...)} will not register any
* named backend at all. {@link #getDefaultFactory()} will return the first active factory. All methods using named backend and the registry of factory were deprecated.
* <p>
* For default implementation, the path is separated in a root URI prefix and the path components. The root URI can be
* used to identify different name spaces or just be `/`.
* <p>
* See javadoc for {@link org.rrd4j.core.RrdBackend} to find out how to create your custom backends.
*
*/
public abstract class RrdBackendFactory implements Closeable {
private static final class Registry {
private static final Map<String, RrdBackendFactory> factories = new HashMap<>();
static {
RrdRandomAccessFileBackendFactory fileFactory = new RrdRandomAccessFileBackendFactory();
factories.put(fileFactory.name, fileFactory);
RrdMemoryBackendFactory memoryFactory = new RrdMemoryBackendFactory();
factories.put(memoryFactory.name, memoryFactory);
RrdNioBackendFactory nioFactory = new RrdNioBackendFactory();
factories.put(nioFactory.name, nioFactory);
RrdSafeFileBackendFactory safeFactory = new RrdSafeFileBackendFactory();
factories.put(safeFactory.name, safeFactory);
defaultFactory = factories.get(DEFAULTFACTORY);
}
private static RrdBackendFactory defaultFactory;
}
/**
* The default factory type. It will also put in the active factories list.
*
*/
public static final String DEFAULTFACTORY = "NIO";
private static final List<RrdBackendFactory> activeFactories = new ArrayList<>();
/**
* Returns backend factory for the given backend factory name.
*
* @param name Backend factory name. Initially supported names are:
* <ul>
* <li><b>FILE</b>: Default factory which creates backends based on the
* java.io.* package. RRD data is stored in files on the disk
* <li><b>SAFE</b>: Default factory which creates backends based on the
* java.io.* package. RRD data is stored in files on the disk. This backend
* is "safe". Being safe means that RRD files can be safely shared between
* several JVM's.
* <li><b>NIO</b>: Factory which creates backends based on the
* java.nio.* package. RRD data is stored in files on the disk
* <li><b>MEMORY</b>: Factory which creates memory-oriented backends.
* RRD data is stored in memory, it gets lost as soon as JVM exits.
* <li><b>BERKELEY</b>: a memory-oriented backend that ensure persistent
* in a <a href="http://www.oracle.com/technetwork/database/berkeleydb/overview/index-093405.html">Berkeley Db</a> storage.
* <li><b>MONGODB</b>: a memory-oriented backend that ensure persistent
* in a <a href="http://www.mongodb.org/">MongoDB</a> storage.
* </ul>
*
* @deprecated Uses active factory instead
* @return Backend factory for the given factory name
*/
@Deprecated
public static synchronized RrdBackendFactory getFactory(String name) {
RrdBackendFactory factory = Registry.factories.get(name);
if (factory != null) {
return factory;
} else {
throw new IllegalArgumentException(
"No backend factory found with the name specified ["
+ name + "]");
}
}
/**
* Registers new (custom) backend factory within the Rrd4j framework.
*
* @deprecated Uses active factory instead
* @param factory Factory to be registered
*/
@Deprecated
public static synchronized void registerFactory(RrdBackendFactory factory) {
String name = factory.getName();
if (!Registry.factories.containsKey(name)) {
Registry.factories.put(name, factory);
}
else {
throw new IllegalArgumentException("Backend factory '" + name + "' cannot be registered twice");
}
}
/**
* Registers new (custom) backend factory within the Rrd4j framework and sets this
* factory as the default.
*
* @deprecated Uses {@link #setActiveFactories(RrdBackendFactory...)} instead.
* @param factory Factory to be registered and set as default
*/
@Deprecated
public static synchronized void registerAndSetAsDefaultFactory(RrdBackendFactory factory) {
registerFactory(factory);
setDefaultFactory(factory.getName());
}
/**
* Returns the default backend factory. This factory is used to construct
* {@link org.rrd4j.core.RrdDb} objects if no factory is specified in the RrdDb constructor.
*
* @return Default backend factory.
*/
public static synchronized RrdBackendFactory getDefaultFactory() {
if (!activeFactories.isEmpty()) {
return activeFactories.get(0);
} else {
return Registry.defaultFactory;
}
}
/**
* Replaces the default backend factory with a new one. This method must be called before
* the first RRD gets created.
* <p>
* It also clear the list of actives factories and set it to the default factory.
* <p>
*
* @deprecated Uses active factory instead
* @param factoryName Name of the default factory..
*/
@Deprecated
public static synchronized void setDefaultFactory(String factoryName) {
// We will allow this only if no RRDs are created
if (!RrdBackend.isInstanceCreated()) {
activeFactories.clear();
activeFactories.add(getFactory(factoryName));
} else {
throw new IllegalStateException(
"Could not change the default backend factory. "
+ "This method must be called before the first RRD gets created");
}
}
/**
* Set the list of active factories, i.e. the factory used to resolve URI.
*
* @param newFactories the new active factories.
*/
public static synchronized void setActiveFactories(RrdBackendFactory... newFactories) {
activeFactories.clear();
activeFactories.addAll(Arrays.asList(newFactories));
}
/**
* Add factories to the list of active factories, i.e. the factory used to resolve URI.
*
* @deprecated Uses {@link #addActiveFactories(RrdBackendFactory...)} instead.
* @param newFactories active factories to add.
*/
@Deprecated
public static synchronized void addFactories(RrdBackendFactory... newFactories) {
addActiveFactories(newFactories);
}
/**
* Add factories to the list of active factories, i.e. the factory used to resolve URI.
*
* @param newFactories active factories to add.
*/
public static synchronized void addActiveFactories(RrdBackendFactory... newFactories) {
activeFactories.addAll(Arrays.asList(newFactories));
}
/**
* For a given URI, try to find a factory that can manage it in the list of active factories.
*
* @param uri URI to try.
* @return a {@link RrdBackendFactory} that can manage that URI.
* @throws IllegalArgumentException when no matching factory is found.
*/
public static synchronized RrdBackendFactory findFactory(URI uri) {
// If no active factory defined, will try the default factory
if (activeFactories.isEmpty() && Registry.defaultFactory.canStore(uri)) {
return Registry.defaultFactory;
} else {
for (RrdBackendFactory tryfactory: activeFactories) {
if (tryfactory.canStore(uri)) {
return tryfactory;
}
}
throw new IllegalArgumentException(
"no matching backend factory for " + uri);
}
}
private static final Pattern URIPATTERN = Pattern.compile("^(?:(?<scheme>[a-zA-Z][a-zA-Z0-9+-\\.]*):)?(?://(?<authority>[^/\\?#]*))?(?<path>[^\\?#]*)(?:\\?(?<query>[^#]*))?(?:#(?<fragment>.*))?$");
/**
* Try to detect an URI from a path. It's needed because of windows path that look's like an URI
* and to URL-encode the path.
*
* @param rrdpath
* @return an URI
*/
public static URI buildGenericUri(String rrdpath) {
Matcher urimatcher = URIPATTERN.matcher(rrdpath);
if (urimatcher.matches()) {
String scheme = urimatcher.group("scheme");
String authority = urimatcher.group("authority");
String path = urimatcher.group("path");
String query = urimatcher.group("query");
String fragment = urimatcher.group("fragment");
try {
// If scheme is a single letter, it's not a scheme, but a windows path
if (scheme != null && scheme.length() == 1) {
return new File(rrdpath).toURI();
}
// A scheme and a not absolute path, it's an opaque URI
if (scheme != null && path.charAt(0) != '/') {
return new URI(scheme, path, query);
}
// A relative file was given, ensure that it's OK if it was on a non-unix plateform
if (File.separatorChar != '/' && scheme == null) {
path = path.replace(File.separatorChar, '/');
}
return new URI(scheme, authority, path, query, fragment);
} catch (URISyntaxException ex) {
throw new IllegalArgumentException(ex.getMessage(), ex);
}
}
throw new IllegalArgumentException("Not an URI pattern");
}
private static class ClosingReference extends PhantomReference<RrdDb> {
private RrdBackend backend;
public ClosingReference(RrdDb db, RrdBackend backend,
ReferenceQueue<? super RrdDb> q) {
super(db, q);
this.backend = backend;
}
@Override
public void clear() {
try {
backend.close();
} catch (IOException e) {
}
backend = null;
super.clear();
}
}
private final ReferenceQueue<RrdDb> refQueue = new ReferenceQueue<>();
protected final String name;
protected final boolean cachingAllowed;
protected final String scheme;
protected final boolean validateHeader;
protected RrdBackendFactory() {
RrdBackendAnnotation annotation = getClass().getAnnotation(RrdBackendAnnotation.class);
if (annotation != null) {
name = annotation.name();
cachingAllowed = annotation.cachingAllowed();
if (annotation.scheme() != null && ! annotation.scheme().isEmpty()) {
scheme = annotation.scheme();
} else {
scheme = name.toLowerCase(Locale.ENGLISH);
}
validateHeader = annotation.shouldValidateHeader();
} else {
name = getName();
cachingAllowed = RrdBackendAnnotation.DEFAULT_CACHING_ALLOWED;
scheme = getName().toLowerCase(Locale.ENGLISH);
validateHeader = true;
}
}
/**
* Check that all phantom reference are indeed safely closed.
*/
public void checkClosing() {
while(true) {
ClosingReference ref = (ClosingReference) refQueue.poll();
if (ref == null) {
break;
} else if (ref.backend != null) {
try {
ref.backend.close();
} catch (IOException e) {
}
}
}
}
/**
* @return the scheme name for URI, default to getName().toLowerCase()
*/
public String getScheme() {
return scheme;
}
protected URI getRootUri() {
try {
return new URI(getScheme(), null, "/", null, null);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid scheme " + getScheme());
}
}
public boolean canStore(URI uri) {
return false;
}
/**
* Try to match an URI against a root URI using a few rules:
* <ul>
* <li>scheme must match if they are given.
* <li>authority must match if they are given.
* <li>if uri is opaque (scheme:nonabsolute), the scheme specific part is resolve as a relative path.
* <li>query and fragment is kept as is.
* </ul>
*
* @param rootUri
* @param uri
* @param relative
* @return a calculate normalized absolute URI or null if the tried URL don't match against the root.
*/
protected URI resolve(URI rootUri, URI uri, boolean relative) {
String scheme = uri.getScheme();
if (scheme != null && ! scheme.equals(rootUri.getScheme())) {
throw new IllegalArgumentException(String.format("scheme %s not compatible with %s", scheme, rootUri.getScheme()));
} else if (scheme == null) {
scheme = rootUri.getScheme();
}
String authority = uri.getAuthority();
if (authority != null && ! authority.equals(rootUri.getAuthority())) {
throw new IllegalArgumentException("URI credential not compatible");
} else if (authority == null) {
authority = rootUri.getAuthority();
}
String path;
if (uri.isOpaque()) {
// try to resolve an opaque uri as scheme:relativepath
path = uri.getSchemeSpecificPart();
} else if (! uri.isAbsolute()) {
// A relative URI, resolve it against the root
path = rootUri.resolve(uri).normalize().getPath();
} else {
path = uri.normalize().getPath();
}
if (! path.startsWith(rootUri.getPath())) {
throw new IllegalArgumentException(String.format("URI destination path %s not root with %s", path, rootUri.getPath()));
}
String query = uri.getQuery();
String fragment = uri.getFragment();
try {
authority = authority != null ? authority : "";
query = query != null ? "?" + URLEncoder.encode(query, "UTF-8") : "";
fragment = fragment != null ? "#" + URLEncoder.encode(fragment, "UTF-8") : "";
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("UTF-8 is missing");
}
String newUriString = String.format("%s://%s%s%s%s", scheme, authority, path , query, fragment);
URI newURI = URI.create(newUriString);
if (relative) {
return rootUri.relativize(newURI);
} else {
return newURI;
}
}
/**
* Ensure that an URI is returned in a non-ambiguous way.
*
* @param uri a valid URI for this backend.
* @return the canonized URI.
*/
public URI getCanonicalUri(URI uri) {
return resolve(getRootUri(), uri, false);
}
/**
* Transform an path in a valid URI for this backend.
*
* @param path
* @return
*/
public URI getUri(String path) {
URI rootUri = getRootUri();
if (path.startsWith("/")) {
path = path.substring(1);
}
try {
return new URI(getScheme(), rootUri.getAuthority(), rootUri.getPath() + path, null, null);
} catch (URISyntaxException ex) {
throw new IllegalArgumentException(ex.getMessage(), ex);
}
}
/**
* Extract the local path from an URI.
*
* @param uri The URI to parse.
* @return the local path from the URI.
*/
public String getPath(URI uri) {
URI rootUri = getRootUri();
uri = resolve(rootUri, uri, true);
if (uri == null) {
return null;
}
return "/" + uri.getPath();
}
protected abstract RrdBackend open(String path, boolean readOnly) throws IOException;
/**
* Creates RrdBackend object for the given storage path.
*
* @param path Storage path
* @param readOnly True, if the storage should be accessed in read/only mode.
* False otherwise.
* @return Backend object which handles all I/O operations for the given storage path
* @throws java.io.IOException Thrown in case of I/O error.
*/
RrdBackend getBackend(RrdDb rrdDb, String path, boolean readOnly) throws IOException {
checkClosing();
RrdBackend backend = open(path, readOnly);
backend.done(this, new ClosingReference(rrdDb, backend, refQueue));
return backend;
}
/**
* Creates RrdBackend object for the given storage path.
* @param rrdDb
*
* @param uri Storage uri
* @param readOnly True, if the storage should be accessed in read/only mode.
* False otherwise.
* @return Backend object which handles all I/O operations for the given storage path
* @throws java.io.IOException Thrown in case of I/O error.
*/
RrdBackend getBackend(RrdDb rrdDb, URI uri, boolean readOnly) throws IOException {
checkClosing();
RrdBackend backend = open(getPath(uri), readOnly);
backend.done(this, new ClosingReference(rrdDb, backend, refQueue));
return backend;
}
/**
* Determines if a storage with the given path already exists.
*
* @param path Storage path
* @throws java.io.IOException in case of I/O error.
* @return a boolean.
*/
protected abstract boolean exists(String path) throws IOException;
/**
* Determines if a storage with the given URI already exists.
*
* @param uri Storage URI.
* @throws java.io.IOException in case of I/O error.
* @return a boolean.
*/
protected boolean exists(URI uri) throws IOException {
return exists(getPath(uri));
}
/**
* Determines if the header should be validated.
*
* @param path Storage path
* @throws java.io.IOException if header validation fails
* @return a boolean.
*/
protected boolean shouldValidateHeader(String path) throws IOException {
return validateHeader;
}
/**
* Determines if the header should be validated.
*
* @param uri Storage URI
* @throws java.io.IOException if header validation fails
* @return a boolean.
*/
protected boolean shouldValidateHeader(URI uri) throws IOException {
return shouldValidateHeader(getPath(uri));
}
/**
* Returns the name (primary ID) for the factory.
*
* @return Name of the factory.
*/
public String getName() {
return name;
}
/**
* A generic close handle, default implementation does nothing.
* @since 3.4
* @throws IOException
*/
public void close() throws IOException {
}
}

View File

@ -0,0 +1,79 @@
package org.rrd4j.core;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* Abstract byte array based backend.
*
*/
public abstract class RrdByteArrayBackend extends ByteBufferBackend {
private byte[] buffer;
/**
* <p>Constructor for RrdByteArrayBackend.</p>
*
* @param path a {@link java.lang.String} object.
*/
protected RrdByteArrayBackend(String path) {
super(path);
}
protected void setBuffer(byte[] buffer) {
this.buffer = buffer;
setByteBuffer(ByteBuffer.wrap(buffer));
}
protected byte[] getBuffer() {
return buffer;
}
/**
* <p>read.</p>
*
* @param offset a long.
* @param bytes an array of byte.
* @throws java.io.IOException if any.
* @throws java.lang.IllegalArgumentException if offset is bigger that the possible length.
*/
@Override
protected synchronized void read(long offset, byte[] bytes) throws IOException {
if (offset < 0 || offset > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Illegal offset: " + offset);
}
if (offset + bytes.length <= buffer.length) {
System.arraycopy(buffer, (int) offset, bytes, 0, bytes.length);
}
else {
throw new RrdBackendException("Not enough bytes available in RRD buffer; RRD " + getPath());
}
}
/**
* {@inheritDoc}
*
* @return Number of RRD bytes held in memory.
*/
public long getLength() {
return buffer.length;
}
/**
* {@inheritDoc}
*
* <p>It will reserves a memory section as a RRD storage.</p>
*
* @throws java.lang.IllegalArgumentException if length is bigger that the possible length.
*/
protected void setLength(long length) throws IOException {
if (length < 0 || length > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Illegal length: " + length);
}
buffer = new byte[(int) length];
setByteBuffer(ByteBuffer.wrap(buffer));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,563 @@
package org.rrd4j.core;
import java.io.IOException;
import java.net.URI;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* <p>This class should be used to synchronize access to RRD files
* in a multithreaded environment. This class should be also used to prevent opening of
* too many RRD files at the same time (thus avoiding operating system limits).
* </p>
* <p>It should not be called directly. Use {@link RrdDb.Builder#usePool()} instead.</p>
*/
public class RrdDbPool {
private static class RrdDbPoolSingletonHolder {
static final RrdDbPool instance = new RrdDbPool();
private RrdDbPoolSingletonHolder() {}
}
/**
* Initial capacity of the pool i.e. maximum number of simultaneously open RRD files. The pool will
* never open too many RRD files at the same time.
*/
public static final int INITIAL_CAPACITY = 200;
/*
* The RrdEntry stored in the pool can be of tree kind:
* - null, the URI is available, just for it and play
* - placeholder is true, it's not the real RrdDb entry, just a place holder
* meaning that some other thread is using it.
* - placehold is false, this is the real entry pointing to a RrdDb. It's
* only used by the current thread.
*
*/
private static class RrdEntry {
RrdDb rrdDb = null;
int count = 0;
final CountDownLatch waitempty;
final CountDownLatch inuse;
final boolean placeholder;
final URI uri;
RrdEntry(boolean placeholder, URI canonicalPath) {
this.placeholder = placeholder;
this.uri = canonicalPath;
if (placeholder) {
inuse = new CountDownLatch(1);
waitempty = null;
} else {
inuse = null;
waitempty = new CountDownLatch(1);
}
}
@Override
public String toString() {
if (this.placeholder) {
return "RrdEntry [inuse=" + inuse.getCount()+ ", uri=" + uri + "]";
} else {
return "RrdEntry [rrdDb=" + rrdDb + ", count=" + count + ", uri=" + uri + "]";
}
}
}
/**
* Creates a single instance of the class on the first call,
* or returns already existing one. Uses Initialization On Demand Holder idiom.
*
* @return Single instance of this class
* @throws java.lang.RuntimeException Thrown if the default RRD backend is not derived from the {@link org.rrd4j.core.RrdFileBackendFactory}
*/
public static RrdDbPool getInstance() {
return RrdDbPoolSingletonHolder.instance;
}
private final AtomicInteger usage = new AtomicInteger(0);
private final ReentrantLock countLock = new ReentrantLock();
private final Condition full = countLock.newCondition();
private int maxCapacity = INITIAL_CAPACITY;
private final ConcurrentMap<URI, RrdEntry> pool = new ConcurrentHashMap<>(INITIAL_CAPACITY);
private final RrdBackendFactory defaultFactory;
/**
* Constructor for RrdDbPool.
* @since 3.5
*/
public RrdDbPool() {
defaultFactory = RrdBackendFactory.getDefaultFactory();
}
/**
* Returns the number of open RRD files.
*
* @return Number of currently open RRD files held in the pool.
*/
public int getOpenFileCount() {
return usage.get();
}
/**
* Returns an array of open file URI.
*
* @return Array with {@link URI} to open RRD files held in the pool.
*/
public URI[] getOpenUri() {
//Direct toarray from keySet can fail
Set<URI> files = new HashSet<>();
files.addAll(pool.keySet());
return files.toArray(new URI[files.size()]);
}
/**
* Returns an array of open file path.
*
* @return Array with canonical path to open RRD files held in the pool.
*/
public String[] getOpenFiles() {
//Direct toarray from keySet can fail
Set<String> files = new HashSet<>();
for (RrdEntry i: pool.values()) {
files.add(i.rrdDb.getPath());
}
return files.toArray(new String[files.size()]);
}
private RrdEntry getEntry(URI uri, boolean cancreate) throws InterruptedException {
RrdEntry ref = null;
try {
do {
ref = pool.get(uri);
if (ref == null) {
//Slot empty
//If still absent put a place holder, and create the entry to return
try {
countLock.lockInterruptibly();
while (ref == null && usage.get() >= maxCapacity && cancreate) {
full.await();
ref = pool.get(uri);
}
if (ref == null && cancreate) {
ref = pool.putIfAbsent(uri, new RrdEntry(true, uri));
if (ref == null) {
ref = new RrdEntry(false, uri);
usage.incrementAndGet();
}
}
} finally {
countLock.unlock();
}
} else if (! ref.placeholder) {
// Real entry, try to put a place holder if some one didn't get it meanwhile
if ( ! pool.replace(uri, ref, new RrdEntry(true, uri))) {
//Dummy ref, a new iteration is needed
ref = new RrdEntry(true, uri);
}
} else {
// a place holder, wait for the using task to finish
ref.inuse.await();
}
} while (ref != null && ref.placeholder);
return ref;
} catch (InterruptedException | RuntimeException e) {
// Oups we were interrupted, put everything back and go away
passNext(ACTION.SWAP, ref);
Thread.currentThread().interrupt();
throw e;
}
}
private enum ACTION {
SWAP, DROP;
}
private void passNext(ACTION a, RrdEntry e) {
if (e == null) {
return;
}
RrdEntry o = null;
switch (a) {
case SWAP:
o = pool.put(e.uri, e);
break;
case DROP:
o = pool.remove(e.uri);
if(usage.decrementAndGet() < maxCapacity) {
try {
countLock.lockInterruptibly();
full.signalAll();
countLock.unlock();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
break;
}
//task finished, waiting on a place holder can go on
if(o != null) {
o.inuse.countDown();
}
}
/**
* Releases RrdDb reference previously obtained from the pool. When a reference is released, its usage
* count is decremented by one. If usage count drops to zero, the underlying RRD file will be closed.
*
* @param rrdDb RrdDb reference to be returned to the pool
* @throws java.io.IOException Thrown in case of I/O error
* @deprecated a pool remember if it was open directly or from the pool, no need to manage it manually any more
*/
@Deprecated
public void release(RrdDb rrdDb) throws IOException {
// null pointer should not kill the thread, just ignore it
if (rrdDb == null) {
return;
}
URI dburi = rrdDb.getUri();
RrdEntry ref = null;
try {
ref = getEntry(dburi, false);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("release interrupted for " + rrdDb, e);
}
if (ref == null) {
return;
}
if (ref.count <= 0) {
passNext(ACTION.DROP, ref);
throw new IllegalStateException("Could not release [" + rrdDb.getPath() + "], the file was never requested");
}
if (--ref.count == 0) {
if(ref.rrdDb == null) {
passNext(ACTION.DROP, ref);
throw new IllegalStateException("Could not release [" + rrdDb.getPath() + "], pool corruption");
}
try {
ref.rrdDb.internalClose();
} finally {
passNext(ACTION.DROP, ref);
//If someone is waiting for an empty entry, signal it
ref.waitempty.countDown();
}
} else {
passNext(ACTION.SWAP, ref);
}
}
/**
* <p>Requests a RrdDb reference for the given RRD file path.</p>
* <ul>
* <li>If the file is already open, previously returned RrdDb reference will be returned. Its usage count
* will be incremented by one.
* <li>If the file is not already open and the number of already open RRD files is less than
* {@link #INITIAL_CAPACITY}, the file will be open and a new RrdDb reference will be returned.
* If the file is not already open and the number of already open RRD files is equal to
* {@link #INITIAL_CAPACITY}, the method blocks until some RRD file is closed.
* </ul>
* <p>The path is transformed internally to URI using the default factory, that is the reference that will
* be used elsewhere.</p>
*
* @param path Path to existing RRD file
* @return reference for the give RRD file
* @throws java.io.IOException Thrown in case of I/O error
* @deprecated Use the {@link org.rrd4j.core.RrdDb.Builder} instead.
*/
@Deprecated
public RrdDb requestRrdDb(String path) throws IOException {
return requestRrdDb(defaultFactory.getUri(path), defaultFactory);
}
/**
* <p>Requests a RrdDb reference for the given RRD file path.</p>
* <ul>
* <li>If the file is already open, previously returned RrdDb reference will be returned. Its usage count
* will be incremented by one.
* <li>If the file is not already open and the number of already open RRD files is less than
* {@link #INITIAL_CAPACITY}, the file will be open and a new RrdDb reference will be returned.
* If the file is not already open and the number of already open RRD files is equal to
* {@link #INITIAL_CAPACITY}, the method blocks until some RRD file is closed.
* </ul>
*
* @param uri {@link URI} to existing RRD file
* @return reference for the give RRD file
* @throws java.io.IOException Thrown in case of I/O error
* @deprecated Use the {@link org.rrd4j.core.RrdDb.Builder} instead.
*/
@Deprecated
public RrdDb requestRrdDb(URI uri) throws IOException {
RrdBackendFactory factory = RrdBackendFactory.findFactory(uri);
return requestRrdDb(uri, factory);
}
RrdDb requestRrdDb(URI uri, RrdBackendFactory factory) throws IOException {
uri = factory.getCanonicalUri(uri);
RrdEntry ref = null;
try {
ref = getEntry(uri, true);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("request interrupted for " + uri, e);
}
//Someone might have already open it, rechecks
if (ref.count == 0) {
try {
ref.rrdDb = RrdDb.getBuilder().setPath(factory.getPath(uri)).setBackendFactory(factory).setPool(this).build();
} catch (IOException | RuntimeException e) {
passNext(ACTION.DROP, ref);
throw e;
}
}
ref.count++;
passNext(ACTION.SWAP, ref);
return ref.rrdDb;
}
/**
* Wait for a empty reference with no usage
* @param uri
* @return an reference with no usage
* @throws IOException
* @throws InterruptedException
*/
private RrdEntry waitEmpty(URI uri) throws IOException, InterruptedException {
RrdEntry ref = getEntry(uri, true);
try {
while (ref.count != 0) {
//Not empty, give it back, but wait for signal
passNext(ACTION.SWAP, ref);
ref.waitempty.await();
ref = getEntry(uri, true);
}
return ref;
} catch (InterruptedException e) {
passNext(ACTION.SWAP, ref);
Thread.currentThread().interrupt();
throw e;
}
}
/**
* Got an empty reference, use it only if slots are available
* But don't hold any lock waiting for it
* @param uri
* @return an reference with no usage
* @throws InterruptedException
* @throws IOException
*/
private RrdEntry requestEmpty(URI uri) throws InterruptedException, IOException {
RrdEntry ref = waitEmpty(uri);
ref.count = 1;
return ref;
}
/**
* <p>Requests a RrdDb reference for the given RRD file definition object.</p>
* <ul>
* <li>If the file with the path specified in the RrdDef object is already open,
* the method blocks until the file is closed.
* <li>If the file is not already open and the number of already open RRD files is less than
* {@link #INITIAL_CAPACITY}, a new RRD file will be created and a its RrdDb reference will be returned.
* If the file is not already open and the number of already open RRD files is equal to
* {@link #INITIAL_CAPACITY}, the method blocks until some RRD file is closed.
* </ul>
*
* @param rrdDef Definition of the RRD file to be created
* @return Reference to the newly created RRD file
* @throws java.io.IOException Thrown in case of I/O error
* @deprecated Use the {@link org.rrd4j.core.RrdDb.Builder} instead.
*/
@Deprecated
public RrdDb requestRrdDb(RrdDef rrdDef) throws IOException {
return requestRrdDb(rrdDef, RrdBackendFactory.findFactory(rrdDef.getUri()));
}
RrdDb requestRrdDb(RrdDef rrdDef, RrdBackendFactory backend) throws IOException {
RrdEntry ref = null;
try {
URI uri = backend.getCanonicalUri(rrdDef.getUri());
ref = requestEmpty(uri);
ref.rrdDb = RrdDb.getBuilder().setRrdDef(rrdDef).setBackendFactory(backend).setPool(this).build();
return ref.rrdDb;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("request interrupted for new rrdDef " + rrdDef.getPath(), e);
} catch (RuntimeException e) {
passNext(ACTION.DROP, ref);
ref = null;
throw e;
} finally {
if (ref != null) {
passNext(ACTION.SWAP, ref);
}
}
}
/**
* <p>Requests a RrdDb reference for the given path. The file will be created from
* external data (from XML dump or RRDTool's binary RRD file).</p>
* <ul>
* <li>If the file with the path specified is already open,
* the method blocks until the file is closed.
* <li>If the file is not already open and the number of already open RRD files is less than
* {@link #INITIAL_CAPACITY}, a new RRD file will be created and a its RrdDb reference will be returned.
* If the file is not already open and the number of already open RRD files is equal to
* {@link #INITIAL_CAPACITY}, the method blocks until some RRD file is closed.
* </ul>
* <p>The path is transformed internally to URI using the default factory, that is the reference that will
* be used elsewhere.</p>
*
* @param path Path to RRD file which should be created
* @param sourcePath Path to external data which is to be converted to Rrd4j's native RRD file format
* @return Reference to the newly created RRD file
* @throws java.io.IOException Thrown in case of I/O error
* @deprecated Use the {@link org.rrd4j.core.RrdDb.Builder} instead.
*/
@Deprecated
public RrdDb requestRrdDb(String path, String sourcePath)
throws IOException {
URI uri = RrdBackendFactory.getDefaultFactory().getUri(path);
RrdBackendFactory backend = RrdBackendFactory.getDefaultFactory();
return requestRrdDb(RrdDb.getBuilder().setExternalPath(sourcePath), uri, backend);
}
/**
* <p>Requests a RrdDb reference for the given path. The file will be created from
* external data (from XML dump or RRDTool's binary RRD file).</p>
* <ul>
* <li>If the file with the path specified is already open,
* the method blocks until the file is closed.
* <li>If the file is not already open and the number of already open RRD files is less than
* {@link #INITIAL_CAPACITY}, a new RRD file will be created and a its RrdDb reference will be returned.
* If the file is not already open and the number of already open RRD files is equal to
* {@link #INITIAL_CAPACITY}, the method blocks until some RRD file is closed.
* </ul>
* <p>The path is transformed internally to URI using the default factory, that is the reference that will
* be used elsewhere.</p>
*
* @param uri Path to RRD file which should be created
* @param sourcePath Path to external data which is to be converted to Rrd4j's native RRD file format
* @return Reference to the newly created RRD file
* @throws java.io.IOException Thrown in case of I/O error
* @deprecated Use the {@link org.rrd4j.core.RrdDb.Builder} instead.
*/
@Deprecated
public RrdDb requestRrdDb(URI uri, String sourcePath)
throws IOException {
RrdBackendFactory backend = RrdBackendFactory.getDefaultFactory();
return requestRrdDb(RrdDb.getBuilder().setExternalPath(sourcePath), uri, backend);
}
private RrdDb requestRrdDb(RrdDb.Builder builder, URI uri, RrdBackendFactory backend)
throws IOException {
RrdEntry ref = null;
uri = backend.getCanonicalUri(uri);
try {
ref = requestEmpty(uri);
ref.rrdDb = builder.setPath(uri).setBackendFactory(backend).setPool(this).build();
return ref.rrdDb;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("request interrupted for new rrd " + uri, e);
} catch (RuntimeException e) {
passNext(ACTION.DROP, ref);
ref = null;
throw e;
} finally {
if (ref != null) {
passNext(ACTION.SWAP, ref);
}
}
}
RrdDb requestRrdDb(URI uri, RrdBackendFactory backend, DataImporter importer) throws IOException {
return requestRrdDb(RrdDb.getBuilder().setImporter(importer), uri, backend);
}
/**
* Sets the maximum number of simultaneously open RRD files.
*
* @param newCapacity Maximum number of simultaneously open RRD files.
*/
public void setCapacity(int newCapacity) {
int oldUsage = usage.getAndSet(maxCapacity);
try {
if (oldUsage != 0) {
throw new RuntimeException("Can only be done on a empty pool");
}
} finally {
usage.set(oldUsage);
}
maxCapacity = newCapacity;
}
/**
* Returns the maximum number of simultaneously open RRD files.
*
* @return maximum number of simultaneously open RRD files
*/
public int getCapacity() {
return maxCapacity;
}
/**
* Returns the number of usage for a RRD.
*
* @param rrdDb RrdDb reference for which informations is needed.
* @return the number of request for this rrd
* @throws java.io.IOException if any.
*/
public int getOpenCount(RrdDb rrdDb) throws IOException {
return getOpenCount(rrdDb.getUri());
}
/**
* Returns the number of usage for a RRD.
*
* @param path RRD's path for which informations is needed.
* @return the number of request for this file
* @throws java.io.IOException if any.
*/
public int getOpenCount(String path) throws IOException {
return getOpenCount(defaultFactory.getUri(path));
}
/**
* Returns the number of usage for a RRD.
*
* @param uri RRD's uri for which informations is needed.
* @return the number of request for this file
* @throws java.io.IOException if any.
*/
public int getOpenCount(URI uri) throws IOException {
RrdEntry ref = null;
try {
ref = getEntry(uri, false);
if (ref == null)
return 0;
else {
return ref.count;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("getOpenCount interrupted", e);
} finally {
if (ref != null) {
passNext(ACTION.SWAP, ref);
}
}
}
}

View File

@ -0,0 +1,862 @@
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<>();
private List<ArcDef> arcDefs = new ArrayList<>();
/**
* <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;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((arcDefs == null) ? 0 : arcDefs.hashCode());
result = prime * result + ((dsDefs == null) ? 0 : dsDefs.hashCode());
result = prime * result + (int) (step ^ (step >>> 32));
return result;
}
/**
* <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();
}
}

View File

@ -0,0 +1,228 @@
package org.rrd4j.core;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.rrd4j.DsType;
import org.rrd4j.ConsolFun;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Calendar;
/**
* <p>Class used to create an arbitrary number of {@link org.rrd4j.core.RrdDef} (RRD definition) objects
* from a single XML template. XML template can be supplied as an XML InputSource,
* XML file or XML formatted string.</p>
*
* <p>Here is an example of a properly formatted XML template with all available
* options in it (unwanted options can be removed):</p>
* <pre>
* &lt;rrd_def&gt;
* &lt;path&gt;test.rrd&lt;/path&gt;
* &lt;!-- not mandatory --&gt;
* &lt;start&gt;1000123456&lt;/start&gt;
* &lt;!-- not mandatory --&gt;
* &lt;step&gt;300&lt;/step&gt;
* &lt;!-- at least one datasource must be supplied --&gt;
* &lt;datasource&gt;
* &lt;name&gt;input&lt;/name&gt;
* &lt;type&gt;COUNTER&lt;/type&gt;
* &lt;heartbeat&gt;300&lt;/heartbeat&gt;
* &lt;min&gt;0&lt;/min&gt;
* &lt;max&gt;U&lt;/max&gt;
* &lt;/datasource&gt;
* &lt;datasource&gt;
* &lt;name&gt;temperature&lt;/name&gt;
* &lt;type&gt;GAUGE&lt;/type&gt;
* &lt;heartbeat&gt;400&lt;/heartbeat&gt;
* &lt;min&gt;U&lt;/min&gt;
* &lt;max&gt;1000&lt;/max&gt;
* &lt;/datasource&gt;
* &lt;!-- at least one archive must be supplied --&gt;
* &lt;archive&gt;
* &lt;cf&gt;AVERAGE&lt;/cf&gt;
* &lt;xff&gt;0.5&lt;/xff&gt;
* &lt;steps&gt;1&lt;/steps&gt;
* &lt;rows&gt;600&lt;/rows&gt;
* &lt;/archive&gt;
* &lt;archive&gt;
* &lt;cf&gt;MAX&lt;/cf&gt;
* &lt;xff&gt;0.6&lt;/xff&gt;
* &lt;steps&gt;6&lt;/steps&gt;
* &lt;rows&gt;7000&lt;/rows&gt;
* &lt;/archive&gt;
* &lt;/rrd_def&gt;
* </pre>
* <p>Notes on the template syntax:</p>
* <ul>
* <li>There is a strong relation between the XML template syntax and the syntax of
* {@link org.rrd4j.core.RrdDef} class methods. If you are not sure what some XML tag means, check javadoc
* for the corresponding class.
* <li>starting timestamp can be supplied either as a long integer
* (like: 1000243567) or as an ISO formatted string (like: 2004-02-21 12:25:45)
* <li>whitespaces are not harmful
* <li>floating point values: anything that cannot be parsed will be treated as Double.NaN
* (like: U, unknown, 12r.23)
* <li>comments are allowed.
* </ul>
* <p>Any template value (text between <code>&lt;some_tag&gt;</code> and
* <code>&lt;/some_tag&gt;</code>) can be replaced with
* a variable of the following form: <code>${variable_name}</code>. Use
* {@link org.rrd4j.core.XmlTemplate#setVariable(String, String) setVariable()}
* methods from the base class to replace template variables with real values
* at runtime.</p>
*
* <p>Typical usage scenario:</p>
* <ul>
* <li>Create your XML template and save it to a file (template.xml, for example)
* <li>Replace hardcoded template values with variables if you want to change them during runtime.
* For example, RRD path should not be hardcoded in the template - you probably want to create
* many different RRD files from the same XML template. For example, your XML
* template could start with:
* <pre>
* &lt;rrd_def&gt;
* &lt;path&gt;${path}&lt;/path&gt;
* &lt;step&gt;300&lt;/step&gt;
* ...
* </pre>
* <li>In your Java code, create RrdDefTemplate object using your XML template file:
* <pre>
* RrdDefTemplate t = new RrdDefTemplate(new File(template.xml));
* </pre>
* <li>Then, specify real values for template variables:
* <pre>
* t.setVariable("path", "demo/test.rrd");
* </pre>
* <li>Once all template variables are set, just use the template object to create RrdDef
* object. This object is actually used to create Rrd4j RRD files:
* <pre>
* RrdDef def = t.getRrdDef();
* RrdDb rrd = new RrdDb(def);
* rrd.close();
* </pre>
* </ul>
* You should create new RrdDefTemplate object only once for each XML template. Single template
* object can be reused to create as many RrdDef objects as needed, with different values
* specified for template variables. XML syntax check is performed only once - the first
* definition object gets created relatively slowly, but it will be created much faster next time.
*
*/
public class RrdDefTemplate extends XmlTemplate {
/**
* Creates RrdDefTemplate object from any parsable XML input source. Read general information
* for this class to find an example of a properly formatted RrdDef XML source.
*
* @param xmlInputSource Xml input source
* @throws java.io.IOException Thrown in case of I/O error
* @throws java.lang.IllegalArgumentException Thrown in case of XML related error (parsing error, for example)
*/
public RrdDefTemplate(InputSource xmlInputSource) throws IOException {
super(xmlInputSource);
}
/**
* Creates RrdDefTemplate object from the string containing XML template.
* Read general information for this class to see an example of a properly formatted XML source.
*
* @param xmlString String containing XML template
* @throws java.io.IOException Thrown in case of I/O error
* @throws java.lang.IllegalArgumentException Thrown in case of XML related error (parsing error, for example)
*/
public RrdDefTemplate(String xmlString) throws IOException {
super(xmlString);
}
/**
* Creates RrdDefTemplate object from the file containing XML template.
* Read general information for this class to see an example of a properly formatted XML source.
*
* @param xmlFile File object representing file with XML template
* @throws java.io.IOException Thrown in case of I/O error
* @throws java.lang.IllegalArgumentException Thrown in case of XML related error (parsing error, for example)
*/
public RrdDefTemplate(File xmlFile) throws IOException {
super(xmlFile);
}
/**
* Returns RrdDef object constructed from the underlying XML template. Before this method
* is called, values for all non-optional placeholders must be supplied. To specify
* placeholder values at runtime, use some of the overloaded
* {@link org.rrd4j.core.XmlTemplate#setVariable(String, String) setVariable()} methods. Once this method
* returns, all placeholder values are preserved. To remove them all, call inherited
* {@link org.rrd4j.core.XmlTemplate#clearValues() clearValues()} method explicitly.<p>
*
* @return RrdDef object constructed from the underlying XML template,
* with all placeholders replaced with real values. This object can be passed to the constructor
* of the new RrdDb object.
* @throws java.lang.IllegalArgumentException Thrown (in most cases) if the value for some placeholder
* was not supplied through {@link org.rrd4j.core.XmlTemplate#setVariable(String, String) setVariable()}
* method call
*/
public RrdDef getRrdDef() {
if (!"rrd_def".equals(root.getTagName())) {
throw new IllegalArgumentException("XML definition must start with <rrd_def>");
}
validateTagsOnlyOnce(root, new String[]{
"path*", "uri*", "start", "step", "datasource*", "archive*"
});
// PATH must be supplied or exception is thrown
RrdDef rrdDef;
if (hasChildNode(root, "path")) {
String path = getChildValue(root, "path");
rrdDef = new RrdDef(path);
} else if (hasChildNode(root, "uri")) {
String uri = getChildValue(root, "uri");
try {
rrdDef = new RrdDef(new URI(uri));
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Wrong URI: " + uri);
}
} else {
throw new IllegalArgumentException("Neither path or URI defined");
}
try {
String startStr = getChildValue(root, "start");
Calendar startGc = Util.getCalendar(startStr);
rrdDef.setStartTime(startGc);
}
catch (Exception e) {
// START is not mandatory
}
try {
long step = getChildValueAsLong(root, "step");
rrdDef.setStep(step);
}
catch (Exception e) {
// STEP is not mandatory
}
// datsources
Node[] dsNodes = getChildNodes(root, "datasource");
for (Node dsNode : dsNodes) {
validateTagsOnlyOnce(dsNode, new String[]{
"name", "type", "heartbeat", "min", "max"
});
String name = getChildValue(dsNode, "name");
DsType type = DsType.valueOf(getChildValue(dsNode, "type"));
long heartbeat = getChildValueAsLong(dsNode, "heartbeat");
double min = getChildValueAsDouble(dsNode, "min");
double max = getChildValueAsDouble(dsNode, "max");
rrdDef.addDatasource(name, type, heartbeat, min, max);
}
// archives
Node[] arcNodes = getChildNodes(root, "archive");
for (Node arcNode : arcNodes) {
validateTagsOnlyOnce(arcNode, new String[]{
"cf", "xff", "steps", "rows"
});
ConsolFun consolFun = ConsolFun.valueOf(getChildValue(arcNode, "cf"));
double xff = getChildValueAsDouble(arcNode, "xff");
int steps = getChildValueAsInt(arcNode, "steps");
int rows = getChildValueAsInt(arcNode, "rows");
rrdDef.addArchive(consolFun, xff, steps, rows);
}
return rrdDef;
}
}

View File

@ -0,0 +1,41 @@
package org.rrd4j.core;
import java.io.IOException;
class RrdDouble<U extends RrdUpdater<U>> extends RrdPrimitive<U> {
private double cache;
private boolean cached = false;
RrdDouble(RrdUpdater<U> updater, boolean isConstant) throws IOException {
super(updater, RrdDouble.RRD_DOUBLE, isConstant);
}
RrdDouble(RrdUpdater<U> updater) throws IOException {
super(updater, RrdDouble.RRD_DOUBLE, false);
}
void set(double value) throws IOException {
if (!isCachingAllowed()) {
writeDouble(value);
}
// caching allowed
else if (!cached || !Util.equal(cache, value)) {
// update cache
writeDouble(cache = value);
cached = true;
}
}
double get() throws IOException {
if (!isCachingAllowed()) {
return readDouble();
}
else {
if (!cached) {
cache = readDouble();
cached = true;
}
return cache;
}
}
}

View File

@ -0,0 +1,35 @@
package org.rrd4j.core;
import java.io.IOException;
class RrdDoubleArray<U extends RrdUpdater<U>> extends RrdPrimitive<U> {
private int length;
RrdDoubleArray(RrdUpdater<U> updater, int length) throws IOException {
super(updater, RrdPrimitive.RRD_DOUBLE, length, false);
this.length = length;
}
void set(int index, double value) throws IOException {
set(index, value, 1);
}
void set(int index, double value, int count) throws IOException {
// rollovers not allowed!
assert index + count <= length : "Invalid robin index supplied: index=" + index +
", count=" + count + ", length=" + length;
writeDouble(index, value, count);
}
double get(int index) throws IOException {
assert index < length : "Invalid index supplied: " + index + ", length=" + length;
return readDouble(index);
}
double[] get(int index, int count) throws IOException {
assert index + count <= length : "Invalid index/count supplied: " + index +
"/" + count + " (length=" + length + ")";
return readDouble(index, count);
}
}

View File

@ -0,0 +1,79 @@
package org.rrd4j.core;
import java.io.IOException;
class RrdDoubleMatrix<U extends RrdUpdater<U>> extends RrdPrimitive<U> {
private static final String LENGTH = ", length=";
private final int rows;
private final int columns;
RrdDoubleMatrix(RrdUpdater<U> updater, int row, int column, boolean shouldInitialize) throws IOException {
super(updater, RrdPrimitive.RRD_DOUBLE, row * column, false);
this.rows = row;
this.columns = column;
if (shouldInitialize)
writeDouble(0, Double.NaN, rows * columns);
}
void set(int column, int index, double value) throws IOException {
writeDouble(columns * index + column, value);
}
void set(int column, int index, double value, int count) throws IOException {
// rollovers not allowed!
assert index + count <= rows : "Invalid robin index supplied: index=" + index +
", count=" + count + LENGTH + rows;
for (int i = columns * index + column, c = 0; c < count; i += columns, c++)
writeDouble(i, value);
}
/**
* <p>set.</p>
*
* @param column a int.
* @param index a int.
* @param newValues an array of double.
* @throws java.io.IOException if any.
*/
public void set(int column, int index, double[] newValues) throws IOException {
int count = newValues.length;
// rollovers not allowed!
assert index + count <= rows : "Invalid robin index supplied: index=" + index +
", count=" + count + LENGTH + rows;
for (int i = columns * index + column, c = 0; c < count; i += columns, c++)
writeDouble(i, newValues[c]);
}
double get(int column, int index) throws IOException {
assert index < rows : "Invalid index supplied: " + index + LENGTH + rows;
return readDouble(columns * index + column);
}
double[] get(int column, int index, int count) throws IOException {
assert index + count <= rows : "Invalid index/count supplied: " + index +
"/" + count + " (length=" + rows + ")";
double[] values = new double[count];
for (int i = columns * index + column, c = 0; c < count; i += columns, c++) {
values[c] = readDouble(i);
}
return values;
}
/**
* <p>Getter for the field <code>columns</code>.</p>
*
* @return a int.
*/
public int getColumns() {
return columns;
}
/**
* <p>Getter for the field <code>rows</code>.</p>
*
* @return a int.
*/
public int getRows() {
return rows;
}
}

View File

@ -0,0 +1,46 @@
package org.rrd4j.core;
import java.io.IOException;
class RrdEnum<U extends RrdUpdater<U>, E extends Enum<E>> extends RrdPrimitive<U> {
private E cache;
private final Class<E> clazz;
RrdEnum(RrdUpdater<U> updater, boolean isConstant, Class<E> clazz) throws IOException {
super(updater, RrdPrimitive.RRD_STRING, isConstant);
this.clazz = clazz;
}
RrdEnum(RrdUpdater<U> updater, Class<E> clazz) throws IOException {
this(updater, false, clazz);
}
void set(E value) throws IOException {
if (!isCachingAllowed()) {
writeEnum(value);
}
// caching allowed
else if (cache == null || cache != value) {
// update cache
writeEnum((cache = value));
}
}
E get() throws IOException {
if (!isCachingAllowed()) {
return readEnum(clazz);
}
else {
if (cache == null) {
cache = readEnum(clazz);
}
return cache;
}
}
String name() throws IOException {
return get().name();
}
}

View File

@ -0,0 +1,22 @@
package org.rrd4j.core;
import java.io.IOException;
/**
* A general purpose RRD4J exception.
*
* @since 3.4
*/
public class RrdException extends IOException {
private static final long serialVersionUID = 1L;
public RrdException(String message) {
super(message);
}
public RrdException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,22 @@
package org.rrd4j.core;
import java.io.IOException;
/**
* An abstract backend which is used to store RRD data to ordinary files on the disk.
* <p>
* Every backend storing RRD data as ordinary files should inherit from it, some check are done
* in the code for instanceof.
*
*/
public interface RrdFileBackend {
/**
* Returns canonical path to the file on the disk.
*
* @return Canonical file path
* @throws java.io.IOException Thrown in case of I/O error
*/
String getCanonicalPath() throws IOException;
}

View File

@ -0,0 +1,73 @@
package org.rrd4j.core;
import java.io.File;
import java.io.IOException;
import java.net.URI;
/**
* An abstract backend factory which is used to store RRD data to ordinary files on the disk.
* <p>
* Every backend factory storing RRD data as ordinary files should inherit from it, some check are done
* in the code for instanceof.
*
*/
public abstract class RrdFileBackendFactory extends RrdBackendFactory {
/**
* {@inheritDoc}
*
* Method to determine if a file with the given path already exists.
*/
@Override
protected boolean exists(String path) {
return Util.fileExists(path);
}
/** {@inheritDoc} */
@Override
public boolean canStore(URI uri) {
if ((uri.isOpaque() || uri.isAbsolute()) && ! "file".equals(uri.getScheme())) {
return false;
} else if (uri.getAuthority() != null || uri.getFragment() != null || uri.getQuery() != null) {
return false;
} else {
return true;
}
}
@Override
public URI getCanonicalUri(URI uri) {
try {
if (uri.isOpaque()) {
return new File(uri.getSchemeSpecificPart()).getCanonicalFile().toURI();
} else if (uri.isAbsolute()) {
return new File(uri).getCanonicalFile().toURI();
} else {
return new File(uri.getPath()).getCanonicalFile().toURI();
}
} catch (IOException e) {
throw new IllegalArgumentException("can't get canonical URI from " + uri + ": " + e);
}
}
@Override
public URI getUri(String path) {
try {
return new File(path).getCanonicalFile().toURI();
} catch (IOException e) {
throw new IllegalArgumentException("can't get canonical URI from path " + path + ": " + e);
}
}
@Override
public String getPath(URI uri) {
if (uri.isOpaque()) {
return uri.getSchemeSpecificPart();
} else if (uri.isAbsolute()) {
return new File(uri).getPath();
} else {
return uri.getPath();
}
}
}

View File

@ -0,0 +1,41 @@
package org.rrd4j.core;
import java.io.IOException;
class RrdInt<U extends RrdUpdater<U>> extends RrdPrimitive<U> {
private int cache;
private boolean cached = false;
RrdInt(RrdUpdater<U> updater, boolean isConstant) throws IOException {
super(updater, RrdPrimitive.RRD_INT, isConstant);
}
RrdInt(RrdUpdater<U> updater) throws IOException {
this(updater, false);
}
void set(int value) throws IOException {
if (!isCachingAllowed()) {
writeInt(value);
}
// caching allowed
else if (!cached || cache != value) {
// update cache
writeInt(cache = value);
cached = true;
}
}
int get() throws IOException {
if (!isCachingAllowed()) {
return readInt();
}
else {
if (!cached) {
cache = readInt();
cached = true;
}
return cache;
}
}
}

View File

@ -0,0 +1,41 @@
package org.rrd4j.core;
import java.io.IOException;
class RrdLong<U extends RrdUpdater<U>> extends RrdPrimitive<U> {
private long cache;
private boolean cached = false;
RrdLong(RrdUpdater<U> updater, boolean isConstant) throws IOException {
super(updater, RrdPrimitive.RRD_LONG, isConstant);
}
RrdLong(RrdUpdater<U> updater) throws IOException {
this(updater, false);
}
void set(long value) throws IOException {
if (!isCachingAllowed()) {
writeLong(value);
}
// caching allowed
else if (!cached || cache != value) {
// update cache
writeLong(cache = value);
cached = true;
}
}
long get() throws IOException {
if (!isCachingAllowed()) {
return readLong();
}
else {
if (!cached) {
cache = readLong();
cached = true;
}
return cache;
}
}
}

View File

@ -0,0 +1,47 @@
package org.rrd4j.core;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* Backend to be used to store all RRD bytes in memory.
*
*/
public class RrdMemoryBackend extends ByteBufferBackend {
private ByteBuffer dbb = null;
/**
* <p>Constructor for RrdMemoryBackend.</p>
*
* @param path a {@link java.lang.String} object.
*/
protected RrdMemoryBackend(String path) {
super(path);
}
@Override
protected void setLength(long length) throws IOException {
if (length < 0 || length > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Illegal length: " + length);
}
dbb = ByteBuffer.allocate((int) length);
setByteBuffer(dbb);
}
@Override
public long getLength() throws IOException {
return dbb.capacity();
}
/**
* This method is required by the base class definition, but it does not
* releases any memory resources at all.
*
* @throws java.io.IOException if any.
*/
@Override
protected void close() throws IOException {
// Don't release ressources, as backend are cached by the factory and reused
}
}

View File

@ -0,0 +1,71 @@
package org.rrd4j.core;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Factory class which creates actual {@link org.rrd4j.core.RrdMemoryBackend} objects. Rrd4j's support
* for in-memory RRDs is still experimental. You should know that all active RrdMemoryBackend
* objects are held in memory, each backend object stores RRD data in one big byte array. This
* implementation is therefore quite basic and memory hungry but runs very fast.
* <p>
* Calling {@link org.rrd4j.core.RrdDb#close() close()} on RrdDb objects does not release any memory at all
* (RRD data must be available for the next <code>new RrdDb(path)</code> call. To release allocated
* memory, you'll have to call {@link #delete(java.lang.String) delete(path)} method of this class.
*
*/
@RrdBackendAnnotation(name="MEMORY", shouldValidateHeader=false)
public class RrdMemoryBackendFactory extends RrdBackendFactory {
protected final Map<String, RrdMemoryBackend> backends = new ConcurrentHashMap<>();
/**
* {@inheritDoc}
*
* Creates RrdMemoryBackend object.
*/
protected RrdBackend open(String id, boolean readOnly) throws IOException {
RrdMemoryBackend backend;
if (backends.containsKey(id)) {
backend = backends.get(id);
}
else {
backend = new RrdMemoryBackend(id);
backends.put(id, backend);
}
return backend;
}
@Override
public boolean canStore(URI uri) {
return uri.getScheme().equals(getScheme());
}
/**
* {@inheritDoc}
*
* Method to determine if a memory storage with the given ID already exists.
*/
protected boolean exists(String id) {
return backends.containsKey(id);
}
/**
* Removes the storage with the given ID from the memory.
*
* @param id Storage ID
* @return a boolean.
*/
public boolean delete(String id) {
if (backends.containsKey(id)) {
backends.remove(id);
return true;
}
else {
return false;
}
}
}

View File

@ -0,0 +1,200 @@
package org.rrd4j.core;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Backend which is used to store RRD data to ordinary disk files
* using java.nio.* package. This is the default backend engine.
*
*/
public class RrdNioBackend extends ByteBufferBackend implements RrdFileBackend {
// The java 8- methods
private static final Method cleanerMethod;
private static final Method cleanMethod;
// The java 9+ methods
private static final Method invokeCleaner;
private static final Object unsafe;
static {
// Temporary variable, because destinations variables are final
// And it interfere with exceptions
Method cleanerMethodTemp;
Method cleanMethodTemp;
Method invokeCleanerTemp;
Object unsafeTemp;
try {
// The java 8- way, using sun.nio.ch.DirectBuffer.cleaner().clean()
Class<?> directBufferClass = RrdRandomAccessFileBackend.class.getClassLoader().loadClass("sun.nio.ch.DirectBuffer");
Class<?> cleanerClass = RrdNioBackend.class.getClassLoader().loadClass("sun.misc.Cleaner");
cleanerMethodTemp = directBufferClass.getMethod("cleaner");
cleanerMethodTemp.setAccessible(true);
cleanMethodTemp = cleanerClass.getMethod("clean");
cleanMethodTemp.setAccessible(true);
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) {
cleanerMethodTemp = null;
cleanMethodTemp = null;
}
try {
// The java 9+ way, using unsafe.invokeCleaner(buffer)
Field singleoneInstanceField = RrdRandomAccessFileBackend.class.getClassLoader().loadClass("sun.misc.Unsafe").getDeclaredField("theUnsafe");
singleoneInstanceField.setAccessible(true);
unsafeTemp = singleoneInstanceField.get(null);
invokeCleanerTemp = unsafeTemp.getClass().getMethod("invokeCleaner", ByteBuffer.class);
} catch (NoSuchFieldException | SecurityException
| IllegalArgumentException | IllegalAccessException
| NoSuchMethodException | ClassNotFoundException e) {
invokeCleanerTemp = null;
unsafeTemp = null;
}
cleanerMethod = cleanerMethodTemp;
cleanMethod = cleanMethodTemp;
invokeCleaner = invokeCleanerTemp;
unsafe = unsafeTemp;
}
private MappedByteBuffer byteBuffer;
private final FileChannel file;
private final boolean readOnly;
private ScheduledFuture<?> syncRunnableHandle = null;
/**
* Creates RrdFileBackend object for the given file path, backed by java.nio.* classes.
*
* @param path Path to a file
* @param readOnly True, if file should be open in a read-only mode. False otherwise
* @param syncPeriod See {@link org.rrd4j.core.RrdNioBackendFactory#setSyncPeriod(int)} for explanation
* @throws java.io.IOException Thrown in case of I/O error
* @param threadPool a {@link org.rrd4j.core.RrdSyncThreadPool} object, it can be null.
*/
protected RrdNioBackend(String path, boolean readOnly, RrdSyncThreadPool threadPool, int syncPeriod) throws IOException {
super(path);
Set<StandardOpenOption> options = new HashSet<>(3);
options.add(StandardOpenOption.READ);
options.add(StandardOpenOption.CREATE);
if (! readOnly) {
options.add(StandardOpenOption.WRITE);
}
file = FileChannel.open(Paths.get(path), options);
this.readOnly = readOnly;
try {
mapFile(file.size());
} catch (IOException | RuntimeException ex) {
file.close();
super.close();
throw ex;
}
try {
if (!readOnly && threadPool != null) {
Runnable syncRunnable = new Runnable() {
public void run() {
sync();
}
};
syncRunnableHandle = threadPool.scheduleWithFixedDelay(syncRunnable, syncPeriod, syncPeriod, TimeUnit.SECONDS);
}
} catch (RuntimeException rte) {
unmapFile();
file.close();
super.close();
throw rte;
}
}
private void mapFile(long length) throws IOException {
if (length > 0) {
FileChannel.MapMode mapMode =
readOnly ? FileChannel.MapMode.READ_ONLY : FileChannel.MapMode.READ_WRITE;
byteBuffer = file.map(mapMode, 0, length);
setByteBuffer(byteBuffer);
}
}
private void unmapFile() {
if (byteBuffer != null && byteBuffer.isDirect()) {
try {
if (cleanMethod != null) {
Object cleaner = cleanerMethod.invoke(byteBuffer);
cleanMethod.invoke(cleaner);
} else {
invokeCleaner.invoke(unsafe, byteBuffer);
}
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
throw new RuntimeException(ex);
}
}
byteBuffer = null;
}
/**
* {@inheritDoc}
*
* Sets length of the underlying RRD file. This method is called only once, immediately
* after a new RRD file gets created.
* @throws java.lang.IllegalArgumentException if the length is bigger that the possible mapping position (2GiB).
*/
protected synchronized void setLength(long newLength) throws IOException {
if (newLength < 0 || newLength > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Illegal offset: " + newLength);
}
unmapFile();
file.truncate(newLength);
mapFile(newLength);
}
/**
* Closes the underlying RRD file.
*
* @throws java.io.IOException Thrown in case of I/O error.
*/
@Override
public synchronized void close() throws IOException {
// cancel synchronization
try {
if (!readOnly && syncRunnableHandle != null) {
syncRunnableHandle.cancel(false);
syncRunnableHandle = null;
sync();
}
unmapFile();
} finally {
file.close();
super.close();
}
}
/**
* This method forces all data cached in memory but not yet stored in the file,
* to be stored in it.
*/
protected synchronized void sync() {
if (byteBuffer != null) {
byteBuffer.force();
}
}
@Override
public synchronized long getLength() throws IOException {
return file.size();
}
@Override
public String getCanonicalPath() throws IOException {
return Paths.get(getPath()).toAbsolutePath().normalize().toString();
}
}

View File

@ -0,0 +1,198 @@
package org.rrd4j.core;
import java.io.IOException;
import java.util.concurrent.ScheduledExecutorService;
/**
* Factory class which creates actual {@link org.rrd4j.core.RrdNioBackend} objects. This is the default factory since
* 1.4.0 version.
* <h3>Managing the thread pool</h3>
* <p>Each RrdNioBackendFactory is optionally backed by a {@link org.rrd4j.core.RrdSyncThreadPool}, which it uses to sync the memory-mapped files to
* disk. In order to avoid having these threads live longer than they should, it is recommended that clients create and
* destroy thread pools at the appropriate time in their application's life time. Failure to manage thread pools
* appropriately may lead to the thread pool hanging around longer than necessary, which in turn may cause memory leaks.</p>
* <p>if sync period is negative, no sync thread will be launched</p>
*
*/
@RrdBackendAnnotation(name="NIO", shouldValidateHeader=true)
public class RrdNioBackendFactory extends RrdFileBackendFactory {
/**
* Period in seconds between consecutive synchronizations when
* sync-mode is set to SYNC_BACKGROUND. By default in-memory cache will be
* transferred to the disc every 300 seconds (5 minutes). Default value can be
* changed via {@link #setSyncPeriod(int)} method.
*/
public static final int DEFAULT_SYNC_PERIOD = 300; // seconds
private static int defaultSyncPeriod = DEFAULT_SYNC_PERIOD;
/**
* The core pool size for the sync executor. Defaults to 6.
*/
public static final int DEFAULT_SYNC_CORE_POOL_SIZE = 6;
private static int defaultSyncPoolSize = DEFAULT_SYNC_CORE_POOL_SIZE;
/**
* Returns time between two consecutive background synchronizations. If not changed via
* {@link #setSyncPeriod(int)} method call, defaults to {@link #DEFAULT_SYNC_PERIOD}.
* See {@link #setSyncPeriod(int)} for more information.
*
* @return Time in seconds between consecutive background synchronizations.
*/
public static int getSyncPeriod() {
return defaultSyncPeriod;
}
/**
* Sets time between consecutive background synchronizations. If negative, it will disabling syncing for
* all NIO backend factory.
*
* @param syncPeriod Time in seconds between consecutive background synchronizations.
*/
public static void setSyncPeriod(int syncPeriod) {
RrdNioBackendFactory.defaultSyncPeriod = syncPeriod;
}
/**
* Returns the number of synchronizing threads. If not changed via
* {@link #setSyncPoolSize(int)} method call, defaults to {@link #DEFAULT_SYNC_CORE_POOL_SIZE}.
* See {@link #setSyncPoolSize(int)} for more information.
*
* @return Number of synchronizing threads.
*/
public static int getSyncPoolSize() {
return defaultSyncPoolSize;
}
/**
* Sets the number of synchronizing threads. It must be set before the first use of this factory.
* It will not have any effect afterward.
*
* @param syncPoolSize Number of synchronizing threads.
*/
public static void setSyncPoolSize(int syncPoolSize) {
RrdNioBackendFactory.defaultSyncPoolSize = syncPoolSize;
}
private final int syncPeriod;
/**
* The thread pool to pass to newly-created RrdNioBackend instances.
*/
private RrdSyncThreadPool syncThreadPool;
/**
* Creates a new RrdNioBackendFactory with default settings.
*/
public RrdNioBackendFactory() {
this(RrdNioBackendFactory.defaultSyncPeriod, DefaultSyncThreadPool.INSTANCE);
}
/**
* Creates a new RrdNioBackendFactory.
*
* @param syncPeriod If syncPeriod is negative or 0, sync threads are disabled.
*/
public RrdNioBackendFactory(int syncPeriod) {
this(syncPeriod, syncPeriod > 0 ? DefaultSyncThreadPool.INSTANCE : null);
}
/**
* Creates a new RrdNioBackendFactory.
*
* @param syncPeriod
* @param syncPoolSize The number of threads to use to sync the mapped file to disk, if inferior to 0, sync threads are disabled.
*/
public RrdNioBackendFactory(int syncPeriod, int syncPoolSize) {
this(syncPeriod, syncPoolSize > 0 ? new RrdSyncThreadPool(syncPoolSize) : null);
}
/**
* Creates a new RrdNioBackendFactory.
*
* @param syncPeriod
* @param syncThreadPool If null, disable background sync threads
*/
public RrdNioBackendFactory(int syncPeriod, ScheduledExecutorService syncThreadPool) {
this(syncPeriod, syncThreadPool != null ? new RrdSyncThreadPool(syncThreadPool) :null);
}
/**
* Creates a new RrdNioBackendFactory.
*
* @param syncPeriod
* @param syncThreadPool If null, disable background sync threads
*/
public RrdNioBackendFactory(int syncPeriod, RrdSyncThreadPool syncThreadPool) {
if (syncThreadPool != null && syncPeriod < 0) {
throw new IllegalArgumentException("Both thread pool defined and negative sync period");
}
this.syncPeriod = syncPeriod;
this.syncThreadPool = syncThreadPool;
}
/**
* <p>Setter for the field <code>syncThreadPool</code>.</p>
*
* @param syncThreadPool the RrdSyncThreadPool to use to sync the memory-mapped files.
* @deprecated Create a custom instance instead
*/
@Deprecated
public void setSyncThreadPool(RrdSyncThreadPool syncThreadPool) {
this.syncThreadPool = syncThreadPool;
}
/**
* <p>Setter for the field <code>syncThreadPool</code>.</p>
*
* @param syncThreadPool the ScheduledExecutorService that will back the RrdSyncThreadPool used to sync the memory-mapped files.
* @deprecated Create a custom instance instead
*/
@Deprecated
public void setSyncThreadPool(ScheduledExecutorService syncThreadPool) {
this.syncThreadPool = new RrdSyncThreadPool(syncThreadPool);
}
/**
* {@inheritDoc}
*
* Creates RrdNioBackend object for the given file path.
*/
protected RrdBackend open(String path, boolean readOnly) throws IOException {
return new RrdNioBackend(path, readOnly, syncThreadPool, syncPeriod);
}
/**
* @return The {@link RrdSyncThreadPool} or null if syncing is disabled
*/
public RrdSyncThreadPool getSyncThreadPool() {
return syncThreadPool;
}
@Override
public void close() throws IOException {
if (syncThreadPool != null) {
syncThreadPool.shutdown();
}
}
/**
* This is a holder class as per the "initialisation on demand" Java idiom. The only purpose of this holder class is
* to ensure that the thread pool is created lazily the first time that it is needed, and not before.
* <p/>
* In practice this thread pool will be used if clients rely on the factory returned by {@link
* org.rrd4j.core.RrdBackendFactory#getDefaultFactory()}, but not if clients provide their own backend instance when
* creating {@code RrdDb} instances or syncing was not disabled.
*/
private static class DefaultSyncThreadPool
{
/**
* The default thread pool used to periodically sync the mapped file to disk with.
*/
static final RrdSyncThreadPool INSTANCE = new RrdSyncThreadPool(defaultSyncPoolSize);
private DefaultSyncThreadPool() {}
}
}

View File

@ -0,0 +1,114 @@
package org.rrd4j.core;
import java.io.IOException;
abstract class RrdPrimitive<U extends RrdUpdater<U>> {
static final int STRING_LENGTH = 20;
static final int RRD_INT = 0, RRD_LONG = 1, RRD_DOUBLE = 2, RRD_STRING = 3;
static final int[] RRD_PRIM_SIZES = {4, 8, 8, 2 * STRING_LENGTH};
private RrdBackend backend;
private int byteCount;
private final long pointer;
private final boolean cachingAllowed;
RrdPrimitive(RrdUpdater<U> updater, int type, boolean isConstant) throws IOException {
this(updater, type, 1, isConstant);
}
RrdPrimitive(RrdUpdater<U> updater, int type, int count, boolean isConstant) throws IOException {
this.backend = updater.getRrdBackend();
this.byteCount = RRD_PRIM_SIZES[type] * count;
this.pointer = updater.getRrdAllocator().allocate(byteCount);
this.cachingAllowed = isConstant || backend.isCachingAllowed();
}
final byte[] readBytes() throws IOException {
byte[] b = new byte[byteCount];
backend.read(pointer, b);
return b;
}
final void writeBytes(byte[] b) throws IOException {
assert b.length == byteCount : "Invalid number of bytes supplied to RrdPrimitive.write method";
backend.write(pointer, b);
}
final int readInt() throws IOException {
return backend.readInt(pointer);
}
final void writeInt(int value) throws IOException {
backend.writeInt(pointer, value);
}
final long readLong() throws IOException {
return backend.readLong(pointer);
}
final void writeLong(long value) throws IOException {
backend.writeLong(pointer, value);
}
final double readDouble() throws IOException {
return backend.readDouble(pointer);
}
final double readDouble(int index) throws IOException {
long offset = pointer + index * RRD_PRIM_SIZES[RRD_DOUBLE];
return backend.readDouble(offset);
}
final double[] readDouble(int index, int count) throws IOException {
long offset = pointer + index * RRD_PRIM_SIZES[RRD_DOUBLE];
return backend.readDouble(offset, count);
}
final void writeDouble(double value) throws IOException {
backend.writeDouble(pointer, value);
}
final void writeDouble(int index, double value) throws IOException {
long offset = pointer + index * RRD_PRIM_SIZES[RRD_DOUBLE];
backend.writeDouble(offset, value);
}
final void writeDouble(int index, double value, int count) throws IOException {
long offset = pointer + index * RRD_PRIM_SIZES[RRD_DOUBLE];
backend.writeDouble(offset, value, count);
}
final void writeDouble(int index, double[] values) throws IOException {
long offset = pointer + index * RRD_PRIM_SIZES[RRD_DOUBLE];
backend.writeDouble(offset, values);
}
final String readString() throws IOException {
return backend.readString(pointer);
}
final void writeString(String value) throws IOException {
backend.writeString(pointer, value);
}
protected final <E extends Enum<E>> E readEnum(Class<E> clazz) throws IOException {
String value = backend.readString(pointer);
if (value == null || value.isEmpty()) {
return null;
} else {
try {
return Enum.valueOf(clazz, value);
} catch (IllegalArgumentException e) {
throw new InvalidRrdException("Invalid value for " + clazz.getSimpleName(), e);
}
}
}
protected final <E extends Enum<E>> void writeEnum(E value) throws IOException {
writeString(value.name());
}
final boolean isCachingAllowed() {
return cachingAllowed;
}
}

View File

@ -0,0 +1,90 @@
package org.rrd4j.core;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Paths;
/**
* Backend which is used to store RRD data to ordinary files on the disk. This was the
* default factory before 1.4.0 version. This backend is based on the RandomAccessFile class (java.io.* package).
*
*/
public class RrdRandomAccessFileBackend extends RrdBackend implements RrdFileBackend {
/**
* Random access file handle.
*/
protected final RandomAccessFile rafile;
/**
* Creates RrdFileBackend object for the given file path, backed by RandomAccessFile object.
*
* @param path Path to a file
* @param readOnly True, if file should be open in a read-only mode. False otherwise
* @throws java.io.IOException Thrown in case of I/O error
*/
protected RrdRandomAccessFileBackend(String path, boolean readOnly) throws IOException {
super(path);
this.rafile = new RandomAccessFile(path, readOnly ? "r" : "rw");
}
/**
* Closes the underlying RRD file.
*
* @throws java.io.IOException Thrown in case of I/O error
*/
protected void close() throws IOException {
rafile.close();
}
/**
* Writes bytes to the underlying RRD file on the disk
*
* @param offset Starting file offset
* @param b Bytes to be written.
* @throws java.io.IOException Thrown in case of I/O error
*/
protected void write(long offset, byte[] b) throws IOException {
rafile.seek(offset);
rafile.write(b);
}
/**
* Reads a number of bytes from the RRD file on the disk
*
* @param offset Starting file offset
* @param b Buffer which receives bytes read from the file.
* @throws java.io.IOException Thrown in case of I/O error.
*/
public void read(long offset, byte[] b) throws IOException {
rafile.seek(offset);
if (rafile.read(b) != b.length) {
throw new RrdBackendException("Not enough bytes available in file " + getPath());
}
}
@Override
public String getCanonicalPath() throws IOException {
return Paths.get(getPath()).toAbsolutePath().normalize().toString();
}
/**
* {@inheritDoc}
*
*/
@Override
public long getLength() throws IOException {
return rafile.length();
}
/**
* {@inheritDoc}
*
* Sets length of the underlying RRD file. This method is called only once, immediately
* after a new RRD file gets created.
*/
@Override
protected void setLength(long length) throws IOException {
rafile.setLength(length);
}
}

View File

@ -0,0 +1,22 @@
package org.rrd4j.core;
import java.io.IOException;
/**
* Factory class which creates actual {@link org.rrd4j.core.RrdRandomAccessFileBackend} objects. This was the default
* backend factory in Rrd4j before 1.4.0 release.
*
*/
@RrdBackendAnnotation(name="FILE", shouldValidateHeader=true)
public class RrdRandomAccessFileBackendFactory extends RrdFileBackendFactory {
/**
* {@inheritDoc}
*
* Creates RrdFileBackend object for the given file path.
*/
protected RrdBackend open(String path, boolean readOnly) throws IOException {
return new RrdRandomAccessFileBackend(path, readOnly);
}
}

View File

@ -0,0 +1,130 @@
package org.rrd4j.core;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.concurrent.atomic.AtomicLong;
/**
* Backend which is used to store RRD data to ordinary files on the disk, using locking. This backend
* is SAFE: it locks the underlying RRD file during update/fetch operations, and caches only static
* parts of a RRD file in memory. Therefore, this backend is safe to be used when RRD files should
* be shared between several JVMs at the same time. However, this backend is a little bit slow
* since it does not use fast java.nio.* package (it's still based on the RandomAccessFile class).
*
*/
public class RrdSafeFileBackend extends RrdRandomAccessFileBackend {
private static final Counters counters = new Counters();
private FileLock lock;
/**
* Creates RrdFileBackend object for the given file path, backed by RandomAccessFile object.
*
* @param path Path to a file
* @param lockWaitTime lock waiting time in milliseconds.
* @param lockRetryPeriod lock retry period in milliseconds.
* @throws java.io.IOException Thrown in case of I/O error.
*/
public RrdSafeFileBackend(String path, long lockWaitTime, long lockRetryPeriod)
throws IOException {
super(path, false);
try {
lockFile(lockWaitTime, lockRetryPeriod);
}
catch (IOException ioe) {
super.close();
throw ioe;
}
}
private void lockFile(long lockWaitTime, long lockRetryPeriod) throws IOException {
long entryTime = System.currentTimeMillis();
FileChannel channel = rafile.getChannel();
lock = channel.tryLock(0, Long.MAX_VALUE, false);
if (lock != null) {
counters.registerQuickLock();
return;
}
do {
try {
Thread.sleep(lockRetryPeriod);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
// NOP
}
lock = channel.tryLock(0, Long.MAX_VALUE, false);
if (lock != null) {
counters.registerDelayedLock();
return;
}
}
while (System.currentTimeMillis() - entryTime <= lockWaitTime);
counters.registerError();
throw new RrdBackendException("Could not obtain exclusive lock on file: " + getPath() +
"] after " + lockWaitTime + " milliseconds");
}
/**
* <p>close.</p>
*
* @throws java.io.IOException if any.
*/
@Override
public void close() throws IOException {
try {
if (lock != null) {
lock.release();
lock = null;
counters.registerUnlock();
}
}
finally {
super.close();
}
}
/**
* <p>getLockInfo.</p>
*
* @return a {@link java.lang.String} object.
*/
public static String getLockInfo() {
return counters.getInfo();
}
static class Counters {
final AtomicLong locks = new AtomicLong(0);
final AtomicLong quickLocks = new AtomicLong(0);
final AtomicLong unlocks = new AtomicLong(0);
final AtomicLong locked = new AtomicLong(0);
final AtomicLong errors = new AtomicLong(0);
void registerQuickLock() {
locks.getAndIncrement();
quickLocks.getAndIncrement();
locked.getAndIncrement();
}
void registerDelayedLock() {
locks.getAndIncrement();
locked.getAndIncrement();
}
void registerUnlock() {
unlocks.getAndIncrement();
locked.getAndDecrement();
}
void registerError() {
errors.getAndIncrement();
}
String getInfo() {
return "LOCKS=" + locks + ", " + "UNLOCKS=" + unlocks + ", " +
"DELAYED_LOCKS=" + (locks.get() - quickLocks.get()) + ", " + "LOCKED=" + locked + ", " +
"ERRORS=" + errors;
}
}
}

View File

@ -0,0 +1,91 @@
package org.rrd4j.core;
import java.io.IOException;
/**
* Factory class which creates actual {@link org.rrd4j.core.RrdSafeFileBackend} objects.
*
*/
@RrdBackendAnnotation(name="SAFE", shouldValidateHeader=true, cachingAllowed=false)
public class RrdSafeFileBackendFactory extends RrdRandomAccessFileBackendFactory {
/**
* Default time (in milliseconds) this backend will wait for a file lock.
*/
public static final long LOCK_WAIT_TIME = 3000L;
private static long defaultLockWaitTime = LOCK_WAIT_TIME;
/**
* Default time between two consecutive file locking attempts.
*/
public static final long LOCK_RETRY_PERIOD = 50L;
private static long defaultLockRetryPeriod = LOCK_RETRY_PERIOD;
private final long lockWaitTime;
private final long lockRetryPeriod;
/**
* Generate a factory using the default system wide lock settings
*/
public RrdSafeFileBackendFactory() {
lockWaitTime = defaultLockWaitTime;
lockRetryPeriod = defaultLockRetryPeriod;
}
/**
* Generate a factory with custom lock settings
* @param lockWaitTime
* @param lockRetryPeriod
*/
public RrdSafeFileBackendFactory(long lockWaitTime, long lockRetryPeriod) {
this.lockWaitTime = lockWaitTime;
this.lockRetryPeriod = lockRetryPeriod;
}
/**
* {@inheritDoc}
*
* Creates RrdSafeFileBackend object for the given file path.
*/
@Override
protected RrdBackend open(String path, boolean readOnly) throws IOException {
return new RrdSafeFileBackend(path, lockWaitTime, lockRetryPeriod);
}
/**
* Returns time this backend will wait for a file lock.
*
* @return Time (in milliseconds) this backend will wait for a file lock.
*/
public static long getLockWaitTime() {
return defaultLockWaitTime;
}
/**
* Sets time this backend will wait for a file lock.
*
* @param lockWaitTime Maximum lock wait time (in milliseconds)
*/
public static void setLockWaitTime(long lockWaitTime) {
RrdSafeFileBackendFactory.defaultLockWaitTime = lockWaitTime;
}
/**
* Returns time between two consecutive file locking attempts.
*
* @return Time (im milliseconds) between two consecutive file locking attempts.
*/
public static long getLockRetryPeriod() {
return defaultLockRetryPeriod;
}
/**
* Sets time between two consecutive file locking attempts.
*
* @param lockRetryPeriod time (in milliseconds) between two consecutive file locking attempts.
*/
public static void setLockRetryPeriod(long lockRetryPeriod) {
RrdSafeFileBackendFactory.defaultLockRetryPeriod = lockRetryPeriod;
}
}

View File

@ -0,0 +1,38 @@
package org.rrd4j.core;
import java.io.IOException;
class RrdString<U extends RrdUpdater<U>> extends RrdPrimitive<U> {
private String cache;
RrdString(RrdUpdater<U> updater, boolean isConstant) throws IOException {
super(updater, RrdPrimitive.RRD_STRING, isConstant);
}
RrdString(RrdUpdater<U> updater) throws IOException {
this(updater, false);
}
void set(String value) throws IOException {
if (!isCachingAllowed()) {
writeString(value);
}
// caching allowed
else if (cache == null || !cache.equals(value)) {
// update cache
writeString(cache = value);
}
}
String get() throws IOException {
if (!isCachingAllowed()) {
return readString();
}
else {
if (cache == null) {
cache = readString();
}
return cache;
}
}
}

View File

@ -0,0 +1,170 @@
package org.rrd4j.core;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* Thread pool used by {@link org.rrd4j.core.RrdNioBackend} instances to periodically sync the mapped file to disk. Note that instances
* of RrdSyncThreadPool must be disposed of by calling {@link #shutdown()}.
* <p>
* For ease of use in standalone applications, clients may choose to register a shutdown hook by calling
* {@link #registerShutdownHook()}. However, in web applications it is best to explicitly {@code shutdown()} the pool
* when the application is un-deployed, usually within a {@code javax.servlet.ServletContextListener}.
*
* @since 2.2
*/
public class RrdSyncThreadPool
{
/**
* The reference to the shutdown hook, or null.
*/
private final AtomicReference<Thread> shutdownHook = new AtomicReference<>();
/**
* The {@link java.util.concurrent.ScheduledExecutorService} used to periodically sync the mapped file to disk with.
* Defaults to {@value org.rrd4j.core.RrdNioBackendFactory#DEFAULT_SYNC_CORE_POOL_SIZE} threads.
*/
private final ScheduledExecutorService syncExecutor;
/**
* Creates a new RrdSyncThreadPool with a default pool size of {@value org.rrd4j.core.RrdNioBackendFactory#DEFAULT_SYNC_CORE_POOL_SIZE}.
*/
public RrdSyncThreadPool() {
this(RrdNioBackendFactory.DEFAULT_SYNC_CORE_POOL_SIZE);
}
/**
* Creates a new RrdSyncThreadPool with a user-provided ScheduledExecutorService.
*
* @param syncExecutor the ScheduledExecutorService to use for sync'ing mapped files to disk
*/
public RrdSyncThreadPool(ScheduledExecutorService syncExecutor)
{
if (syncExecutor == null) {
throw new NullPointerException("syncExecutor");
}
this.syncExecutor = syncExecutor;
}
/**
* Creates a new RrdSyncThreadPool with the given pool size.
*
* @param syncPoolSize the number of threads to use to sync the mapped file to disk
*/
public RrdSyncThreadPool(int syncPoolSize) {
this(syncPoolSize, null);
}
/**
* Creates a new RrdSyncThreadPool with the given pool size. Threads will be created by {@code threadFactory}.
*
* @param syncPoolSize the number of threads to use to sync the mapped file to disk
* @param threadFactory the ThreadFactory to use for creating threads
*/
public RrdSyncThreadPool(int syncPoolSize, ThreadFactory threadFactory) {
ThreadFactory poolThreadFactory = threadFactory;
if (poolThreadFactory == null) {
poolThreadFactory = new DaemonThreadFactory("RRD4J Sync-ThreadPool for " + this);
}
this.syncExecutor = Executors.newScheduledThreadPool(syncPoolSize, poolThreadFactory);
}
/**
* Registers a shutdown hook that destroys the underlying thread pool when when the JVM is about to quit.
*
* @return this
* @see #unregisterShutdownHook()
*/
public RrdSyncThreadPool registerShutdownHook() {
Thread shutdownThread = new ShutdownThread();
// if this returns null, then this pool has not registered a hook yet
boolean wasNull = shutdownHook.compareAndSet(null, shutdownThread);
if (wasNull) {
// Add a shutdown hook to stop the thread pool gracefully when the application exits
Runtime.getRuntime().addShutdownHook(shutdownThread);
}
return this;
}
/**
* Unregisters the shutdown hook installed by {@link #registerShutdownHook()}. Has no effect if the hook is not
* currently installed.
*
* @see #unregisterShutdownHook()
*/
public void unregisterShutdownHook() {
// if this returns a non-null value, then the hook needs to be uninstalled
Thread shutdownThread = shutdownHook.getAndSet(null);
if (shutdownThread != null) {
Runtime.getRuntime().removeShutdownHook(shutdownThread);
}
}
/**
* Shuts down this thread pool in an orderly manner. Has no effect if it has already been called previously.
*/
public void shutdown() {
unregisterShutdownHook();
syncExecutor.shutdown();
}
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
return syncExecutor.scheduleWithFixedDelay(command, initialDelay, delay, unit);
}
/**
* Daemon thread factory used by the monitor executors.
* <p>
* This factory creates all new threads used by an Executor in the same ThreadGroup.
* If there is a SecurityManager, it uses the group of System.getSecurityManager(), else the group
* of the thread instantiating this DaemonThreadFactory. Each new thread is created as a daemon thread
* with priority Thread.NORM_PRIORITY. New threads have names accessible via Thread.getName()
* of "<pool-name> Pool [Thread-M]", where M is the sequence number of the thread created by this factory.
*/
static class DaemonThreadFactory implements ThreadFactory
{
final ThreadGroup group;
final AtomicInteger threadNumber = new AtomicInteger(1);
final String poolName;
DaemonThreadFactory(String poolName) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
this.poolName = poolName;
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, poolName + " [Thread-" + threadNumber.getAndIncrement() + "]");
t.setDaemon(true);
t.setContextClassLoader(null);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
private class ShutdownThread extends Thread
{
public ShutdownThread() {
// include the RrdSyncThreadPool's toString in the thread name
super("RRD4J Sync-ThreadPool-Shutdown for " + RrdSyncThreadPool.this);
}
@Override
public void run()
{
// Progress and failure logging arising from the following code cannot be logged, since the
// behavior of logging is undefined in shutdown hooks.
syncExecutor.shutdown();
}
}
}

View File

@ -0,0 +1,106 @@
package org.rrd4j.core;
import java.io.IOException;
import org.rrd4j.ConsolFun;
import org.rrd4j.DsType;
import org.rrd4j.core.jrrd.RRDatabase;
class RrdToolReader extends DataImporter {
private RRDatabase rrd;
RrdToolReader(String rrdPath) throws IOException {
rrd = new RRDatabase(rrdPath);
}
public String getVersion() {
return rrd.getHeader().getVersion();
}
public long getLastUpdateTime() {
return Util.getTimestamp(rrd.getLastUpdate());
}
public long getStep() {
return rrd.getHeader().getPDPStep();
}
public int getDsCount() {
return rrd.getHeader().getDSCount();
}
public int getArcCount() throws IOException {
return rrd.getNumArchives();
}
public String getDsName(int dsIndex) {
return rrd.getDataSource(dsIndex).getName();
}
@Override
public DsType getDsType(int dsIndex) throws IOException {
return rrd.getDataSource(dsIndex).getType().getDsType();
}
public long getHeartbeat(int dsIndex) {
return rrd.getDataSource(dsIndex).getMinimumHeartbeat();
}
public double getMinValue(int dsIndex) {
return rrd.getDataSource(dsIndex).getMinimum();
}
public double getMaxValue(int dsIndex) {
return rrd.getDataSource(dsIndex).getMaximum();
}
public double getLastValue(int dsIndex) {
String valueStr = rrd.getDataSource(dsIndex).getPDPStatusBlock().getLastReading();
return Util.parseDouble(valueStr);
}
public double getAccumValue(int dsIndex) {
return rrd.getDataSource(dsIndex).getPDPStatusBlock().getValue();
}
public long getNanSeconds(int dsIndex) {
return rrd.getDataSource(dsIndex).getPDPStatusBlock().getUnknownSeconds();
}
public ConsolFun getConsolFun(int arcIndex) {
return rrd.getArchive(arcIndex).getType().getConsolFun();
}
public double getXff(int arcIndex) {
return rrd.getArchive(arcIndex).getXff();
}
public int getSteps(int arcIndex) {
return rrd.getArchive(arcIndex).getPdpCount();
}
public int getRows(int arcIndex) throws IOException {
return rrd.getArchive(arcIndex).getRowCount();
}
public double getStateAccumValue(int arcIndex, int dsIndex) throws IOException {
return rrd.getArchive(arcIndex).getCDPStatusBlock(dsIndex).getValue();
}
public int getStateNanSteps(int arcIndex, int dsIndex) throws IOException {
return rrd.getArchive(arcIndex).getCDPStatusBlock(dsIndex).getUnknownDatapoints();
}
public double[] getValues(int arcIndex, int dsIndex) throws IOException {
return rrd.getArchive(arcIndex).getValues()[dsIndex];
}
@Override
void release() throws IOException {
if (rrd != null) {
rrd.close();
rrd = null;
}
}
}

View File

@ -0,0 +1,594 @@
package org.rrd4j.core;
import org.rrd4j.ConsolFun;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* Class used to perform various complex operations on RRD files. Use an instance of the
* RrdToolkit class to:
*
* <ul>
* <li>add datasource to a RRD file.
* <li>add archive to a RRD file.
* <li>remove datasource from a RRD file.
* <li>remove archive from a RRD file.
* </ul>
*
* All these operations can be performed on the copy of the original RRD file, or on the
* original file itself (with possible backup file creation).
* <p>
* <b><u>IMPORTANT</u></b>: NEVER use methods found in this class on 'live' RRD files
* (files which are currently in use).
*
*/
@SuppressWarnings("deprecation")
public class RrdToolkit {
private static final String SOURCE_AND_DESTINATION_PATHS_ARE_THE_SAME = "Source and destination paths are the same";
private RrdToolkit() {
}
/**
* Creates a new RRD file with one more datasource in it. RRD file is created based on the
* existing one (the original RRD file is not modified at all). All data from
* the original RRD file is copied to the new one.
*
* @param sourcePath path to a RRD file to import data from (will not be modified)
* @param destPath path to a new RRD file (will be created)
* @param newDatasource Datasource definition to be added to the new RRD file
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void addDatasource(String sourcePath, String destPath, DsDef newDatasource)
throws IOException {
addDatasources(sourcePath, destPath, Collections.singleton(newDatasource));
}
/**
* Creates a new RRD file with one more datasource in it. RRD file is created based on the
* existing one (the original RRD file is not modified at all). All data from
* the original RRD file is copied to the new one.
*
* @param sourcePath path to a RRD file to import data from (will not be modified)
* @param destPath path to a new RRD file (will be created)
* @param newDatasources Datasource definitions to be added to the new RRD file
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void addDatasources(String sourcePath, String destPath, Iterable<DsDef> newDatasources)
throws IOException {
if (Util.sameFilePath(sourcePath, destPath)) {
throw new IllegalArgumentException(SOURCE_AND_DESTINATION_PATHS_ARE_THE_SAME);
}
try (RrdDb rrdSource = RrdDb.getBuilder().setPath(sourcePath).build()) {
RrdDef rrdDef = rrdSource.getRrdDef();
rrdDef.setPath(destPath);
for (DsDef newDatasource : newDatasources) {
rrdDef.addDatasource(newDatasource);
}
try (RrdDb rrdDest = RrdDb.getBuilder().setRrdDef(rrdDef).build()) {
rrdSource.copyStateTo(rrdDest);
}
}
}
/**
* <p>Adds one more datasource to a RRD file.</p>
* <p>WARNING: This method is potentially dangerous! It will modify your RRD file.
* It is highly recommended to preserve the original RRD file (<i>saveBackup</i>
* should be set to <code>true</code>). The backup file will be created in the same
* directory as the original one with <code>.bak</code> extension added to the
* original name.</p>
* <p>Before applying this method, be sure that the specified RRD file is not in use
* (not open)</p>
*
* @param sourcePath path to a RRD file to add datasource to.
* @param newDatasource Datasource definition to be added to the RRD file
* @param saveBackup true, if backup of the original file should be created;
* false, otherwise
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void addDatasource(String sourcePath, DsDef newDatasource, boolean saveBackup) throws IOException {
addDatasources(sourcePath, Collections.singleton(newDatasource), saveBackup);
}
/**
* <p>Adds datasources to a RRD file.</p>
* <p>WARNING: This method is potentially dangerous! It will modify your RRD file.
* It is highly recommended to preserve the original RRD file (<i>saveBackup</i>
* should be set to <code>true</code>). The backup file will be created in the same
* directory as the original one with <code>.bak</code> extension added to the
* original name.</p>
* <p>Before applying this method, be sure that the specified RRD file is not in use
* (not open)</p>
*
* @param sourcePath path to a RRD file to add datasource to.
* @param newDatasources Datasource definitions to be added to the RRD file
* @param saveBackup true, if backup of the original file should be created;
* false, otherwise
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void addDatasources(String sourcePath, Iterable<DsDef> newDatasources, boolean saveBackup) throws IOException {
String destPath = Util.getTmpFilename();
addDatasources(sourcePath, destPath, newDatasources);
copyFile(destPath, sourcePath, saveBackup);
}
/**
* Creates a new RRD file with one datasource removed. RRD file is created based on the
* existing one (the original RRD file is not modified at all). All remaining data from
* the original RRD file is copied to the new one.
*
* @param sourcePath path to a RRD file to import data from (will not be modified)
* @param destPath path to a new RRD file (will be created)
* @param dsName Name of the Datasource to be removed from the new RRD file
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void removeDatasource(String sourcePath, String destPath, String dsName)
throws IOException {
if (Util.sameFilePath(sourcePath, destPath)) {
throw new IllegalArgumentException(SOURCE_AND_DESTINATION_PATHS_ARE_THE_SAME);
}
try (RrdDb rrdSource = RrdDb.getBuilder().setPath(sourcePath).build()) {
RrdDef rrdDef = rrdSource.getRrdDef();
rrdDef.setPath(destPath);
rrdDef.removeDatasource(dsName);
try (RrdDb rrdDest = RrdDb.getBuilder().setRrdDef(rrdDef).build()) {
rrdSource.copyStateTo(rrdDest);
}
}
}
/**
* <p>Removes single datasource from a RRD file.</p>
* <p>WARNING: This method is potentially dangerous! It will modify your RRD file.
* It is highly recommended to preserve the original RRD file (<i>saveBackup</i>
* should be set to <code>true</code>). The backup file will be created in the same
* directory as the original one with <code>.bak</code> extension added to the
* original name.</p>
* <p>Before applying this method, be sure that the specified RRD file is not in use
* (not open)</p>
*
* @param sourcePath path to a RRD file to remove datasource from.
* @param dsName Name of the Datasource to be removed from the RRD file
* @param saveBackup true, if backup of the original file should be created;
* false, otherwise
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void removeDatasource(String sourcePath, String dsName, boolean saveBackup) throws IOException {
String destPath = Util.getTmpFilename();
removeDatasource(sourcePath, destPath, dsName);
copyFile(destPath, sourcePath, saveBackup);
}
/**
* Renames single datasource in the given RRD file.
*
* @param sourcePath Path to a RRD file
* @param oldDsName Old datasource name
* @param newDsName New datasource name
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void renameDatasource(String sourcePath, String oldDsName, String newDsName) throws IOException {
try (RrdDb rrd = RrdDb.getBuilder().setPath(sourcePath).build()) {
if (rrd.containsDs(oldDsName)) {
Datasource datasource = rrd.getDatasource(oldDsName);
datasource.setDsName(newDsName);
} else {
throw new IllegalArgumentException("Could not find datasource [" + oldDsName + "] in file " + sourcePath);
}
}
}
/**
* Updates single or all datasource names in the specified RRD file
* by appending '!' (if not already present). Datasources with names ending with '!'
* will never store NaNs in RRA archives (zero value will be used instead). Might be useful
* from time to time
*
* @param sourcePath Path to a RRD file
* @param dsName Datasource name or null if you want to rename all datasources
* @return Number of datasources successfully renamed
* @throws java.io.IOException Thrown in case of I/O error
*/
public static int forceZerosForNans(String sourcePath, String dsName) throws IOException {
try (RrdDb rrd = RrdDb.getBuilder().setPath(sourcePath).build()) {
Datasource[] datasources;
if (dsName == null) {
datasources = rrd.getDatasources();
} else {
if (rrd.containsDs(dsName)) {
datasources = new Datasource[]{rrd.getDatasource(dsName)};
} else {
throw new IllegalArgumentException("Could not find datasource [" + dsName + "] in file " + sourcePath);
}
}
int count = 0;
for (Datasource datasource : datasources) {
String currentDsName = datasource.getName();
if (!currentDsName.endsWith(DsDef.FORCE_ZEROS_FOR_NANS_SUFFIX)) {
datasource.setDsName(currentDsName + DsDef.FORCE_ZEROS_FOR_NANS_SUFFIX);
count++;
}
}
return count;
}
}
/**
* Creates a new RRD file with one more archive in it. RRD file is created based on the
* existing one (the original RRD file is not modified at all). All data from
* the original RRD file is copied to the new one.
*
* @param sourcePath path to a RRD file to import data from (will not be modified)
* @param destPath path to a new RRD file (will be created)
* @param newArchive Archive definition to be added to the new RRD file
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void addArchive(String sourcePath, String destPath, ArcDef newArchive) throws IOException {
if (Util.sameFilePath(sourcePath, destPath)) {
throw new IllegalArgumentException(SOURCE_AND_DESTINATION_PATHS_ARE_THE_SAME);
}
try (RrdDb rrdSource = RrdDb.getBuilder().setPath(sourcePath).build()) {
RrdDef rrdDef = rrdSource.getRrdDef();
rrdDef.setPath(destPath);
rrdDef.addArchive(newArchive);
try (RrdDb rrdDest = RrdDb.getBuilder().setRrdDef(rrdDef).build()) {
rrdSource.copyStateTo(rrdDest);
}
}
}
/**
* <p>Adds one more archive to a RRD file.</p>
* <p>WARNING: This method is potentially dangerous! It will modify your RRD file.
* It is highly recommended to preserve the original RRD file (<i>saveBackup</i>
* should be set to <code>true</code>). The backup file will be created in the same
* directory as the original one with <code>.bak</code> extension added to the
* original name.</p>
* <p>Before applying this method, be sure that the specified RRD file is not in use
* (not open)</p>
*
* @param sourcePath path to a RRD file to add datasource to.
* @param newArchive Archive definition to be added to the RRD file
* @param saveBackup true, if backup of the original file should be created;
* false, otherwise
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void addArchive(String sourcePath, ArcDef newArchive, boolean saveBackup) throws IOException {
String destPath = Util.getTmpFilename();
addArchive(sourcePath, destPath, newArchive);
copyFile(destPath, sourcePath, saveBackup);
}
/**
* Creates a new RRD file with one archive removed. RRD file is created based on the
* existing one (the original RRD file is not modified at all). All relevant data from
* the original RRD file is copied to the new one.
*
* @param sourcePath path to a RRD file to import data from (will not be modified)
* @param destPath path to a new RRD file (will be created)
* @param consolFun Consolidation function of Archive which should be removed
* @param steps Number of steps for Archive which should be removed
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void removeArchive(String sourcePath, String destPath, ConsolFun consolFun, int steps) throws IOException {
if (Util.sameFilePath(sourcePath, destPath)) {
throw new IllegalArgumentException(SOURCE_AND_DESTINATION_PATHS_ARE_THE_SAME);
}
try (RrdDb rrdSource = RrdDb.getBuilder().setPath(sourcePath).build()) {
RrdDef rrdDef = rrdSource.getRrdDef();
rrdDef.setPath(destPath);
rrdDef.removeArchive(consolFun, steps);
try (RrdDb rrdDest = RrdDb.getBuilder().setRrdDef(rrdDef).build()) {
rrdSource.copyStateTo(rrdDest);
}
}
}
/**
* <p>Removes one archive from a RRD file.</p>
* <p>WARNING: This method is potentially dangerous! It will modify your RRD file.
* It is highly recommended to preserve the original RRD file (<i>saveBackup</i>
* should be set to <code>true</code>). The backup file will be created in the same
* directory as the original one with <code>.bak</code> extension added to the
* original name.</p>
* <p>Before applying this method, be sure that the specified RRD file is not in use
* (not open)</p>
*
* @param sourcePath path to a RRD file to add datasource to.
* @param consolFun Consolidation function of Archive which should be removed
* @param steps Number of steps for Archive which should be removed
* @param saveBackup true, if backup of the original file should be created;
* false, otherwise
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void removeArchive(String sourcePath, ConsolFun consolFun, int steps, boolean saveBackup) throws IOException {
String destPath = Util.getTmpFilename();
removeArchive(sourcePath, destPath, consolFun, steps);
copyFile(destPath, sourcePath, saveBackup);
}
private static void copyFile(String sourcePath, String destPath, boolean saveBackup)
throws IOException {
File source = new File(sourcePath);
File dest = new File(destPath);
if (saveBackup) {
String backupPath = getBackupPath(destPath);
File backup = new File(backupPath);
deleteFile(backup);
if (!dest.renameTo(backup)) {
throw new RrdException("Could not create backup file " + backupPath);
}
}
deleteFile(dest);
if (!source.renameTo(dest)) {
//Rename failed so try to copy and erase
try(FileChannel sourceStream = new FileInputStream(source).getChannel(); FileChannel destinationStream = new FileOutputStream(dest).getChannel()) {
long count = 0;
final long size = sourceStream.size();
while(count < size) {
count += destinationStream.transferFrom(sourceStream, count, size-count);
}
deleteFile(source);
}
}
}
private static String getBackupPath(String destPath) {
StringBuilder backupPath = new StringBuilder(destPath);
do {
backupPath.append( ".bak");
}
while (Util.fileExists(backupPath.toString()));
return backupPath.toString();
}
/**
* Sets datasource heartbeat to a new value.
*
* @param sourcePath Path to existing RRD file (will be updated)
* @param datasourceName Name of the datasource in the specified RRD file
* @param newHeartbeat New datasource heartbeat
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void setDsHeartbeat(String sourcePath, String datasourceName, long newHeartbeat) throws IOException {
try (RrdDb rrd = RrdDb.getBuilder().setPath(sourcePath).build()) {
Datasource ds = rrd.getDatasource(datasourceName);
ds.setHeartbeat(newHeartbeat);
}
}
/**
* Sets datasource heartbeat to a new value.
*
* @param sourcePath Path to existing RRD file (will be updated)
* @param dsIndex Index of the datasource in the specified RRD file
* @param newHeartbeat New datasource heartbeat
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void setDsHeartbeat(String sourcePath, int dsIndex, long newHeartbeat) throws IOException {
try (RrdDb rrd = RrdDb.getBuilder().setPath(sourcePath).build()) {
Datasource ds = rrd.getDatasource(dsIndex);
ds.setHeartbeat(newHeartbeat);
}
}
/**
* Sets datasource min value to a new value
*
* @param sourcePath Path to existing RRD file (will be updated)
* @param datasourceName Name of the datasource in the specified RRD file
* @param newMinValue New min value for the datasource
* @param filterArchivedValues set to <code>true</code> if archived values less than
* <code>newMinValue</code> should be set to NaN; set to false, otherwise.
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void setDsMinValue(String sourcePath, String datasourceName,
double newMinValue, boolean filterArchivedValues) throws IOException {
try (RrdDb rrd = RrdDb.getBuilder().setPath(sourcePath).build()) {
Datasource ds = rrd.getDatasource(datasourceName);
ds.setMinValue(newMinValue, filterArchivedValues);
}
}
/**
* Sets datasource max value to a new value.
*
* @param sourcePath Path to existing RRD file (will be updated)
* @param datasourceName Name of the datasource in the specified RRD file
* @param newMaxValue New max value for the datasource
* @param filterArchivedValues set to <code>true</code> if archived values greater than
* <code>newMaxValue</code> should be set to NaN; set to false, otherwise.
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void setDsMaxValue(String sourcePath, String datasourceName,
double newMaxValue, boolean filterArchivedValues) throws IOException {
try (RrdDb rrd = RrdDb.getBuilder().setPath(sourcePath).build()) {
Datasource ds = rrd.getDatasource(datasourceName);
ds.setMaxValue(newMaxValue, filterArchivedValues);
}
}
/**
* Updates valid value range for the given datasource.
*
* @param sourcePath Path to existing RRD file (will be updated)
* @param datasourceName Name of the datasource in the specified RRD file
* @param newMinValue New min value for the datasource
* @param newMaxValue New max value for the datasource
* @param filterArchivedValues set to <code>true</code> if archived values outside
* of the specified min/max range should be replaced with NaNs.
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void setDsMinMaxValue(String sourcePath, String datasourceName,
double newMinValue, double newMaxValue, boolean filterArchivedValues)
throws IOException {
try (RrdDb rrd = RrdDb.getBuilder().setPath(sourcePath).build()) {
Datasource ds = rrd.getDatasource(datasourceName);
ds.setMinMaxValue(newMinValue, newMaxValue, filterArchivedValues);
}
}
/**
* Sets single archive's X-files factor to a new value.
*
* @param sourcePath Path to existing RRD file (will be updated)
* @param consolFun Consolidation function of the target archive
* @param steps Number of steps of the target archive
* @param newXff New X-files factor for the target archive
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void setArcXff(String sourcePath, ConsolFun consolFun, int steps,
double newXff) throws IOException {
try (RrdDb rrd = RrdDb.getBuilder().setPath(sourcePath).build()) {
Archive arc = rrd.getArchive(consolFun, steps);
arc.setXff(newXff);
}
}
/**
* Creates new RRD file based on the existing one, but with a different
* size (number of rows) for a single archive. The archive to be resized
* is identified by its consolidation function and the number of steps.
*
* @param sourcePath Path to the source RRD file (will not be modified)
* @param destPath Path to the new RRD file (will be created)
* @param consolFun Consolidation function of the archive to be resized
* @param numSteps Number of steps of the archive to be resized
* @param newRows New archive size (number of archive rows)
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void resizeArchive(String sourcePath, String destPath, ConsolFun consolFun,
int numSteps, int newRows) throws IOException {
if (Util.sameFilePath(sourcePath, destPath)) {
throw new IllegalArgumentException(SOURCE_AND_DESTINATION_PATHS_ARE_THE_SAME);
}
if (newRows < 2) {
throw new IllegalArgumentException("New archive size must be at least 2");
}
try (RrdDb rrdSource = RrdDb.getBuilder().setPath(sourcePath).build()) {
RrdDef rrdDef = rrdSource.getRrdDef();
ArcDef arcDef = rrdDef.findArchive(consolFun, numSteps);
if (arcDef.getRows() != newRows) {
arcDef.setRows(newRows);
rrdDef.setPath(destPath);
RrdDb rrdDest = new RrdDb(rrdDef);
try {
rrdSource.copyStateTo(rrdDest);
} finally {
rrdDest.close();
}
}
}
}
/**
* Modifies existing RRD file, by resizing its chosen archive. The archive to be resized
* is identified by its consolidation function and the number of steps.
*
* @param sourcePath Path to the RRD file (will be modified)
* @param consolFun Consolidation function of the archive to be resized
* @param numSteps Number of steps of the archive to be resized
* @param newRows New archive size (number of archive rows)
* @param saveBackup true, if backup of the original file should be created;
* false, otherwise
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void resizeArchive(String sourcePath, ConsolFun consolFun,
int numSteps, int newRows, boolean saveBackup) throws IOException {
String destPath = Util.getTmpFilename();
resizeArchive(sourcePath, destPath, consolFun, numSteps, newRows);
copyFile(destPath, sourcePath, saveBackup);
}
private static void deleteFile(File file) throws IOException {
if (file.exists()) {
Files.delete(file.toPath());
}
}
/**
* Splits single RRD file with several datasources into a number of smaller RRD files
* with a single datasource in it. All archived values are preserved. If
* you have a RRD file named 'traffic.rrd' with two datasources, 'in' and 'out', this
* method will create two files (with a single datasource, in the same directory)
* named 'in-traffic.rrd' and 'out-traffic.rrd'.
*
* @param sourcePath Path to a RRD file with multiple datasources defined
* @throws java.io.IOException Thrown in case of I/O error
*/
public static void split(String sourcePath) throws IOException {
try (RrdDb rrdSource = RrdDb.getBuilder().setPath(sourcePath).build()) {
String[] dsNames = rrdSource.getDsNames();
for (String dsName : dsNames) {
RrdDef rrdDef = rrdSource.getRrdDef();
rrdDef.setPath(createSplitPath(dsName, sourcePath));
rrdDef.saveSingleDatasource(dsName);
try (RrdDb rrdDest = RrdDb.getBuilder().setRrdDef(rrdDef).build()) {
rrdSource.copyStateTo(rrdDest);
}
}
}
}
/**
* Returns list of canonical file names with the specified extension in the given directory. This
* method is not RRD related, but might come handy to create a quick list of all RRD files
* in the given directory.
*
* @param directory Source directory
* @param extension File extension (like ".rrd", ".jrb", ".rrd.jrb")
* @param resursive true if all subdirectories should be traversed for the same extension, false otherwise
* @return Array of sorted canonical file names with the given extension
* @throws java.io.IOException Thrown in case of I/O error
*/
public static String[] getCanonicalPaths(String directory, final String extension, boolean resursive)
throws IOException {
File baseDir = new File(directory);
if (!baseDir.isDirectory()) {
throw new RrdException("Not a directory: " + directory);
}
List<String> fileList = new LinkedList<>();
traverseDirectory(new File(directory), extension, resursive, fileList);
String[] result = fileList.toArray(new String[fileList.size()]);
Arrays.sort(result);
return result;
}
private static void traverseDirectory(File directory, String extension, boolean recursive, List<String> list)
throws IOException {
File[] files = directory.listFiles();
for (File file : files) {
if (file.isDirectory() && recursive) {
// traverse subdirectories only if recursive flag is specified
traverseDirectory(file, extension, recursive, list);
} else if (file.isFile() && file.getName().endsWith(extension)) {
list.add(file.getCanonicalPath());
}
}
}
private static String createSplitPath(String dsName, String sourcePath) {
File file = new File(sourcePath);
String newName = dsName + "-" + file.getName();
String path = file.getAbsolutePath();
String parentDir = path.substring(0, 1 + path.lastIndexOf(Util.getFileSeparator()));
return parentDir + newName;
}
}

View File

@ -0,0 +1,27 @@
package org.rrd4j.core;
import java.io.IOException;
interface RrdUpdater<T extends RrdUpdater<T>> {
/**
* <p>getRrdBackend.</p>
*
* @return a {@link org.rrd4j.core.RrdBackend} object.
*/
RrdBackend getRrdBackend();
/**
* <p>copyStateTo.</p>
*
* @param updater a {@link org.rrd4j.core.RrdUpdater} object.
* @throws java.io.IOException if any.
*/
void copyStateTo(T updater) throws IOException;
/**
* <p>getRrdAllocator.</p>
*
* @return a {@link org.rrd4j.core.RrdAllocator} object.
*/
RrdAllocator getRrdAllocator();
}

View File

@ -0,0 +1,236 @@
package org.rrd4j.core;
import java.io.IOException;
import java.util.StringTokenizer;
import java.util.Arrays;
/**
* <p>Class to represent data source values for the given timestamp. Objects of this
* class are never created directly (no public constructor is provided). To learn more how
* to update RRDs, see RRDTool's
* <a href="../../../../man/rrdupdate.html" target="man">rrdupdate man page</a>.</p>
* <p>To update a RRD with Rrd4j use the following procedure:</p>
* <ol>
* <li>Obtain empty Sample object by calling method {@link org.rrd4j.core.RrdDb#createSample(long)
* createSample()} on respective {@link RrdDb RrdDb} object.
* <li>Adjust Sample timestamp if necessary (see {@link #setTime(long) setTime()} method).
* <li>Supply data source values (see {@link #setValue(String, double) setValue()}).
* <li>Call Sample's {@link #update() update()} method.
* </ol>
* <p>Newly created Sample object contains all data source values set to 'unknown'.
* You should specify only 'known' data source values. However, if you want to specify
* 'unknown' values too, use <code>Double.NaN</code>.</p>
*
* @author Sasa Markovic
*/
public class Sample {
private final RrdDb parentDb;
private long time;
private final String[] dsNames;
private final double[] values;
Sample(RrdDb parentDb, long time) throws IOException {
this.parentDb = parentDb;
this.time = time;
this.dsNames = parentDb.getDsNames();
values = new double[dsNames.length];
clearValues();
}
private void clearValues() {
Arrays.fill(values, Double.NaN);
}
/**
* Sets single data source value in the sample.
*
* @param dsName Data source name.
* @param value Data source value.
* @return This <code>Sample</code> object
* @throws java.lang.IllegalArgumentException Thrown if invalid data source name is supplied.
*/
public Sample setValue(String dsName, double value) {
for (int i = 0; i < values.length; i++) {
if (dsNames[i].equals(dsName)) {
values[i] = value;
return this;
}
}
throw new IllegalArgumentException("Datasource " + dsName + " not found");
}
/**
* Sets single datasource value using data source index. Data sources are indexed by
* the order specified during RRD creation (zero-based).
*
* @param i Data source index
* @param value Data source values
* @return This <code>Sample</code> object
* @throws java.lang.IllegalArgumentException Thrown if data source index is invalid.
*/
public Sample setValue(int i, double value) {
if (i < values.length) {
values[i] = value;
return this;
}
throw new IllegalArgumentException("Sample datasource index " + i + " out of bounds");
}
/**
* Sets some (possibly all) data source values in bulk. Data source values are
* assigned in the order of their definition inside the RRD.
*
* @param values Data source values.
* @return This <code>Sample</code> object
* @throws java.lang.IllegalArgumentException Thrown if the number of supplied values is zero or greater
* than the number of data sources defined in the RRD.
*/
public Sample setValues(double... values) {
if (values.length <= this.values.length) {
System.arraycopy(values, 0, this.values, 0, values.length);
return this;
}
throw new IllegalArgumentException("Invalid number of values specified (found " +
values.length + ", only " + dsNames.length + " allowed)");
}
/**
* Returns all current data source values in the sample.
*
* @return Data source values.
*/
public double[] getValues() {
return values;
}
/**
* Returns sample timestamp (in seconds, without milliseconds).
*
* @return Sample timestamp.
*/
public long getTime() {
return time;
}
/**
* Sets sample timestamp. Timestamp should be defined in seconds (without milliseconds).
*
* @param time New sample timestamp.
* @return This <code>Sample</code> object
*/
public Sample setTime(long time) {
this.time = time;
return this;
}
/**
* Returns an array of all data source names. If you try to set value for the data source
* name not in this array, an exception is thrown.
*
* @return Acceptable data source names.
*/
public String[] getDsNames() {
return dsNames;
}
/**
* <p>Sets sample timestamp and data source values in a fashion similar to RRDTool.
* Argument string should be composed in the following way:
* <code>timestamp:value1:value2:...:valueN</code>.</p>
* <p>You don't have to supply all datasource values. Unspecified values will be treated
* as unknowns. To specify unknown value in the argument string, use letter 'U'.</p>
*
* @param timeAndValues <p>String made by concatenating sample timestamp with corresponding
* data source values delmited with colons. For example:</p>
*
* <pre>
* 1005234132:12.2:35.6:U:24.5
* NOW:12.2:35.6:U:24.5
* </pre>
* <p>'N' stands for the current timestamp (can be replaced with 'NOW')<p>
* Method will throw an exception if timestamp is invalid (cannot be parsed as Long, and is not 'N'
* or 'NOW'). Datasource value which cannot be parsed as 'double' will be silently set to NaN.</p>
* @return This <code>Sample</code> object
* @throws java.lang.IllegalArgumentException Thrown if too many datasource values are supplied
*/
public Sample set(String timeAndValues) {
StringTokenizer tokenizer = new StringTokenizer(timeAndValues, ":", false);
int n = tokenizer.countTokens();
if (n > values.length + 1) {
throw new IllegalArgumentException("Invalid number of values specified (found " +
values.length + ", " + dsNames.length + " allowed)");
}
String timeToken = tokenizer.nextToken();
try {
time = Long.parseLong(timeToken);
}
catch (NumberFormatException nfe) {
if ("N".equalsIgnoreCase(timeToken) || "NOW".equalsIgnoreCase(timeToken)) {
time = Util.getTime();
}
else {
throw new IllegalArgumentException("Invalid sample timestamp: " + timeToken);
}
}
for (int i = 0; tokenizer.hasMoreTokens(); i++) {
try {
values[i] = Double.parseDouble(tokenizer.nextToken());
}
catch (NumberFormatException nfe) {
// NOP, value is already set to NaN
}
}
return this;
}
/**
* Stores sample in the corresponding RRD. If the update operation succeeds,
* all datasource values in the sample will be set to Double.NaN (unknown) values.
*
* @throws java.io.IOException Thrown in case of I/O error.
*/
public void update() throws IOException {
parentDb.store(this);
clearValues();
}
/**
* Creates sample with the timestamp and data source values supplied
* in the argument string and stores sample in the corresponding RRD.
* This method is just a shortcut for:
* <pre>
* set(timeAndValues);
* update();
* </pre>
*
* @param timeAndValues String made by concatenating sample timestamp with corresponding
* data source values delmited with colons. For example:<br>
* <code>1005234132:12.2:35.6:U:24.5</code><br>
* <code>NOW:12.2:35.6:U:24.5</code>
* @throws java.io.IOException Thrown in case of I/O error.
*/
public void setAndUpdate(String timeAndValues) throws IOException {
set(timeAndValues);
update();
}
/**
* Dumps sample content using the syntax of RRDTool's update command.
*
* @return Sample dump.
*/
public String dump() {
StringBuilder buffer = new StringBuilder("update \"");
buffer.append(parentDb.getRrdBackend().getPath()).append("\" ").append(time);
for (double value : values) {
buffer.append(':');
buffer.append(Util.formatDouble(value, "U", false));
}
return buffer.toString();
}
String getRrdToolCommand() {
return dump();
}
}

View File

@ -0,0 +1,799 @@
package org.rrd4j.core;
import org.rrd4j.core.timespec.TimeParser;
import org.rrd4j.core.timespec.TimeSpec;
import org.rrd4j.ConsolFun;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.*;
import java.io.*;
import java.net.JarURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Pattern;
/**
* Class defines various utility functions used in Rrd4j.
*
* @author Sasa Markovic
*/
public class Util {
/** Constant <code>MAX_LONG=Long.MAX_VALUE</code> */
public static final long MAX_LONG = Long.MAX_VALUE;
/** Constant <code>MIN_LONG=-Long.MAX_VALUE</code> */
public static final long MIN_LONG = -Long.MAX_VALUE;
/** Constant <code>MAX_DOUBLE=Double.MAX_VALUE</code> */
public static final double MAX_DOUBLE = Double.MAX_VALUE;
/** Constant <code>MIN_DOUBLE=-Double.MAX_VALUE</code> */
public static final double MIN_DOUBLE = -Double.MAX_VALUE;
// pattern RRDTool uses to format doubles in XML files
static final String PATTERN = "0.0000000000E00";
// directory under $USER_HOME used for demo graphs storing
static final String RRD4J_DIR = "rrd4j-demo";
static final ThreadLocal<NumberFormat> df = new ThreadLocal<NumberFormat>() {
@Override
protected NumberFormat initialValue() {
DecimalFormat ldf = (DecimalFormat) NumberFormat.getNumberInstance(Locale.ENGLISH);
ldf.applyPattern(PATTERN);
ldf.setPositivePrefix("+");
return ldf;
}
};
private static final Pattern SPRINTF_PATTERN = Pattern.compile("([^%]|^)%([^a-zA-Z%]*)l(f|g|e)");
private Util() {
}
/**
* Converts an array of long primitives to an array of doubles.
*
* @param array input array of long values.
* @return Same array but with all values as double.
*/
public static double[] toDoubleArray(final long[] array) {
double[] values = new double[array.length];
for (int i = 0; i < array.length; i++)
values[i] = array[i];
return values;
}
/**
* Returns current timestamp in seconds (without milliseconds). Returned timestamp
* is obtained with the following expression:
* <p>
* <code>(System.currentTimeMillis() + 500L) / 1000L</code>
*
* @return Current timestamp
*/
public static long getTime() {
return (System.currentTimeMillis() + 500L) / 1000L;
}
/**
* Just an alias for {@link #getTime()} method.
*
* @return Current timestamp (without milliseconds)
*/
public static long getTimestamp() {
return getTime();
}
/**
* Rounds the given timestamp to the nearest whole &quot;step&quot;. Rounded value is obtained
* from the following expression:
* <p>
* <code>timestamp - timestamp % step;</code>
*
* @param timestamp Timestamp in seconds
* @param step Step in seconds
* @return "Rounded" timestamp
*/
public static long normalize(long timestamp, long step) {
return timestamp - timestamp % step;
}
/**
* Returns the greater of two double values, but treats NaN as the smallest possible
* value. Note that <code>Math.max()</code> behaves differently for NaN arguments.
*
* @param x an argument
* @param y another argument
* @return the lager of arguments
*/
public static double max(double x, double y) {
return Double.isNaN(x) ? y : Double.isNaN(y) ? x : Math.max(x, y);
}
/**
* Returns the smaller of two double values, but treats NaN as the greatest possible
* value. Note that <code>Math.min()</code> behaves differently for NaN arguments.
*
* @param x an argument
* @param y another argument
* @return the smaller of arguments
*/
public static double min(double x, double y) {
return Double.isNaN(x) ? y : Double.isNaN(y) ? x : Math.min(x, y);
}
/**
* Calculates sum of two doubles, but treats NaNs as zeros.
*
* @param x First double
* @param y Second double
* @return Sum(x,y) calculated as <code>Double.isNaN(x)? y: Double.isNaN(y)? x: x + y;</code>
*/
public static double sum(double x, double y) {
return Double.isNaN(x) ? y : Double.isNaN(y) ? x : x + y;
}
static String formatDouble(double x, String nanString, boolean forceExponents) {
if (Double.isNaN(x)) {
return nanString;
}
if (forceExponents) {
return df.get().format(x);
}
return Double.toString(x);
}
static String formatDouble(double x, boolean forceExponents) {
return formatDouble(x, Double.toString(Double.NaN), forceExponents);
}
/**
* Formats double as a string using exponential notation (RRDTool like). Used for debugging
* through the project.
*
* @param x value to be formatted
* @return string like "+1.234567E+02"
*/
public static String formatDouble(double x) {
return formatDouble(x, true);
}
/**
* Returns <code>Date</code> object for the given timestamp (in seconds, without
* milliseconds)
*
* @param timestamp Timestamp in seconds.
* @return Corresponding Date object.
*/
public static Date getDate(long timestamp) {
return new Date(timestamp * 1000L);
}
/**
* Returns <code>Calendar</code> object for the given timestamp
* (in seconds, without milliseconds)
*
* @param timestamp Timestamp in seconds.
* @return Corresponding Calendar object.
*/
public static Calendar getCalendar(long timestamp) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(timestamp * 1000L);
return calendar;
}
/**
* Returns <code>Calendar</code> object for the given Date object
*
* @param date Date object
* @return Corresponding Calendar object.
*/
public static Calendar getCalendar(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
return calendar;
}
/**
* Returns timestamp (unix epoch) for the given Date object
*
* @param date Date object
* @return Corresponding timestamp (without milliseconds)
*/
public static long getTimestamp(Date date) {
// round to whole seconds, ignore milliseconds
return (date.getTime() + 499L) / 1000L;
}
/**
* Returns timestamp (unix epoch) for the given Calendar object
*
* @param gc Calendar object
* @return Corresponding timestamp (without milliseconds)
*/
public static long getTimestamp(Calendar gc) {
return getTimestamp(gc.getTime());
}
/**
* Returns timestamp (unix epoch) for the given year, month, day, hour and minute.
*
* @param year Year
* @param month Month (zero-based)
* @param day Day in month
* @param hour Hour
* @param min Minute
* @return Corresponding timestamp
*/
public static long getTimestamp(int year, int month, int day, int hour, int min) {
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.set(year, month, day, hour, min);
return Util.getTimestamp(calendar);
}
/**
* Returns timestamp (unix epoch) for the given year, month and day.
*
* @param year Year
* @param month Month (zero-based)
* @param day Day in month
* @return Corresponding timestamp
*/
public static long getTimestamp(int year, int month, int day) {
return Util.getTimestamp(year, month, day, 0, 0);
}
/**
* <p>Parses at-style time specification and returns the corresponding timestamp. For example:</p>
* <pre>
* long t = Util.getTimestamp("now-1d");
* </pre>
*
* @param atStyleTimeSpec at-style time specification. For the complete explanation of the syntax
* allowed see RRDTool's <code>rrdfetch</code> man page.<p>
* @return timestamp in seconds since epoch.
*/
public static long getTimestamp(String atStyleTimeSpec) {
TimeSpec timeSpec = new TimeParser(atStyleTimeSpec).parse();
return timeSpec.getTimestamp();
}
/**
* <p>Parses two related at-style time specifications and returns corresponding timestamps. For example:</p>
* <pre>
* long[] t = Util.getTimestamps("end-1d","now");
* </pre>
*
* @param atStyleTimeSpec1 Starting at-style time specification. For the complete explanation of the syntax
* allowed see RRDTool's <code>rrdfetch</code> man page.<p>
* @param atStyleTimeSpec2 Ending at-style time specification. For the complete explanation of the syntax
* allowed see RRDTool's <code>rrdfetch</code> man page.<p>
* @return An array of two longs representing starting and ending timestamp in seconds since epoch.
*/
public static long[] getTimestamps(String atStyleTimeSpec1, String atStyleTimeSpec2) {
TimeSpec timeSpec1 = new TimeParser(atStyleTimeSpec1).parse();
TimeSpec timeSpec2 = new TimeParser(atStyleTimeSpec2).parse();
return TimeSpec.getTimestamps(timeSpec1, timeSpec2);
}
/**
* Parses input string as a double value. If the value cannot be parsed, Double.NaN
* is returned (NumberFormatException is never thrown).
*
* @param valueStr String representing double value
* @return a double corresponding to the input string
*/
public static double parseDouble(String valueStr) {
double value;
try {
value = Double.parseDouble(valueStr);
}
catch (NumberFormatException nfe) {
value = Double.NaN;
}
return value;
}
/**
* Checks if a string can be parsed as double.
*
* @param s Input string
* @return <code>true</code> if the string can be parsed as double, <code>false</code> otherwise
*/
public static boolean isDouble(String s) {
try {
Double.parseDouble(s);
return true;
}
catch (NumberFormatException nfe) {
return false;
}
}
/**
* Parses input string as a boolean value. The parser is case insensitive.
*
* @param valueStr String representing boolean value
* @return <code>true</code>, if valueStr equals to 'true', 'on', 'yes', 'y' or '1';
* <code>false</code> in all other cases.
*/
public static boolean parseBoolean(String valueStr) {
return valueStr !=null && (valueStr.equalsIgnoreCase("true") ||
valueStr.equalsIgnoreCase("on") ||
valueStr.equalsIgnoreCase("yes") ||
valueStr.equalsIgnoreCase("y") ||
valueStr.equalsIgnoreCase("1"));
}
/**
* Parses input string as color. The color string should be of the form #RRGGBB (no alpha specified,
* opaque color) or #RRGGBBAA (alpa specified, transparent colors). Leading character '#' is
* optional.
*
* @param valueStr Input string, for example #FFAA24, #AABBCC33, 010203 or ABC13E4F
* @return Paint object
* @throws java.lang.IllegalArgumentException If the input string is not 6 or 8 characters long (without optional '#')
*/
public static Paint parseColor(String valueStr) {
String c = valueStr.startsWith("#") ? valueStr.substring(1) : valueStr;
if (c.length() != 6 && c.length() != 8) {
throw new IllegalArgumentException("Invalid color specification: " + valueStr);
}
String r = c.substring(0, 2), g = c.substring(2, 4), b = c.substring(4, 6);
if (c.length() == 6) {
return new Color(Integer.parseInt(r, 16), Integer.parseInt(g, 16), Integer.parseInt(b, 16));
}
else {
String a = c.substring(6);
return new Color(Integer.parseInt(r, 16), Integer.parseInt(g, 16),
Integer.parseInt(b, 16), Integer.parseInt(a, 16));
}
}
/**
* Returns file system separator string.
*
* @return File system separator ("/" on Unix, "\" on Windows)
*/
public static String getFileSeparator() {
return System.getProperty("file.separator");
}
/**
* Returns path to user's home directory.
*
* @return Path to users home directory, with file separator appended.
*/
public static String getUserHomeDirectory() {
return System.getProperty("user.home") + getFileSeparator();
}
/**
* Returns path to directory used for placement of Rrd4j demo graphs and creates it
* if necessary.
*
* @return Path to demo directory (defaults to $HOME/rrd4j/) if directory exists or
* was successfully created. Null if such directory could not be created.
*/
public static String getRrd4jDemoDirectory() {
Path root;
if (System.getProperty("rrd4j.demopath") != null) {
root = Paths.get(System.getProperty("rrd4j.demopath"));
} else {
root = Paths.get(getUserHomeDirectory(), RRD4J_DIR);
}
try {
Files.createDirectories(root);
return root.toAbsolutePath().toString() + File.separator;
} catch (IOException e) {
return null;
}
}
/**
* Returns full path to the file stored in the demo directory of Rrd4j
*
* @param filename Partial path to the file stored in the demo directory of Rrd4j
* (just name and extension, without parent directories)
* @return Full path to the file
*/
public static String getRrd4jDemoPath(String filename) {
String demoDir = getRrd4jDemoDirectory();
if (demoDir != null) {
return demoDir + filename;
}
else {
return null;
}
}
static boolean sameFilePath(String pathname1, String pathname2) throws IOException {
Path path1 = Paths.get(pathname1);
Path path2 = Paths.get(pathname2);
if (Files.exists(path1) != Files.exists(path2)) {
return false;
} else if (Files.exists(path1) && Files.exists(path2)){
path1 = Paths.get(pathname1).toRealPath().normalize();
path2 = Paths.get(pathname2).toRealPath().normalize();
return Files.isSameFile(path1, path2);
} else {
return false;
}
}
static int getMatchingDatasourceIndex(RrdDb rrd1, int dsIndex, RrdDb rrd2) throws IOException {
String dsName = rrd1.getDatasource(dsIndex).getName();
try {
return rrd2.getDsIndex(dsName);
}
catch (IllegalArgumentException e) {
return -1;
}
}
static int getMatchingArchiveIndex(RrdDb rrd1, int arcIndex, RrdDb rrd2)
throws IOException {
Archive archive = rrd1.getArchive(arcIndex);
ConsolFun consolFun = archive.getConsolFun();
int steps = archive.getSteps();
try {
return rrd2.getArcIndex(consolFun, steps);
}
catch (IllegalArgumentException e) {
return -1;
}
}
static String getTmpFilename() throws IOException {
return File.createTempFile("rrd4j_", ".tmp").getCanonicalPath();
}
static final String ISO_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; // ISO
/**
* Creates Calendar object from a string. The string should represent
* either a long integer (UNIX timestamp in seconds without milliseconds,
* like "1002354657") or a human readable date string in the format "yyyy-MM-dd HH:mm:ss"
* (like "2004-02-25 12:23:45").
*
* @param timeStr Input string
* @return Calendar object
*/
public static Calendar getCalendar(String timeStr) {
// try to parse it as long
try {
long timestamp = Long.parseLong(timeStr);
return Util.getCalendar(timestamp);
}
catch (NumberFormatException e) {
}
// not a long timestamp, try to parse it as data
SimpleDateFormat df = new SimpleDateFormat(ISO_DATE_FORMAT);
df.setLenient(false);
try {
Date date = df.parse(timeStr);
return Util.getCalendar(date);
}
catch (ParseException e) {
throw new IllegalArgumentException("Time/date not in " + ISO_DATE_FORMAT +
" format: " + timeStr);
}
}
/**
* Various DOM utility functions.
*/
public static class Xml {
private static class SingletonHelper {
private static final DocumentBuilderFactory factory;
static {
factory = DocumentBuilderFactory.newInstance();
try {
factory.setIgnoringComments(true);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setValidating(false);
factory.setNamespaceAware(false);
} catch (ParserConfigurationException e) {
throw new UnsupportedOperationException("Missing DOM feature: " + e.getMessage(), e);
}
}
}
private static final ErrorHandler eh = new ErrorHandler() {
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
public void warning(SAXParseException exception) throws SAXException {
throw exception;
}
};
private Xml() {
}
public static Node[] getChildNodes(Node parentNode) {
return getChildNodes(parentNode, null);
}
public static Node[] getChildNodes(Node parentNode, String childName) {
ArrayList<Node> nodes = new ArrayList<>();
NodeList nodeList = parentNode.getChildNodes();
for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE && (childName == null || node.getNodeName().equals(childName))) {
nodes.add(node);
}
}
return nodes.toArray(new Node[0]);
}
public static Node getFirstChildNode(Node parentNode, String childName) {
Node[] childs = getChildNodes(parentNode, childName);
if (childs.length > 0) {
return childs[0];
}
throw new IllegalArgumentException("XML Error, no such child: " + childName);
}
public static boolean hasChildNode(Node parentNode, String childName) {
Node[] childs = getChildNodes(parentNode, childName);
return childs.length > 0;
}
// -- Wrapper around getChildValue with trim
public static String getChildValue(Node parentNode, String childName) {
return getChildValue(parentNode, childName, true);
}
public static String getChildValue(Node parentNode, String childName, boolean trim) {
NodeList children = parentNode.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeName().equals(childName)) {
return getValue(child, trim);
}
}
throw new IllegalStateException("XML Error, no such child: " + childName);
}
// -- Wrapper around getValue with trim
public static String getValue(Node node) {
return getValue(node, true);
}
public static String getValue(Node node, boolean trimValue) {
String value = null;
Node child = node.getFirstChild();
if (child != null) {
value = child.getNodeValue();
if (value != null && trimValue) {
value = value.trim();
}
}
return value;
}
public static int getChildValueAsInt(Node parentNode, String childName) {
String valueStr = getChildValue(parentNode, childName);
return Integer.parseInt(valueStr);
}
public static int getValueAsInt(Node node) {
String valueStr = getValue(node);
return Integer.parseInt(valueStr);
}
public static long getChildValueAsLong(Node parentNode, String childName) {
String valueStr = getChildValue(parentNode, childName);
return Long.parseLong(valueStr);
}
public static long getValueAsLong(Node node) {
String valueStr = getValue(node);
return Long.parseLong(valueStr);
}
public static double getChildValueAsDouble(Node parentNode, String childName) {
String valueStr = getChildValue(parentNode, childName);
return Util.parseDouble(valueStr);
}
public static double getValueAsDouble(Node node) {
String valueStr = getValue(node);
return Util.parseDouble(valueStr);
}
public static boolean getChildValueAsBoolean(Node parentNode, String childName) {
String valueStr = getChildValue(parentNode, childName);
return Util.parseBoolean(valueStr);
}
public static boolean getValueAsBoolean(Node node) {
String valueStr = getValue(node);
return Util.parseBoolean(valueStr);
}
public static Element getRootElement(InputSource inputSource) throws IOException {
try {
DocumentBuilder builder = SingletonHelper.factory.newDocumentBuilder();
builder.setErrorHandler(eh);
Document doc = builder.parse(inputSource);
return doc.getDocumentElement();
}
catch (ParserConfigurationException | SAXException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
public static Element getRootElement(String xmlString) throws IOException {
return getRootElement(new InputSource(new StringReader(xmlString)));
}
public static Element getRootElement(File xmlFile) throws IOException {
try (Reader reader = new FileReader(xmlFile)) {
return getRootElement(new InputSource(reader));
}
}
}
private static long lastLap = System.currentTimeMillis();
/**
* Function used for debugging purposes and performance bottlenecks detection.
* Probably of no use for end users of Rrd4j.
*
* @return String representing time in seconds since last
* <code>getLapTime()</code> method call.
*/
public static String getLapTime() {
long newLap = System.currentTimeMillis();
double seconds = (newLap - lastLap) / 1000.0;
lastLap = newLap;
return "[" + seconds + " sec]";
}
/**
* <p>Returns the root directory of the Rrd4j distribution. Useful in some demo applications,
* probably of no use anywhere else.</p>
* <p>The function assumes that all Rrd4j .class files are placed under
* the &lt;root&gt;/classes subdirectory and that all jars (libraries) are placed in the
* &lt;root&gt;/lib subdirectory (the original Rrd4j directory structure).</p>
*
* @return absolute path to Rrd4j's home directory
*/
public static String getRrd4jHomeDirectory() {
String homedir = null;
try {
String className = Util.class.getName().replace('.', '/');
URI uri = Util.class.getResource("/" + className + ".class").toURI();
if ("file".equals(uri.getScheme())) {
homedir = Paths.get(uri).toString();
}
else if ("jar".equals(uri.getScheme())) {
// JarURLConnection doesn't open the JAR
JarURLConnection connection = (JarURLConnection) uri.toURL().openConnection();
homedir = connection.getJarFileURL().getFile();
}
} catch (URISyntaxException | IOException e) {
}
if (homedir != null) {
return Paths.get(homedir).toAbsolutePath().toString();
} else {
return null;
}
}
/**
* Compares two doubles but treats all NaNs as equal.
* In Java (by default) Double.NaN == Double.NaN always returns <code>false</code>
*
* @param x the first value
* @param y the second value
* @return <code>true</code> if x and y are both equal to Double.NaN, or if x == y. <code>false</code> otherwise
*/
public static boolean equal(double x, double y) {
return (Double.isNaN(x) && Double.isNaN(y)) || (x == y);
}
/**
* Returns canonical file path for the given file path
*
* @param path Absolute or relative file path
* @return Canonical file path
* @throws java.io.IOException Thrown if canonical file path could not be resolved
*/
public static String getCanonicalPath(String path) throws IOException {
return new File(path).getCanonicalPath();
}
/**
* Returns last modification time for the given file.
*
* @param file File object representing file on the disk
* @return Last modification time in seconds (without milliseconds)
*/
public static long getLastModified(String file) {
return (new File(file).lastModified() + 500L) / 1000L;
}
/**
* Checks if the file with the given file name exists
*
* @param filename File name
* @return <code>true</code> if file exists, <code>false</code> otherwise
*/
public static boolean fileExists(String filename) {
return new File(filename).exists();
}
/**
* Finds max value for an array of doubles (NaNs are ignored). If all values in the array
* are NaNs, NaN is returned.
*
* @param values Array of double values
* @return max value in the array (NaNs are ignored)
*/
public static double max(double[] values) {
double max = Double.NaN;
for (double value : values) {
max = Util.max(max, value);
}
return max;
}
/**
* Finds min value for an array of doubles (NaNs are ignored). If all values in the array
* are NaNs, NaN is returned.
*
* @param values Array of double values
* @return min value in the array (NaNs are ignored)
*/
public static double min(double[] values) {
double min = Double.NaN;
for (double value : values) {
min = Util.min(min, value);
}
return min;
}
/**
* Equivalent of the C-style sprintf function.
*
* @param format Format string
* @param args Arbitrary list of arguments
* @return Formatted string
* @param l a {@link java.util.Locale} object.
*/
public static String sprintf(Locale l, String format, Object... args) {
String fmt = SPRINTF_PATTERN.matcher(format).replaceAll("$1%$2$3");
return String.format(l, fmt, args);
}
}

View File

@ -0,0 +1,127 @@
package org.rrd4j.core;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.rrd4j.ConsolFun;
import org.rrd4j.DsType;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
class XmlReader extends DataImporter {
private Element root;
private Node[] dsNodes, arcNodes;
XmlReader(String xmlFilePath) throws IOException {
root = Util.Xml.getRootElement(new File(xmlFilePath));
dsNodes = Util.Xml.getChildNodes(root, "ds");
arcNodes = Util.Xml.getChildNodes(root, "rra");
}
public String getVersion() {
return Util.Xml.getChildValue(root, "version");
}
public long getLastUpdateTime() {
return Util.Xml.getChildValueAsLong(root, "lastupdate");
}
public long getStep() {
return Util.Xml.getChildValueAsLong(root, "step");
}
public int getDsCount() {
return dsNodes.length;
}
public int getArcCount() {
return arcNodes.length;
}
public String getDsName(int dsIndex) {
return Util.Xml.getChildValue(dsNodes[dsIndex], "name");
}
@Override
public DsType getDsType(int dsIndex) {
String dsTypeName = Util.Xml.getChildValue(dsNodes[dsIndex], "type");
return DsType.valueOf(dsTypeName.toUpperCase(Locale.ENGLISH));
}
public long getHeartbeat(int dsIndex) {
return Util.Xml.getChildValueAsLong(dsNodes[dsIndex], "minimal_heartbeat");
}
public double getMinValue(int dsIndex) {
return Util.Xml.getChildValueAsDouble(dsNodes[dsIndex], "min");
}
public double getMaxValue(int dsIndex) {
return Util.Xml.getChildValueAsDouble(dsNodes[dsIndex], "max");
}
public double getLastValue(int dsIndex) {
return Util.Xml.getChildValueAsDouble(dsNodes[dsIndex], "last_ds");
}
public double getAccumValue(int dsIndex) {
return Util.Xml.getChildValueAsDouble(dsNodes[dsIndex], "value");
}
public long getNanSeconds(int dsIndex) {
return Util.Xml.getChildValueAsLong(dsNodes[dsIndex], "unknown_sec");
}
public ConsolFun getConsolFun(int arcIndex) {
return ConsolFun.valueOf(Util.Xml.getChildValue(arcNodes[arcIndex], "cf"));
}
public double getXff(int arcIndex) {
Node arc = arcNodes[arcIndex];
Node[] params = Util.Xml.getChildNodes(arc, "params");
//RRD4J xml, xff is in the archive definition
if(params.length == 0) {
return Util.Xml.getChildValueAsDouble(arc, "xff");
}
//RRDTool xml, xff is in the archive definition
else {
return Util.Xml.getChildValueAsDouble(params[0], "xff");
}
}
public int getSteps(int arcIndex) {
return Util.Xml.getChildValueAsInt(arcNodes[arcIndex], "pdp_per_row");
}
public double getStateAccumValue(int arcIndex, int dsIndex) {
Node cdpNode = Util.Xml.getFirstChildNode(arcNodes[arcIndex], "cdp_prep");
Node[] dsNodes = Util.Xml.getChildNodes(cdpNode, "ds");
return Util.Xml.getChildValueAsDouble(dsNodes[dsIndex], "value");
}
public int getStateNanSteps(int arcIndex, int dsIndex) {
Node cdpNode = Util.Xml.getFirstChildNode(arcNodes[arcIndex], "cdp_prep");
Node[] dsNodes = Util.Xml.getChildNodes(cdpNode, "ds");
return Util.Xml.getChildValueAsInt(dsNodes[dsIndex], "unknown_datapoints");
}
public int getRows(int arcIndex) {
Node dbNode = Util.Xml.getFirstChildNode(arcNodes[arcIndex], "database");
Node[] rows = Util.Xml.getChildNodes(dbNode, "row");
return rows.length;
}
public double[] getValues(int arcIndex, int dsIndex) {
Node dbNode = Util.Xml.getFirstChildNode(arcNodes[arcIndex], "database");
Node[] rows = Util.Xml.getChildNodes(dbNode, "row");
double[] values = new double[rows.length];
for (int i = 0; i < rows.length; i++) {
Node[] vNodes = Util.Xml.getChildNodes(rows[i], "v");
Node vNode = vNodes[dsIndex];
values[i] = Util.parseDouble(vNode.getFirstChild().getNodeValue().trim());
}
return values;
}
}

View File

@ -0,0 +1,466 @@
package org.rrd4j.core;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Class used as a base class for various XML template related classes. Class provides
* methods for XML source parsing and XML tree traversing. XML source may have unlimited
* number of placeholders (variables) in the format <code>${variable_name}</code>.
* Methods are provided to specify variable values at runtime.
* Note that this class has limited functionality: XML source gets parsed, and variable
* values are collected. You have to extend this class to do something more useful.
*/
public abstract class XmlTemplate {
private static final String PATTERN_STRING = "\\$\\{(\\w+)\\}";
private static final Pattern PATTERN = Pattern.compile(PATTERN_STRING);
protected Element root;
private HashMap<String, Object> valueMap = new HashMap<>();
private HashSet<Node> validatedNodes = new HashSet<>();
/**
* <p>Constructor for XmlTemplate.</p>
*
* @param xmlSource a {@link org.xml.sax.InputSource} object.
* @throws java.io.IOException if any.
*/
protected XmlTemplate(InputSource xmlSource) throws IOException {
root = Util.Xml.getRootElement(xmlSource);
}
/**
* <p>Constructor for XmlTemplate.</p>
*
* @param xmlString a {@link java.lang.String} object.
* @throws java.io.IOException if any.
*/
protected XmlTemplate(String xmlString) throws IOException {
root = Util.Xml.getRootElement(xmlString);
}
/**
* <p>Constructor for XmlTemplate.</p>
*
* @param xmlFile a {@link java.io.File} object.
* @throws java.io.IOException if any.
*/
protected XmlTemplate(File xmlFile) throws IOException {
root = Util.Xml.getRootElement(xmlFile);
}
/**
* Removes all placeholder-value mappings.
*/
public void clearValues() {
valueMap.clear();
}
/**
* Sets value for a single XML template variable. Variable name should be specified
* without leading '${' and ending '}' placeholder markers. For example, for a placeholder
* <code>${start}</code>, specify <code>start</code> for the <code>name</code> parameter.
*
* @param name variable name
* @param value value to be set in the XML template
*/
public void setVariable(String name, String value) {
valueMap.put(name, value);
}
/**
* Sets value for a single XML template variable. Variable name should be specified
* without leading '${' and ending '}' placeholder markers. For example, for a placeholder
* <code>${start}</code>, specify <code>start</code> for the <code>name</code> parameter.
*
* @param name variable name
* @param value value to be set in the XML template
*/
public void setVariable(String name, int value) {
valueMap.put(name, Integer.valueOf(value));
}
/**
* Sets value for a single XML template variable. Variable name should be specified
* without leading '${' and ending '}' placeholder markers. For example, for a placeholder
* <code>${start}</code>, specify <code>start</code> for the <code>name</code> parameter.
*
* @param name variable name
* @param value value to be set in the XML template
*/
public void setVariable(String name, long value) {
valueMap.put(name, Long.valueOf(value));
}
/**
* Sets value for a single XML template variable. Variable name should be specified
* without leading '${' and ending '}' placeholder markers. For example, for a placeholder
* <code>${start}</code>, specify <code>start</code> for the <code>name</code> parameter.
*
* @param name variable name
* @param value value to be set in the XML template
*/
public void setVariable(String name, double value) {
valueMap.put(name, Double.valueOf(value));
}
/**
* Sets value for a single XML template variable. Variable name should be specified
* without leading '${' and ending '}' placeholder markers. For example, for a placeholder
* <code>${start}</code>, specify <code>start</code> for the <code>name</code> parameter.
*
* @param name variable name
* @param value value to be set in the XML template
*/
public void setVariable(String name, Color value) {
String r = byteToHex(value.getRed());
String g = byteToHex(value.getGreen());
String b = byteToHex(value.getBlue());
String a = byteToHex(value.getAlpha());
valueMap.put(name, "#" + r + g + b + a);
}
private String byteToHex(int i) {
StringBuilder s = new StringBuilder(Integer.toHexString(i));
while (s.length() < 2) {
s.insert(0, "0");
}
return s.toString();
}
/**
* Sets value for a single XML template variable. Variable name should be specified
* without leading '${' and ending '}' placeholder markers. For example, for a placeholder
* <code>${start}</code>, specify <code>start</code> for the <code>name</code> parameter.
*
* @param name variable name
* @param value value to be set in the XML template
*/
public void setVariable(String name, Date value) {
setVariable(name, Util.getTimestamp(value));
}
/**
* Sets value for a single XML template variable. Variable name should be specified
* without leading '${' and ending '}' placeholder markers. For example, for a placeholder
* <code>${start}</code>, specify <code>start</code> for the <code>name</code> parameter.
*
* @param name variable name
* @param value value to be set in the XML template
*/
public void setVariable(String name, Calendar value) {
setVariable(name, Util.getTimestamp(value));
}
/**
* Sets value for a single XML template variable. Variable name should be specified
* without leading '${' and ending '}' placeholder markers. For example, for a placeholder
* <code>${start}</code>, specify <code>start</code> for the <code>name</code> parameter.
*
* @param name variable name
* @param value value to be set in the XML template
*/
public void setVariable(String name, boolean value) {
valueMap.put(name, Boolean.toString(value));
}
/**
* Searches the XML template to see if there are variables in there that
* will need to be set.
*
* @return True if variables were detected, false if not.
*/
public boolean hasVariables() {
return PATTERN.matcher(root.toString()).find();
}
/**
* Returns the list of variables that should be set in this template.
*
* @return List of variable names as an array of strings.
*/
public String[] getVariables() {
ArrayList<String> list = new ArrayList<>();
Matcher m = PATTERN.matcher(root.toString());
while (m.find()) {
String var = m.group(1);
if (!list.contains(var)) {
list.add(var);
}
}
return list.toArray(new String[list.size()]);
}
/**
* <p>getChildNodes.</p>
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @param childName a {@link java.lang.String} object.
* @return an array of {@link org.w3c.dom.Node} objects.
*/
protected static Node[] getChildNodes(Node parentNode, String childName) {
return Util.Xml.getChildNodes(parentNode, childName);
}
/**
* <p>getChildNodes.</p>
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @return an array of {@link org.w3c.dom.Node} objects.
*/
protected static Node[] getChildNodes(Node parentNode) {
return Util.Xml.getChildNodes(parentNode, null);
}
/**
* <p>getFirstChildNode.</p>
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @param childName a {@link java.lang.String} object.
* @return a {@link org.w3c.dom.Node} object.
*/
protected static Node getFirstChildNode(Node parentNode, String childName) {
return Util.Xml.getFirstChildNode(parentNode, childName);
}
/**
* <p>hasChildNode.</p>
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @param childName a {@link java.lang.String} object.
* @return a boolean.
*/
protected boolean hasChildNode(Node parentNode, String childName) {
return Util.Xml.hasChildNode(parentNode, childName);
}
/**
* <p>getChildValue.</p>
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @param childName a {@link java.lang.String} object.
* @return a {@link java.lang.String} object.
*/
protected String getChildValue(Node parentNode, String childName) {
return getChildValue(parentNode, childName, true);
}
/**
* <p>getChildValue.</p>
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @param childName a {@link java.lang.String} object.
* @param trim a boolean.
* @return a {@link java.lang.String} object.
*/
protected String getChildValue(Node parentNode, String childName, boolean trim) {
String value = Util.Xml.getChildValue(parentNode, childName, trim);
return resolveMappings(value);
}
/**
* <p>getValue.</p>
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @return a {@link java.lang.String} object.
*/
protected String getValue(Node parentNode) {
return getValue(parentNode, true);
}
/**
* <p>getValue.</p>
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @param trim a boolean.
* @return a {@link java.lang.String} object.
*/
protected String getValue(Node parentNode, boolean trim) {
String value = Util.Xml.getValue(parentNode, trim);
return resolveMappings(value);
}
private String resolveMappings(String templateValue) {
if (templateValue == null) {
return null;
}
Matcher matcher = PATTERN.matcher(templateValue);
StringBuilder result = new StringBuilder();
int lastMatchEnd = 0;
while (matcher.find()) {
String var = matcher.group(1);
if (valueMap.containsKey(var)) {
// mapping found
result.append(templateValue.substring(lastMatchEnd, matcher.start()));
result.append(valueMap.get(var).toString());
lastMatchEnd = matcher.end();
}
else {
// no mapping found - this is illegal
// throw runtime exception
throw new IllegalArgumentException("No mapping found for template variable ${" + var + "}");
}
}
result.append(templateValue.substring(lastMatchEnd));
return result.toString();
}
/**
* getChildValueAsInt.
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @param childName a {@link java.lang.String} object.
* @return a int.
*/
protected int getChildValueAsInt(Node parentNode, String childName) {
String valueStr = getChildValue(parentNode, childName);
return Integer.parseInt(valueStr);
}
/**
* getValueAsInt.
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @return a int.
*/
protected int getValueAsInt(Node parentNode) {
String valueStr = getValue(parentNode);
return Integer.parseInt(valueStr);
}
/**
* getChildValueAsLong.
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @param childName a {@link java.lang.String} object.
* @return a long.
*/
protected long getChildValueAsLong(Node parentNode, String childName) {
String valueStr = getChildValue(parentNode, childName);
return Long.parseLong(valueStr);
}
/**
* getValueAsLong.
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @return a long.
*/
protected long getValueAsLong(Node parentNode) {
String valueStr = getValue(parentNode);
return Long.parseLong(valueStr);
}
/**
* getChildValueAsDouble.
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @param childName a {@link java.lang.String} object.
* @return a double.
*/
protected double getChildValueAsDouble(Node parentNode, String childName) {
String valueStr = getChildValue(parentNode, childName);
return Util.parseDouble(valueStr);
}
/**
* getValueAsDouble.
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @return a double.
*/
protected double getValueAsDouble(Node parentNode) {
String valueStr = getValue(parentNode);
return Util.parseDouble(valueStr);
}
/**
* getChildValueAsBoolean.
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @param childName a {@link java.lang.String} object.
* @return a boolean.
*/
protected boolean getChildValueAsBoolean(Node parentNode, String childName) {
String valueStr = getChildValue(parentNode, childName);
return Util.parseBoolean(valueStr);
}
/**
* getValueAsBoolean.
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @return a boolean.
*/
protected boolean getValueAsBoolean(Node parentNode) {
String valueStr = getValue(parentNode);
return Util.parseBoolean(valueStr);
}
/**
* getValueAsColor.
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @return a {@link java.awt.Paint} object.
*/
protected Paint getValueAsColor(Node parentNode) {
String rgbStr = getValue(parentNode);
return Util.parseColor(rgbStr);
}
/**
* isEmptyNode.
*
* @param node a {@link org.w3c.dom.Node} object.
* @return a boolean.
*/
protected boolean isEmptyNode(Node node) {
// comment node or empty text node
return node.getNodeName().equals("#comment") ||
(node.getNodeName().equals("#text") && node.getNodeValue().trim().length() == 0);
}
/**
* validateTagsOnlyOnce.
*
* @param parentNode a {@link org.w3c.dom.Node} object.
* @param allowedChildNames an array of {@link java.lang.String} objects.
*/
protected void validateTagsOnlyOnce(Node parentNode, String[] allowedChildNames) {
// validate node only once
if (validatedNodes.contains(parentNode)) {
return;
}
Node[] childs = getChildNodes(parentNode);
main:
for (Node child : childs) {
String childName = child.getNodeName();
for (int j = 0; j < allowedChildNames.length; j++) {
if (allowedChildNames[j].equals(childName)) {
// only one such tag is allowed
allowedChildNames[j] = "<--removed-->";
continue main;
}
else if (allowedChildNames[j].equals(childName + "*")) {
// several tags allowed
continue main;
}
}
if (!isEmptyNode(child)) {
throw new IllegalArgumentException("Unexpected tag encountered: <" + childName + ">");
}
}
// everything is OK
validatedNodes.add(parentNode);
}
}

View File

@ -0,0 +1,210 @@
package org.rrd4j.core;
import java.awt.Color;
import java.awt.Font;
import java.io.File;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Locale;
import java.util.TimeZone;
/**
* Extremely simple utility class used to create XML documents.
*/
public class XmlWriter {
static final String INDENT_STR = " ";
private static final String STYLE = "style";
private static final ThreadLocal<SimpleDateFormat> ISOLIKE = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ", Locale.ENGLISH);
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
return sdf;
}
};
private final PrintWriter writer;
private final StringBuilder indent = new StringBuilder("");
private final Deque<String> openTags = new LinkedList<>();
/**
* Creates XmlWriter with the specified output stream to send XML code to.
*
* @param stream Output stream which receives XML code
*/
public XmlWriter(OutputStream stream) {
writer = new PrintWriter(stream, true);
}
/**
* Creates XmlWriter with the specified output stream to send XML code to.
*
* @param stream Output stream which receives XML code
* @param autoFlush is the stream to be flushed automatically
*/
public XmlWriter(OutputStream stream, boolean autoFlush) {
writer = new PrintWriter(stream, autoFlush);
}
/**
* Opens XML tag
*
* @param tag XML tag name
*/
public void startTag(String tag) {
writer.println(indent + "<" + tag + ">");
openTags.push(tag);
indent.append(INDENT_STR);
}
/**
* Closes the corresponding XML tag
*/
public void closeTag() {
String tag = openTags.pop();
indent.setLength(indent.length() - INDENT_STR.length());
writer.println(indent + "</" + tag + ">");
}
/**
* Writes &lt;tag&gt;value&lt;/tag&gt; to output stream
*
* @param tag XML tag name
* @param value value to be placed between <code>&lt;tag&gt;</code> and <code>&lt;/tag&gt;</code>
*/
public void writeTag(String tag, Object value) {
if (value != null) {
writer.println(indent + "<" + tag + ">" +
escape(value.toString()) + "</" + tag + ">");
}
else {
writer.println(indent + "<" + tag + "></" + tag + ">");
}
}
/**
* Writes &lt;tag&gt;value&lt;/tag&gt; to output stream
*
* @param tag XML tag name
* @param value value to be placed between <code>&lt;tag&gt;</code> and <code>&lt;/tag&gt;</code>
*/
public void writeTag(String tag, int value) {
writeTag(tag, Integer.toString(value));
}
/**
* Writes &lt;tag&gt;value&lt;/tag&gt; to output stream
*
* @param tag XML tag name
* @param value value to be placed between <code>&lt;tag&gt;</code> and <code>&lt;/tag&gt;</code>
*/
public void writeTag(String tag, long value) {
writeTag(tag, Long.toString(value));
}
/**
* Writes &lt;tag&gt;value&lt;/tag&gt; to output stream
*
* @param tag XML tag name
* @param value value to be placed between <code>&lt;tag&gt;</code> and <code>&lt;/tag&gt;</code>
* @param nanString a {@link java.lang.String} object.
*/
public void writeTag(String tag, double value, String nanString) {
writeTag(tag, Util.formatDouble(value, nanString, true));
}
/**
* Writes &lt;tag&gt;value&lt;/tag&gt; to output stream
*
* @param tag XML tag name
* @param value value to be placed between <code>&lt;tag&gt;</code> and <code>&lt;/tag&gt;</code>
*/
public void writeTag(String tag, double value) {
writeTag(tag, Util.formatDouble(value, true));
}
/**
* Writes &lt;tag&gt;value&lt;/tag&gt; to output stream
*
* @param tag XML tag name
* @param value value to be placed between <code>&lt;tag&gt;</code> and <code>&lt;/tag&gt;</code>
*/
public void writeTag(String tag, boolean value) {
writeTag(tag, Boolean.toString(value));
}
/**
* Writes &lt;tag&gt;value&lt;/tag&gt; to output stream
*
* @param tag XML tag name
* @param value value to be placed between <code>&lt;tag&gt;</code> and <code>&lt;/tag&gt;</code>
*/
public void writeTag(String tag, Color value) {
int rgb = value.getRGB() & 0xFFFFFF;
writeTag(tag, "#" + Integer.toHexString(rgb).toUpperCase());
}
/**
* Writes &lt;tag&gt;value&lt;/tag&gt; to output stream
*
* @param tag XML tag name
* @param value value to be placed between <code>&lt;tag&gt;</code> and <code>&lt;/tag&gt;</code>
*/
public void writeTag(String tag, Font value) {
startTag(tag);
writeTag("name", value.getName());
int style = value.getStyle();
if ((style & Font.BOLD) != 0 && (style & Font.ITALIC) != 0) {
writeTag(STYLE, "BOLDITALIC");
}
else if ((style & Font.BOLD) != 0) {
writeTag(STYLE, "BOLD");
}
else if ((style & Font.ITALIC) != 0) {
writeTag(STYLE, "ITALIC");
}
else {
writeTag(STYLE, "PLAIN");
}
writeTag("size", value.getSize());
closeTag();
}
/**
* Writes &lt;tag&gt;value&lt;/tag&gt; to output stream
*
* @param tag XML tag name
* @param value value to be placed between <code>&lt;tag&gt;</code> and <code>&lt;/tag&gt;</code>
*/
public void writeTag(String tag, File value) {
writeTag(tag, value.getPath());
}
/**
* Flushes the output stream
*/
public void flush() {
writer.flush();
}
/**
* Writes XML comment to output stream
*
* @param comment comment string
*/
public void writeComment(Object comment) {
if (comment instanceof Date) {
comment = ISOLIKE.get().format((Date) comment);
}
writer.println(indent + "<!-- " + escape(comment.toString()) + " -->");
}
private static String escape(String s) {
return s.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
}
}

View File

@ -0,0 +1,380 @@
package org.rrd4j.core.jrrd;
import java.io.IOException;
import java.io.PrintStream;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
/**
* Instances of this class model an archive section of an RRD file.
*
* @author <a href="mailto:ciaran@codeloop.com">Ciaran Treanor</a>
* @version $Revision: 1.1 $
*/
public class Archive {
private static enum rra_par_en {RRA_cdp_xff_val, RRA_hw_alpha};
final RRDatabase db;
/** Header offset within file in bytes */
final long headerOffset;
/** Header size in bytes */
private final long headerSize;
/** Data offset within file in bytes */
long dataOffset;
private final ConsolidationFunctionType type;
/** Data row count */
final int rowCount;
final int pdpCount;
final double xff;
/// Following fields are initialized during RRDatabase construction
/// and in fact immutable
/** Consolitation data points */
List<CDPStatusBlock> cdpStatusBlocks;
/** Row for last modification time of database */
int currentRow;
/** Cached content */
private double[][] values;
Archive(RRDatabase db) throws IOException {
this.db = db;
RRDFile file = db.rrdFile;
headerOffset = file.getFilePointer();
type = ConsolidationFunctionType.valueOf(file.readString(Constants.CF_NAM_SIZE).toUpperCase());
file.align(file.getBits() / 8);
rowCount = file.readLong();
pdpCount = file.readLong();
UnivalArray par = file.getUnivalArray(10);
xff = par.getDouble(rra_par_en.RRA_cdp_xff_val);
headerSize = file.getFilePointer() - headerOffset;
}
/**
* Returns the type of function used to calculate the consolidated data point.
*
* @return the type of function used to calculate the consolidated data point.
*/
public ConsolidationFunctionType getType() {
return type;
}
void loadCDPStatusBlocks(RRDFile file, int numBlocks) throws IOException {
cdpStatusBlocks = new ArrayList<CDPStatusBlock>();
for (int i = 0; i < numBlocks; i++) {
cdpStatusBlocks.add(new CDPStatusBlock(file));
}
}
/**
* Returns the <code>CDPStatusBlock</code> at the specified position in this archive.
*
* @param index index of <code>CDPStatusBlock</code> to return.
* @return the <code>CDPStatusBlock</code> at the specified position in this archive.
*/
public CDPStatusBlock getCDPStatusBlock(int index) {
return cdpStatusBlocks.get(index);
}
/**
* Returns an iterator over the CDP status blocks in this archive in proper sequence.
*
* @return an iterator over the CDP status blocks in this archive in proper sequence.
* @see CDPStatusBlock
*/
public Iterator<CDPStatusBlock> getCDPStatusBlocks() {
return cdpStatusBlocks.iterator();
}
void loadCurrentRow(RRDFile file) throws IOException {
currentRow = file.readLong();
}
void loadData(RRDFile file, int dsCount) throws IOException {
dataOffset = file.getFilePointer();
// Skip over the data to position ourselves at the start of the next archive
file.skipBytes(Constants.SIZE_OF_DOUBLE * rowCount * dsCount);
}
void loadData(DataChunk chunk)
throws IOException {
long rowIndexPointer;
if (chunk.startOffset < 0) {
rowIndexPointer = currentRow + 1L;
}
else {
rowIndexPointer = currentRow + chunk.startOffset + 1L;
}
if (rowIndexPointer < rowCount) {
db.rrdFile.seek((dataOffset + (chunk.dsCount * rowIndexPointer * Constants.SIZE_OF_DOUBLE)));
} else {
// Safety net: prevent from reading random portions of file
// if something went wrong
db.rrdFile.seekToEndOfFile();
}
double[][] data = chunk.data;
/*
* This is also terrible - cleanup - CT
*/
int row = 0;
for (int i = chunk.startOffset; i < rowCount - chunk.endOffset; i++, row++) {
if (i < 0) { // no valid data yet
Arrays.fill(data[row], Double.NaN);
}
else if (i >= rowCount) { // past valid data area
Arrays.fill(data[row], Double.NaN);
}
else { // inside the valid are but the pointer has to be wrapped
if (rowIndexPointer >= rowCount) {
rowIndexPointer -= rowCount;
db.rrdFile.seek(dataOffset + (chunk.dsCount * rowIndexPointer * Constants.SIZE_OF_DOUBLE));
}
for (int ii = 0; ii < chunk.dsCount; ii++) {
data[row][ii] = db.rrdFile.readDouble();
}
rowIndexPointer++;
}
}
}
void printInfo(PrintStream s, NumberFormat numberFormat, int index) {
StringBuilder sb = new StringBuilder("rra[");
sb.append(index);
s.print(sb);
s.print("].cf = \"");
s.print(type);
s.println("\"");
s.print(sb);
s.print("].rows = ");
s.println(rowCount);
s.print(sb);
s.print("].pdp_per_row = ");
s.println(pdpCount);
s.print(sb);
s.print("].xff = ");
s.println(xff);
sb.append("].cdp_prep[");
int cdpIndex = 0;
for (Iterator<CDPStatusBlock> i = cdpStatusBlocks.iterator(); i.hasNext();) {
CDPStatusBlock cdp = i.next();
s.print(sb);
s.print(cdpIndex);
s.print("].value = ");
double value = cdp.value;
s.println(Double.isNaN(value)
? "NaN"
: numberFormat.format(value));
s.print(sb);
s.print(cdpIndex++);
s.print("].unknown_datapoints = ");
s.println(cdp.unknownDatapoints);
}
}
void toXml(PrintStream s) {
try {
s.println("\t<rra>");
s.print("\t\t<cf> ");
s.print(type);
s.println(" </cf>");
s.print("\t\t<pdp_per_row> ");
s.print(pdpCount);
s.print(" </pdp_per_row> <!-- ");
s.print(db.header.pdpStep * pdpCount);
s.println(" seconds -->");
s.print("\t\t<xff> ");
s.print(xff);
s.println(" </xff>");
s.println();
s.println("\t\t<cdp_prep>");
for (int i = 0; i < cdpStatusBlocks.size(); i++) {
cdpStatusBlocks.get(i).toXml(s);
}
s.println("\t\t</cdp_prep>");
s.println("\t\t<database>");
long timer = -(rowCount - 1);
int counter = 0;
int row = currentRow;
db.rrdFile.seek(dataOffset + (row + 1) * db.header.dsCount * Constants.SIZE_OF_DOUBLE);
long lastUpdate = db.lastUpdate.getTime() / 1000;
int pdpStep = db.header.pdpStep;
NumberFormat numberFormat = new DecimalFormat("0.0000000000E0", DecimalFormatSymbols.getInstance(Locale.US));
SimpleDateFormat dateFormat =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
while (counter++ < rowCount) {
row++;
if (row == rowCount) {
row = 0;
db.rrdFile.seek(dataOffset);
}
long now = (lastUpdate - lastUpdate % (pdpCount * pdpStep))
+ (timer * pdpCount * pdpStep);
timer++;
s.print("\t\t\t<!-- ");
s.print(dateFormat.format(new Date(now * 1000)));
s.print(" / ");
s.print(now);
s.print(" --> ");
s.println("<row>");
for (int col = 0; col < db.header.dsCount; col++) {
s.print("<v> ");
double value = db.rrdFile.readDouble();
// NumberFormat doesn't know how to handle NaN
if (Double.isNaN(value)) {
s.print("NaN");
}
else {
s.print(numberFormat.format(value));
}
s.print(" </v>");
}
s.println("</row>");
}
s.println("\t\t</database>");
s.println("\t</rra>");
}
catch (IOException e) { // Is the best thing to do here?
throw new RuntimeException(e.getMessage());
}
}
/**
* <p>Getter for the field <code>values</code>.</p>
*
* @return an array of double.
* @throws java.io.IOException if any.
*/
public double[][] getValues() throws IOException {
if (values != null) {
return values;
}
values = new double[db.header.dsCount][rowCount];
int row = currentRow;
db.rrdFile.seek(dataOffset + (row + 1) * db.header.dsCount * Constants.SIZE_OF_DOUBLE);
for (int counter = 0; counter < rowCount; counter++) {
row++;
if (row == rowCount) {
row = 0;
db.rrdFile.seek(dataOffset);
}
for (int col = 0; col < db.header.dsCount; col++) {
double value = db.rrdFile.readDouble();
values[col][counter] = value;
}
}
return values;
}
/**
* Returns the number of primary data points required for a consolidated
* data point in this archive.
*
* @return the number of primary data points required for a consolidated
* data point in this archive.
*/
public int getPdpCount() {
return pdpCount;
}
/**
* Returns the number of entries in this archive.
*
* @return the number of entries in this archive.
*/
public int getRowCount() {
return rowCount;
}
/**
* Returns the X-Files Factor for this archive.
*
* @return the X-Files Factor for this archive.
*/
public double getXff() {
return xff;
}
/**
* Returns a summary the contents of this archive.
*
* @return a summary of the information contained in this archive.
*/
public String toString() {
StringBuilder sb = new StringBuilder("[Archive: OFFSET=0x");
sb.append(Long.toHexString(headerOffset))
.append(", SIZE=0x")
.append(Long.toHexString(headerSize))
.append(", type=")
.append(type)
.append(", rowCount=")
.append(rowCount)
.append(", pdpCount=")
.append(pdpCount)
.append(", xff=")
.append(xff)
.append(", currentRow=")
.append(currentRow)
.append("]");
for(CDPStatusBlock cdp: cdpStatusBlocks) {
sb.append("\n\t\t");
sb.append(cdp.toString());
}
return sb.toString();
}
}

View File

@ -0,0 +1,93 @@
package org.rrd4j.core.jrrd;
import java.io.IOException;
import java.io.PrintStream;
/**
* Instances of this class model the consolidation data point status from an RRD file.
*
* @author <a href="mailto:ciaran@codeloop.com">Ciaran Treanor</a>
* @version $Revision: 1.1 $
*/
public class CDPStatusBlock {
private static enum cdp_par_en {
CDP_val, CDP_unkn_pdp_cnt, CDP_hw_intercept, CDP_hw_last_intercept, CDP_hw_slope,
CDP_hw_last_slope, CDP_null_count,
CDP_last_null_count, CDP_primary_val, CDP_secondary_val
}
/** Byte offset within file */
final long offset;
/** Size of block in bytes */
final long size;
final int unknownDatapoints;
final double value;
final double secondary_value;
final double primary_value;
CDPStatusBlock(RRDFile file) throws IOException {
//Should read MAX_CDP_PAR_EN = 10
//Size should be 0x50
offset = file.getFilePointer();
UnivalArray scratch = file.getUnivalArray(10);
value = scratch.getDouble(cdp_par_en.CDP_val);
unknownDatapoints = (int) scratch.getDouble(cdp_par_en.CDP_unkn_pdp_cnt);
primary_value = scratch.getDouble(cdp_par_en.CDP_primary_val);
secondary_value = scratch.getDouble(cdp_par_en.CDP_secondary_val);
size = file.getFilePointer() - offset;
}
/**
* Returns the number of unknown primary data points that were integrated.
*
* @return the number of unknown primary data points that were integrated.
*/
public int getUnknownDatapoints() {
return unknownDatapoints;
}
/**
* Returns the value of this consolidated data point.
*
* @return the value of this consolidated data point.
*/
public double getValue() {
return value;
}
void toXml(PrintStream s) {
s.print("\t\t\t<ds><value> ");
s.print(value);
s.print(" </value> <unknown_datapoints> ");
s.print(unknownDatapoints);
s.println(" </unknown_datapoints></ds>");
}
/**
* Returns a summary the contents of this CDP status block.
*
* @return a summary of the information contained in the CDP status block.
*/
public String toString() {
StringBuilder sb = new StringBuilder("[CDPStatusBlock: OFFSET=0x");
sb.append(Long.toHexString(offset));
sb.append(", SIZE=0x");
sb.append(Long.toHexString(size));
sb.append(", unknownDatapoints=");
sb.append(unknownDatapoints);
sb.append(", value=");
sb.append(value);
sb.append(", primaryValue=");
sb.append(primary_value);
sb.append(", secondaryValue=");
sb.append(secondary_value);
sb.append("]");
return sb.toString();
}
}

View File

@ -0,0 +1,74 @@
package org.rrd4j.core.jrrd;
import org.rrd4j.ConsolFun;
/**
* Class ConsolidationFunctionType
*
* @author <a href="mailto:ciaran@codeloop.com">Ciaran Treanor</a>
* @version $Revision: 1.1 $
*/
public enum ConsolidationFunctionType {
AVERAGE {
@Override
public ConsolFun getConsolFun() {
return ConsolFun.AVERAGE;
}
},
MIN {
@Override
public ConsolFun getConsolFun() {
return ConsolFun.MIN;
}
},
MAX {
@Override
public ConsolFun getConsolFun() {
return ConsolFun.MAX;
}
},
LAST {
@Override
public ConsolFun getConsolFun() {
return ConsolFun.LAST;
}
},
HWPREDICT {
@Override
public ConsolFun getConsolFun() {
throw new UnsupportedOperationException("HWPREDICT not supported");
}
},
SEASONAL {
@Override
public ConsolFun getConsolFun() {
throw new UnsupportedOperationException("SEASONAL not supported");
}
},
DEVPREDICT {
@Override
public ConsolFun getConsolFun() {
throw new UnsupportedOperationException("DEVPREDICT not supported");
}
},
DEVSEASONAL {
@Override
public ConsolFun getConsolFun() {
throw new UnsupportedOperationException("DEVSEASONAL not supported");
}
},
FAILURES {
@Override
public ConsolFun getConsolFun() {
throw new UnsupportedOperationException("FAILURES not supported");
}
},
MHWPREDICT {
@Override
public ConsolFun getConsolFun() {
throw new UnsupportedOperationException("MHWPREDICT not supported");
}
};
public abstract ConsolFun getConsolFun();
}

View File

@ -0,0 +1,26 @@
package org.rrd4j.core.jrrd;
interface Constants {
/** Constant <code>DS_NAM_SIZE=20</code> */
final int DS_NAM_SIZE = 20;
/** Constant <code>DST_SIZE=20</code> */
final int DST_SIZE = 20;
/** Constant <code>CF_NAM_SIZE=20</code> */
final int CF_NAM_SIZE = 20;
/** Constant <code>LAST_DS_LEN=30</code> */
final int LAST_DS_LEN = 30;
/** Constant <code>COOKIE="RRD"</code> */
static final String COOKIE = "RRD";
/** Constant <code>MAX_SUPPORTED_VERSION=3</code> */
public static final int MAX_SUPPORTED_VERSION = 3;
/** Constant <code>UNDEFINED_VERSION="UNDEF"</code> */
public static final String UNDEFINED_VERSION = "UNDEF";
/** Constant <code>UNDEFINED_VERSION_AS_INT=-1</code> */
public static final int UNDEFINED_VERSION_AS_INT = -1;
/** Constant <code>VERSION_WITH_LAST_UPDATE_SEC=3</code> */
public static int VERSION_WITH_LAST_UPDATE_SEC = 3;
/** Constant <code>FLOAT_COOKIE=8.642135E130</code> */
static final double FLOAT_COOKIE = 8.642135E130;
/** Constant <code>SIZE_OF_DOUBLE=8</code> */
public static final int SIZE_OF_DOUBLE = 8;
}

View File

@ -0,0 +1,129 @@
package org.rrd4j.core.jrrd;
import java.util.Map;
import org.rrd4j.data.LinearInterpolator;
import org.rrd4j.data.Plottable;
/**
* Models a chunk of result data from an RRDatabase.
*
* @author <a href="mailto:ciaran@codeloop.com">Ciaran Treanor</a>
* @version $Revision: 1.1 $
*/
public class DataChunk {
private static final String NEWLINE = System.getProperty("line.separator");
/** Start time in seconds since epoch */
private final long startTime;
/** Row number offset relative to current row. Can be negative */
final int startOffset;
/** Row number offset relative to current row */
final int endOffset;
/** Step in seconds */
private final long step;
/** Number of datasources must be equal to number of datasources in file */
final int dsCount;
final double[][] data;
private final int rows;
/** Map datasource name to datasource index */
private final Map<String, Integer> nameindex;
DataChunk(Map<String, Integer> nameindex, long startTime, int startOffset, int endOffset, long step, int dsCount, int rows) {
this.nameindex = nameindex;
this.startTime = startTime;
this.startOffset = startOffset;
this.endOffset = endOffset;
this.step = step;
this.dsCount = dsCount;
this.rows = rows;
data = new double[rows][dsCount];
}
/**
* Returns a summary of the contents of this data chunk. The first column is
* the time (RRD format) and the following columns are the data source
* values.
*
* @return a summary of the contents of this data chunk.
*/
public String toString() {
StringBuilder sb = new StringBuilder();
long time = startTime;
for (int row = 0; row < rows; row++, time += step) {
sb.append(time);
sb.append(": ");
for (int ds = 0; ds < dsCount; ds++) {
sb.append(data[row][ds]);
sb.append(" ");
}
sb.append(NEWLINE);
}
return sb.toString();
}
public int getStart() {
return startOffset;
}
public int getEnd() {
return endOffset;
}
public long getStep() {
return step;
}
public int getDsCount() {
return dsCount;
}
/**
* <p>Getter for the field <code>data</code>.</p>
*
* @return the data
*/
public double[][] getData() {
return data;
}
/**
* <p>Getter for the time stamps values.</p>
*
* @return array of time stamps in seconds
*/
public long[] getTimestamps() {
long[] date = new long[rows];
long time = startTime;
for (int row = 0; row < rows; row++, time += step) {
date[row] = time;
}
return date;
}
/**
* Extract a datasource from the datachunck given is name as a Plottable
*
* @param name the datasource name
* @return a plottable for the datasource
*/
public Plottable toPlottable(String name) {
Integer dsId = nameindex.get(name);
if(dsId == null)
throw new RuntimeException("datasource not not found: " + name);
long[] date = new long[rows];
double[] results = new double[rows];
long time = startTime;
for (int row = 0; row < rows; row++, time += step) {
date[row] = time;
results[row] = data[row][dsId];
}
return new LinearInterpolator(date, results);
}
}

View File

@ -0,0 +1,198 @@
package org.rrd4j.core.jrrd;
import java.io.IOException;
import java.io.PrintStream;
import java.text.NumberFormat;
/**
* Instances of this class model a data source in an RRD file.
*
* @author <a href="mailto:ciaran@codeloop.com">Ciaran Treanor</a>
* @version $Revision: 1.1 $
*/
public class DataSource {
private static enum ds_param_en { DS_mrhb_cnt, DS_min_val, DS_max_val, DS_cde }
private final long offset;
private final long size;
private final String name;
private final DataSourceType type;
private final int minimumHeartbeat;
private final double minimum;
private final double maximum;
// initialized during RRDatabase construction
private PDPStatusBlock pdpStatusBlock;
DataSource(RRDFile file) throws IOException {
offset = file.getFilePointer();
name = file.readString(Constants.DS_NAM_SIZE);
type = DataSourceType.valueOf(file.readString(Constants.DST_SIZE).toUpperCase());
UnivalArray par = file.getUnivalArray(10);
minimumHeartbeat = (int) par.getLong(ds_param_en.DS_mrhb_cnt);
minimum = par.getDouble(ds_param_en.DS_min_val);
maximum = par.getDouble(ds_param_en.DS_max_val);
size = file.getFilePointer() - offset;
}
void loadPDPStatusBlock(RRDFile file) throws IOException {
pdpStatusBlock = new PDPStatusBlock(file);
}
/**
* Returns the primary data point status block for this data source.
*
* @return the primary data point status block for this data source.
*/
public PDPStatusBlock getPDPStatusBlock() {
return pdpStatusBlock;
}
/**
* Returns the minimum required heartbeat for this data source.
*
* @return the minimum required heartbeat for this data source.
*/
public int getMinimumHeartbeat() {
return minimumHeartbeat;
}
/**
* Returns the minimum value input to this data source can have.
*
* @return the minimum value input to this data source can have.
*/
public double getMinimum() {
return minimum;
}
/**
* Returns the type this data source is.
*
* @return the type this data source is.
* @see DataSourceType
*/
public DataSourceType getType() {
return type;
}
/**
* Returns the maximum value input to this data source can have.
*
* @return the maximum value input to this data source can have.
*/
public double getMaximum() {
return maximum;
}
/**
* Returns the name of this data source.
*
* @return the name of this data source.
*/
public String getName() {
return name;
}
void printInfo(PrintStream s, NumberFormat numberFormat) {
StringBuilder sb = new StringBuilder("ds[");
sb.append(name);
s.print(sb);
s.print("].type = \"");
s.print(type);
s.println("\"");
s.print(sb);
s.print("].minimal_heartbeat = ");
s.println(minimumHeartbeat);
s.print(sb);
s.print("].min = ");
s.println(Double.isNaN(minimum)
? "NaN"
: numberFormat.format(minimum));
s.print(sb);
s.print("].max = ");
s.println(Double.isNaN(maximum)
? "NaN"
: numberFormat.format(maximum));
s.print(sb);
s.print("].last_ds = ");
s.println(pdpStatusBlock.lastReading);
s.print(sb);
s.print("].value = ");
double value = pdpStatusBlock.value;
s.println(Double.isNaN(value)
? "NaN"
: numberFormat.format(value));
s.print(sb);
s.print("].unknown_sec = ");
s.println(pdpStatusBlock.unknownSeconds);
}
void toXml(PrintStream s) {
s.println("\t<ds>");
s.print("\t\t<name> ");
s.print(name);
s.println(" </name>");
s.print("\t\t<type> ");
s.print(type);
s.println(" </type>");
s.print("\t\t<minimal_heartbeat> ");
s.print(minimumHeartbeat);
s.println(" </minimal_heartbeat>");
s.print("\t\t<min> ");
s.print(minimum);
s.println(" </min>");
s.print("\t\t<max> ");
s.print(maximum);
s.println(" </max>");
s.println();
s.println("\t\t<!-- PDP Status -->");
s.print("\t\t<last_ds> ");
s.print(pdpStatusBlock.lastReading);
s.println(" </last_ds>");
s.print("\t\t<value> ");
s.print(pdpStatusBlock.value);
s.println(" </value>");
s.print("\t\t<unknown_sec> ");
s.print(pdpStatusBlock.unknownSeconds);
s.println(" </unknown_sec>");
s.println("\t</ds>");
s.println();
}
/**
* Returns a summary the contents of this data source.
*
* @return a summary of the information contained in this data source.
*/
public String toString() {
StringBuilder sb = new StringBuilder("[DataSource: OFFSET=0x");
sb.append(Long.toHexString(offset));
sb.append(", SIZE=0x");
sb.append(Long.toHexString(size));
sb.append(", name=");
sb.append(name);
sb.append(", type=");
sb.append(type.toString());
sb.append(", minHeartbeat=");
sb.append(minimumHeartbeat);
sb.append(", min=");
sb.append(minimum);
sb.append(", max=");
sb.append(maximum);
sb.append("]");
sb.append("\n\t\t");
sb.append(pdpStatusBlock.toString());
return sb.toString();
}
}

View File

@ -0,0 +1,44 @@
package org.rrd4j.core.jrrd;
import org.rrd4j.DsType;
/**
* Class DataSourceType
*
* @author <a href="mailto:ciaran@codeloop.com">Ciaran Treanor</a>
* @version $Revision: 1.1 $
*/
public enum DataSourceType {
COUNTER {
@Override
public DsType getDsType() {
return DsType.COUNTER;
}
},
ABSOLUTE {
@Override
public DsType getDsType() {
return DsType.ABSOLUTE;
}
},
GAUGE {
@Override
public DsType getDsType() {
return DsType.GAUGE;
}
},
DERIVE {
@Override
public DsType getDsType() {
return DsType.DERIVE;
}
},
CDEF {
@Override
public DsType getDsType() {
throw new UnsupportedOperationException("CDEF not supported");
}
};
public abstract DsType getDsType();
}

View File

@ -0,0 +1,127 @@
package org.rrd4j.core.jrrd;
import java.io.IOException;
import org.rrd4j.core.InvalidRrdException;
/**
* Instances of this class model the header section of an RRD file.
*
* @author <a href="mailto:ciaran@codeloop.com">Ciaran Treanor</a>
* @version $Revision: 1.1 $
*/
public class Header implements Constants {
private static final double FLOAT_COOKIE = 8.642135E130;
private static final long offset = 0;
private final long size;
final String version;
private final int iVersion;
/** Number of data sources */
final int dsCount;
/** Number of archives within file */
final int rraCount;
final int pdpStep;
Header(RRDFile file) throws IOException {
if (!file.readString(4).equals(COOKIE)) {
throw new InvalidRrdException("Invalid COOKIE");
}
version = file.readString(5);
try {
iVersion = Integer.parseInt(version);
} catch (NumberFormatException e) {
throw new RuntimeException("Unsupported RRD version (" + version + ")");
}
if (iVersion > MAX_SUPPORTED_VERSION) {
throw new RuntimeException("Unsupported RRD version (" + version + ")");
}
file.align();
// Consume the FLOAT_COOKIE
double cookie = file.readDouble();
if(cookie != FLOAT_COOKIE) {
throw new RuntimeException("This RRD was created on another architecture");
}
dsCount = file.readLong();
rraCount = file.readLong();
pdpStep = file.readLong();
// Skip rest of stat_head_t.par
@SuppressWarnings("unused")
UnivalArray par = file.getUnivalArray(10);
size = file.getFilePointer() - offset;
}
/**
* Returns the version of the database.
*
* @return the version of the database.
*/
public String getVersion() {
return version;
}
/**
* Returns the version of the database as an integer.
*
* @return the version of the database.
*/
public int getVersionAsInt() {
return iVersion;
}
/**
* Returns the number of <code>DataSource</code>s in the database.
*
* @return the number of <code>DataSource</code>s in the database.
*/
public int getDSCount() {
return dsCount;
}
/**
* Returns the number of <code>Archive</code>s in the database.
*
* @return the number of <code>Archive</code>s in the database.
*/
public int getRRACount() {
return rraCount;
}
/**
* Returns the primary data point interval in seconds.
*
* @return the primary data point interval in seconds.
*/
public int getPDPStep() {
return pdpStep;
}
/**
* Returns a summary the contents of this header.
*
* @return a summary of the information contained in this header.
*/
public String toString() {
StringBuilder sb = new StringBuilder("[Header: OFFSET=0x00, SIZE=0x");
sb.append(Long.toHexString(size));
sb.append(", version=");
sb.append(version);
sb.append(", dsCount=");
sb.append(dsCount);
sb.append(", rraCount=");
sb.append(rraCount);
sb.append(", pdpStep=");
sb.append(pdpStep);
sb.append("]");
return sb.toString();
}
}

View File

@ -0,0 +1,81 @@
package org.rrd4j.core.jrrd;
import java.io.IOException;
/**
* Instances of this class model the primary data point status from an RRD file.
*
* @author <a href="mailto:ciaran@codeloop.com">Ciaran Treanor</a>
* @version $Revision: 1.1 $
*/
public class PDPStatusBlock {
private long offset;
private long size;
String lastReading;
int unknownSeconds;
double value;
private static enum pdp_par_en {PDP_unkn_sec_cnt, PDP_val};
PDPStatusBlock(RRDFile file) throws IOException {
offset = file.getFilePointer();
lastReading = file.readString(Constants.LAST_DS_LEN);
UnivalArray scratch = file.getUnivalArray(10);
unknownSeconds = (int) scratch.getLong(pdp_par_en.PDP_unkn_sec_cnt);
value = scratch.getDouble(pdp_par_en.PDP_val);
size = file.getFilePointer() - offset;
}
/**
* Returns the last reading from the data source.
*
* @return the last reading from the data source.
*/
public String getLastReading() {
return lastReading;
}
/**
* Returns the current value of the primary data point.
*
* @return the current value of the primary data point.
*/
public double getValue() {
return value;
}
/**
* Returns the number of seconds of the current primary data point is
* unknown data.
*
* @return the number of seconds of the current primary data point is unknown data.
*/
public int getUnknownSeconds() {
return unknownSeconds;
}
/**
* Returns a summary the contents of this PDP status block.
*
* @return a summary of the information contained in this PDP status block.
*/
public String toString() {
StringBuilder sb = new StringBuilder("[PDPStatus: OFFSET=0x");
sb.append(Long.toHexString(offset));
sb.append(", SIZE=0x");
sb.append(Long.toHexString(size));
sb.append(", lastReading=");
sb.append(lastReading);
sb.append(", unknownSeconds=");
sb.append(unknownSeconds);
sb.append(", value=");
sb.append(value);
sb.append("]");
return sb.toString();
}
}

View File

@ -0,0 +1,218 @@
package org.rrd4j.core.jrrd;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import org.rrd4j.core.InvalidRrdException;
/**
* This class is used read information from an RRD file. Writing
* to RRD files is not currently supported. It uses NIO's RandomAccessFile to read the file
* <p/>
* Currently this can read RRD files that were generated on Solaris (Sparc)
* and Linux (x86).
*
* @author <a href="mailto:ciaran@codeloop.com">Ciaran Treanor</a>
* @version $Revision: 1.1 $
*/
class RRDFile implements Constants {
/** Constant <code>FLOAT_COOKIE_BIG_ENDIAN={0x5B, 0x1F, 0x2B, 0x43,
(byte) 0xC7, (byte) 0xC0, 0x25,
0x2F}</code> */
private static final byte[] FLOAT_COOKIE_BIG_ENDIAN = {0x5B, 0x1F, 0x2B, 0x43,
(byte) 0xC7, (byte) 0xC0, 0x25,
0x2F};
/** Constant <code>FLOAT_COOKIE_LITTLE_ENDIAN={0x2F, 0x25, (byte) 0xC0,
(byte) 0xC7, 0x43, 0x2B, 0x1F,
0x5B}</code> */
private static final byte[] FLOAT_COOKIE_LITTLE_ENDIAN = {0x2F, 0x25, (byte) 0xC0,
(byte) 0xC7, 0x43, 0x2B, 0x1F,
0x5B};
private int alignment;
private int longSize = 4;
private final FileInputStream underlying;
private final MappedByteBuffer mappedByteBuffer;
private ByteOrder order;
RRDFile(String name) throws IOException {
this(new File(name));
}
RRDFile(File file) throws IOException {
long len = file.length();
if (len > Integer.MAX_VALUE) {
throw new IllegalArgumentException(
"RRDFile cannot read files larger than 2**31 because of limitations of java.nio.ByteBuffer");
}
boolean ok = false;
try {
underlying = new FileInputStream(file);
mappedByteBuffer = underlying.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, len);
initDataLayout(file);
ok = true;
} finally {
if (!ok) {
try {
close();
} catch (Throwable ignored) {
}
// and then rethrow
}
}
}
private void initDataLayout(File file) throws IOException {
if (file.exists()) { // Load the data formats from the file
byte[] buffer = new byte[32];
mappedByteBuffer.get(buffer);
ByteBuffer bbuffer = ByteBuffer.wrap(buffer);
int index;
if ((index = indexOf(FLOAT_COOKIE_BIG_ENDIAN, buffer)) != -1) {
order = ByteOrder.BIG_ENDIAN;
}
else if ((index = indexOf(FLOAT_COOKIE_LITTLE_ENDIAN, buffer))
!= -1) {
order = ByteOrder.LITTLE_ENDIAN;
}
else {
throw new InvalidRrdException("Invalid RRD file");
}
mappedByteBuffer.order(order);
bbuffer.order(order);
switch (index) {
case 12:
alignment = 4;
break;
case 16:
alignment = 8;
break;
default:
throw new RuntimeException("Unsupported architecture");
}
bbuffer.position(index + 8);
//We cannot have dsCount && rracount == 0
//If one is 0, it's a 64 bits rrd
int int1 = bbuffer.getInt(); //Should be dsCount in ILP32
int int2 = bbuffer.getInt(); //Should be rraCount in ILP32
if(int1 == 0 || int2 ==0) {
longSize = 8;
}
}
else { // Default to data formats for this hardware architecture
}
// Reset file pointer to start of file
mappedByteBuffer.rewind();
}
private int indexOf(byte[] pattern, byte[] array) {
return (new String(array)).indexOf(new String(pattern));
}
boolean isBigEndian() {
return order == ByteOrder.BIG_ENDIAN;
}
int getAlignment() {
return alignment;
}
double readDouble() throws IOException {
return mappedByteBuffer.getDouble();
}
int readInt() throws IOException {
return mappedByteBuffer.getInt();
}
int readLong() throws IOException {
if(longSize == 4) {
return mappedByteBuffer.getInt();
}
else {
return (int) mappedByteBuffer.getLong();
}
}
String readString(int maxLength) throws IOException {
byte[] array = new byte[maxLength];
mappedByteBuffer.get(array);
return new String(array, 0, maxLength).trim();
}
void skipBytes(int n) throws IOException {
mappedByteBuffer.position(mappedByteBuffer.position() + n);
}
int align(int boundary) throws IOException {
int skip = (int) (boundary - (mappedByteBuffer.position() % boundary)) % boundary;
if (skip != 0) {
mappedByteBuffer.position(mappedByteBuffer.position() + skip);
}
return skip;
}
int align() throws IOException {
return align(alignment);
}
long info() throws IOException {
return mappedByteBuffer.position();
}
long getFilePointer() throws IOException {
return mappedByteBuffer.position();
}
void close() throws IOException {
if (underlying != null) {
underlying.close();
}
}
void read(ByteBuffer bb) throws IOException{
int count = bb.remaining();
bb.put((ByteBuffer) mappedByteBuffer.duplicate().limit(mappedByteBuffer.position() + count));
mappedByteBuffer.position(mappedByteBuffer.position() + count);
}
UnivalArray getUnivalArray(int size) throws IOException {
return new UnivalArray(this, size);
}
/**
* @return the long size in bits for this file
*/
int getBits() {
return longSize * 8;
}
public void seek(long position) {
mappedByteBuffer.position((int) position);
}
public void seekToEndOfFile() {
mappedByteBuffer.position(mappedByteBuffer.limit());
}
}

View File

@ -0,0 +1,529 @@
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;
import org.rrd4j.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: 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);
if (archive == null) {
throw new RrdException("No matching archive found");
}
// Tune the parameters
stepSeconds = (long) 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();
}
}

View File

@ -0,0 +1,60 @@
package org.rrd4j.core.jrrd;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* This class is used to read a unival from a file
* unival is a rrdtool type, defined in rrd_format.h
* @author Fabrice Bacchella <fbacchella@spamcop.net>
*
*/
class UnivalArray {
private final ByteBuffer buffer;
private final int sizeoflong;
/**
* Read an UnivalArray from a rrd native file at the current position
*
* @param file the RRdFile
* @param size the numer of elements in the array
* @throws java.io.IOException if any.
*/
public UnivalArray(RRDFile file, int size) throws IOException {
sizeoflong = file.getBits();
buffer = ByteBuffer.allocate(size * 8);
if(file.isBigEndian())
buffer.order(ByteOrder.BIG_ENDIAN);
else
buffer.order(ByteOrder.LITTLE_ENDIAN);
file.align();
file.read(buffer);
}
/**
* <p>getLong.</p>
*
* @param e a {@link java.lang.Enum} object.
* @return a long.
*/
public long getLong(Enum<?> e) {
buffer.position(8 * e.ordinal());
if(sizeoflong == 64)
return buffer.getLong();
else
return buffer.getInt();
}
/**
* <p>getDouble.</p>
*
* @param e a {@link java.lang.Enum} object.
* @return a double.
*/
public double getDouble(Enum<?> e) {
buffer.position(8 * e.ordinal());
return buffer.getDouble();
}
}

View File

@ -0,0 +1,31 @@
/**
* <p>This package provides read-only access to natives RRD file.</p>
*
* Currently this can read RRD files that were generated big or little endian machines, 32 or 64 bits word, and 4 or 8 bytes alignment.
* So it's know to work on a least
* <ul>
* <li> x86 Linux
* <li> x86_64 Linux
* <li> x86_64 Solaris
* <li> sparc v8 (32 bits) Solaris
* <li> sparc v9 (64 bits) Solaris
* </ul>
* <p>But it should work on other environments too.</p>
* <p>Typical usage:</p>
* <pre>
* RRDatabase db = new RRDatabase("native.rrd");
* RrdGraphDef() gd = RrdGraphDef();
* Calendar endCal = Calendar.getInstance();
* endCal.set(Calendar.MILLISECOND, 0);
* Calendar startCal = (Calendar) endCal.clone();
* startCal.add(Calendar.DATE, -1);
* DataChunk chunk = db.getData(ConsolidationFunctionType.AVERAGE, startCal.getTime(), endCal.getTime(), 1L);
* for(String name: db.getDataSourcesName()) {
* gd.datasource(name, chunk.toPlottable(name));
* }
* </pre>
*
* @author <a href="mailto:ciaran@codeloop.com">Ciaran Treanor</a>
* @version $Revision: 1.1 $
*/
package org.rrd4j.core.jrrd;

View File

@ -0,0 +1,4 @@
/**
* core RRD4J implementation.
*/
package org.rrd4j.core;

View File

@ -0,0 +1,179 @@
package org.rrd4j.core.timespec;
import org.rrd4j.core.Util;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* <p>Small swing-based utility to convert timestamps (seconds since epoch) to readable dates and vice versa.
* Supports at-style time specification (like "now-2d", "noon yesterday") and other human-readable
* data formats:</p>
* <ul>
* <li>MM/dd/yy HH:mm:ss
* <li>dd.MM.yy HH:mm:ss
* <li>dd.MM.yy HH:mm:ss
* <li>MM/dd/yy HH:mm
* <li>dd.MM.yy HH:mm
* <li>yy-MM-dd HH:mm
* <li>MM/dd/yy
* <li>dd.MM.yy
* <li>yy-MM-dd
* <li>HH:mm MM/dd/yy
* <li>HH:mm dd.MM.yy
* <li>HH:mm yy-MM-dd
* <li>HH:mm:ss MM/dd/yy
* <li>HH:mm:ss dd.MM.yy
* <li>HH:mm:ss yy-MM-dd
* </ul>
* The current timestamp is displayed in the title bar :)
*
*/
public class Epoch extends JFrame {
private static final String[] supportedFormats = {
"MM/dd/yy HH:mm:ss", "dd.MM.yy HH:mm:ss", "yy-MM-dd HH:mm:ss", "MM/dd/yy HH:mm",
"dd.MM.yy HH:mm", "yy-MM-dd HH:mm", "MM/dd/yy", "dd.MM.yy", "yy-MM-dd", "HH:mm MM/dd/yy",
"HH:mm dd.MM.yy", "HH:mm yy-MM-dd", "HH:mm:ss MM/dd/yy", "HH:mm:ss dd.MM.yy", "HH:mm:ss yy-MM-dd"
};
@SuppressWarnings("unchecked")
private static final ThreadLocal<SimpleDateFormat>[] parsers = new ThreadLocal[supportedFormats.length];
private static final String helpText;
private Timer timer = new Timer(1000, new ActionListener() {
public void actionPerformed(ActionEvent e) {
showTimestamp();
}
});
static {
for (int i = 0; i < parsers.length; i++) {
final String format = supportedFormats[i];
parsers[i] = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
SimpleDateFormat sdf = new SimpleDateFormat(format);
sdf.setLenient(true);
return sdf;
}
};
}
StringBuilder tooltipBuff = new StringBuilder("<html><b>Supported input formats:</b><br>");
for (String supportedFormat : supportedFormats) {
tooltipBuff.append(supportedFormat).append("<br>");
}
tooltipBuff.append("<b>AT-style time specification</b><br>");
tooltipBuff.append("timestamp<br><br>");
tooltipBuff.append("Copyright (c) 2013 The RRD4J Authors. Copyright (c) 2001-2005 Sasa Markovic and Ciaran Treanor. Copyright (c) 2013 The OpenNMS Group, Inc. Licensed under the Apache License, Version 2.0.</html>");
helpText = tooltipBuff.toString();
}
private JLabel topLabel = new JLabel("Enter timestamp or readable date:");
private JTextField inputField = new JTextField(25);
private JButton convertButton = new JButton("Convert");
private JButton helpButton = new JButton("Help");
private static final ThreadLocal<SimpleDateFormat> OUTPUT_DATE_FORMAT = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("MM/dd/yy HH:mm:ss EEE");
}
};
Epoch() {
super("Epoch");
constructUI();
timer.start();
}
private void constructUI() {
JPanel c = (JPanel) getContentPane();
c.setLayout(new BorderLayout(3, 3));
c.add(topLabel, BorderLayout.NORTH);
c.add(inputField, BorderLayout.WEST);
c.add(convertButton, BorderLayout.CENTER);
convertButton.setToolTipText(helpText);
convertButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
convert();
}
});
c.add(helpButton, BorderLayout.EAST);
helpButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(helpButton, helpText, "Epoch Help", JOptionPane.INFORMATION_MESSAGE);
}
});
inputField.requestFocus();
getRootPane().setDefaultButton(convertButton);
setResizable(false);
setDefaultCloseOperation(EXIT_ON_CLOSE);
pack();
centerOnScreen();
setVisible(true);
}
void centerOnScreen() {
Toolkit t = Toolkit.getDefaultToolkit();
Dimension screenSize = t.getScreenSize();
Dimension frameSize = getPreferredSize();
double x = (screenSize.getWidth() - frameSize.getWidth()) / 2;
double y = (screenSize.getHeight() - frameSize.getHeight()) / 2;
setLocation((int) x, (int) y);
}
private void convert() {
String time = inputField.getText().trim();
if (time.length() > 0) {
// try simple timestamp
try {
long timestamp = Long.parseLong(time);
Date date = new Date(timestamp * 1000L);
formatDate(date);
}
catch (NumberFormatException nfe) {
// failed, try as a date
try {
inputField.setText(Long.toString(parseDate(time)));
}
catch (Exception e) {
inputField.setText("Could not convert, sorry");
}
}
}
}
private void showTimestamp() {
long timestamp = Util.getTime();
setTitle(timestamp + " seconds since epoch");
}
void formatDate(Date date) {
inputField.setText(OUTPUT_DATE_FORMAT.get().format(date));
}
private long parseDate(String time) {
for (ThreadLocal<SimpleDateFormat> parser : parsers) {
try {
return Util.getTimestamp(parser.get().parse(time));
}
catch (ParseException e) {
}
}
return new TimeParser(time).parse().getTimestamp();
}
/**
* Main method which runs this utility.
*
* @param args Not used.
*/
public static void main(String[] args) {
new Epoch();
}
}

View File

@ -0,0 +1,404 @@
package org.rrd4j.core.timespec;
import org.rrd4j.core.Util;
/**
* Class which parses at-style time specification (described in detail on the rrdfetch man page),
* used in all RRDTool commands. This code is in most parts just a java port of Tobi's parsetime.c
* code.
*
*/
public class TimeParser {
private static final int PREVIOUS_OP = -1;
TimeToken token;
TimeScanner scanner;
TimeSpec spec;
int op = TimeToken.PLUS;
int prev_multiplier = -1;
/**
* Constructs TimeParser instance from the given input string.
*
* @param dateString at-style time specification (read rrdfetch man page
* for the complete explanation)
*/
public TimeParser(String dateString) {
scanner = new TimeScanner(dateString);
spec = new TimeSpec(dateString);
}
private void expectToken(int desired, String errorMessage) {
token = scanner.nextToken();
if (token.token_id != desired) {
throw new IllegalArgumentException(errorMessage);
}
}
private void plusMinus(int doop) {
if (doop >= 0) {
op = doop;
expectToken(TimeToken.NUMBER, "There should be number after " +
(op == TimeToken.PLUS ? '+' : '-'));
prev_multiplier = -1; /* reset months-minutes guessing mechanics */
}
int delta = Integer.parseInt(token.value);
token = scanner.nextToken();
if (token.token_id == TimeToken.MONTHS_MINUTES) {
/* hard job to guess what does that -5m means: -5mon or -5min? */
switch (prev_multiplier) {
case TimeToken.DAYS:
case TimeToken.WEEKS:
case TimeToken.MONTHS:
case TimeToken.YEARS:
token = scanner.resolveMonthsMinutes(TimeToken.MONTHS);
break;
case TimeToken.SECONDS:
case TimeToken.MINUTES:
case TimeToken.HOURS:
token = scanner.resolveMonthsMinutes(TimeToken.MINUTES);
break;
default:
if (delta < 6) {
token = scanner.resolveMonthsMinutes(TimeToken.MONTHS);
}
else {
token = scanner.resolveMonthsMinutes(TimeToken.MINUTES);
}
}
}
prev_multiplier = token.token_id;
delta *= (op == TimeToken.PLUS) ? +1 : -1;
switch (token.token_id) {
case TimeToken.YEARS:
spec.dyear += delta;
return;
case TimeToken.MONTHS:
spec.dmonth += delta;
return;
case TimeToken.WEEKS:
delta *= 7;
/* FALLTHRU */
case TimeToken.DAYS:
spec.dday += delta;
return;
case TimeToken.HOURS:
spec.dhour += delta;
return;
case TimeToken.MINUTES:
spec.dmin += delta;
return;
case TimeToken.SECONDS:
default: // default is 'seconds'
spec.dsec += delta;
}
}
/**
* Try and read a "timeofday" specification. This method will be called
* when we see a plain number at the start of a time, which means we could be
* reading a time, or a day. If it turns out to be a date, then this method restores
* the scanner state to what it was at entry, and returns without setting anything.
*/
private void timeOfDay() {
int hour, minute = 0;
/* save token status in case we must abort */
scanner.saveState();
/* first pick out the time of day - we assume a HH (COLON|DOT) MM time */
if (token.value.length() > 2) {
//Definitely not an hour specification; probably a date or something. Give up now
return;
}
hour = Integer.parseInt(token.value);
token = scanner.nextToken();
if (token.token_id == TimeToken.SLASH) {
/* guess we are looking at a date */
token = scanner.restoreState();
return;
}
if (token.token_id == TimeToken.COLON || token.token_id == TimeToken.DOT) {
expectToken(TimeToken.NUMBER, "Parsing HH:MM or HH.MM syntax, expecting MM as number, got none");
minute = Integer.parseInt(token.value);
if (minute > 59) {
throw new IllegalArgumentException("Parsing HH:MM or HH.MM syntax, got MM = " +
minute + " (>59!)");
}
token = scanner.nextToken();
if(token.token_id == TimeToken.DOT) {
//Oh look, another dot; must have actually been a date in DD.MM.YYYY format. Give up and return
token = scanner.restoreState();
return;
}
}
/* check if an AM or PM specifier was given */
if (token.token_id == TimeToken.AM || token.token_id == TimeToken.PM) {
if (hour > 12) {
throw new IllegalArgumentException("There cannot be more than 12 AM or PM hours");
}
if (token.token_id == TimeToken.PM) {
if (hour != 12) {
/* 12:xx PM is 12:xx, not 24:xx */
hour += 12;
}
}
else {
if (hour == 12) {
/* 12:xx AM is 00:xx, not 12:xx */
hour = 0;
}
}
token = scanner.nextToken();
}
else if (hour > 23) {
/* guess it was not a time then, probably a date ... */
token = scanner.restoreState();
return;
}
spec.hour = hour;
spec.min = minute;
spec.sec = 0;
if (spec.hour == 24) {
spec.hour = 0;
spec.day++;
}
}
private void assignDate(long mday, long mon, long year) {
if (year > 138) {
if (year > 1970) {
year -= 1900;
}
else {
throw new IllegalArgumentException("Invalid year " + year + " (should be either 00-99 or >1900)");
}
}
else if (year >= 0 && year < 38) {
year += 100; /* Allow year 2000-2037 to be specified as */
} /* 00-37 until the problem of 2038 year will */
/* arise for unices with 32-bit time_t */
if (year < 70) {
throw new IllegalArgumentException("Won't handle dates before epoch (01/01/1970), sorry");
}
spec.year = (int) year;
spec.month = (int) mon;
spec.day = (int) mday;
}
private void day() {
long mday = 0, wday, mon, year = spec.year;
switch (token.token_id) {
case TimeToken.YESTERDAY:
spec.day--;
/* FALLTRHU */
case TimeToken.TODAY: /* force ourselves to stay in today - no further processing */
token = scanner.nextToken();
break;
case TimeToken.TOMORROW:
spec.day++;
token = scanner.nextToken();
break;
case TimeToken.JAN:
case TimeToken.FEB:
case TimeToken.MAR:
case TimeToken.APR:
case TimeToken.MAY:
case TimeToken.JUN:
case TimeToken.JUL:
case TimeToken.AUG:
case TimeToken.SEP:
case TimeToken.OCT:
case TimeToken.NOV:
case TimeToken.DEC:
/* do month mday [year] */
mon = (token.token_id - TimeToken.JAN);
expectToken(TimeToken.NUMBER, "the day of the month should follow month name");
mday = Long.parseLong(token.value);
token = scanner.nextToken();
if (token.token_id == TimeToken.NUMBER) {
year = Long.parseLong(token.value);
token = scanner.nextToken();
}
else {
year = spec.year;
}
assignDate(mday, mon, year);
break;
case TimeToken.SUN:
case TimeToken.MON:
case TimeToken.TUE:
case TimeToken.WED:
case TimeToken.THU:
case TimeToken.FRI:
case TimeToken.SAT:
/* do a particular day of the week */
wday = (token.token_id - TimeToken.SUN);
spec.day += (wday - spec.wday);
token = scanner.nextToken();
break;
case TimeToken.NUMBER:
/* get numeric <sec since 1970>, MM/DD/[YY]YY, or DD.MM.[YY]YY */
mon = Long.parseLong(token.value);
if (mon > 10L * 365L * 24L * 60L * 60L) {
spec.localtime(mon);
token = scanner.nextToken();
break;
}
if (mon > 19700101 && mon < 24000101) { /*works between 1900 and 2400 */
year = mon / 10000;
mday = mon % 100;
mon = (mon / 100) % 100;
token = scanner.nextToken();
}
else {
token = scanner.nextToken();
if (mon <= 31 && (token.token_id == TimeToken.SLASH || token.token_id == TimeToken.DOT)) {
int sep = token.token_id;
expectToken(TimeToken.NUMBER, "there should be " +
(sep == TimeToken.DOT ? "month" : "day") +
" number after " +
(sep == TimeToken.DOT ? '.' : '/'));
mday = Long.parseLong(token.value);
token = scanner.nextToken();
if (token.token_id == sep) {
expectToken(TimeToken.NUMBER, "there should be year number after " +
(sep == TimeToken.DOT ? '.' : '/'));
year = Long.parseLong(token.value);
token = scanner.nextToken();
}
/* flip months and days for European timing */
if (sep == TimeToken.DOT) {
long x = mday;
mday = mon;
mon = x;
}
}
}
mon--;
if (mon < 0 || mon > 11) {
throw new IllegalArgumentException("Did you really mean month " + (mon + 1));
}
if (mday < 1 || mday > 31) {
throw new IllegalArgumentException("I'm afraid that " + mday + " is not a valid day of the month");
}
assignDate(mday, mon, year);
break;
}
}
/**
* Parses the input string specified in the constructor.
*
* @return Object representing parsed date/time.
*/
public TimeSpec parse() {
long now = Util.getTime();
int hr = 0;
/* this MUST be initialized to zero for midnight/noon/teatime */
/* establish the default time reference */
spec.localtime(now);
token = scanner.nextToken();
switch (token.token_id) {
case TimeToken.PLUS:
case TimeToken.MINUS:
break; /* jump to OFFSET-SPEC part */
case TimeToken.START:
spec.type = TimeSpec.TYPE_START;
/* FALLTHRU */
case TimeToken.END:
if (spec.type != TimeSpec.TYPE_START) {
spec.type = TimeSpec.TYPE_END;
}
spec.year = spec.month = spec.day = spec.hour = spec.min = spec.sec = 0;
/* FALLTHRU */
case TimeToken.NOW:
int time_reference = token.token_id;
token = scanner.nextToken();
if (token.token_id == TimeToken.PLUS || token.token_id == TimeToken.MINUS) {
break;
}
if (time_reference != TimeToken.NOW) {
throw new IllegalArgumentException("Words 'start' or 'end' MUST be followed by +|- offset");
}
else if (token.token_id != TimeToken.EOF) {
throw new IllegalArgumentException("If 'now' is followed by a token it must be +|- offset");
}
break;
/* Only absolute time specifications below */
case TimeToken.NUMBER:
timeOfDay();
/* fix month parsing */
case TimeToken.JAN:
case TimeToken.FEB:
case TimeToken.MAR:
case TimeToken.APR:
case TimeToken.MAY:
case TimeToken.JUN:
case TimeToken.JUL:
case TimeToken.AUG:
case TimeToken.SEP:
case TimeToken.OCT:
case TimeToken.NOV:
case TimeToken.DEC:
case TimeToken.TODAY:
case TimeToken.YESTERDAY:
case TimeToken.TOMORROW:
day();
if (token.token_id != TimeToken.NUMBER) {
break;
}
//Allows (but does not require) the time to be specified after the day. This extends the rrdfetch specification
timeOfDay();
break;
/* evil coding for TEATIME|NOON|MIDNIGHT - we've initialized
* hr to zero up above, then fall into this case in such a
* way so we add +12 +4 hours to it for teatime, +12 hours
* to it for noon, and nothing at all for midnight, then
* set our rettime to that hour before leaping into the
* month scanner
*/
case TimeToken.TEATIME:
hr += 4;
/* FALLTHRU */
case TimeToken.NOON:
hr += 12;
/* FALLTHRU */
case TimeToken.MIDNIGHT:
spec.hour = hr;
spec.min = 0;
spec.sec = 0;
token = scanner.nextToken();
day();
break;
default:
throw new IllegalArgumentException("Unparsable time: " + token.value);
}
/*
* the OFFSET-SPEC part
*
* (NOTE, the sc_tokid was prefetched for us by the previous code)
*/
if (token.token_id == TimeToken.PLUS || token.token_id == TimeToken.MINUS) {
scanner.setContext(false);
while (token.token_id == TimeToken.PLUS || token.token_id == TimeToken.MINUS ||
token.token_id == TimeToken.NUMBER) {
if (token.token_id == TimeToken.NUMBER) {
plusMinus(PREVIOUS_OP);
}
else {
plusMinus(token.token_id);
}
token = scanner.nextToken();
/* We will get EOF eventually but that's OK, since
token() will return us as many EOFs as needed */
}
}
/* now we should be at EOF */
if (token.token_id != TimeToken.EOF) {
throw new IllegalArgumentException("Unparsable trailing text: " + token.value);
}
return spec;
}
}

View File

@ -0,0 +1,191 @@
package org.rrd4j.core.timespec;
class TimeScanner {
private String dateString;
private int pos, pos_save;
private TimeToken token, token_save;
static final TimeToken[] WORDS = {
new TimeToken("midnight", TimeToken.MIDNIGHT), /* 00:00:00 of today or tomorrow */
new TimeToken("noon", TimeToken.NOON), /* 12:00:00 of today or tomorrow */
new TimeToken("teatime", TimeToken.TEATIME), /* 16:00:00 of today or tomorrow */
new TimeToken("am", TimeToken.AM), /* morning times for 0-12 clock */
new TimeToken("pm", TimeToken.PM), /* evening times for 0-12 clock */
new TimeToken("tomorrow", TimeToken.TOMORROW),
new TimeToken("yesterday", TimeToken.YESTERDAY),
new TimeToken("today", TimeToken.TODAY),
new TimeToken("now", TimeToken.NOW),
new TimeToken("n", TimeToken.NOW),
new TimeToken("start", TimeToken.START),
new TimeToken("s", TimeToken.START),
new TimeToken("end", TimeToken.END),
new TimeToken("e", TimeToken.END),
new TimeToken("jan", TimeToken.JAN),
new TimeToken("feb", TimeToken.FEB),
new TimeToken("mar", TimeToken.MAR),
new TimeToken("apr", TimeToken.APR),
new TimeToken("may", TimeToken.MAY),
new TimeToken("jun", TimeToken.JUN),
new TimeToken("jul", TimeToken.JUL),
new TimeToken("aug", TimeToken.AUG),
new TimeToken("sep", TimeToken.SEP),
new TimeToken("oct", TimeToken.OCT),
new TimeToken("nov", TimeToken.NOV),
new TimeToken("dec", TimeToken.DEC),
new TimeToken("january", TimeToken.JAN),
new TimeToken("february", TimeToken.FEB),
new TimeToken("march", TimeToken.MAR),
new TimeToken("april", TimeToken.APR),
new TimeToken("may", TimeToken.MAY),
new TimeToken("june", TimeToken.JUN),
new TimeToken("july", TimeToken.JUL),
new TimeToken("august", TimeToken.AUG),
new TimeToken("september", TimeToken.SEP),
new TimeToken("october", TimeToken.OCT),
new TimeToken("november", TimeToken.NOV),
new TimeToken("december", TimeToken.DEC),
new TimeToken("sunday", TimeToken.SUN),
new TimeToken("sun", TimeToken.SUN),
new TimeToken("monday", TimeToken.MON),
new TimeToken("mon", TimeToken.MON),
new TimeToken("tuesday", TimeToken.TUE),
new TimeToken("tue", TimeToken.TUE),
new TimeToken("wednesday", TimeToken.WED),
new TimeToken("wed", TimeToken.WED),
new TimeToken("thursday", TimeToken.THU),
new TimeToken("thu", TimeToken.THU),
new TimeToken("friday", TimeToken.FRI),
new TimeToken("fri", TimeToken.FRI),
new TimeToken("saturday", TimeToken.SAT),
new TimeToken("sat", TimeToken.SAT),
new TimeToken(null, 0) /*** SENTINEL ***/
};
static TimeToken[] MULTIPLIERS = {
new TimeToken("second", TimeToken.SECONDS), /* seconds multiplier */
new TimeToken("seconds", TimeToken.SECONDS), /* (pluralized) */
new TimeToken("sec", TimeToken.SECONDS), /* (generic) */
new TimeToken("s", TimeToken.SECONDS), /* (short generic) */
new TimeToken("minute", TimeToken.MINUTES), /* minutes multiplier */
new TimeToken("minutes", TimeToken.MINUTES), /* (pluralized) */
new TimeToken("min", TimeToken.MINUTES), /* (generic) */
new TimeToken("m", TimeToken.MONTHS_MINUTES), /* (short generic) */
new TimeToken("hour", TimeToken.HOURS), /* hours ... */
new TimeToken("hours", TimeToken.HOURS), /* (pluralized) */
new TimeToken("hr", TimeToken.HOURS), /* (generic) */
new TimeToken("h", TimeToken.HOURS), /* (short generic) */
new TimeToken("day", TimeToken.DAYS), /* days ... */
new TimeToken("days", TimeToken.DAYS), /* (pluralized) */
new TimeToken("d", TimeToken.DAYS), /* (short generic) */
new TimeToken("week", TimeToken.WEEKS), /* week ... */
new TimeToken("weeks", TimeToken.WEEKS), /* (pluralized) */
new TimeToken("wk", TimeToken.WEEKS), /* (generic) */
new TimeToken("w", TimeToken.WEEKS), /* (short generic) */
new TimeToken("month", TimeToken.MONTHS), /* week ... */
new TimeToken("months", TimeToken.MONTHS), /* (pluralized) */
new TimeToken("mon", TimeToken.MONTHS), /* (generic) */
new TimeToken("year", TimeToken.YEARS), /* year ... */
new TimeToken("years", TimeToken.YEARS), /* (pluralized) */
new TimeToken("yr", TimeToken.YEARS), /* (generic) */
new TimeToken("y", TimeToken.YEARS), /* (short generic) */
new TimeToken(null, 0) /*** SENTINEL ***/
};
TimeToken[] specials = WORDS;
/**
* <p>Constructor for TimeScanner.</p>
*
* @param dateString The date as {@link java.lang.String} to parse.
*/
public TimeScanner(String dateString) {
this.dateString = dateString;
}
void setContext(boolean parsingWords) {
specials = parsingWords ? WORDS : MULTIPLIERS;
}
TimeToken nextToken() {
StringBuilder buffer = new StringBuilder("");
while (pos < dateString.length()) {
char c = dateString.charAt(pos++);
if (Character.isWhitespace(c) || c == '_' || c == ',') {
continue;
}
buffer.append(c);
if (Character.isDigit(c)) {
// pick as many digits as possible
while (pos < dateString.length()) {
char next = dateString.charAt(pos);
if (Character.isDigit(next)) {
buffer.append(next);
pos++;
}
else {
break;
}
}
String value = buffer.toString();
return token = new TimeToken(value, TimeToken.NUMBER);
}
if (Character.isLetter(c)) {
// pick as many letters as possible
while (pos < dateString.length()) {
char next = dateString.charAt(pos);
if (Character.isLetter(next)) {
buffer.append(next);
pos++;
}
else {
break;
}
}
String value = buffer.toString();
return token = new TimeToken(value, parseToken(value));
}
switch (c) {
case ':':
return token = new TimeToken(":", TimeToken.COLON);
case '.':
return token = new TimeToken(".", TimeToken.DOT);
case '+':
return token = new TimeToken("+", TimeToken.PLUS);
case '-':
return token = new TimeToken("-", TimeToken.MINUS);
case '/':
return token = new TimeToken("/", TimeToken.SLASH);
default:
pos--;
return token = new TimeToken(null, TimeToken.EOF);
}
}
return token = new TimeToken(null, TimeToken.EOF);
}
TimeToken resolveMonthsMinutes(int newId) {
assert token.token_id == TimeToken.MONTHS_MINUTES;
assert newId == TimeToken.MONTHS || newId == TimeToken.MINUTES;
return token = new TimeToken(token.value, newId);
}
void saveState() {
token_save = token;
pos_save = pos;
}
TimeToken restoreState() {
pos = pos_save;
return token = token_save;
}
private int parseToken(String arg) {
for (int i = 0; specials[i].value != null; i++) {
if (specials[i].value.equalsIgnoreCase(arg)) {
return specials[i].token_id;
}
}
return TimeToken.ID;
}
}

View File

@ -0,0 +1,140 @@
package org.rrd4j.core.timespec;
import org.rrd4j.core.Util;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
/**
* Simple class to represent time obtained by parsing at-style date specification (described
* in detail on the rrdfetch man page. See javadoc for {@link org.rrd4j.core.timespec.TimeParser}
* for more information.
*
*/
public class TimeSpec {
static final int TYPE_ABSOLUTE = 0;
static final int TYPE_START = 1;
static final int TYPE_END = 2;
int type = TYPE_ABSOLUTE;
int year, month, day, hour, min, sec;
int wday;
int dyear, dmonth, dday, dhour, dmin, dsec;
String dateString;
TimeSpec context;
TimeSpec(String dateString) {
this.dateString = dateString;
}
void localtime(long timestamp) {
GregorianCalendar date = new GregorianCalendar();
date.setTime(new Date(timestamp * 1000L));
year = date.get(Calendar.YEAR) - 1900;
month = date.get(Calendar.MONTH);
day = date.get(Calendar.DAY_OF_MONTH);
hour = date.get(Calendar.HOUR_OF_DAY);
min = date.get(Calendar.MINUTE);
sec = date.get(Calendar.SECOND);
wday = date.get(Calendar.DAY_OF_WEEK) - Calendar.SUNDAY;
}
GregorianCalendar getTime() {
GregorianCalendar gc;
// absolute time, this is easy
if (type == TYPE_ABSOLUTE) {
gc = new GregorianCalendar(year + 1900, month, day, hour, min, sec);
}
// relative time, we need a context to evaluate it
else if (context != null && context.type == TYPE_ABSOLUTE) {
gc = context.getTime();
}
// how would I guess what time it was?
else {
throw new IllegalStateException("Relative times like '" +
dateString + "' require proper absolute context to be evaluated");
}
gc.add(Calendar.YEAR, dyear);
gc.add(Calendar.MONTH, dmonth);
gc.add(Calendar.DAY_OF_MONTH, dday);
gc.add(Calendar.HOUR_OF_DAY, dhour);
gc.add(Calendar.MINUTE, dmin);
gc.add(Calendar.SECOND, dsec);
return gc;
}
/**
* <p>Returns the corresponding timestamp (seconds since Epoch). Example:</p>
* <pre>
* TimeParser p = new TimeParser("now-1day");
* TimeSpec ts = p.parse();
* System.out.println("Timestamp was: " + ts.getTimestamp();
* </pre>
*
* @return Timestamp (in seconds, no milliseconds)
*/
public long getTimestamp() {
return Util.getTimestamp(getTime());
}
String dump() {
return (type == TYPE_ABSOLUTE ? "ABSTIME" : type == TYPE_START ? "START" : "END") +
": " + year + "/" + month + "/" + day +
"/" + hour + "/" + min + "/" + sec + " (" +
dyear + "/" + dmonth + "/" + dday +
"/" + dhour + "/" + dmin + "/" + dsec + ")";
}
/**
* <p>Use this static method to resolve relative time references and obtain the corresponding
* Calendar objects. Example:</p>
* <pre>
* TimeParser pStart = new TimeParser("now-1month"); // starting time
* TimeParser pEnd = new TimeParser("start+1week"); // ending time
* TimeSpec specStart = pStart.parse();
* TimeSpec specEnd = pEnd.parse();
* GregorianCalendar[] gc = TimeSpec.getTimes(specStart, specEnd);
* </pre>
*
* @param spec1 Starting time specification
* @param spec2 Ending time specification
* @return Two element array containing Calendar objects
*/
public static Calendar[] getTimes(TimeSpec spec1, TimeSpec spec2) {
if (spec1.type == TYPE_START || spec2.type == TYPE_END) {
throw new IllegalArgumentException("Recursive time specifications not allowed");
}
spec1.context = spec2;
spec2.context = spec1;
return new Calendar[]{
spec1.getTime(),
spec2.getTime()
};
}
/**
* <p>Use this static method to resolve relative time references and obtain the corresponding
* timestamps (seconds since epoch). Example:</p>
* <pre>
* TimeParser pStart = new TimeParser("now-1month"); // starting time
* TimeParser pEnd = new TimeParser("start+1week"); // ending time
* TimeSpec specStart = pStart.parse();
* TimeSpec specEnd = pEnd.parse();
* long[] ts = TimeSpec.getTimestamps(specStart, specEnd);
* </pre>
*
* @param spec1 Starting time specification
* @param spec2 Ending time specification
* @return array containing two timestamps (in seconds since epoch)
*/
public static long[] getTimestamps(TimeSpec spec1, TimeSpec spec2) {
Calendar[] gcs = getTimes(spec1, spec2);
return new long[] {
Util.getTimestamp(gcs[0]), Util.getTimestamp(gcs[1])
};
}
}

View File

@ -0,0 +1,121 @@
package org.rrd4j.core.timespec;
class TimeToken {
/** Constant <code>MIDNIGHT=1</code> */
public static final int MIDNIGHT = 1;
/** Constant <code>NOON=2</code> */
public static final int NOON = 2;
/** Constant <code>TEATIME=3</code> */
public static final int TEATIME = 3;
/** Constant <code>PM=4</code> */
public static final int PM = 4;
/** Constant <code>AM=5</code> */
public static final int AM = 5;
/** Constant <code>YESTERDAY=6</code> */
public static final int YESTERDAY = 6;
/** Constant <code>TODAY=7</code> */
public static final int TODAY = 7;
/** Constant <code>TOMORROW=8</code> */
public static final int TOMORROW = 8;
/** Constant <code>NOW=9</code> */
public static final int NOW = 9;
/** Constant <code>START=10</code> */
public static final int START = 10;
/** Constant <code>END=11</code> */
public static final int END = 11;
/** Constant <code>SECONDS=12</code> */
public static final int SECONDS = 12;
/** Constant <code>MINUTES=13</code> */
public static final int MINUTES = 13;
/** Constant <code>HOURS=14</code> */
public static final int HOURS = 14;
/** Constant <code>DAYS=15</code> */
public static final int DAYS = 15;
/** Constant <code>WEEKS=16</code> */
public static final int WEEKS = 16;
/** Constant <code>MONTHS=17</code> */
public static final int MONTHS = 17;
/** Constant <code>YEARS=18</code> */
public static final int YEARS = 18;
/** Constant <code>MONTHS_MINUTES=19</code> */
public static final int MONTHS_MINUTES = 19;
/** Constant <code>NUMBER=20</code> */
public static final int NUMBER = 20;
/** Constant <code>PLUS=21</code> */
public static final int PLUS = 21;
/** Constant <code>MINUS=22</code> */
public static final int MINUS = 22;
/** Constant <code>DOT=23</code> */
public static final int DOT = 23;
/** Constant <code>COLON=24</code> */
public static final int COLON = 24;
/** Constant <code>SLASH=25</code> */
public static final int SLASH = 25;
/** Constant <code>ID=26</code> */
public static final int ID = 26;
/** Constant <code>JUNK=27</code> */
public static final int JUNK = 27;
/** Constant <code>JAN=28</code> */
public static final int JAN = 28;
/** Constant <code>FEB=29</code> */
public static final int FEB = 29;
/** Constant <code>MAR=30</code> */
public static final int MAR = 30;
/** Constant <code>APR=31</code> */
public static final int APR = 31;
/** Constant <code>MAY=32</code> */
public static final int MAY = 32;
/** Constant <code>JUN=33</code> */
public static final int JUN = 33;
/** Constant <code>JUL=34</code> */
public static final int JUL = 34;
/** Constant <code>AUG=35</code> */
public static final int AUG = 35;
/** Constant <code>SEP=36</code> */
public static final int SEP = 36;
/** Constant <code>OCT=37</code> */
public static final int OCT = 37;
/** Constant <code>NOV=38</code> */
public static final int NOV = 38;
/** Constant <code>DEC=39</code> */
public static final int DEC = 39;
/** Constant <code>SUN=40</code> */
public static final int SUN = 40;
/** Constant <code>MON=41</code> */
public static final int MON = 41;
/** Constant <code>TUE=42</code> */
public static final int TUE = 42;
/** Constant <code>WED=43</code> */
public static final int WED = 43;
/** Constant <code>THU=44</code> */
public static final int THU = 44;
/** Constant <code>FRI=45</code> */
public static final int FRI = 45;
/** Constant <code>SAT=46</code> */
public static final int SAT = 46;
/** Constant <code>EOF=-1</code> */
public static final int EOF = -1;
final String value; /* token name */
final int token_id; /* token id */
/**
* <p>Constructor for TimeToken.</p>
*
* @param value a {@link java.lang.String} object.
* @param token_id a int.
*/
public TimeToken(String value, int token_id) {
this.value = value;
this.token_id = token_id;
}
/**
* <p>toString.</p>
*
* @return a {@link java.lang.String} object.
*/
public String toString() {
return value + " [" + token_id + "]";
}
}

View File

@ -1,5 +1,6 @@
2020-02-25 zzz
* Replace jrobin with rrd4j 3.5 (ticket #2684)
* Graphs: Replace jrobin with rrd4j 3.5 (ticket #2684)
* NetDB: Don't send 'fake hash' for exploration any more
* 2020-02-25 0.9.45 released

View File

@ -18,7 +18,7 @@ public class RouterVersion {
/** deprecated */
public final static String ID = "Monotone";
public final static String VERSION = CoreVersion.VERSION;
public final static long BUILD = 1;
public final static long BUILD = 2;
/** for example "-test" */
public final static String EXTRA = "";