package org.rrd4j.core;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
/**
* <p>Base implementation class for all backend classes. Each Round Robin Database object
* ({@link org.rrd4j.core.RrdDb} object) is backed with a single RrdBackend object which performs
* actual I/O operations on the underlying storage. Rrd4j supports
* multiple backends out of the box. E.g.:</p>
* <ul>
* <li>{@link org.rrd4j.core.RrdRandomAccessFileBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdRandomAccessFileBackendFactory} class. This was the default backend used in all
* Rrd4j releases prior to 1.4.0. It uses java.io.* package and
* RandomAccessFile class to store RRD data in files on the disk.
*
* <li>{@link org.rrd4j.core.RrdNioBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdNioBackendFactory} class. The backend uses java.io.* and java.nio.*
* classes (mapped ByteBuffer) to store RRD data in files on the disk. This backend is fast, very fast,
* but consumes a lot of memory (borrowed not from the JVM but from the underlying operating system
* directly). <b>This is the default backend used in Rrd4j since 1.4.0 release.</b>
*
* <li>{@link org.rrd4j.core.RrdMemoryBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdMemoryBackendFactory} class. This backend stores all data in memory. Once
* JVM exits, all data gets lost. The backend is extremely fast and memory hungry.
* </ul>
*
* <p>To create your own backend in order to provide some custom type of RRD storage,
* you should do the following:</p>
*
* <ul>
* <li>Create your custom RrdBackend class (RrdCustomBackend, for example)
* by extending RrdBackend class. You have to implement all abstract methods defined
* in the base class.
*
* <li>Create your custom RrdBackendFactory class (RrdCustomBackendFactory,
* for example) by extending RrdBackendFactory class. You have to implement all
* abstract methods defined in the base class. Your custom factory class will actually
* create custom backend objects when necessary.
*
* <li>Create instance of your custom RrdBackendFactory and register it as a regular
* factory available to Rrd4j framework. See javadoc for {@link org.rrd4j.core.RrdBackendFactory} to
* find out how to do this.
* </ul>
*
* @author Sasa Markovic
*/
public abstract class RrdBackend {
private 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 boolean instanceCreated = false;
private final String path;
private RrdBackendFactory factory;
private long nextBigStringOffset = -1;
/**
* 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;
}
/**
* 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.
*
* @throws java.io.IOException Thrown in case of I/O error
*/
public void close() throws IOException {
}
/**
* This method suggests the caching policy to the Rrd4j frontend (high-level) classes. If <code>true</code>
* is returned, frontend classes will cache frequently used parts of a RRD file in memory to improve
* performance. If <code>false</code> is returned, high level classes will never cache RRD file sections
* in memory.
*
* @return <code>true</code> if file caching is enabled, <code>false</code> otherwise. By default, the
* method returns <code>true</code> but it can be overridden in subclasses.
*/
protected boolean isCachingAllowed() {
return true;
}
/**
* 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;
}
private 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);
}
final void writeInt(long offset, int value) throws IOException {
write(offset, getIntBytes(value));
}
final void writeLong(long offset, long value) throws IOException {
write(offset, getLongBytes(value));
}
final void writeDouble(long offset, double value) throws IOException {
write(offset, getDoubleBytes(value));
}
final void writeDouble(long offset, double value, int count) throws IOException {
byte[] b = getDoubleBytes(value);
byte[] image = new byte[8 * count];
for (int i = 0, k = 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);
}
final void writeDouble(long offset, double[] values) throws IOException {
int count = values.length;
byte[] image = new byte[8 * count];
for (int i = 0, k = 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);
}
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 = (int) 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);
}
private 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());
}
private short readShort(long offset) throws IOException {
byte[] b = new byte[2];
read(offset, b);
return (short) (((b[0] << 8) & 0x0000FF00) + ((b[1] << 0) & 0x000000FF));
}
final int readInt(long offset) throws IOException {
byte[] b = new byte[4];
read(offset, b);
return getInt(b);
}
final long readLong(long offset) throws IOException {
byte[] b = new byte[8];
read(offset, b);
return getLong(b);
}
final double readDouble(long offset) throws IOException {
byte[] b = new byte[8];
read(offset, b);
return getDouble(b);
}
final 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];
for (int i = 0, k = -1; 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;
}
final String readString(long offset) throws IOException {
ByteBuffer bbuf = ByteBuffer.allocate(RrdPrimitive.STRING_LENGTH * 2);
bbuf.order(BYTEORDER);
read(offset, bbuf.array());
bbuf.position(0);
bbuf.limit(RrdPrimitive.STRING_LENGTH * 2);
CharBuffer cbuf = bbuf.asCharBuffer();
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.put(i, ' ');
} else {
break;
}
}
if (realStringOffset > 0) {
int bigStringSize = readShort(realStringOffset);
// Signed to unsigned arithmetic
if (bigStringSize < 0) {
bigStringSize += MAXUNSIGNEDSHORT + 1;
}
ByteBuffer realStringbuf = ByteBuffer.allocate(bigStringSize * 2);
bbuf.order(BYTEORDER);
read(realStringOffset - bigStringSize * 2, realStringbuf.array());
return realStringbuf.asCharBuffer().toString().trim();
} else {
return cbuf.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) {
byte[] bytes = getLongBytes(Double.doubleToLongBits(value));
return bytes;
}
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] << 0) & 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;
}
/**
* @return the factory
*/
public RrdBackendFactory getFactory() {
return factory;
}
/**
* Privately called method.
*
* @param factory the factory to set
*/
void setFactory(RrdBackendFactory factory) {
this.factory = factory;
}
}