package org.rrd4j.core;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Base (abstract) backend factory class which holds references to all concrete
* backend factories and defines abstract methods which must be implemented in
* all concrete factory implementations.
* <p>
*
* Factory classes are used to create concrete {@link org.rrd4j.core.RrdBackend} implementations.
* Each factory creates unlimited number of specific backend objects.
*
* Rrd4j supports six different backend types (backend factories) out of the box:
* <ul>
* <li>{@link org.rrd4j.core.RrdRandomAccessFileBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdRandomAccessFileBackendFactory} class. This was the default backend used in all
* Rrd4j releases before 1.4.0 release. It uses java.io.* package and RandomAccessFile class to store
* RRD data in files on the disk.
*
* <li>{@link org.rrd4j.core.RrdSafeFileBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdSafeFileBackendFactory} class. It uses java.io.* package and RandomAccessFile class to store
* RRD data in files on the disk. This backend is SAFE:
* it locks the underlying RRD file during update/fetch operations, and caches only static
* parts of a RRD file in memory. Therefore, this backend is safe to be used when RRD files should
* be shared <b>between several JVMs</b> at the same time. However, this backend is *slow* since it does
* not use fast java.nio.* package (it's still based on the RandomAccessFile class).
*
* <li>{@link org.rrd4j.core.RrdNioBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdNioBackendFactory} class. The backend uses java.io.* and java.nio.*
* classes (mapped ByteBuffer) to store RRD data in files on the disk. This is the default backend
* since 1.4.0 release.
*
* <li>{@link org.rrd4j.core.RrdMemoryBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdMemoryBackendFactory} class. This backend stores all data in memory. Once
* JVM exits, all data gets lost. The backend is extremely fast and memory hungry.
*
* <li>{@link org.rrd4j.core.RrdBerkeleyDbBackend}: objects of this class are created from the
* {@link org.rrd4j.core.RrdBerkeleyDbBackendFactory} class. It stores RRD data to ordinary disk files
* using <a href="http://www.oracle.com/technetwork/database/berkeleydb/overview/index-093405.html">Oracle Berkeley DB</a> Java Edition.
*
* <li>{@link org.rrd4j.core.RrdMongoDBBackend}: objects of this class are created from the {@link org.rrd4j.core.RrdMongoDBBackendFactory} class.
* It stores data in a {@link com.mongodb.DBCollection} from <a href="http://www.mongodb.org/">MongoDB</a>.
* </ul>
*
* Each backend factory used to be identified by its {@link #getName() name}. Constructors
* are provided in the {@link org.rrd4j.core.RrdDb} class to create RrdDb objects (RRD databases)
* backed with a specific backend.
* <p>
* A more generic management was added in version 3.2 that allows multiple instance 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.
* <p>
* For default implementation, the path is separated in a root URI prefix and the path components. The root URI can be
* used to identify different name spaces or just be ```/```.
* <p>
* See javadoc for {@link org.rrd4j.core.RrdBackend} to find out how to create your custom backends.
*
*/
public abstract class RrdBackendFactory {
/**
* The default factory type. It will also put in the active factories list.
*
*/
public static final String DEFAULTFACTORY = "NIO";
private static final Map<String, RrdBackendFactory> factories = new ConcurrentHashMap<String, RrdBackendFactory>();
private static final List<RrdBackendFactory> activeFactories = new ArrayList<>();
private static final ReadWriteLock lock = new ReentrantReadWriteLock();
static {
RrdRandomAccessFileBackendFactory fileFactory = new RrdRandomAccessFileBackendFactory();
registerFactory(fileFactory);
RrdMemoryBackendFactory memoryFactory = new RrdMemoryBackendFactory();
registerFactory(memoryFactory);
RrdNioBackendFactory nioFactory = new RrdNioBackendFactory();
registerFactory(nioFactory);
RrdSafeFileBackendFactory safeFactory = new RrdSafeFileBackendFactory();
registerFactory(safeFactory);
setActiveFactories(RrdBackendFactory.getFactory(DEFAULTFACTORY));
}
/**
* Returns backend factory for the given backend factory name.
*
* @param name Backend factory name. Initially supported names are:
* <ul>
* <li><b>FILE</b>: Default factory which creates backends based on the
* java.io.* package. RRD data is stored in files on the disk
* <li><b>SAFE</b>: Default factory which creates backends based on the
* java.io.* package. RRD data is stored in files on the disk. This backend
* is "safe". Being safe means that RRD files can be safely shared between
* several JVM's.
* <li><b>NIO</b>: Factory which creates backends based on the
* java.nio.* package. RRD data is stored in files on the disk
* <li><b>MEMORY</b>: Factory which creates memory-oriented backends.
* RRD data is stored in memory, it gets lost as soon as JVM exits.
* <li><b>BERKELEY</b>: a memory-oriented backend that ensure persistens
* in a <a href="http://www.oracle.com/technetwork/database/berkeleydb/overview/index-093405.html">Berkeley Db</a> storage.
* <li><b>MONGODB</b>: a memory-oriented backend that ensure persistens
* in a <a href="http://www.mongodb.org/">MongoDB</a> storage.
* </ul>
* @return Backend factory for the given factory name
*/
public static RrdBackendFactory getFactory(String name) {
lock.readLock().lock();
try {
RrdBackendFactory factory = factories.get(name);
if (factory != null) {
return factory;
} else {
throw new IllegalArgumentException(
"No backend factory found with the name specified ["
+ name + "]");
}
} finally {
lock.readLock().unlock();
}
}
/**
* Registers new (custom) backend factory within the Rrd4j framework.
*
* @param factory Factory to be registered
*/
public static void registerFactory(RrdBackendFactory factory) {
lock.writeLock().lock();
try {
String name = factory.getName();
if (!factories.containsKey(name)) {
factories.put(name, factory);
}
else {
throw new IllegalArgumentException("Backend factory '" + name + "' cannot be registered twice");
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Registers new (custom) backend factory within the Rrd4j framework and sets this
* factory as the default.
*
* @param factory Factory to be registered and set as default
*/
public static void registerAndSetAsDefaultFactory(RrdBackendFactory factory) {
lock.writeLock().lock();
try {
registerFactory(factory);
setDefaultFactory(factory.getName());
} finally {
lock.writeLock().unlock();
}
}
/**
* 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 RrdBackendFactory getDefaultFactory() {
lock.readLock().lock();
try {
return activeFactories.get(0);
} finally {
lock.readLock().unlock();
}
}
/**
* Replaces the default backend factory with a new one. This method must be called before
* the first RRD gets created.
* <p>
* It also clear the list of actives factories and set it to the default factory.
* <p>
*
* @param factoryName Name of the default factory..
*/
public static void setDefaultFactory(String factoryName) {
lock.writeLock().lock();
try {
// 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");
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Set the list of active factories, i.e. the factory used to resolve URI.
*
* @param newFactories the new active factories.
*/
public static void setActiveFactories(RrdBackendFactory... newFactories) {
lock.writeLock().lock();
try {
activeFactories.clear();
activeFactories.addAll(Arrays.asList(newFactories));
} finally {
lock.writeLock().unlock();
}
}
/**
* Add factories to the list of active factories, i.e. the factory used to resolve URI.
*
* @param newFactories active factories to add.
*/
public static void addFactories(RrdBackendFactory... newFactories) {
lock.writeLock().lock();
try {
activeFactories.addAll(Arrays.asList(newFactories));
} finally {
lock.writeLock().unlock();
}
}
/**
* For a given URI, try to find a factory that can manage it.
*
* @param uri URI to try.
* @return a {@link RrdBackendFactory} that can manage that URI.
* @throws IllegalArgumentException when no matching factory is found.
*/
public static RrdBackendFactory findFactory(URI uri) {
lock.readLock().lock();
try {
for (RrdBackendFactory tryfactory : activeFactories) {
if (tryfactory.canStore(uri)) {
return tryfactory;
}
}
throw new IllegalArgumentException(
"no matching backend factory for " + uri);
} finally {
lock.readLock().unlock();
}
}
private static final Pattern URIPATTERN = Pattern.compile("^(?:(?<scheme>[a-zA-Z][a-zA-Z0-9+-\\.]*):)?(?://(?<authority>[^/\\?#]*))?(?<path>[^\\?#]*)(?:\\?(?<query>[^#]*))?(?:#(?<fragment>.*))?$");
/**
* Try to detect an URI from a path. It's needed because of windows path that look's like an URI
* and to URL-encode the path.
*
* @param rrdpath
* @return an URI
*/
public static URI buildGenericUri(String rrdpath) {
Matcher urimatcher = URIPATTERN.matcher(rrdpath);
if (urimatcher.matches()) {
String scheme = urimatcher.group("scheme");
String authority = urimatcher.group("authority");
String path = urimatcher.group("path");
String query = urimatcher.group("query");
String fragment = urimatcher.group("fragment");
try {
// If scheme is a single letter, it's not a scheme, but a windows path
if (scheme != null && scheme.length() == 1) {
return new File(rrdpath).toURI();
}
// A scheme and a not absolute path, it's an opaque URI
if (scheme != null && path.charAt(0) != '/') {
return new URI(scheme, path, query);
}
// A relative file was given, ensure that it's OK if it was on a non-unix plateform
if (File.separatorChar != '/' && scheme == null) {
path = path.replace(File.separatorChar, '/');
}
return new URI(scheme, authority, path, query, fragment);
} catch (URISyntaxException ex) {
throw new IllegalArgumentException(ex.getMessage(), ex);
}
}
throw new IllegalArgumentException("Not an URI pattern");
}
/**
* @return the scheme name for URI, default to getName().toLowerCase()
*/
public String getScheme() {
return getName().toLowerCase();
}
protected URI getRootUri() {
try {
return new URI(getScheme(), null, "/", null, null);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid scheme " + getScheme());
}
}
public boolean canStore(URI uri) {
return false;
}
/**
* Try to match an URI against a root URI using a few rules:
* <ul>
* <li>scheme must match if they are given.
* <li>authority must match if they are given.
* <li>if uri is opaque (scheme:nonabsolute), the scheme specific part is resolve as a relative path.
* <li>query and fragment is kept as is.
* </ul>
*
* @param rootUri
* @param uri
* @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())) {
return null;
} else if (scheme == null) {
scheme = rootUri.getScheme();
}
String authority = uri.getAuthority();
if (authority != null && ! authority.equals(rootUri.getAuthority())) {
return null;
} 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())) {
return null;
}
String query = uri.getQuery();
String fragment = uri.getFragment();
String newUriString = String.format("%s://%s%s%s%s", scheme, authority, path , query != null ? "?" + query : "", fragment != null ? "#" + 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 ths 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();
}
/**
* 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.
*/
protected abstract RrdBackend open(String path, boolean readOnly) throws IOException;
/**
* Creates RrdBackend object for the given storage path.
*
* @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.
*/
protected RrdBackend open(URI uri, boolean readOnly) throws IOException {
return open(getPath(uri), readOnly);
}
/**
* 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 abstract boolean shouldValidateHeader(String path) throws IOException;
/**
* 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 abstract String getName();
}