diff --git a/apps/jrobin/java/src/com/tomgibara/crinch/hashing/Hash.java b/apps/jrobin/java/src/com/tomgibara/crinch/hashing/Hash.java new file mode 100644 index 0000000000..f389ae83df --- /dev/null +++ b/apps/jrobin/java/src/com/tomgibara/crinch/hashing/Hash.java @@ -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; + +/** + *

+ * Implementations of this interface can generate one hash value for a given + * object. Depending upon the implementation, null values may be supported. + *

+ * + * @author tomgibara + * + * @param + * the type of objects for which hashes may be generated + */ + +public interface Hash { + + 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; + +} diff --git a/apps/jrobin/java/src/com/tomgibara/crinch/hashing/HashRange.java b/apps/jrobin/java/src/com/tomgibara/crinch/hashing/HashRange.java new file mode 100644 index 0000000000..91ff84f839 --- /dev/null +++ b/apps/jrobin/java/src/com/tomgibara/crinch/hashing/HashRange.java @@ -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 + "]"; + } + +} diff --git a/apps/jrobin/java/src/com/tomgibara/crinch/hashing/PerfectStringHash.java b/apps/jrobin/java/src/com/tomgibara/crinch/hashing/PerfectStringHash.java new file mode 100644 index 0000000000..e622e5cc2f --- /dev/null +++ b/apps/jrobin/java/src/com/tomgibara/crinch/hashing/PerfectStringHash.java @@ -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; + +/** + *

+ * A "minimal perfect hash" for Strings. After construction with an array of + * n unique non-null strings, an instance of this class will return a + * unique hash value h (0 <= h < n) for any string s in the + * array. A negative has value will typically be returned for a string that is + * not in the array. + *

+ * + *

+ * However, the supplied array is not 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 + * n. + *

+ * + *

+ * IMPORTANT NOTE: The array of strings supplied to the + * constructor will be mutated: it is re-ordered so that + * hash(a[i]) == i. Application code must generally use this + * information to map hash values back onto the appropriate string value. + *

+ * + *

+ * NOTE: Good performance of this algorithm is predicated on + * string hash values being cached by the String class. Experience + * indicates that is is a good assumption. + *

+ * + * + * @author Tom Gibara + */ + +public class PerfectStringHash implements Hash { + + // 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 comparator = new Comparator() { + @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 hash(values[i]) == i. + */ + + 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]; + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/ArcDef.java b/apps/jrobin/java/src/org/rrd4j/core/ArcDef.java new file mode 100644 index 0000000000..1be7c07612 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/ArcDef.java @@ -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: + * + *
    + *
  • consolidation function + *
  • X-files factor + *
  • number of steps + *
  • number of rows. + *
+ * For the complete explanation of all archive definition parameters, see RRDTool's + * rrdcreate man page + * + * + * @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. + *

For the complete explanation of all archive definition parameters, see RRDTool's + * rrdcreate man page

+ * + * @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; + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/ArcState.java b/apps/jrobin/java/src/org/rrd4j/core/ArcState.java new file mode 100644 index 0000000000..7a184fc8ec --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/ArcState.java @@ -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 { + private Archive parentArc; + + private RrdDouble accumValue; + private RrdLong 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(); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/Archive.java b/apps/jrobin/java/src/org/rrd4j/core/Archive.java new file mode 100644 index 0000000000..f954372ce5 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/Archive.java @@ -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. + *

+ * 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 { + private final RrdDb parentDb; + + // definition + private final RrdEnum consolFun; + protected final RrdDouble xff; + protected final RrdInt steps; + protected final RrdInt 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[] 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 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 >= 0 and < 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(); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/ByteBufferBackend.java b/apps/jrobin/java/src/org/rrd4j/core/ByteBufferBackend.java new file mode 100644 index 0000000000..b21c8f7fd2 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/ByteBufferBackend.java @@ -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; + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/DataImporter.java b/apps/jrobin/java/src/org/rrd4j/core/DataImporter.java new file mode 100644 index 0000000000..989961df76 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/DataImporter.java @@ -0,0 +1,84 @@ +package org.rrd4j.core; + +import java.io.Closeable; +import java.io.IOException; + +import org.rrd4j.ConsolFun; +import org.rrd4j.DsType; + +/** + *

An abstract class to import data from external source.

+ * @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(); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/Datasource.java b/apps/jrobin/java/src/org/rrd4j/core/Datasource.java new file mode 100644 index 0000000000..aa4820111b --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/Datasource.java @@ -0,0 +1,479 @@ +package org.rrd4j.core; + +import java.io.IOException; + +import org.rrd4j.DsType; + +/** + *

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).

+ *

Normally, you don't need to manipulate Datasource objects directly, it's up to + * Rrd4j framework to do it for you.

+ * + * @author Sasa Markovic + */ +public class Datasource implements RrdUpdater { + 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 dsName; + private final RrdEnum dsType; + private final RrdLong heartbeat; + private final RrdDouble minValue, maxValue; + + // state variables + private RrdDouble lastValue; + private RrdLong nanSeconds; + private RrdDouble 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); + } + + /** + *

Setter for the field dsType.

+ * + * @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 filterArchivedValues + * argument is set to true, all archived values less then minValue will + * be fixed to NaN. + * + * @param minValue New minimal value. Specify Double.NaN 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 filterArchivedValues + * argument is set to true, all archived values greater then maxValue will + * be fixed to NaN. + * + * @param maxValue New maximal value. Specify Double.NaN 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 filterArchivedValues + * argument is set to true, all archived values less then minValue or + * greater then maxValue will be fixed to NaN. + * + * @param minValue New minimal value. Specify Double.NaN if no min + * value should be set. + * @param maxValue New maximal value. Specify Double.NaN 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(); + } +} + diff --git a/apps/jrobin/java/src/org/rrd4j/core/DsDef.java b/apps/jrobin/java/src/org/rrd4j/core/DsDef.java new file mode 100644 index 0000000000..41c148f732 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/DsDef.java @@ -0,0 +1,154 @@ +package org.rrd4j.core; + +import org.rrd4j.DsType; + +/** + *

Class to represent single data source definition within the RRD. + * Datasource definition consists of the following five elements:

+ *
    + *
  • data source name + *
  • data source type + *
  • heartbeat + *
  • minimal value + *
  • maximal value + *
+ *

For the complete explanation of all source definition parameters, see RRDTool's + * rrdcreate man page.

+ * + * @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. + *

+ * For the complete explanation of all source definition parameters, see RRDTool's + * rrdcreate man page + *

+ * IMPORTANT NOTE: 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 Double.NaN if unknown. + * @param maxValue Maximal value. Use Double.NaN 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); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/FetchData.java b/apps/jrobin/java/src/org/rrd4j/core/FetchData.java new file mode 100644 index 0000000000..142c2ecd55 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/FetchData.java @@ -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. + *

+ * + * 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). + *

+ * + * 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. + *

+ * + * 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. + *

+ * + * @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 x and y + * in this FetchData and you want to calculate values for (x+y)/2 use something like: + *

+ * getRpnValues("x,y,+,2,/"); + * + * @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 x and y + * in this FetchData and you want to calculate MAX value of (x+y)/2 use something like: + *

+ * getRpnAggregate("x,y,+,2,/", "MAX"); + * + * @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 x and y + * in this FetchData and you want to calculate MIN, MAX, LAST, FIRST, AVERAGE and TOTAL value + * of (x+y)/2 use something like: + *

+ * getRpnAggregates("x,y,+,2,/"); + * + * @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. + *

+ * + * 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. + *

+ * + * Read more about this topic at:

+ * Rednet or
+ * Bytemark. + * + * @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; + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/FetchRequest.java b/apps/jrobin/java/src/org/rrd4j/core/FetchRequest.java new file mode 100644 index 0000000000..9ffe43d128 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/FetchRequest.java @@ -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 + * rrdfetch man page. + *

+ * You cannot create FetchRequest 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 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; + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/Header.java b/apps/jrobin/java/src/org/rrd4j/core/Header.java new file mode 100644 index 0000000000..a20a7b8179 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/Header.java @@ -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). + *

+ * + * Normally, you don't need to manipulate the Header object directly - Rrd4j framework + * does it for you. + *

+ * + * @author Sasa Markovic* + */ +public class Header implements RrdUpdater

{ + 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
signature; + private RrdLong
step; + private RrdInt
dsCount, arcCount; + private RrdLong
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 Rrd4j, version x.x. + * + * @return RRD signature + * @throws java.io.IOException Thrown in case of I/O error + */ + public String getSignature() throws IOException { + return signature.get(); + } + + /** + *

getInfo.

+ * + * @return a {@link java.lang.String} object. + * @throws java.io.IOException if any. + */ + public String getInfo() throws IOException { + return getSignature().substring(SIGNATURE_LENGTH); + } + + /** + *

setInfo.

+ * + * @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(); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/InvalidRrdException.java b/apps/jrobin/java/src/org/rrd4j/core/InvalidRrdException.java new file mode 100644 index 0000000000..f84c37d2e2 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/InvalidRrdException.java @@ -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); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/Robin.java b/apps/jrobin/java/src/org/rrd4j/core/Robin.java new file mode 100644 index 0000000000..cc71de28da --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/Robin.java @@ -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. + *

+ * 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 { + + /** + * 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 [minValue, maxValue] interval (inclusive) + * will be silently replaced with NaN. + * + * @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(); + + /** + *

update.

+ * + * @param newValues an array of double. + * @throws java.io.IOException if any. + */ + void update(double[] newValues) throws IOException; + + /** + *

dump.

+ * + * @return a {@link java.lang.String} object. + * @throws java.io.IOException if any. + */ + String dump() throws IOException; + + /** + *

store.

+ * + * @param newValue a double. + * @throws java.io.IOException if any. + */ + void store(double newValue) throws IOException; + + /** + *

bulkStore.

+ * + * @param newValue a double. + * @param bulkCount a int. + * @throws java.io.IOException if any. + */ + void bulkStore(double newValue, int bulkCount) throws IOException; + + /** + *

getValues.

+ * + * @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; +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RobinArray.java b/apps/jrobin/java/src/org/rrd4j/core/RobinArray.java new file mode 100644 index 0000000000..0ece198c55 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RobinArray.java @@ -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.

+ *

+ * 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 pointer; + private final RrdDoubleArray 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() + */ + /** + *

Getter for the field values.

+ * + * @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); + } + } + + /** + *

update.

+ * + * @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); + } + + /** + *

dump.

+ * + * @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() + */ + /** + *

getParent.

+ * + * @return a {@link org.rrd4j.core.Archive} object. + */ + public Archive getParent() { + return parentArc; + } + + /* (non-Javadoc) + * @see org.rrd4j.core.Robin#getSize() + */ + /** + *

getSize.

+ * + * @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() + */ + /** + *

getRrdBackend.

+ * + * @return a {@link org.rrd4j.core.RrdBackend} object. + */ + public RrdBackend getRrdBackend() { + return parentArc.getRrdBackend(); + } + + /* (non-Javadoc) + * @see org.rrd4j.core.Robin#getRrdAllocator() + */ + /** + *

getRrdAllocator.

+ * + * @return a {@link org.rrd4j.core.RrdAllocator} object. + */ + public RrdAllocator getRrdAllocator() { + return parentArc.getRrdAllocator(); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RobinMatrix.java b/apps/jrobin/java/src/org/rrd4j/core/RobinMatrix.java new file mode 100644 index 0000000000..8035716a5c --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RobinMatrix.java @@ -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.

+ *

+ * 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 pointer; + private final RrdDoubleMatrix values; + private int rows; + private int column; + + RobinMatrix(Archive parentArc, RrdDoubleMatrix values, RrdInt 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); + } + } + + /** + *

update.

+ * + * @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); + } + + /** + *

dump.

+ * + * @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 [minValue, maxValue] interval (inclusive) + * will be silently replaced with NaN. + */ + 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(); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdAllocator.java b/apps/jrobin/java/src/org/rrd4j/core/RrdAllocator.java new file mode 100644 index 0000000000..f0ff7d7267 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdAllocator.java @@ -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; + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdBackend.java b/apps/jrobin/java/src/org/rrd4j/core/RrdBackend.java new file mode 100644 index 0000000000..3216ca6aff --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdBackend.java @@ -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; + +/** + *

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.:

+ *
    + *
  • {@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. + * + *
  • {@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). This is the default backend used in Rrd4j since 1.4.0 release. + * + *
  • {@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. + *
+ * + *

To create your own backend in order to provide some custom type of RRD storage, + * you should do the following:

+ * + *
    + *
  • Create your custom RrdBackend class (RrdCustomBackend, for example) + * by extending RrdBackend class. You have to implement all abstract methods defined + * in the base class. + * + *
  • 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. + * + *
  • 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. + *
+ * + * @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 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 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 true + * is returned, frontend classes will cache frequently used parts of a RRD file in memory to improve + * performance. If false is returned, high level classes will never cache RRD file sections + * in memory. + * + * @return true if file caching is enabled, false otherwise. By default, the + * method returns true 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; + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdBackendAnnotation.java b/apps/jrobin/java/src/org/rrd4j/core/RrdBackendAnnotation.java new file mode 100644 index 0000000000..83243ac01e --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdBackendAnnotation.java @@ -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(); +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdBackendException.java b/apps/jrobin/java/src/org/rrd4j/core/RrdBackendException.java new file mode 100644 index 0000000000..7228f3ab75 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdBackendException.java @@ -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); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdBackendFactory.java b/apps/jrobin/java/src/org/rrd4j/core/RrdBackendFactory.java new file mode 100644 index 0000000000..e5ac6acdf8 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdBackendFactory.java @@ -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. + *

+ * + * 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: + *

    + *
  • {@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. + * + *
  • {@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 between several JVMs 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). + * + *
  • {@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. + * + *
  • {@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. + * + *
  • {@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 Oracle Berkeley DB Java Edition. + * + *
  • {@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 MongoDB. + *
+ *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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 `/`. + *

+ * 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 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 activeFactories = new ArrayList<>(); + + /** + * Returns backend factory for the given backend factory name. + * + * @param name Backend factory name. Initially supported names are: + *

    + *
  • FILE: Default factory which creates backends based on the + * java.io.* package. RRD data is stored in files on the disk + *
  • SAFE: 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. + *
  • NIO: Factory which creates backends based on the + * java.nio.* package. RRD data is stored in files on the disk + *
  • MEMORY: Factory which creates memory-oriented backends. + * RRD data is stored in memory, it gets lost as soon as JVM exits. + *
  • BERKELEY: a memory-oriented backend that ensure persistent + * in a Berkeley Db storage. + *
  • MONGODB: a memory-oriented backend that ensure persistent + * in a MongoDB storage. + *
+ * + * @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. + *

+ * It also clear the list of actives factories and set it to the default factory. + *

+ * + * @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("^(?:(?[a-zA-Z][a-zA-Z0-9+-\\.]*):)?(?://(?[^/\\?#]*))?(?[^\\?#]*)(?:\\?(?[^#]*))?(?:#(?.*))?$"); + + /** + * 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 { + private RrdBackend backend; + public ClosingReference(RrdDb db, RrdBackend backend, + ReferenceQueue q) { + super(db, q); + this.backend = backend; + } + @Override + public void clear() { + try { + backend.close(); + } catch (IOException e) { + } + backend = null; + super.clear(); + } + } + + private final ReferenceQueue 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: + *

    + *
  • scheme must match if they are given. + *
  • authority must match if they are given. + *
  • if uri is opaque (scheme:nonabsolute), the scheme specific part is resolve as a relative path. + *
  • query and fragment is kept as is. + *
+ * + * @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 { + + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdByteArrayBackend.java b/apps/jrobin/java/src/org/rrd4j/core/RrdByteArrayBackend.java new file mode 100644 index 0000000000..60fbcb8da3 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdByteArrayBackend.java @@ -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; + + /** + *

Constructor for RrdByteArrayBackend.

+ * + * @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; + } + + /** + *

read.

+ * + * @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} + * + *

It will reserves a memory section as a RRD storage.

+ * + * @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)); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdDb.java b/apps/jrobin/java/src/org/rrd4j/core/RrdDb.java new file mode 100644 index 0000000000..8729f23ef7 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdDb.java @@ -0,0 +1,1489 @@ +package org.rrd4j.core; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.util.Date; + +import org.rrd4j.ConsolFun; + +/** + *

Main class used to create and manipulate round robin databases (RRDs). Use this class to perform + * update and fetch operations on existing RRDs, to create new RRD from + * the definition (object of class {@link org.rrd4j.core.RrdDef RrdDef}) or + * from XML file (dumped content of RRDTool's or Rrd4j's RRD file).

+ *

Each RRD is backed with some kind of storage. For example, RRDTool supports only one kind of + * storage (disk file). On the contrary, Rrd4j gives you freedom to use other storage (backend) types + * even to create your own backend types for some special purposes. Rrd4j by default stores + * RRD data in files (as RRDTool), but you might choose to store RRD data in memory (this is + * supported in Rrd4j), to use java.nio.* instead of java.io.* package for file manipulation + * (also supported) or to store whole RRDs in the SQL database + * (you'll have to extend some classes to do this).

+ *

Note that Rrd4j uses binary format different from RRDTool's format. You cannot + * use this class to manipulate RRD files created with RRDTool. However, if you perform + * the same sequence of create, update and fetch operations, you will get exactly the same + * results from Rrd4j and RRDTool.

+ *

You will not be able to use Rrd4j API if you are not familiar with + * basic RRDTool concepts. Good place to start is the + * official RRD tutorial + * and relevant RRDTool man pages: rrdcreate, + * rrdupdate, + * rrdfetch and + * rrdgraph. + * For RRDTool's advanced graphing capabilities (RPN extensions), also supported in Rrd4j, + * there is an excellent + * CDEF tutorial.

+ * + * @see RrdBackend + * @see RrdBackendFactory + */ +public class RrdDb implements RrdUpdater, Closeable { + + /** + * Builder for {@link RrdDb} instances. + * + * @author Fabrice Bacchella + * @since 3.5 + */ + public static class Builder { + private String path = null; + private URI uri = null; + private RrdBackendFactory factory = RrdBackendFactory.getDefaultFactory(); + private boolean readOnly = false; + private String externalPath = null; + private DataImporter importer = null; + private RrdDef rrdDef = null; + private boolean usePool = false; + private RrdDbPool pool; + + private Builder() { + + } + + /** + * Builds a {@link RrdDb} instance. + * + * @return a new build RrdDb + * @throws IOException in case of I/O error. + * @throws IllegalArgumentException if the builder settings were incomplete + */ + public RrdDb build() throws IOException { + if (rrdDef != null) { + factory = checkFactory(rrdDef.getUri(), factory); + if (usePool) { + return resolvePool(pool).requestRrdDb(rrdDef, factory); + } else { + return new RrdDb(rrdDef, factory, pool); + } + } else if (path != null || uri != null) { + URI rrdUri = buildUri(path, uri, factory); + factory = checkFactory(rrdUri, factory); + rrdUri = factory.getCanonicalUri(rrdUri); + if (!factory.canStore(rrdUri)) { + throw new IllegalArgumentException("Given a factory incompatible with the URI"); + } + if (importer == null && externalPath == null) { + if (usePool) { + return resolvePool(pool).requestRrdDb(rrdUri, factory); + } else { + return new RrdDb(null, rrdUri, readOnly, factory, pool); + } + } else { + try (DataImporter rrdImporter = resoleImporter(externalPath, importer)) { + if (usePool) { + return resolvePool(pool).requestRrdDb(rrdUri, factory, importer); + } else { + return new RrdDb(null, rrdUri, null, rrdImporter, factory, pool); + } + } + } + } else { + throw new IllegalArgumentException("Incomplete builder definition"); + } + } + + /** + * Import an external rrd data, import definition must have been done using {@link #setExternalPath(String)} + * or {@link #setImporter(DataImporter)} + * + * @throws IOException in case of I/O error. + * @throws IllegalArgumentException if the builder settings were incomplete + */ + @SuppressWarnings("deprecation") + public void doimport() throws IOException { + if (rrdDef != null || (importer == null && externalPath == null)) { + throw new IllegalArgumentException("Not an importing configuration"); + } + if (path == null && uri == null) { + throw new IllegalArgumentException("No rrd destination given"); + } + URI rrdUri = buildUri(path, uri, factory); + factory = checkFactory(rrdUri, factory); + if (!factory.canStore(rrdUri)) { + throw new IllegalArgumentException("Given a factory incompatible with the URI"); + } + try (DataImporter rrdImporter = resoleImporter(externalPath, importer)) { + if (usePool) { + RrdDb db = resolvePool(pool).requestRrdDb(rrdUri, factory, importer); + resolvePool(pool).release(db); + } else { + try (RrdDb db = new RrdDb(path, rrdUri, null, rrdImporter, factory, null)) { + } + } + } + } + + public Builder setPath(String path) { + this.rrdDef = null; + this.path = path; + this.uri = null; + return this; + } + + public Builder setPath(URI uri) { + this.rrdDef = null; + this.uri = uri; + this.path = null; + return this; + } + + public Builder setBackendFactory(RrdBackendFactory factory) { + this.factory = factory; + return this; + } + + public Builder setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + return this; + } + + public Builder readOnly() { + this.readOnly = true; + return this; + } + + public Builder setUsePool(boolean usePool) { + this.usePool = usePool; + return this; + } + + /** + * Activate the pool usage + * + * @return + */ + public Builder usePool() { + this.usePool = true; + return this; + } + + /** + * Set the pool that will be used if {@link #usePool} is true. If not defined, + * the singleton instance will be used. + * + * @param pool + * @return + */ + public Builder setPool(RrdDbPool pool) { + this.pool = pool; + return this; + } + + public Builder setExternalPath(String externalPath) { + this.externalPath = externalPath; + this.importer = null; + this.rrdDef = null; + this.readOnly = false; + return this; + } + + public Builder setImporter(DataImporter importer) { + this.importer = importer; + this.externalPath = null; + this.rrdDef = null; + this.readOnly = false; + return this; + } + + public Builder setRrdToolImporter(String externalPath) throws IOException { + this.importer = new RrdToolReader(externalPath); + this.externalPath = null; + this.rrdDef = null; + this.readOnly = false; + return this; + } + + public Builder setRrdDef(RrdDef rrdDef) { + this.rrdDef = rrdDef; + this.importer = null; + this.externalPath = null; + this.path = null; + this.uri = null; + this.readOnly = false; + return this; + } + + private static RrdBackendFactory checkFactory(URI uri, RrdBackendFactory factory) { + if (factory == null) { + return RrdBackendFactory.findFactory(uri); + } else { + return factory; + } + } + + private static URI buildUri(String rrdPath, URI rrdUri, RrdBackendFactory factory) { + if (rrdUri != null) { + return rrdUri; + } else if (factory == null) { + return RrdBackendFactory.buildGenericUri(rrdPath); + } else { + return factory.getCanonicalUri(RrdBackendFactory.buildGenericUri(rrdPath)); + } + } + + private static DataImporter resoleImporter(String externalPath, DataImporter importer) throws IOException { + if (importer != null) { + return importer; + } else { + if (externalPath.startsWith(PREFIX_RRDTool)) { + String rrdToolPath = externalPath.substring(PREFIX_RRDTool.length()); + return new RrdToolReader(rrdToolPath); + } else if (externalPath.startsWith(PREFIX_XML)) { + externalPath = externalPath.substring(PREFIX_XML.length()); + return new XmlReader(externalPath); + } else { + return new XmlReader(externalPath); + } + } + } + + private static RrdDbPool resolvePool(RrdDbPool pool) { + return pool != null ? pool : RrdDbPool.getInstance(); + } + + } + + public static Builder getBuilder() { + return new Builder(); + } + + /** + * Prefix to identify external XML file source used in various RrdDb constructors. + */ + public static final String PREFIX_XML = "xml:/"; + + /** + * Prefix to identify external RRDTool file source used in various RrdDb constructors. + */ + public static final String PREFIX_RRDTool = "rrdtool:/"; + + static final int XML_BUFFER_CAPACITY = 100000; // bytes + + private final RrdBackend backend; + private final RrdAllocator allocator = new RrdAllocator(); + + private final Header header; + private final Datasource[] datasources; + private final Archive[] archives; + private final RrdDbPool pool; + + private boolean closed = false; + + /** + *

Constructor used to create new RRD object from the definition. If the rrdDef was constructed + * giving an {@link java.net.URI}, {@link org.rrd4j.core.RrdBackendFactory#findFactory(URI)} will be used to resolve the needed factory. If not, or a relative + * URI was given, this RRD object will be backed + * with a storage (backend) of the default type. Initially, storage type defaults to "NIO" + * (RRD bytes will be put in a file on the disk). Default storage type can be changed with a static + * {@link org.rrd4j.core.RrdBackendFactory#setDefaultFactory(String)} method call.

+ *

New RRD file structure is specified with an object of class + * {@link RrdDef RrdDef}. The underlying RRD storage is created as soon + * as the constructor returns.

+ *

Typical scenario:

+ *
+     * // create new RRD definition
+     * RrdDef def = new RrdDef("test.rrd", 300);
+     * def.addDatasource("input", DsType.DT_COUNTER, 600, 0, Double.NaN);
+     * def.addDatasource("output", DsType.DT_COUNTER, 600, 0, Double.NaN);
+     * def.addArchive(ConsolFun.CF_AVERAGE, 0.5, 1, 600);
+     * def.addArchive(ConsolFun.CF_AVERAGE, 0.5, 6, 700);
+     * def.addArchive(ConsolFun.CF_AVERAGE, 0.5, 24, 797);
+     * def.addArchive(ConsolFun.CF_AVERAGE, 0.5, 288, 775);
+     * def.addArchive(ConsolFun.CF_MAX, 0.5, 1, 600);
+     * def.addArchive(ConsolFun.CF_MAX, 0.5, 6, 700);
+     * def.addArchive(ConsolFun.CF_MAX, 0.5, 24, 797);
+     * def.addArchive(ConsolFun.CF_MAX, 0.5, 288, 775);
+     *
+     * // RRD definition is now completed, create the database!
+     * RrdDb rrd = new RrdDb(def);
+     * // new RRD file has been created on your disk
+     * 
+ * + * @param rrdDef Object describing the structure of the new RRD file. + * @throws java.io.IOException Thrown in case of I/O error. + * @deprecated Use the builder instead. + */ + @Deprecated + public RrdDb(RrdDef rrdDef) throws IOException { + this(rrdDef, null, null); + } + + /** + *

Returns a new RRD object from the definition. If the rrdDef was constructed + * giving an {@link java.net.URI}, {@link org.rrd4j.core.RrdBackendFactory#findFactory(URI)} will be used to resolve + * the needed factory. If not, or a relative URI was given, this RRD object will be backed + * with a storage (backend) of the default type. Initially, storage type defaults to "NIO" + * (RRD bytes will be put in a file on the disk). Default storage type can be changed with a static + * {@link org.rrd4j.core.RrdBackendFactory#setDefaultFactory(String)} method call.

+ *

New RRD file structure is specified with an object of class + * {@link RrdDef RrdDef}. The underlying RRD storage is created as soon + * as the method returns.

+ *

Typical scenario:

+ *
+     * // create new RRD definition
+     * RrdDef def = new RrdDef("test.rrd", 300);
+     * def.addDatasource("input", DsType.DT_COUNTER, 600, 0, Double.NaN);
+     * def.addDatasource("output", DsType.DT_COUNTER, 600, 0, Double.NaN);
+     * def.addArchive(ConsolFun.CF_AVERAGE, 0.5, 1, 600);
+     * def.addArchive(ConsolFun.CF_AVERAGE, 0.5, 6, 700);
+     * def.addArchive(ConsolFun.CF_AVERAGE, 0.5, 24, 797);
+     * def.addArchive(ConsolFun.CF_AVERAGE, 0.5, 288, 775);
+     * def.addArchive(ConsolFun.CF_MAX, 0.5, 1, 600);
+     * def.addArchive(ConsolFun.CF_MAX, 0.5, 6, 700);
+     * def.addArchive(ConsolFun.CF_MAX, 0.5, 24, 797);
+     * def.addArchive(ConsolFun.CF_MAX, 0.5, 288, 775);
+     *
+     * // RRD definition is now completed, create the database!
+     * RrdDb rrd = RrdDb.of(def);
+     * // new RRD file has been created on your disk
+     * 
+ * + * @param rrdDef Object describing the structure of the new RRD file. + * @throws java.io.IOException Thrown in case of I/O error. + */ + public static RrdDb of(RrdDef rrdDef) throws IOException { + return new RrdDb(rrdDef, null, null); + } + + /** + *

Constructor used to create new RRD object from the definition object but with a storage + * (backend) different from default.

+ * + *

Rrd4j uses factories to create RRD backend objects. There are three different + * backend factories supplied with Rrd4j, and each factory has its unique name:

+ *
    + *
  • FILE: backends created from this factory will store RRD data to files by using + * java.io.* classes and methods + *
  • NIO: backends created from this factory will store RRD data to files by using + * java.nio.* classes and methods + *
  • MEMORY: backends created from this factory will store RRD data in memory. This might + * be useful in runtime environments which prohibit disk utilization, or for storing temporary, + * non-critical data (it gets lost as soon as JVM exits). + *
+ *

For example, to create RRD in memory, use the following code:

+ *
+     * RrdBackendFactory factory = RrdBackendFactory.getFactory("MEMORY");
+     * RrdDb rrdDb = new RrdDb(rrdDef, factory);
+     * rrdDb.close();
+     * 
+ *

New RRD file structure is specified with an object of class + * {@link RrdDef RrdDef}. The underlying RRD storage is created as soon + * as the constructor returns.

+ * + * @param rrdDef RRD definition object + * @param factory The factory which will be used to create storage for this RRD + * @throws java.io.IOException Thrown in case of I/O error + * @see RrdBackendFactory + * @deprecated Use the builder instead. + */ + @Deprecated + public RrdDb(RrdDef rrdDef, RrdBackendFactory factory) throws IOException { + this(rrdDef, factory, null); + } + + private RrdDb(RrdDef rrdDef, RrdBackendFactory factory, RrdDbPool pool) throws IOException { + this.pool = pool; + factory = Builder.checkFactory(rrdDef.getUri(), factory); + + if (!rrdDef.hasDatasources()) { + throw new IllegalArgumentException("No RRD datasource specified. At least one is needed."); + } + if (!rrdDef.hasArchives()) { + throw new IllegalArgumentException("No RRD archive specified. At least one is needed."); + } + + backend = factory.getBackend(this, rrdDef.getUri(), false); + try { + backend.setLength(rrdDef.getEstimatedSize()); + // create header + header = new Header(this, rrdDef); + // create datasources + DsDef[] dsDefs = rrdDef.getDsDefs(); + datasources = new Datasource[dsDefs.length]; + for (int i = 0; i < dsDefs.length; i++) { + datasources[i] = new Datasource(this, dsDefs[i]); + } + // create archives + ArcDef[] arcDefs = rrdDef.getArcDefs(); + archives = new Archive[arcDefs.length]; + for (int i = 0; i < arcDefs.length; i++) { + archives[i] = new Archive(this, arcDefs[i]); + } + } catch (IOException e) { + backend.rrdClose(); + throw e; + } + } + + /** + *

Constructor used to open already existing RRD. The path will be parsed as an URI and checked against the active factories. If + * it's a relative URI (no scheme given, or just a plain path), the default factory will be used.

+ *

Constructor obtains read or read/write access to this RRD.

+ * + * @param path Path to existing RRD. + * @param readOnly Should be set to false if you want to update + * the underlying RRD. If you want just to fetch data from the RRD file + * (read-only access), specify true. If you try to update RRD file + * open in read-only mode (readOnly set to true), + * IOException will be thrown. + * @throws java.io.IOException Thrown in case of I/O error. + * @deprecated Use the builder instead. + */ + @Deprecated + public RrdDb(String path, boolean readOnly) throws IOException { + this(path, null, readOnly, null, null); + } + + /** + *

Constructor used to open already existing RRD. The URI will checked against the active factories. If + * it's a relative URI (no scheme given, or just a plain path), the default factory will be used.

+ *

Constructor obtains read or read/write access to this RRD.

+ * + * @param uri URI to existing RRD. + * @param readOnly Should be set to false if you want to update + * the underlying RRD. If you want just to fetch data from the RRD file + * (read-only access), specify true. If you try to update RRD file + * open in read-only mode (readOnly set to true), + * IOException will be thrown. + * @throws java.io.IOException Thrown in case of I/O error. + * @deprecated Use the builder instead. + */ + @Deprecated + public RrdDb(URI uri, boolean readOnly) throws IOException { + this(null, uri, readOnly, null, null); + } + + /** + *

Constructor used to open already existing RRD backed + * with a storage (backend) different from default. Constructor + * obtains read or read/write access to this RRD.

+ * + * @param path Path to existing RRD. + * @param readOnly Should be set to false if you want to update + * the underlying RRD. If you want just to fetch data from the RRD file + * (read-only access), specify true. If you try to update RRD file + * open in read-only mode (readOnly set to true), + * IOException will be thrown. + * @param factory Backend factory which will be used for this RRD. + * @throws FileNotFoundException Thrown if the requested file does not exist. + * @throws java.io.IOException Thrown in case of general I/O error (bad RRD file, for example). + * @see RrdBackendFactory + * @deprecated Use the builder instead. + */ + @Deprecated + public RrdDb(String path, boolean readOnly, RrdBackendFactory factory) throws IOException { + this(path, null, readOnly, factory, null); + } + + /** + *

Constructor used to open already existing RRD. The path will be parsed as an URI and checked against the active factories. If + * it's a relative URI (no scheme given, or just a plain path), the default factory will be used.

+ *

Constructor obtains read/write access to this RRD.

+ * + * @param path Path to existing RRD. + * @throws java.io.IOException Thrown in case of I/O error. + * @deprecated Use the builder instead. + */ + @Deprecated + public RrdDb(String path) throws IOException { + this(path, null, false, null, null); + } + + /** + *

Opens an existing RRD with read/write access. + * The path will be parsed as an URI and checked against the active factories. + * If it's a relative URI (no scheme given, or just a plain path), the default factory will be used.

+ * + * @param path Path to existing RRD. + * @throws java.io.IOException Thrown in case of I/O error. + */ + public static RrdDb of(String path) throws IOException { + return new RrdDb(path, null, false, null, null); + } + + /** + *

Constructor used to open already existing RRD. The URI will checked against the active factories. If + * it's a relative URI (no scheme given, or just a plain path), the default factory will be used.

+ *

Constructor obtains read/write access to this RRD.

+ * + * @param uri URI to existing RRD. + * @throws java.io.IOException Thrown in case of I/O error. + * @deprecated Use the builder instead. + */ + @Deprecated + public RrdDb(URI uri) throws IOException { + this(null, uri, false, null, null); + } + + /** + *

Opens an existing RRD with read/write access. + * The URI will checked against the active factories. + * If it's a relative URI (no scheme given, or just a plain path), the default factory will be used.

+ * + * @param uri URI to existing RRD. + * @throws java.io.IOException Thrown in case of I/O error. + */ + public static RrdDb of(URI uri) throws IOException { + return new RrdDb(null, uri, false, null, null); + } + + /** + * Constructor used to open already existing RRD in R/W mode with a storage (backend) type + * different from default. + * + * @param path Path to existing RRD. + * @param factory Backend factory used to create this RRD. + * @throws java.io.IOException Thrown in case of I/O error. + * @see RrdBackendFactory + * @deprecated Use the builder instead. + */ + @Deprecated + public RrdDb(String path, RrdBackendFactory factory) throws IOException { + this(path, null, false, factory, null); + } + + private RrdDb(String rrdPath, URI rrdUri, boolean readOnly, RrdBackendFactory factory, RrdDbPool pool) throws IOException { + this.pool = pool; + rrdUri = Builder.buildUri(rrdPath, rrdUri, factory); + factory = Builder.checkFactory(rrdUri, factory); + // opens existing RRD file - throw exception if the file does not exist... + if (!factory.exists(rrdUri)) { + throw new FileNotFoundException("Could not open " + rrdUri + " [non existent]"); + } + backend = factory.getBackend(this, rrdUri, readOnly); + try { + // restore header + header = new Header(this, (RrdDef) null); + + if (factory.shouldValidateHeader(rrdUri)) { + header.validateHeader(); + } + + // restore datasources + int dsCount = header.getDsCount(); + datasources = new Datasource[dsCount]; + for (int i = 0; i < dsCount; i++) { + datasources[i] = new Datasource(this, null); + } + // restore archives + int arcCount = header.getArcCount(); + archives = new Archive[arcCount]; + for (int i = 0; i < arcCount; i++) { + archives[i] = new Archive(this, null); + } + } catch (IOException e) { + backend.rrdClose(); + throw e; + } + } + + /** + *

Constructor used to create RRD files from external file sources. + * Supported external file sources are:

+ *
    + *
  • RRDTool/Rrd4j XML file dumps (i.e files created with rrdtool dump command). + *
  • RRDTool binary files. + *
+ *

The path for the new rrd will be parsed as an URI and checked against the active factories. If + * it's a relative URI (no scheme given, or just a plain path), the default factory will be used.

+ *

Rrd4j and RRDTool use the same format for XML dump and this constructor should be used to + * (re)create Rrd4j RRD files from XML dumps. First, dump the content of a RRDTool + * RRD file (use command line):

+ *
+     * rrdtool dump original.rrd > original.xml
+     * 
+ *

Than, use the file original.xml to create Rrd4j RRD file named + * copy.rrd:

+ *
+     * RrdDb rrd = new RrdDb("copy.rrd", "original.xml");
+     * 
+ *

or:

+ *
+     * RrdDb rrd = new RrdDb("copy.rrd", "xml:/original.xml");
+     * 
+ *

See documentation for {@link #dumpXml(String) dumpXml()} method + * to see how to convert Rrd4j files to RRDTool's format.

+ *

To read RRDTool files directly, specify rrdtool:/ prefix in the + * externalPath argument. For example, to create Rrd4j compatible file named + * copy.rrd from the file original.rrd created with RRDTool, use + * the following code:

+ *
+     * RrdDb rrd = new RrdDb("copy.rrd", "rrdtool:/original.rrd");
+     * 
+ *

Note that the prefix xml:/ or rrdtool:/ is necessary to distinguish + * between XML and RRDTool's binary sources. If no prefix is supplied, XML format is assumed.

+ * + * @param rrdPath Path to a RRD file which will be created + * @param externalPath Path to an external file which should be imported, with an optional + * xml:/ or rrdtool:/ prefix. + * @throws java.io.IOException Thrown in case of I/O error + * @deprecated Use the builder instead. + */ + @Deprecated + public RrdDb(String rrdPath, String externalPath) throws IOException { + this(rrdPath, null, externalPath, null, null, null); + } + + /** + *

Constructor used to create RRD files from external file sources. + * Supported external file sources are:

+ *
    + *
  • RRDTool/Rrd4j XML file dumps (i.e files created with rrdtool dump command). + *
  • RRDTool binary files. + *
+ *

The path for the new rrd will be parsed as an URI and checked against the active factories. If + * it's a relative URI (no scheme given, or just a plain path), the default factory will be used.

+ *

Rrd4j and RRDTool use the same format for XML dump and this constructor should be used to + * (re)create Rrd4j RRD files from XML dumps. First, dump the content of a RRDTool + * RRD file (use command line):

+ *
+     * rrdtool dump original.rrd > original.xml
+     * 
+ *

Than, use the file original.xml to create Rrd4j RRD file named + * copy.rrd:

+ *
+     * RrdDb rrd = new RrdDb("copy.rrd", "original.xml");
+     * 
+ *

or:

+ *
+     * RrdDb rrd = new RrdDb("copy.rrd", "xml:/original.xml");
+     * 
+ *

See documentation for {@link #dumpXml(String) dumpXml()} method + * to see how to convert Rrd4j files to RRDTool's format.

+ *

To read RRDTool files directly, specify rrdtool:/ prefix in the + * externalPath argument. For example, to create Rrd4j compatible file named + * copy.rrd from the file original.rrd created with RRDTool, use + * the following code:

+ *
+     * RrdDb rrd = new RrdDb("copy.rrd", "rrdtool:/original.rrd");
+     * 
+ *

Note that the prefix xml:/ or rrdtool:/ is necessary to distinguish + * between XML and RRDTool's binary sources. If no prefix is supplied, XML format is assumed.

+ * + * @param uri Path to a RRD file which will be created + * @param externalPath Path to an external file which should be imported, with an optional + * xml:/ or rrdtool:/ prefix. + * @throws java.io.IOException Thrown in case of I/O error + * @deprecated Use the builder instead. + */ + @Deprecated + public RrdDb(URI uri, String externalPath) throws IOException { + this(null, uri, externalPath, null, null, null); + } + + /** + *

Constructor used to create RRD files from external file sources with a backend type + * different from default. Supported external file sources are:

+ *
    + *
  • RRDTool/Rrd4j XML file dumps (i.e files created with rrdtool dump command). + *
  • RRDTool binary files. + *
+ *

Rrd4j and RRDTool use the same format for XML dump and this constructor should be used to + * (re)create Rrd4j RRD files from XML dumps. First, dump the content of a RRDTool + * RRD file (use command line):

+ *
+     * rrdtool dump original.rrd > original.xml
+     * 
+ *

Than, use the file original.xml to create Rrd4j RRD file named + * copy.rrd:

+ *
+     * RrdDb rrd = new RrdDb("copy.rrd", "original.xml");
+     * 
+ *

or:

+ *
+     * RrdDb rrd = new RrdDb("copy.rrd", "xml:/original.xml");
+     * 
+ *

See documentation for {@link #dumpXml(String) dumpXml()} method + * to see how to convert Rrd4j files to RRDTool's format.

+ *

To read RRDTool files directly, specify rrdtool:/ prefix in the + * externalPath argument. For example, to create Rrd4j compatible file named + * copy.rrd from the file original.rrd created with RRDTool, use + * the following code:

+ *
+     * RrdDb rrd = new RrdDb("copy.rrd", "rrdtool:/original.rrd");
+     * 
+ *

Note that the prefix xml:/ or rrdtool:/ is necessary to distinguish + * between XML and RRDTool's binary sources. If no prefix is supplied, XML format is assumed.

+ * + * @param rrdPath Path to RRD which will be created + * @param externalPath Path to an external file which should be imported, with an optional + * xml:/ or rrdtool:/ prefix. + * @param factory Backend factory which will be used to create storage (backend) for this RRD. + * @throws java.io.IOException Thrown in case of I/O error + * @see RrdBackendFactory + * @deprecated Use the builder instead. + */ + @Deprecated + public RrdDb(String rrdPath, String externalPath, RrdBackendFactory factory) throws IOException { + this(rrdPath, null, externalPath, null, factory, null); + } + + private RrdDb(String rrdPath, URI rrdUri, String externalPath, DataImporter importer, RrdBackendFactory factory, RrdDbPool pool) throws IOException { + this.pool = pool; + rrdUri = Builder.buildUri(rrdPath, rrdUri, factory); + factory = Builder.checkFactory(rrdUri, factory); + + backend = factory.getBackend(this, rrdUri, false); + try (DataImporter reader = Builder.resoleImporter(externalPath, importer)) { + backend.setLength(reader.getEstimatedSize()); + // create header + header = new Header(this, reader); + // create datasources + datasources = new Datasource[reader.getDsCount()]; + for (int i = 0; i < datasources.length; i++) { + datasources[i] = new Datasource(this, reader, i); + } + // create archives + archives = new Archive[reader.getArcCount()]; + for (int i = 0; i < archives.length; i++) { + archives[i] = new Archive(this, reader, i); + } + } catch (IOException e) { + backend.rrdClose(); + throw e; + } + } + + /** + * Closes RRD. No further operations are allowed on this RrdDb object. + * + * @throws java.io.IOException Thrown in case of I/O related error. + */ + @SuppressWarnings("deprecation") + public synchronized void close() throws IOException { + if (pool != null) { + pool.release(this); + } else { + internalClose(); + } + } + + void internalClose() throws IOException { + if (!closed) { + closed = true; + backend.rrdClose(); + } + } + + /** + * Returns true if the RRD is closed. + * + * @return true if closed, false otherwise + */ + public boolean isClosed() { + return closed; + } + + /** + * Returns RRD header. + * + * @return Header object + */ + public Header getHeader() { + return header; + } + + /** + * Returns Datasource object for the given datasource index. + * + * @param dsIndex Datasource index (zero based) + * @return Datasource object + */ + public Datasource getDatasource(int dsIndex) { + return datasources[dsIndex]; + } + + /** + * Returns Archive object for the given archive index. + * + * @param arcIndex Archive index (zero based) + * @return Archive object + */ + public Archive getArchive(int arcIndex) { + return archives[arcIndex]; + } + + /** + * Returns an array of datasource names defined in RRD. + * + * @return Array of datasource names. + * @throws java.io.IOException Thrown in case of I/O error. + */ + public String[] getDsNames() throws IOException { + int n = datasources.length; + String[] dsNames = new String[n]; + for (int i = 0; i < n; i++) { + dsNames[i] = datasources[i].getName(); + } + return dsNames; + } + + /** + *

Creates new sample with the given timestamp and all datasource values set to + * 'unknown'. Use returned Sample object to specify + * datasource values for the given timestamp. See documentation for + * {@link Sample Sample} for an explanation how to do this.

+ *

Once populated with data source values, call Sample's + * {@link org.rrd4j.core.Sample#update() update()} method to actually + * store sample in the RRD associated with it.

+ * + * @param time Sample timestamp rounded to the nearest second (without milliseconds). + * @return Fresh sample with the given timestamp and all data source values set to 'unknown'. + * @throws java.io.IOException Thrown in case of I/O error. + */ + public Sample createSample(long time) throws IOException { + return new Sample(this, time); + } + + /** + *

Creates new sample with the current timestamp and all data source values set to + * 'unknown'. Use returned Sample object to specify + * datasource values for the current timestamp. See documentation for + * {@link Sample Sample} for an explanation how to do this.

+ *

Once populated with data source values, call Sample's + * {@link org.rrd4j.core.Sample#update() update()} method to actually + * store sample in the RRD associated with it.

+ * + * @return Fresh sample with the current timestamp and all data source values set to 'unknown'. + * @throws java.io.IOException Thrown in case of I/O error. + */ + public Sample createSample() throws IOException { + return createSample(Util.getTime()); + } + + /** + * Prepares fetch request to be executed on this RRD. Use returned + * FetchRequest object and its {@link org.rrd4j.core.FetchRequest#fetchData() fetchData()} + * method to actually fetch data from the RRD file. + * + * @param consolFun Consolidation function to be used in fetch request. + * @param fetchStart Starting timestamp for fetch request. + * @param fetchEnd Ending timestamp for fetch request. + * @param resolution Fetch resolution (see RRDTool's + * rrdfetch man page for an + * explanation of this parameter. + * @return Request object that should be used to actually fetch data from RRD + */ + public FetchRequest createFetchRequest(ConsolFun consolFun, long fetchStart, long fetchEnd, long resolution) { + return new FetchRequest(this, consolFun, fetchStart, fetchEnd, resolution); + } + + /** + * Prepares fetch request to be executed on this RRD. Use returned + * FetchRequest object and its {@link org.rrd4j.core.FetchRequest#fetchData() fetchData()} + * method to actually fetch data from this RRD. Data will be fetched with the smallest + * possible resolution (see RRDTool's + * rrdfetch man page + * for the explanation of the resolution parameter). + * + * @param consolFun Consolidation function to be used in fetch request. + * @param fetchStart Starting timestamp for fetch request. + * @param fetchEnd Ending timestamp for fetch request. + * @return Request object that should be used to actually fetch data from RRD. + */ + public FetchRequest createFetchRequest(ConsolFun consolFun, long fetchStart, long fetchEnd) { + return createFetchRequest(consolFun, fetchStart, fetchEnd, 1); + } + + final synchronized void store(Sample sample) throws IOException { + if (closed) { + throw new IllegalStateException("RRD already closed, cannot store this sample"); + } + long newTime = sample.getTime(); + long lastTime = header.getLastUpdateTime(); + if (lastTime >= newTime) { + throw new IllegalArgumentException("Bad sample time: " + newTime + + ". Last update time was " + lastTime + ", at least one second step is required"); + } + double[] newValues = sample.getValues(); + for (int i = 0; i < datasources.length; i++) { + double newValue = newValues[i]; + datasources[i].process(newTime, newValue); + } + header.setLastUpdateTime(newTime); + } + + synchronized FetchData fetchData(FetchRequest request) throws IOException { + if (closed) { + throw new IllegalStateException("RRD already closed, cannot fetch data"); + } + Archive archive = findMatchingArchive(request); + return archive.fetchData(request); + } + + /** + * findMatchingArchive. + * + * @param request a {@link org.rrd4j.core.FetchRequest} object. + * @return a {@link org.rrd4j.core.Archive} object. + * @throws java.io.IOException if any. + */ + public Archive findMatchingArchive(FetchRequest request) throws IOException { + ConsolFun consolFun = request.getConsolFun(); + long fetchStart = request.getFetchStart(); + long fetchEnd = request.getFetchEnd(); + long resolution = request.getResolution(); + Archive bestFullMatch = null; + Archive bestPartialMatch = null; + long bestStepDiff = 0; + long bestMatch = 0; + for (Archive archive : archives) { + if (archive.getConsolFun() == consolFun) { + long arcStep = archive.getArcStep(); + long arcStart = archive.getStartTime() - arcStep; + long fullMatch = fetchEnd - fetchStart; + // we need step difference in either full or partial case + long tmpStepDiff = Math.abs(archive.getArcStep() - resolution); + if (arcStart <= fetchStart) { + // best full match + if (bestFullMatch == null || tmpStepDiff < bestStepDiff) { + bestStepDiff = tmpStepDiff; + bestFullMatch = archive; + } + } else { + // best partial match + long tmpMatch = fullMatch; + tmpMatch -= (arcStart - fetchStart); + if (bestPartialMatch == null || + bestMatch < tmpMatch || + (bestMatch == tmpMatch && tmpStepDiff < bestStepDiff)) { + bestPartialMatch = archive; + bestMatch = tmpMatch; + } + } + } + } + if (bestFullMatch != null) { + return bestFullMatch; + } else if (bestPartialMatch != null) { + return bestPartialMatch; + } else { + throw new IllegalStateException("RRD file does not contain RRA: " + consolFun + " archive"); + } + } + + /** + * Finds the archive that best matches to the start time (time period being start-time until now) + * and requested resolution. + * + * @param consolFun Consolidation function of the datasource. + * @param startTime Start time of the time period in seconds. + * @param resolution Requested fetch resolution. + * @return Reference to the best matching archive. + * @throws java.io.IOException Thrown in case of I/O related error. + */ + public Archive findStartMatchArchive(String consolFun, long startTime, long resolution) throws IOException { + long arcStep; + long diff; + int fallBackIndex = 0; + int arcIndex = -1; + long minDiff = Long.MAX_VALUE; + long fallBackDiff = Long.MAX_VALUE; + + for (int i = 0; i < archives.length; i++) { + if (archives[i].getConsolFun().toString().equals(consolFun)) { + arcStep = archives[i].getArcStep(); + diff = Math.abs(resolution - arcStep); + + // Now compare start time, see if this archive encompasses the requested interval + if (startTime >= archives[i].getStartTime()) { + if (diff == 0) // Best possible match either way + { + return archives[i]; + } else if (diff < minDiff) { + minDiff = diff; + arcIndex = i; + } + } else if (diff < fallBackDiff) { + fallBackDiff = diff; + fallBackIndex = i; + } + } + } + + return (arcIndex >= 0 ? archives[arcIndex] : archives[fallBackIndex]); + } + + /** + * Returns string representing complete internal RRD state. The returned + * string can be printed to stdout and/or used for debugging purposes. + * + * @return String representing internal RRD state. + * @throws java.io.IOException Thrown in case of I/O related error. + */ + public synchronized String dump() throws IOException { + StringBuilder buffer = new StringBuilder(); + buffer.append(header.dump()); + for (Datasource datasource : datasources) { + buffer.append(datasource.dump()); + } + for (Archive archive : archives) { + buffer.append(archive.dump()); + } + return buffer.toString(); + } + + final void archive(Datasource datasource, double value, double lastValue, long numUpdates) throws IOException { + int dsIndex = getDsIndex(datasource.getName()); + for (Archive archive : archives) { + if (ConsolFun.AVERAGE.equals(archive.getConsolFun())) { + archive.archive(dsIndex, value, numUpdates); + } else { + archive.archive(dsIndex, lastValue, numUpdates); + } + } + } + + /** + * Returns internal index number for the given datasource name. + * + * @param dsName Data source name. + * @return Internal index of the given data source name in this RRD. + * @throws java.io.IOException Thrown in case of I/O error. + */ + public int getDsIndex(String dsName) throws IOException { + for (int i = 0; i < datasources.length; i++) { + if (datasources[i].getName().equals(dsName)) { + return i; + } + } + throw new IllegalArgumentException("Unknown datasource name: " + dsName); + } + + /** + * Checks presence of a specific datasource. + * + * @param dsName Datasource name to check + * @return true if datasource is present in this RRD, false otherwise + * @throws java.io.IOException Thrown in case of I/O error. + */ + public boolean containsDs(String dsName) throws IOException { + for (Datasource datasource : datasources) { + if (datasource.getName().equals(dsName)) { + return true; + } + } + return false; + } + + Datasource[] getDatasources() { + return datasources; + } + + Archive[] getArchives() { + return archives; + } + + /** + * Writes the RRD content to OutputStream using XML format. This format + * is fully compatible with RRDTool's XML dump format and can be used for conversion + * purposes or debugging. + * + * @param destination Output stream to receive XML data + * @throws java.io.IOException Thrown in case of I/O related error + */ + public synchronized void dumpXml(OutputStream destination) throws IOException { + XmlWriter writer = new XmlWriter(destination); + writer.startTag("rrd"); + // dump header + header.appendXml(writer); + // dump datasources + for (Datasource datasource : datasources) { + datasource.appendXml(writer); + } + // dump archives + for (Archive archive : archives) { + archive.appendXml(writer); + } + writer.closeTag(); + writer.flush(); + } + + /** + * This method is just an alias for {@link #dumpXml(OutputStream) dumpXml} method. + * + * @param destination a {@link java.io.OutputStream} object. + * @throws java.io.IOException Thrown in case of I/O related error + */ + public synchronized void exportXml(OutputStream destination) throws IOException { + dumpXml(destination); + } + + /** + * Returns string representing internal RRD state in XML format. This format + * is fully compatible with RRDTool's XML dump format and can be used for conversion + * purposes or debugging. + * + * @return Internal RRD state in XML format. + * @throws java.io.IOException Thrown in case of I/O related error + */ + public synchronized String getXml() throws IOException { + ByteArrayOutputStream destination = new ByteArrayOutputStream(XML_BUFFER_CAPACITY); + dumpXml(destination); + return destination.toString(); + } + + /** + * This method is just an alias for {@link #getXml() getXml} method. + * + * @return Internal RRD state in XML format. + * @throws java.io.IOException Thrown in case of I/O related error + */ + public synchronized String exportXml() throws IOException { + return getXml(); + } + + /** + * Dumps internal RRD state to XML file. + * Use this XML file to convert your Rrd4j RRD to RRDTool format. + * + *

Suppose that you have a Rrd4j RRD file original.rrd and you want + * to convert it to RRDTool format. First, execute the following java code:

+ * + * RrdDb rrd = new RrdDb("original.rrd"); + * rrd.dumpXml("original.xml"); + *

+ * Use original.xml file to create the corresponding RRDTool file + * (from your command line): + * + * rrdtool restore copy.rrd original.xml + * + * @param filename Path to XML file which will be created. + * @throws java.io.IOException Thrown in case of I/O related error. + */ + public synchronized void dumpXml(String filename) throws IOException { + try (OutputStream outputStream = new FileOutputStream(filename, false)) { + dumpXml(outputStream); + } + } + + /** + * This method is just an alias for {@link #dumpXml(String) dumpXml(String)} method. + * + * @param filename a {@link java.lang.String} object. + * @throws java.io.IOException Thrown in case of I/O related error + */ + public synchronized void exportXml(String filename) throws IOException { + dumpXml(filename); + } + + /** + * Returns time of last update operation as timestamp (in seconds). + * + * @return Last update time (in seconds). + * @throws java.io.IOException if any. + */ + public synchronized long getLastUpdateTime() throws IOException { + return header.getLastUpdateTime(); + } + + /** + *

Returns RRD definition object which can be used to create new RRD + * with the same creation parameters but with no data in it.

+ *

Example:

+ *
+     * RrdDb rrd1 = new RrdDb("original.rrd");
+     * RrdDef def = rrd1.getRrdDef();
+     * // fix path
+     * def.setPath("empty_copy.rrd");
+     * // create new RRD file
+     * RrdDb rrd2 = new RrdDb(def);
+     * 
+ * + * @return RRD definition. + * @throws java.io.IOException if any. + */ + public synchronized RrdDef getRrdDef() throws IOException { + // set header + long startTime = header.getLastUpdateTime(); + long step = header.getStep(); + int version = header.getVersion(); + String path = backend.getPath(); + RrdDef rrdDef = new RrdDef(path, startTime, step, version); + // add datasources + for (Datasource datasource : datasources) { + DsDef dsDef = new DsDef(datasource.getName(), + datasource.getType(), datasource.getHeartbeat(), + datasource.getMinValue(), datasource.getMaxValue()); + rrdDef.addDatasource(dsDef); + } + // add archives + for (Archive archive : archives) { + ArcDef arcDef = new ArcDef(archive.getConsolFun(), + archive.getXff(), archive.getSteps(), archive.getRows()); + rrdDef.addArchive(arcDef); + } + return rrdDef; + } + + /** + * {@inheritDoc} + *

+ * Copies object's internal state to another RrdDb object. + */ + public synchronized void copyStateTo(RrdDb otherRrd) throws IOException { + header.copyStateTo(otherRrd.header); + for (int i = 0; i < datasources.length; i++) { + int j = Util.getMatchingDatasourceIndex(this, i, otherRrd); + if (j >= 0) { + datasources[i].copyStateTo(otherRrd.datasources[j]); + } + } + for (int i = 0; i < archives.length; i++) { + int j = Util.getMatchingArchiveIndex(this, i, otherRrd); + if (j >= 0) { + archives[i].copyStateTo(otherRrd.archives[j]); + } + } + } + + /** + * Returns Datasource object corresponding to the given datasource name. + * + * @param dsName Datasource name + * @return Datasource object corresponding to the give datasource name or null if not found. + * @throws java.io.IOException Thrown in case of I/O error + */ + public Datasource getDatasource(String dsName) throws IOException { + try { + return getDatasource(getDsIndex(dsName)); + } catch (IllegalArgumentException e) { + return null; + } + } + + /** + * Returns index of Archive object with the given consolidation function and the number + * of steps. Exception is thrown if such archive could not be found. + * + * @param consolFun Consolidation function + * @param steps Number of archive steps + * @return Requested Archive object + * @throws java.io.IOException Thrown in case of I/O error + */ + public int getArcIndex(ConsolFun consolFun, int steps) throws IOException { + for (int i = 0; i < archives.length; i++) { + if (archives[i].getConsolFun() == consolFun && archives[i].getSteps() == steps) { + return i; + } + } + throw new IllegalArgumentException("Could not find archive " + consolFun + "/" + steps); + } + + /** + * Returns Archive object with the given consolidation function and the number + * of steps. + * + * @param consolFun Consolidation function + * @param steps Number of archive steps + * @return Requested Archive object or null if no such archive could be found + * @throws java.io.IOException Thrown in case of I/O error + */ + public Archive getArchive(ConsolFun consolFun, int steps) throws IOException { + try { + return getArchive(getArcIndex(consolFun, steps)); + } catch (IllegalArgumentException e) { + return null; + } + } + + /** + * Returns canonical path to the underlying RRD file. Note that this method makes sense just for + * ordinary RRD files created on the disk - an exception will be thrown for RRD objects created in + * memory or with custom backends. + * + * @return Canonical path to RRD file; + * @throws java.io.IOException Thrown in case of I/O error or if the underlying backend is + * not derived from RrdFileBackend. + */ + public String getCanonicalPath() throws IOException { + if (backend instanceof RrdFileBackend) { + return ((RrdFileBackend) backend).getCanonicalPath(); + } else { + throw new RrdBackendException("The underlying backend has no canonical path"); + } + } + + /** + * Returns the path to this RRD. + * + * @return Path to this RRD. + */ + public String getPath() { + return backend.getPath(); + } + + /** + * Returns the URI to this RRD, as seen by the backend. + * + * @return URI to this RRD. + */ + public URI getUri() { + return backend.getUri(); + } + + /** + * Returns backend object for this RRD which performs actual I/O operations. + * + * @return RRD backend for this RRD. + */ + public RrdBackend getRrdBackend() { + return backend; + } + + /** + * Required to implement RrdUpdater interface. You should never call this method directly. + * + * @return Allocator object + */ + public RrdAllocator getRrdAllocator() { + return allocator; + } + + /** + * Returns an array of bytes representing the whole RRD. + * + * @return All RRD bytes + * @throws java.io.IOException Thrown in case of I/O related error. + */ + public synchronized byte[] getBytes() throws IOException { + return backend.readAll(); + } + + /** + * Sets default backend factory to be used. This method is just an alias for + * {@link org.rrd4j.core.RrdBackendFactory#setDefaultFactory(String)}. + * + * @param factoryName Name of the backend factory to be set as default. + * @throws java.lang.IllegalArgumentException Thrown if invalid factory name is supplied, or not called + * before the first backend object (before the first RrdDb object) is created. + * @deprecated uses {@link RrdBackendFactory#setActiveFactories(RrdBackendFactory...)} instead. + */ + @Deprecated + public static void setDefaultFactory(String factoryName) { + RrdBackendFactory.setDefaultFactory(factoryName); + } + + /** + * Returns an array of last datasource values. The first value in the array corresponds + * to the first datasource defined in the RrdDb and so on. + * + * @return Array of last datasource values + * @throws java.io.IOException Thrown in case of I/O error + */ + public synchronized double[] getLastDatasourceValues() throws IOException { + double[] values = new double[datasources.length]; + for (int i = 0; i < values.length; i++) { + values[i] = datasources[i].getLastValue(); + } + return values; + } + + /** + * Returns the last stored value for the given datasource. + * + * @param dsName Datasource name + * @return Last stored value for the given datasource + * @throws java.io.IOException Thrown in case of I/O error + * @throws java.lang.IllegalArgumentException Thrown if no datasource in this RrdDb matches the given datasource name + */ + public synchronized double getLastDatasourceValue(String dsName) throws IOException { + int dsIndex = getDsIndex(dsName); + return datasources[dsIndex].getLastValue(); + } + + /** + * Returns the number of datasources defined in the file + * + * @return The number of datasources defined in the file + */ + public int getDsCount() { + return datasources.length; + } + + /** + * Returns the number of RRA archives defined in the file + * + * @return The number of RRA archives defined in the file + */ + public int getArcCount() { + return archives.length; + } + + /** + * Returns the last time when some of the archives in this RRD was updated. This time is not the + * same as the {@link #getLastUpdateTime()} since RRD file can be updated without updating any of + * the archives. + * + * @return last time when some of the archives in this RRD was updated + * @throws java.io.IOException Thrown in case of I/O error + */ + public long getLastArchiveUpdateTime() throws IOException { + long last = 0; + for (Archive archive : archives) { + last = Math.max(last, archive.getEndTime()); + } + return last; + } + + /** + * getInfo. + * + * @return a {@link java.lang.String} object. + * @throws java.io.IOException if any. + */ + public synchronized String getInfo() throws IOException { + return header.getInfo(); + } + + /** + * setInfo. + * + * @param info a {@link java.lang.String} object. + * @throws java.io.IOException if any. + */ + public synchronized void setInfo(String info) throws IOException { + header.setInfo(info); + } + + /** + * main. + * + * @param args an array of {@link java.lang.String} objects. + */ + public static void main(String[] args) { + System.out.println("RRD4J :: RRDTool choice for the Java world"); + System.out.println("==============================================================================="); + System.out.println("RRD4J base directory: " + Util.getRrd4jHomeDirectory()); + long time = Util.getTime(); + System.out.println("Current time: " + time + ": " + new Date(time * 1000L)); + System.out.println("-------------------------------------------------------------------------------"); + System.out.println("See https://github.com/rrd4j/rrd4j for more information and the latest version."); + System.out.println("Copyright 2017 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."); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdDbPool.java b/apps/jrobin/java/src/org/rrd4j/core/RrdDbPool.java new file mode 100644 index 0000000000..292338162b --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdDbPool.java @@ -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; + +/** + *

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). + *

+ *

It should not be called directly. Use {@link RrdDb.Builder#usePool()} instead.

+ */ +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 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 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 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); + } + } + + /** + *

Requests a RrdDb reference for the given RRD file path.

+ *
    + *
  • If the file is already open, previously returned RrdDb reference will be returned. Its usage count + * will be incremented by one. + *
  • 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. + *
+ *

The path is transformed internally to URI using the default factory, that is the reference that will + * be used elsewhere.

+ * + * @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); + } + + /** + *

Requests a RrdDb reference for the given RRD file path.

+ *
    + *
  • If the file is already open, previously returned RrdDb reference will be returned. Its usage count + * will be incremented by one. + *
  • 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. + *
+ * + * @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; + } + + /** + *

Requests a RrdDb reference for the given RRD file definition object.

+ *
    + *
  • If the file with the path specified in the RrdDef object is already open, + * the method blocks until the file is closed. + *
  • 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. + *
+ * + * @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); + } + } + } + + /** + *

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).

+ *
    + *
  • If the file with the path specified is already open, + * the method blocks until the file is closed. + *
  • 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. + *
+ *

The path is transformed internally to URI using the default factory, that is the reference that will + * be used elsewhere.

+ * + * @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); + } + + /** + *

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).

+ *
    + *
  • If the file with the path specified is already open, + * the method blocks until the file is closed. + *
  • 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. + *
+ *

The path is transformed internally to URI using the default factory, that is the reference that will + * be used elsewhere.

+ * + * @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); + } + } + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdDef.java b/apps/jrobin/java/src/org/rrd4j/core/RrdDef.java new file mode 100644 index 0000000000..0bfcfb972b --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdDef.java @@ -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; + +/** + *

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 RrdDb constructor + * argument (see documentation for {@link org.rrd4j.core.RrdDb RrdDb} class). RrdDef + * object does not actually create new RRD. It just holds all necessary + * information which will be used during the actual creation process.

+ * + *

RRD definition (RrdDef object) consists of the following elements:

+ * + *
    + *
  • path to RRD that will be created + *
  • starting timestamp + *
  • step + *
  • version, 1 for linear disposition of archives, 2 for matrix disposition + *
  • one or more datasource definitions + *
  • one or more archive definitions + *
+ *

RrdDef provides API to set all these elements. For the complete explanation of all + * RRD definition parameters, see RRDTool's + * rrdcreate man page.

+ * + * @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 DEFAULTVERSION=2 */ + 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 dsDefs = new ArrayList<>(); + private List arcDefs = new ArrayList<>(); + + /** + *

Creates new RRD definition object with the given path. + * When this object is passed to + * RrdDb constructor, new RRD will be created using the + * specified path.

+ *

The will be transformed internally to an URI using the default backend factory.

+ * + * @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 + * RrdDb constructor, new RRD will be created using the + * specified path. + * + * @param uri URI to the new RRD. + */ + public RrdDef(URI uri) { + this.uri = uri; + } + + /** + *

Creates new RRD definition object with the given path and step.

+ *

The will be transformed internally to an URI using the default backend factory.

+ * + * @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; + } + + /** + *

Creates new RRD definition object with the given path, starting timestamp + * and step.

+ *

The will be transformed internally to an URI using the default backend factory.

+ * + * @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; + } + + /** + *

Creates new RRD definition object with the given path, starting timestamp, + * step and version.

+ *

The will be transformed internally to an URI using the default backend factory.

+ * + * @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; + } + + /** + *

Sets path to RRD.

+ *

The will be transformed internally to an URI using the default backend factory.

+ * + * @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 DsDef. + * + * @param dsDef Datasource definition. + */ + public void addDatasource(DsDef dsDef) { + if (dsDefs.contains(dsDef)) { + throw new IllegalArgumentException("Datasource already defined: " + dsDef.dump()); + } + dsDefs.add(dsDef); + } + + /** + *

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 + * rrdcreate man page.

+ *

IMPORTANT NOTE: 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 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 Double.NaN if unknown. + * @param maxValue Maximal acceptable value. Use Double.NaN 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)); + } + + /** + *

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:

+ *
+     * DS:name:type:heartbeat:minValue:maxValue
+     * 
+ *

For example:

+ *
+     * DS:input:COUNTER:600:0:U
+     * 
+ *

For more information on datasource definition parameters see rrdcreate + * man page.

+ * + * @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 ArcDef. + * + * @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 + * rrdcreate man page. + * + * @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)); + } + + /** + *

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:

+ *
+     * RRA:consolidationFunction:XFilesFactor:steps:rows
+     * 
+ *

For example:

+ *
+     * RRA:AVERAGE:0.5:10:1000
+     * 
+ *

For more information on archive definition parameters see rrdcreate + * man page.

+ * + * @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 create command. + * + * @return Dumped content of RrdDb 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 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); + } + + /** + *

Exports RrdDef object to output stream in XML format. Generated XML code can be parsed + * with {@link org.rrd4j.core.RrdDefTemplate} class.

+ *

It use a format compatible with previous RRD4J's version, using + * a path, instead of an URI.

+ * + * @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. + *

If compatible is set to true, it returns an XML compatible with previous RRD4J's versions, using + * a path, instead of an URI.

+ * + * @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(); + } + + /** + *

Exports RrdDef object to string in XML format. Generated XML string can be parsed + * with {@link org.rrd4j.core.RrdDefTemplate} class.

+ *

If compatible is set to true, it returns an XML compatible with previous RRD4J's versions, using + * a path, instead of an URI.

+ * + * + * @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(); + } + + /** + *

Exports RrdDef object to string in XML format. Generated XML string can be parsed + * with {@link org.rrd4j.core.RrdDefTemplate} class.

+ *

It use a format compatible with previous RRD4J's version, using + * a path, instead of an URI.

+ * + * @return XML formatted string representing this RrdDef object + */ + public String exportXmlTemplate() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + exportXmlTemplate(out); + return out.toString(); + } + + /** + *

Exports RrdDef object to a file in XML format. Generated XML code can be parsed + * with {@link org.rrd4j.core.RrdDefTemplate} class.

+ *

It use a format compatible with previous RRD4J's version, using + * a path, instead of an URI.

+ * + * @param filePath Path to the file + * @throws java.io.IOException if any. + */ + public void exportXmlTemplate(String filePath) throws IOException { + exportXmlTemplate(filePath, true); + } + + /** + *

Exports RrdDef object to a file in XML format. Generated XML code can be parsed + * with {@link org.rrd4j.core.RrdDefTemplate} class.

+ *

If compatible is set to true, it returns an XML compatible with previous RRD4J versions, using + * a path, instead of an URI.

+ * + * @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} + * + *

Compares the current RrdDef with another. RrdDefs are considered equal if:

+ *
    + *
  • RRD steps match + *
  • all datasources have exactly the same definition in both RrdDef objects (datasource names, + * types, heartbeat, min and max values must match) + *
  • all archives have exactly the same definition in both RrdDef objects (archive consolidation + * functions, X-file factors, step and row counts must match) + *
+ */ + 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; + } + + /** + *

hasDatasources.

+ * + * @return a boolean. + */ + public boolean hasDatasources() { + return !dsDefs.isEmpty(); + } + + /** + *

hasArchives.

+ * + * @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(); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdDefTemplate.java b/apps/jrobin/java/src/org/rrd4j/core/RrdDefTemplate.java new file mode 100644 index 0000000000..99f8665eab --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdDefTemplate.java @@ -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; + +/** + *

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.

+ * + *

Here is an example of a properly formatted XML template with all available + * options in it (unwanted options can be removed):

+ *
+ * <rrd_def>
+ *     <path>test.rrd</path>
+ *     <!-- not mandatory -->
+ *     <start>1000123456</start>
+ *     <!-- not mandatory -->
+ *     <step>300</step>
+ *     <!-- at least one datasource must be supplied -->
+ *     <datasource>
+ *         <name>input</name>
+ *         <type>COUNTER</type>
+ *         <heartbeat>300</heartbeat>
+ *         <min>0</min>
+ *         <max>U</max>
+ *     </datasource>
+ *     <datasource>
+ *         <name>temperature</name>
+ *         <type>GAUGE</type>
+ *         <heartbeat>400</heartbeat>
+ *         <min>U</min>
+ *         <max>1000</max>
+ *     </datasource>
+ *     <!-- at least one archive must be supplied -->
+ *     <archive>
+ *         <cf>AVERAGE</cf>
+ *         <xff>0.5</xff>
+ *         <steps>1</steps>
+ *         <rows>600</rows>
+ *     </archive>
+ *     <archive>
+ *         <cf>MAX</cf>
+ *         <xff>0.6</xff>
+ *         <steps>6</steps>
+ *         <rows>7000</rows>
+ *     </archive>
+ * </rrd_def>
+ * 
+ *

Notes on the template syntax:

+ *
    + *
  • 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. + *
  • 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) + *
  • whitespaces are not harmful + *
  • floating point values: anything that cannot be parsed will be treated as Double.NaN + * (like: U, unknown, 12r.23) + *
  • comments are allowed. + *
+ *

Any template value (text between <some_tag> and + * </some_tag>) can be replaced with + * a variable of the following form: ${variable_name}. Use + * {@link org.rrd4j.core.XmlTemplate#setVariable(String, String) setVariable()} + * methods from the base class to replace template variables with real values + * at runtime.

+ * + *

Typical usage scenario:

+ *
    + *
  • Create your XML template and save it to a file (template.xml, for example) + *
  • 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: + *
    + * <rrd_def>
    + *     <path>${path}</path>
    + *     <step>300</step>
    + *     ...
    + * 
    + *
  • In your Java code, create RrdDefTemplate object using your XML template file: + *
    + * RrdDefTemplate t = new RrdDefTemplate(new File(template.xml));
    + * 
    + *
  • Then, specify real values for template variables: + *
    + * t.setVariable("path", "demo/test.rrd");
    + * 
    + *
  • 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: + *
    + * RrdDef def = t.getRrdDef();
    + * RrdDb rrd = new RrdDb(def);
    + * rrd.close();
    + * 
    + *
+ * 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.

+ * + * @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 "); + } + 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; + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdDouble.java b/apps/jrobin/java/src/org/rrd4j/core/RrdDouble.java new file mode 100644 index 0000000000..182fa7e608 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdDouble.java @@ -0,0 +1,41 @@ +package org.rrd4j.core; + +import java.io.IOException; + +class RrdDouble> extends RrdPrimitive { + private double cache; + private boolean cached = false; + + RrdDouble(RrdUpdater updater, boolean isConstant) throws IOException { + super(updater, RrdDouble.RRD_DOUBLE, isConstant); + } + + RrdDouble(RrdUpdater 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; + } + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdDoubleArray.java b/apps/jrobin/java/src/org/rrd4j/core/RrdDoubleArray.java new file mode 100644 index 0000000000..3d0cad94cc --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdDoubleArray.java @@ -0,0 +1,35 @@ +package org.rrd4j.core; + +import java.io.IOException; + +class RrdDoubleArray> extends RrdPrimitive { + private int length; + + RrdDoubleArray(RrdUpdater 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); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdDoubleMatrix.java b/apps/jrobin/java/src/org/rrd4j/core/RrdDoubleMatrix.java new file mode 100644 index 0000000000..1898c2ff66 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdDoubleMatrix.java @@ -0,0 +1,79 @@ +package org.rrd4j.core; + +import java.io.IOException; + +class RrdDoubleMatrix> extends RrdPrimitive { + private static final String LENGTH = ", length="; + private final int rows; + private final int columns; + + RrdDoubleMatrix(RrdUpdater 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); + } + + /** + *

set.

+ * + * @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; + } + + /** + *

Getter for the field columns.

+ * + * @return a int. + */ + public int getColumns() { + return columns; + } + + /** + *

Getter for the field rows.

+ * + * @return a int. + */ + public int getRows() { + return rows; + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdEnum.java b/apps/jrobin/java/src/org/rrd4j/core/RrdEnum.java new file mode 100644 index 0000000000..e84de7d7ca --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdEnum.java @@ -0,0 +1,46 @@ +package org.rrd4j.core; + +import java.io.IOException; + +class RrdEnum, E extends Enum> extends RrdPrimitive { + + private E cache; + private final Class clazz; + + RrdEnum(RrdUpdater updater, boolean isConstant, Class clazz) throws IOException { + super(updater, RrdPrimitive.RRD_STRING, isConstant); + this.clazz = clazz; + } + + RrdEnum(RrdUpdater updater, Class 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(); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdException.java b/apps/jrobin/java/src/org/rrd4j/core/RrdException.java new file mode 100644 index 0000000000..fe8c065907 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdException.java @@ -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); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdFileBackend.java b/apps/jrobin/java/src/org/rrd4j/core/RrdFileBackend.java new file mode 100644 index 0000000000..dd7aa0601b --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdFileBackend.java @@ -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. + *

+ * 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; + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdFileBackendFactory.java b/apps/jrobin/java/src/org/rrd4j/core/RrdFileBackendFactory.java new file mode 100644 index 0000000000..0664a46a21 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdFileBackendFactory.java @@ -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. + *

+ * 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(); + } + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdInt.java b/apps/jrobin/java/src/org/rrd4j/core/RrdInt.java new file mode 100644 index 0000000000..6a081c2145 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdInt.java @@ -0,0 +1,41 @@ +package org.rrd4j.core; + +import java.io.IOException; + +class RrdInt> extends RrdPrimitive { + private int cache; + private boolean cached = false; + + RrdInt(RrdUpdater updater, boolean isConstant) throws IOException { + super(updater, RrdPrimitive.RRD_INT, isConstant); + } + + RrdInt(RrdUpdater 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; + } + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdLong.java b/apps/jrobin/java/src/org/rrd4j/core/RrdLong.java new file mode 100644 index 0000000000..3f81e991a1 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdLong.java @@ -0,0 +1,41 @@ +package org.rrd4j.core; + +import java.io.IOException; + +class RrdLong> extends RrdPrimitive { + private long cache; + private boolean cached = false; + + RrdLong(RrdUpdater updater, boolean isConstant) throws IOException { + super(updater, RrdPrimitive.RRD_LONG, isConstant); + } + + RrdLong(RrdUpdater 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; + } + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdMemoryBackend.java b/apps/jrobin/java/src/org/rrd4j/core/RrdMemoryBackend.java new file mode 100644 index 0000000000..bf07edc93e --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdMemoryBackend.java @@ -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; + /** + *

Constructor for RrdMemoryBackend.

+ * + * @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 + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdMemoryBackendFactory.java b/apps/jrobin/java/src/org/rrd4j/core/RrdMemoryBackendFactory.java new file mode 100644 index 0000000000..bb97bf5e10 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdMemoryBackendFactory.java @@ -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. + *

+ * 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 new RrdDb(path) 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 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; + } + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdNioBackend.java b/apps/jrobin/java/src/org/rrd4j/core/RrdNioBackend.java new file mode 100644 index 0000000000..75a1b17251 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdNioBackend.java @@ -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 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(); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdNioBackendFactory.java b/apps/jrobin/java/src/org/rrd4j/core/RrdNioBackendFactory.java new file mode 100644 index 0000000000..c3e448c39d --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdNioBackendFactory.java @@ -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. + *

Managing the thread pool

+ *

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.

+ *

if sync period is negative, no sync thread will be launched

+ * + */ +@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; + } + + /** + *

Setter for the field syncThreadPool.

+ * + * @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; + } + + /** + *

Setter for the field syncThreadPool.

+ * + * @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. + *

+ * 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() {} + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdPrimitive.java b/apps/jrobin/java/src/org/rrd4j/core/RrdPrimitive.java new file mode 100644 index 0000000000..55724a73ea --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdPrimitive.java @@ -0,0 +1,114 @@ +package org.rrd4j.core; + +import java.io.IOException; + +abstract class RrdPrimitive> { + 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 updater, int type, boolean isConstant) throws IOException { + this(updater, type, 1, isConstant); + } + + RrdPrimitive(RrdUpdater 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 readEnum(Class 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 > void writeEnum(E value) throws IOException { + writeString(value.name()); + } + + final boolean isCachingAllowed() { + return cachingAllowed; + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdRandomAccessFileBackend.java b/apps/jrobin/java/src/org/rrd4j/core/RrdRandomAccessFileBackend.java new file mode 100644 index 0000000000..99debd41c8 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdRandomAccessFileBackend.java @@ -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); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdRandomAccessFileBackendFactory.java b/apps/jrobin/java/src/org/rrd4j/core/RrdRandomAccessFileBackendFactory.java new file mode 100644 index 0000000000..1b42958f1e --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdRandomAccessFileBackendFactory.java @@ -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); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdSafeFileBackend.java b/apps/jrobin/java/src/org/rrd4j/core/RrdSafeFileBackend.java new file mode 100644 index 0000000000..010a24d3c0 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdSafeFileBackend.java @@ -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"); + } + + /** + *

close.

+ * + * @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(); + } + } + + /** + *

getLockInfo.

+ * + * @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; + } + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdSafeFileBackendFactory.java b/apps/jrobin/java/src/org/rrd4j/core/RrdSafeFileBackendFactory.java new file mode 100644 index 0000000000..f85040e846 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdSafeFileBackendFactory.java @@ -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; + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdString.java b/apps/jrobin/java/src/org/rrd4j/core/RrdString.java new file mode 100644 index 0000000000..64ef2537ef --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdString.java @@ -0,0 +1,38 @@ +package org.rrd4j.core; + +import java.io.IOException; + +class RrdString> extends RrdPrimitive { + private String cache; + + RrdString(RrdUpdater updater, boolean isConstant) throws IOException { + super(updater, RrdPrimitive.RRD_STRING, isConstant); + } + + RrdString(RrdUpdater 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; + } + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdSyncThreadPool.java b/apps/jrobin/java/src/org/rrd4j/core/RrdSyncThreadPool.java new file mode 100644 index 0000000000..de46ef6c1e --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdSyncThreadPool.java @@ -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()}. + *

+ * 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 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. + *

+ * 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 [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(); + } + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdToolReader.java b/apps/jrobin/java/src/org/rrd4j/core/RrdToolReader.java new file mode 100644 index 0000000000..53c3cad4a0 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdToolReader.java @@ -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; + } + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdToolkit.java b/apps/jrobin/java/src/org/rrd4j/core/RrdToolkit.java new file mode 100644 index 0000000000..270dbce625 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdToolkit.java @@ -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: + * + *

    + *
  • add datasource to a RRD file. + *
  • add archive to a RRD file. + *
  • remove datasource from a RRD file. + *
  • remove archive from a RRD file. + *
+ * + * 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). + *

+ * IMPORTANT: 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 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); + } + } + } + + /** + *

Adds one more datasource to a RRD file.

+ *

WARNING: This method is potentially dangerous! It will modify your RRD file. + * It is highly recommended to preserve the original RRD file (saveBackup + * should be set to true). The backup file will be created in the same + * directory as the original one with .bak extension added to the + * original name.

+ *

Before applying this method, be sure that the specified RRD file is not in use + * (not open)

+ * + * @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); + } + + /** + *

Adds datasources to a RRD file.

+ *

WARNING: This method is potentially dangerous! It will modify your RRD file. + * It is highly recommended to preserve the original RRD file (saveBackup + * should be set to true). The backup file will be created in the same + * directory as the original one with .bak extension added to the + * original name.

+ *

Before applying this method, be sure that the specified RRD file is not in use + * (not open)

+ * + * @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 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); + } + } + } + + /** + *

Removes single datasource from a RRD file.

+ *

WARNING: This method is potentially dangerous! It will modify your RRD file. + * It is highly recommended to preserve the original RRD file (saveBackup + * should be set to true). The backup file will be created in the same + * directory as the original one with .bak extension added to the + * original name.

+ *

Before applying this method, be sure that the specified RRD file is not in use + * (not open)

+ * + * @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); + } + } + } + + /** + *

Adds one more archive to a RRD file.

+ *

WARNING: This method is potentially dangerous! It will modify your RRD file. + * It is highly recommended to preserve the original RRD file (saveBackup + * should be set to true). The backup file will be created in the same + * directory as the original one with .bak extension added to the + * original name.

+ *

Before applying this method, be sure that the specified RRD file is not in use + * (not open)

+ * + * @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); + } + } + } + + /** + *

Removes one archive from a RRD file.

+ *

WARNING: This method is potentially dangerous! It will modify your RRD file. + * It is highly recommended to preserve the original RRD file (saveBackup + * should be set to true). The backup file will be created in the same + * directory as the original one with .bak extension added to the + * original name.

+ *

Before applying this method, be sure that the specified RRD file is not in use + * (not open)

+ * + * @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 true if archived values less than + * newMinValue 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 true if archived values greater than + * newMaxValue 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 true 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 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 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; + } + +} + diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdUpdater.java b/apps/jrobin/java/src/org/rrd4j/core/RrdUpdater.java new file mode 100644 index 0000000000..96b8517a1c --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/RrdUpdater.java @@ -0,0 +1,27 @@ +package org.rrd4j.core; + +import java.io.IOException; + +interface RrdUpdater> { + /** + *

getRrdBackend.

+ * + * @return a {@link org.rrd4j.core.RrdBackend} object. + */ + RrdBackend getRrdBackend(); + + /** + *

copyStateTo.

+ * + * @param updater a {@link org.rrd4j.core.RrdUpdater} object. + * @throws java.io.IOException if any. + */ + void copyStateTo(T updater) throws IOException; + + /** + *

getRrdAllocator.

+ * + * @return a {@link org.rrd4j.core.RrdAllocator} object. + */ + RrdAllocator getRrdAllocator(); +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/Sample.java b/apps/jrobin/java/src/org/rrd4j/core/Sample.java new file mode 100644 index 0000000000..67157772f1 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/Sample.java @@ -0,0 +1,236 @@ +package org.rrd4j.core; + +import java.io.IOException; +import java.util.StringTokenizer; +import java.util.Arrays; + +/** + *

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 + * rrdupdate man page.

+ *

To update a RRD with Rrd4j use the following procedure:

+ *
    + *
  1. Obtain empty Sample object by calling method {@link org.rrd4j.core.RrdDb#createSample(long) + * createSample()} on respective {@link RrdDb RrdDb} object. + *
  2. Adjust Sample timestamp if necessary (see {@link #setTime(long) setTime()} method). + *
  3. Supply data source values (see {@link #setValue(String, double) setValue()}). + *
  4. Call Sample's {@link #update() update()} method. + *
+ *

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 Double.NaN.

+ * + * @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 Sample 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 Sample 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 Sample 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 Sample 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; + } + + /** + *

Sets sample timestamp and data source values in a fashion similar to RRDTool. + * Argument string should be composed in the following way: + * timestamp:value1:value2:...:valueN.

+ *

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'.

+ * + * @param timeAndValues

String made by concatenating sample timestamp with corresponding + * data source values delmited with colons. For example:

+ * + *
+     *                      1005234132:12.2:35.6:U:24.5
+     *                      NOW:12.2:35.6:U:24.5
+     *                      
+ *

'N' stands for the current timestamp (can be replaced with 'NOW')

+ * 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.

+ * @return This Sample 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: + *
+     *     set(timeAndValues);
+     *     update();
+     * 
+ * + * @param timeAndValues String made by concatenating sample timestamp with corresponding + * data source values delmited with colons. For example:
+ * 1005234132:12.2:35.6:U:24.5
+ * NOW:12.2:35.6:U:24.5 + * @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(); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/Util.java b/apps/jrobin/java/src/org/rrd4j/core/Util.java new file mode 100644 index 0000000000..60cf2e6406 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/Util.java @@ -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 MAX_LONG=Long.MAX_VALUE */ + public static final long MAX_LONG = Long.MAX_VALUE; + /** Constant MIN_LONG=-Long.MAX_VALUE */ + public static final long MIN_LONG = -Long.MAX_VALUE; + + /** Constant MAX_DOUBLE=Double.MAX_VALUE */ + public static final double MAX_DOUBLE = Double.MAX_VALUE; + /** Constant MIN_DOUBLE=-Double.MAX_VALUE */ + 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 df = new ThreadLocal() { + @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: + *

+ * (System.currentTimeMillis() + 500L) / 1000L + * + * @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 "step". Rounded value is obtained + * from the following expression: + *

+ * timestamp - timestamp % step; + * + * @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 Math.max() 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 Math.min() 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 Double.isNaN(x)? y: Double.isNaN(y)? x: x + y; + */ + 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 Date 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 Calendar 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 Calendar 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); + } + + /** + *

Parses at-style time specification and returns the corresponding timestamp. For example:

+ *
+     * long t = Util.getTimestamp("now-1d");
+     * 
+ * + * @param atStyleTimeSpec at-style time specification. For the complete explanation of the syntax + * allowed see RRDTool's rrdfetch man page.

+ * @return timestamp in seconds since epoch. + */ + public static long getTimestamp(String atStyleTimeSpec) { + TimeSpec timeSpec = new TimeParser(atStyleTimeSpec).parse(); + return timeSpec.getTimestamp(); + } + + /** + *

Parses two related at-style time specifications and returns corresponding timestamps. For example:

+ *
+     * long[] t = Util.getTimestamps("end-1d","now");
+     * 
+ * + * @param atStyleTimeSpec1 Starting at-style time specification. For the complete explanation of the syntax + * allowed see RRDTool's rrdfetch man page.

+ * @param atStyleTimeSpec2 Ending at-style time specification. For the complete explanation of the syntax + * allowed see RRDTool's rrdfetch man page.

+ * @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 true if the string can be parsed as double, false 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 true, if valueStr equals to 'true', 'on', 'yes', 'y' or '1'; + * false 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 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 + * getLapTime() method call. + */ + public static String getLapTime() { + long newLap = System.currentTimeMillis(); + double seconds = (newLap - lastLap) / 1000.0; + lastLap = newLap; + return "[" + seconds + " sec]"; + } + + /** + *

Returns the root directory of the Rrd4j distribution. Useful in some demo applications, + * probably of no use anywhere else.

+ *

The function assumes that all Rrd4j .class files are placed under + * the <root>/classes subdirectory and that all jars (libraries) are placed in the + * <root>/lib subdirectory (the original Rrd4j directory structure).

+ * + * @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 false + * + * @param x the first value + * @param y the second value + * @return true if x and y are both equal to Double.NaN, or if x == y. false 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 true if file exists, false 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); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/XmlReader.java b/apps/jrobin/java/src/org/rrd4j/core/XmlReader.java new file mode 100644 index 0000000000..fe8bf8ff3d --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/XmlReader.java @@ -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; + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/XmlTemplate.java b/apps/jrobin/java/src/org/rrd4j/core/XmlTemplate.java new file mode 100644 index 0000000000..e99665d561 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/XmlTemplate.java @@ -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 ${variable_name}. + * 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 valueMap = new HashMap<>(); + private HashSet validatedNodes = new HashSet<>(); + + /** + *

Constructor for XmlTemplate.

+ * + * @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); + } + + /** + *

Constructor for XmlTemplate.

+ * + * @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); + } + + /** + *

Constructor for XmlTemplate.

+ * + * @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 + * ${start}, specify start for the name 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 + * ${start}, specify start for the name 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 + * ${start}, specify start for the name 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 + * ${start}, specify start for the name 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 + * ${start}, specify start for the name 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 + * ${start}, specify start for the name 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 + * ${start}, specify start for the name 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 + * ${start}, specify start for the name 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 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()]); + } + + /** + *

getChildNodes.

+ * + * @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); + } + + /** + *

getChildNodes.

+ * + * @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); + } + + /** + *

getFirstChildNode.

+ * + * @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); + } + + /** + *

hasChildNode.

+ * + * @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); + } + + /** + *

getChildValue.

+ * + * @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); + } + + /** + *

getChildValue.

+ * + * @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); + } + + /** + *

getValue.

+ * + * @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); + } + + /** + *

getValue.

+ * + * @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); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/XmlWriter.java b/apps/jrobin/java/src/org/rrd4j/core/XmlWriter.java new file mode 100644 index 0000000000..f52fd31937 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/XmlWriter.java @@ -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 ISOLIKE = new ThreadLocal() { + @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 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 + ""); + } + + /** + * Writes <tag>value</tag> to output stream + * + * @param tag XML tag name + * @param value value to be placed between <tag> and </tag> + */ + public void writeTag(String tag, Object value) { + if (value != null) { + writer.println(indent + "<" + tag + ">" + + escape(value.toString()) + ""); + } + else { + writer.println(indent + "<" + tag + ">"); + } + } + + /** + * Writes <tag>value</tag> to output stream + * + * @param tag XML tag name + * @param value value to be placed between <tag> and </tag> + */ + public void writeTag(String tag, int value) { + writeTag(tag, Integer.toString(value)); + } + + /** + * Writes <tag>value</tag> to output stream + * + * @param tag XML tag name + * @param value value to be placed between <tag> and </tag> + */ + public void writeTag(String tag, long value) { + writeTag(tag, Long.toString(value)); + } + + /** + * Writes <tag>value</tag> to output stream + * + * @param tag XML tag name + * @param value value to be placed between <tag> and </tag> + * @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 <tag>value</tag> to output stream + * + * @param tag XML tag name + * @param value value to be placed between <tag> and </tag> + */ + public void writeTag(String tag, double value) { + writeTag(tag, Util.formatDouble(value, true)); + } + + /** + * Writes <tag>value</tag> to output stream + * + * @param tag XML tag name + * @param value value to be placed between <tag> and </tag> + */ + public void writeTag(String tag, boolean value) { + writeTag(tag, Boolean.toString(value)); + } + + /** + * Writes <tag>value</tag> to output stream + * + * @param tag XML tag name + * @param value value to be placed between <tag> and </tag> + */ + public void writeTag(String tag, Color value) { + int rgb = value.getRGB() & 0xFFFFFF; + writeTag(tag, "#" + Integer.toHexString(rgb).toUpperCase()); + } + + /** + * Writes <tag>value</tag> to output stream + * + * @param tag XML tag name + * @param value value to be placed between <tag> and </tag> + */ + 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 <tag>value</tag> to output stream + * + * @param tag XML tag name + * @param value value to be placed between <tag> and </tag> + */ + 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 + ""); + } + + private static String escape(String s) { + return s.replaceAll("<", "<").replaceAll(">", ">"); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/jrrd/Archive.java b/apps/jrobin/java/src/org/rrd4j/core/jrrd/Archive.java new file mode 100644 index 0000000000..c0b34b92bf --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/jrrd/Archive.java @@ -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 Ciaran Treanor + * @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 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(); + + for (int i = 0; i < numBlocks; i++) { + cdpStatusBlocks.add(new CDPStatusBlock(file)); + } + } + + /** + * Returns the CDPStatusBlock at the specified position in this archive. + * + * @param index index of CDPStatusBlock to return. + * @return the CDPStatusBlock 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 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 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"); + s.print("\t\t "); + s.print(type); + s.println(" "); + s.print("\t\t "); + s.print(pdpCount); + s.print(" "); + s.print("\t\t "); + s.print(xff); + s.println(" "); + s.println(); + s.println("\t\t"); + + for (int i = 0; i < cdpStatusBlocks.size(); i++) { + cdpStatusBlocks.get(i).toXml(s); + } + + s.println("\t\t"); + s.println("\t\t"); + + 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.println(""); + for (int col = 0; col < db.header.dsCount; col++) { + s.print(" "); + + 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(" "); + } + + s.println(""); + } + + s.println("\t\t"); + s.println("\t"); + } + catch (IOException e) { // Is the best thing to do here? + throw new RuntimeException(e.getMessage()); + } + } + + /** + *

Getter for the field values.

+ * + * @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(); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/jrrd/CDPStatusBlock.java b/apps/jrobin/java/src/org/rrd4j/core/jrrd/CDPStatusBlock.java new file mode 100644 index 0000000000..c9c82b4360 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/jrrd/CDPStatusBlock.java @@ -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 Ciaran Treanor + * @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 "); + s.print(value); + s.print(" "); + s.print(unknownDatapoints); + s.println(" "); + } + + /** + * 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(); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/jrrd/ConsolidationFunctionType.java b/apps/jrobin/java/src/org/rrd4j/core/jrrd/ConsolidationFunctionType.java new file mode 100644 index 0000000000..632f6dca73 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/jrrd/ConsolidationFunctionType.java @@ -0,0 +1,74 @@ +package org.rrd4j.core.jrrd; + +import org.rrd4j.ConsolFun; + +/** + * Class ConsolidationFunctionType + * + * @author Ciaran Treanor + * @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(); +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/jrrd/Constants.java b/apps/jrobin/java/src/org/rrd4j/core/jrrd/Constants.java new file mode 100644 index 0000000000..5b8ccc5e3a --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/jrrd/Constants.java @@ -0,0 +1,26 @@ +package org.rrd4j.core.jrrd; + +interface Constants { + /** Constant DS_NAM_SIZE=20 */ + final int DS_NAM_SIZE = 20; + /** Constant DST_SIZE=20 */ + final int DST_SIZE = 20; + /** Constant CF_NAM_SIZE=20 */ + final int CF_NAM_SIZE = 20; + /** Constant LAST_DS_LEN=30 */ + final int LAST_DS_LEN = 30; + /** Constant COOKIE="RRD" */ + static final String COOKIE = "RRD"; + /** Constant MAX_SUPPORTED_VERSION=3 */ + public static final int MAX_SUPPORTED_VERSION = 3; + /** Constant UNDEFINED_VERSION="UNDEF" */ + public static final String UNDEFINED_VERSION = "UNDEF"; + /** Constant UNDEFINED_VERSION_AS_INT=-1 */ + public static final int UNDEFINED_VERSION_AS_INT = -1; + /** Constant VERSION_WITH_LAST_UPDATE_SEC=3 */ + public static int VERSION_WITH_LAST_UPDATE_SEC = 3; + /** Constant FLOAT_COOKIE=8.642135E130 */ + static final double FLOAT_COOKIE = 8.642135E130; + /** Constant SIZE_OF_DOUBLE=8 */ + public static final int SIZE_OF_DOUBLE = 8; +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/jrrd/DataChunk.java b/apps/jrobin/java/src/org/rrd4j/core/jrrd/DataChunk.java new file mode 100644 index 0000000000..a5c1fa12d5 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/jrrd/DataChunk.java @@ -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 Ciaran Treanor + * @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 nameindex; + + DataChunk(Map 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; + } + + /** + *

Getter for the field data.

+ * + * @return the data + */ + public double[][] getData() { + return data; + } + + /** + *

Getter for the time stamps values.

+ * + * @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); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/jrrd/DataSource.java b/apps/jrobin/java/src/org/rrd4j/core/jrrd/DataSource.java new file mode 100644 index 0000000000..018f41852e --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/jrrd/DataSource.java @@ -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 Ciaran Treanor + * @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"); + s.print("\t\t "); + s.print(name); + s.println(" "); + s.print("\t\t "); + s.print(type); + s.println(" "); + s.print("\t\t "); + s.print(minimumHeartbeat); + s.println(" "); + s.print("\t\t "); + s.print(minimum); + s.println(" "); + s.print("\t\t "); + s.print(maximum); + s.println(" "); + s.println(); + s.println("\t\t"); + s.print("\t\t "); + s.print(pdpStatusBlock.lastReading); + s.println(" "); + s.print("\t\t "); + s.print(pdpStatusBlock.value); + s.println(" "); + s.print("\t\t "); + s.print(pdpStatusBlock.unknownSeconds); + s.println(" "); + s.println("\t"); + 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(); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/jrrd/DataSourceType.java b/apps/jrobin/java/src/org/rrd4j/core/jrrd/DataSourceType.java new file mode 100644 index 0000000000..afc3bc21eb --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/jrrd/DataSourceType.java @@ -0,0 +1,44 @@ +package org.rrd4j.core.jrrd; + +import org.rrd4j.DsType; + +/** + * Class DataSourceType + * + * @author Ciaran Treanor + * @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(); +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/jrrd/Header.java b/apps/jrobin/java/src/org/rrd4j/core/jrrd/Header.java new file mode 100644 index 0000000000..87223ba04c --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/jrrd/Header.java @@ -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 Ciaran Treanor + * @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 DataSources in the database. + * + * @return the number of DataSources in the database. + */ + public int getDSCount() { + return dsCount; + } + + /** + * Returns the number of Archives in the database. + * + * @return the number of Archives 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(); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/jrrd/PDPStatusBlock.java b/apps/jrobin/java/src/org/rrd4j/core/jrrd/PDPStatusBlock.java new file mode 100644 index 0000000000..95828f4e1e --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/jrrd/PDPStatusBlock.java @@ -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 Ciaran Treanor + * @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(); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/jrrd/RRDFile.java b/apps/jrobin/java/src/org/rrd4j/core/jrrd/RRDFile.java new file mode 100644 index 0000000000..aac650941b --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/jrrd/RRDFile.java @@ -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 + *

+ * Currently this can read RRD files that were generated on Solaris (Sparc) + * and Linux (x86). + * + * @author Ciaran Treanor + * @version $Revision: 1.1 $ + */ +class RRDFile implements Constants { + + /** Constant FLOAT_COOKIE_BIG_ENDIAN={0x5B, 0x1F, 0x2B, 0x43, + (byte) 0xC7, (byte) 0xC0, 0x25, + 0x2F} */ + private static final byte[] FLOAT_COOKIE_BIG_ENDIAN = {0x5B, 0x1F, 0x2B, 0x43, + (byte) 0xC7, (byte) 0xC0, 0x25, + 0x2F}; + /** Constant FLOAT_COOKIE_LITTLE_ENDIAN={0x2F, 0x25, (byte) 0xC0, + (byte) 0xC7, 0x43, 0x2B, 0x1F, + 0x5B} */ + 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()); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/jrrd/RRDatabase.java b/apps/jrobin/java/src/org/rrd4j/core/jrrd/RRDatabase.java new file mode 100644 index 0000000000..c609224c1a --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/jrrd/RRDatabase.java @@ -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 + * Round Robin Database + * (RRD) files. + * + * @author Ciaran Treanor + * @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 dataSources; + private final ArrayList archives; + /** Timestamp of last data modification */ + final Date lastUpdate; + /** Data source name to index */ + private final Map 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(header.dsCount); + + // Load the data sources + dataSources = new ArrayList(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(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 Header for this database. + * + * @return the Header for this database. + */ + public Header getHeader() { + return header; + } + + /** + *

getDataSourcesName.

+ * + * @return a {@link java.util.Set} object. + */ + public Set getDataSourcesName() { + return nameindex.keySet(); + } + + /** + * Returns the date this database was last updated. To convert this date to + * the form returned by rrdtool last call Date.getTime() and + * divide the result by 1000. + * + * @return the date this database was last updated. + */ + public Date getLastUpdate() { + return lastUpdate; + } + + /** + * Returns the DataSource at the specified position in this database. + * + * @param index index of DataSource to return. + * @return the DataSource 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 getDataSources() { + return dataSources.iterator(); + } + + /** + * Returns the Archive at the specified position in this database. + * + * @param index index of Archive to return. + * @return the Archive at the specified position in this database. + */ + public Archive getArchive(int index) { + return archives.get(index); + } + + /** + *

getArchive.

+ * + * @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 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 getArchives(ConsolidationFunctionType type) { + return getArchiveList(type).iterator(); + } + + ArrayList getArchiveList(ConsolidationFunctionType type) { + + ArrayList subset = new ArrayList(); + + 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 double + * 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); + } + + /** + *

getData.

+ * + * @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 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 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 + * rrdtool info + * + * @param s the PrintStream to print the header information to. + * @param numberFormat the format to print doubles 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 + * rrdtool dump + * + * 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(""); + s.print("\t "); + s.print(header.version); + s.println(" "); + s.print("\t "); + s.print(header.pdpStep); + s.println(" "); + s.print("\t "); + s.print(lastUpdate.getTime() / 1000); + s.print(" "); + s.println(); + + for (int i = 0; i < header.dsCount; i++) { + DataSource ds = dataSources.get(i); + + ds.toXml(s); + } + + s.println(""); + + for (int i = 0; i < header.rraCount; i++) { + Archive archive = archives.get(i); + + archive.toXml(s); + } + + s.println(""); + 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(); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/jrrd/UnivalArray.java b/apps/jrobin/java/src/org/rrd4j/core/jrrd/UnivalArray.java new file mode 100644 index 0000000000..5d10be1ecf --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/jrrd/UnivalArray.java @@ -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 + * + */ +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); + } + + /** + *

getLong.

+ * + * @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(); + } + + /** + *

getDouble.

+ * + * @param e a {@link java.lang.Enum} object. + * @return a double. + */ + public double getDouble(Enum e) { + buffer.position(8 * e.ordinal()); + return buffer.getDouble(); + } + +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/jrrd/package-info.java b/apps/jrobin/java/src/org/rrd4j/core/jrrd/package-info.java new file mode 100644 index 0000000000..7b6c1e46d1 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/jrrd/package-info.java @@ -0,0 +1,31 @@ +/** + *

This package provides read-only access to natives RRD file.

+ * + * 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 + *
    + *
  • x86 Linux + *
  • x86_64 Linux + *
  • x86_64 Solaris + *
  • sparc v8 (32 bits) Solaris + *
  • sparc v9 (64 bits) Solaris + *
+ *

But it should work on other environments too.

+ *

Typical usage:

+ *
+ * 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));
+ * }
+ * 
+ * + * @author Ciaran Treanor + * @version $Revision: 1.1 $ + */ +package org.rrd4j.core.jrrd; diff --git a/apps/jrobin/java/src/org/rrd4j/core/package-info.java b/apps/jrobin/java/src/org/rrd4j/core/package-info.java new file mode 100644 index 0000000000..14848b8754 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/package-info.java @@ -0,0 +1,4 @@ +/** + * core RRD4J implementation. + */ +package org.rrd4j.core; diff --git a/apps/jrobin/java/src/org/rrd4j/core/timespec/Epoch.java b/apps/jrobin/java/src/org/rrd4j/core/timespec/Epoch.java new file mode 100644 index 0000000000..b5efe7c673 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/timespec/Epoch.java @@ -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; + +/** + *

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:

+ *
    + *
  • MM/dd/yy HH:mm:ss + *
  • dd.MM.yy HH:mm:ss + *
  • dd.MM.yy 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 + *
+ * 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[] 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() { + @Override + protected SimpleDateFormat initialValue() { + SimpleDateFormat sdf = new SimpleDateFormat(format); + sdf.setLenient(true); + return sdf; + } + }; + } + StringBuilder tooltipBuff = new StringBuilder("Supported input formats:
"); + for (String supportedFormat : supportedFormats) { + tooltipBuff.append(supportedFormat).append("
"); + } + tooltipBuff.append("AT-style time specification
"); + tooltipBuff.append("timestamp

"); + 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."); + 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 OUTPUT_DATE_FORMAT = new ThreadLocal() { + @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 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(); + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/timespec/TimeParser.java b/apps/jrobin/java/src/org/rrd4j/core/timespec/TimeParser.java new file mode 100644 index 0000000000..52a06706df --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/timespec/TimeParser.java @@ -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 , 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; + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/timespec/TimeScanner.java b/apps/jrobin/java/src/org/rrd4j/core/timespec/TimeScanner.java new file mode 100644 index 0000000000..f0297b7c5e --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/timespec/TimeScanner.java @@ -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; + + /** + *

Constructor for TimeScanner.

+ * + * @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; + } +} diff --git a/apps/jrobin/java/src/org/rrd4j/core/timespec/TimeSpec.java b/apps/jrobin/java/src/org/rrd4j/core/timespec/TimeSpec.java new file mode 100644 index 0000000000..dcc6e960a3 --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/timespec/TimeSpec.java @@ -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; + } + + /** + *

Returns the corresponding timestamp (seconds since Epoch). Example:

+ *
+     * TimeParser p = new TimeParser("now-1day");
+     * TimeSpec ts = p.parse();
+     * System.out.println("Timestamp was: " + ts.getTimestamp();
+     * 
+ * + * @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 + ")"; + } + + /** + *

Use this static method to resolve relative time references and obtain the corresponding + * Calendar objects. Example:

+ *
+     * 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);
+     * 
+ * + * @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() + }; + } + + /** + *

Use this static method to resolve relative time references and obtain the corresponding + * timestamps (seconds since epoch). Example:

+ *
+     * 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);
+     * 
+ * + * @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]) + }; + } +} + diff --git a/apps/jrobin/java/src/org/rrd4j/core/timespec/TimeToken.java b/apps/jrobin/java/src/org/rrd4j/core/timespec/TimeToken.java new file mode 100644 index 0000000000..0eb1e546cc --- /dev/null +++ b/apps/jrobin/java/src/org/rrd4j/core/timespec/TimeToken.java @@ -0,0 +1,121 @@ +package org.rrd4j.core.timespec; + +class TimeToken { + /** Constant MIDNIGHT=1 */ + public static final int MIDNIGHT = 1; + /** Constant NOON=2 */ + public static final int NOON = 2; + /** Constant TEATIME=3 */ + public static final int TEATIME = 3; + /** Constant PM=4 */ + public static final int PM = 4; + /** Constant AM=5 */ + public static final int AM = 5; + /** Constant YESTERDAY=6 */ + public static final int YESTERDAY = 6; + /** Constant TODAY=7 */ + public static final int TODAY = 7; + /** Constant TOMORROW=8 */ + public static final int TOMORROW = 8; + /** Constant NOW=9 */ + public static final int NOW = 9; + /** Constant START=10 */ + public static final int START = 10; + /** Constant END=11 */ + public static final int END = 11; + /** Constant SECONDS=12 */ + public static final int SECONDS = 12; + /** Constant MINUTES=13 */ + public static final int MINUTES = 13; + /** Constant HOURS=14 */ + public static final int HOURS = 14; + /** Constant DAYS=15 */ + public static final int DAYS = 15; + /** Constant WEEKS=16 */ + public static final int WEEKS = 16; + /** Constant MONTHS=17 */ + public static final int MONTHS = 17; + /** Constant YEARS=18 */ + public static final int YEARS = 18; + /** Constant MONTHS_MINUTES=19 */ + public static final int MONTHS_MINUTES = 19; + /** Constant NUMBER=20 */ + public static final int NUMBER = 20; + /** Constant PLUS=21 */ + public static final int PLUS = 21; + /** Constant MINUS=22 */ + public static final int MINUS = 22; + /** Constant DOT=23 */ + public static final int DOT = 23; + /** Constant COLON=24 */ + public static final int COLON = 24; + /** Constant SLASH=25 */ + public static final int SLASH = 25; + /** Constant ID=26 */ + public static final int ID = 26; + /** Constant JUNK=27 */ + public static final int JUNK = 27; + /** Constant JAN=28 */ + public static final int JAN = 28; + /** Constant FEB=29 */ + public static final int FEB = 29; + /** Constant MAR=30 */ + public static final int MAR = 30; + /** Constant APR=31 */ + public static final int APR = 31; + /** Constant MAY=32 */ + public static final int MAY = 32; + /** Constant JUN=33 */ + public static final int JUN = 33; + /** Constant JUL=34 */ + public static final int JUL = 34; + /** Constant AUG=35 */ + public static final int AUG = 35; + /** Constant SEP=36 */ + public static final int SEP = 36; + /** Constant OCT=37 */ + public static final int OCT = 37; + /** Constant NOV=38 */ + public static final int NOV = 38; + /** Constant DEC=39 */ + public static final int DEC = 39; + /** Constant SUN=40 */ + public static final int SUN = 40; + /** Constant MON=41 */ + public static final int MON = 41; + /** Constant TUE=42 */ + public static final int TUE = 42; + /** Constant WED=43 */ + public static final int WED = 43; + /** Constant THU=44 */ + public static final int THU = 44; + /** Constant FRI=45 */ + public static final int FRI = 45; + /** Constant SAT=46 */ + public static final int SAT = 46; + /** Constant EOF=-1 */ + public static final int EOF = -1; + + final String value; /* token name */ + final int token_id; /* token id */ + + /** + *

Constructor for TimeToken.

+ * + * @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; + } + + /** + *

toString.

+ * + * @return a {@link java.lang.String} object. + */ + public String toString() { + return value + " [" + token_id + "]"; + } +} diff --git a/history.txt b/history.txt index 2273d91db8..d7a6122a24 100644 --- a/history.txt +++ b/history.txt @@ -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 diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 87e5bffca0..282c18b422 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -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 = "";