From 4fe9a27e2ee2200761d7b2fb37bb58b7f3425390 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Thu, 28 May 2020 10:33:03 +0000
Subject: [PATCH] RRD4J 3.6 (ticket #2716)

---
 LICENSE.txt                                   |   2 +-
 .../src/org/rrd4j/core/ByteBufferBackend.java |   3 +-
 .../java/src/org/rrd4j/core/FetchData.java    |  62 +--
 .../java/src/org/rrd4j/core/RrdBackend.java   |   8 +-
 .../src/org/rrd4j/core/RrdBackendFactory.java |  16 +-
 .../jrobin/java/src/org/rrd4j/core/RrdDb.java |  99 ++--
 .../java/src/org/rrd4j/core/RrdDbPool.java    | 431 +++++++++++-------
 .../java/src/org/rrd4j/core/RrdDef.java       |  62 +--
 .../org/rrd4j/core/RrdFileBackendFactory.java |  31 +-
 .../org/rrd4j/core/RrdNioBackendFactory.java  |  12 +-
 .../rrd4j/core/RrdSafeFileBackendFactory.java |   4 +-
 .../java/src/org/rrd4j/core/RrdToolkit.java   |  40 +-
 apps/jrobin/java/src/org/rrd4j/core/Util.java |  31 +-
 .../java/src/org/rrd4j/core/XmlWriter.java    |  24 +-
 .../src/org/rrd4j/core/timespec/Epoch.java    |  61 +--
 .../src/org/rrd4j/data/DataProcessor.java     |   2 +-
 .../java/src/org/rrd4j/graph/ImageWorker.java |  21 +-
 .../java/src/org/rrd4j/graph/Mapper.java      |   3 -
 .../java/src/org/rrd4j/graph/RrdGraph.java    |  11 +-
 .../java/src/org/rrd4j/graph/RrdGraphDef.java | 135 +++++-
 20 files changed, 643 insertions(+), 415 deletions(-)

diff --git a/LICENSE.txt b/LICENSE.txt
index de121cfb0d..2fc56c5c7d 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -264,7 +264,7 @@ Applications:
    See licenses/LICENSE-Apache2.0.txt
    See licenses/LICENSE-ECLIPSE-1.0.html
 
-   RRD4J 3.5 (jrobin.jar):
+   RRD4J 3.6 (jrobin.jar):
    Copyright (c) 2001-2005 Sasa Markovic and Ciaran Treanor.
    Copyright (c) 2011 The OpenNMS Group, Inc.
    Copyright 2011 The RRD4J Authors.
diff --git a/apps/jrobin/java/src/org/rrd4j/core/ByteBufferBackend.java b/apps/jrobin/java/src/org/rrd4j/core/ByteBufferBackend.java
index b21c8f7fd2..cb8621f8db 100644
--- a/apps/jrobin/java/src/org/rrd4j/core/ByteBufferBackend.java
+++ b/apps/jrobin/java/src/org/rrd4j/core/ByteBufferBackend.java
@@ -114,7 +114,8 @@ public abstract class ByteBufferBackend extends RrdBackend {
      */
     protected synchronized void read(long offset, byte[] b) throws IOException {
         checkOffsetAndByteBuffer(offset);
-        byteBuffer.get(b, (int) offset, b.length);
+        byteBuffer.position((int)offset);
+        byteBuffer.get(b);
     }
 
     @Override
diff --git a/apps/jrobin/java/src/org/rrd4j/core/FetchData.java b/apps/jrobin/java/src/org/rrd4j/core/FetchData.java
index 142c2ecd55..c15dc5d165 100644
--- a/apps/jrobin/java/src/org/rrd4j/core/FetchData.java
+++ b/apps/jrobin/java/src/org/rrd4j/core/FetchData.java
@@ -8,6 +8,7 @@ import java.io.OutputStream;
 import org.rrd4j.ConsolFun;
 import org.rrd4j.data.Aggregates;
 import org.rrd4j.data.DataProcessor;
+import org.rrd4j.data.Variable;
 
 /**
  * Class used to represent data fetched from the RRD.
@@ -405,39 +406,40 @@ public class FetchData {
      */
     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]);
+        try (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(); // values
-            writer.closeTag(); // row
+            writer.closeTag(); // data
+            writer.closeTag(); // fetch_data
+            writer.flush();
         }
-        writer.closeTag(); // data
-        writer.closeTag(); // fetch_data
-        writer.flush();            
     }
-    
+
     /**
      * Dumps fetch data to file in XML format.
      *
diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdBackend.java b/apps/jrobin/java/src/org/rrd4j/core/RrdBackend.java
index 3216ca6aff..4c18ca6bed 100644
--- a/apps/jrobin/java/src/org/rrd4j/core/RrdBackend.java
+++ b/apps/jrobin/java/src/org/rrd4j/core/RrdBackend.java
@@ -339,10 +339,10 @@ public abstract class RrdBackend {
     /**
      * Extract a CharBuffer from the backend, used by readString
      * 
-     * @param offset
-     * @param size
-     * @return
-     * @throws IOException
+     * @param offset the offset in the rrd
+     * @param size the size of the buffer, in character
+     * @return a new CharBuffer
+     * @throws IOException if the read fails
      */
     protected CharBuffer getCharBuffer(long offset, int size) throws IOException {
         ByteBuffer bbuf = ByteBuffer.allocate(size * 2);
diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdBackendFactory.java b/apps/jrobin/java/src/org/rrd4j/core/RrdBackendFactory.java
index b6fb8136e6..ab88da23e5 100644
--- a/apps/jrobin/java/src/org/rrd4j/core/RrdBackendFactory.java
+++ b/apps/jrobin/java/src/org/rrd4j/core/RrdBackendFactory.java
@@ -256,10 +256,10 @@ public abstract class RrdBackendFactory implements Closeable {
     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
+     * Try to detect an URI from a path. It's needed because of Microsoft Windows path that look's like an URI
      * and to URL-encode the path.
      * 
-     * @param rrdpath
+     * @param rrdpath a file URI that can be a Windows path
      * @return an URI
      */
     public static URI buildGenericUri(String rrdpath) {
@@ -380,9 +380,9 @@ public abstract class RrdBackendFactory implements Closeable {
      * <li>query and fragment is kept as is.
      * </ul>
      * 
-     * @param rootUri
-     * @param uri
-     * @param relative
+     * @param rootUri the URI to match against
+     * @param uri an URI that the current backend can handle.
+     * @param relative if true, return an URI relative to the {@code rootUri}
      * @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) {
@@ -442,8 +442,8 @@ public abstract class RrdBackendFactory implements Closeable {
     /**
      * Transform an path in a valid URI for this backend.
      * 
-     * @param path
-     * @return
+     * @param path a path local to the current backend.
+     * @return an URI that the current backend can handle.
      */
     public URI getUri(String path) {
         URI rootUri = getRootUri();
@@ -561,7 +561,7 @@ public abstract class RrdBackendFactory implements Closeable {
     /**
      * A generic close handle, default implementation does nothing.
      * @since 3.4
-     * @throws IOException
+     * @throws IOException if the close fails
      */
     public void close() throws IOException {
 
diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdDb.java b/apps/jrobin/java/src/org/rrd4j/core/RrdDb.java
index 8729f23ef7..d6aca30c53 100644
--- a/apps/jrobin/java/src/org/rrd4j/core/RrdDb.java
+++ b/apps/jrobin/java/src/org/rrd4j/core/RrdDb.java
@@ -65,7 +65,7 @@ public class RrdDb implements RrdUpdater<RrdDb>, Closeable {
         }
 
         /**
-         * Builds a {@link RrdDb} instance.
+         * Builds or imports a {@link RrdDb} instance.
          *
          * @return a new build RrdDb
          * @throws IOException              in case of I/O error.
@@ -108,12 +108,12 @@ public class RrdDb implements RrdUpdater<RrdDb>, Closeable {
 
         /**
          * Import an external rrd data, import definition must have been done using {@link #setExternalPath(String)}
-         * or {@link #setImporter(DataImporter)}
+         * or {@link #setImporter(DataImporter)}.<p>
+         * It can be used when it's not need to keep a reference to the rrd.
          *
          * @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");
@@ -128,8 +128,8 @@ public class RrdDb implements RrdUpdater<RrdDb>, Closeable {
             }
             try (DataImporter rrdImporter = resoleImporter(externalPath, importer)) {
                 if (usePool) {
-                    RrdDb db = resolvePool(pool).requestRrdDb(rrdUri, factory, importer);
-                    resolvePool(pool).release(db);
+                    try (RrdDb db = resolvePool(pool).requestRrdDb(rrdUri, factory, importer)) {
+                    };
                 } else {
                     try (RrdDb db = new RrdDb(path, rrdUri, null, rrdImporter, factory, null)) {
                     }
@@ -151,16 +151,29 @@ public class RrdDb implements RrdUpdater<RrdDb>, Closeable {
             return this;
         }
 
+        /**
+         * @param factory The backend factory to use for that rrd.
+         * @return the same builder.
+         */
         public Builder setBackendFactory(RrdBackendFactory factory) {
             this.factory = factory;
             return this;
         }
 
+        /**
+         * @param readOnly true if the rrd is to be read only
+         * @return the same builder.
+         */
         public Builder setReadOnly(boolean readOnly) {
             this.readOnly = readOnly;
             return this;
         }
 
+        /**
+         * Set the rrd as readonly
+         * 
+         * @return the same builder.
+         */
         public Builder readOnly() {
             this.readOnly = true;
             return this;
@@ -174,7 +187,7 @@ public class RrdDb implements RrdUpdater<RrdDb>, Closeable {
         /**
          * Activate the pool usage
          *
-         * @return
+         * @return the same builder.
          */
         public Builder usePool() {
             this.usePool = true;
@@ -185,14 +198,19 @@ public class RrdDb implements RrdUpdater<RrdDb>, Closeable {
          * Set the pool that will be used if {@link #usePool} is true. If not defined,
          * the singleton instance will be used.
          *
-         * @param pool
-         * @return
+         * @param pool true if a pool is going to be used
+         * @return the same builder.
          */
         public Builder setPool(RrdDbPool pool) {
             this.pool = pool;
             return this;
         }
 
+        /**
+         * Set when the builder will be used to import external data with a predefined source: XML or RRDTool.
+         * @param externalPath an URI-like indication of RRD data to import
+         * @return the same builder.
+         */
         public Builder setExternalPath(String externalPath) {
             this.externalPath = externalPath;
             this.importer = null;
@@ -201,6 +219,11 @@ public class RrdDb implements RrdUpdater<RrdDb>, Closeable {
             return this;
         }
 
+        /**
+         * Set when the builder will be used to import external data with a custom source.
+         * @param importer a custom import
+         * @return the same builder.
+         */
         public Builder setImporter(DataImporter importer) {
             this.importer = importer;
             this.externalPath = null;
@@ -209,6 +232,12 @@ public class RrdDb implements RrdUpdater<RrdDb>, Closeable {
             return this;
         }
 
+        /**
+         * Set when the builder will be used to import a RRDTool file.
+         * @param externalPath the path to a RRDTool file
+         * @return the same builder.
+         * @throws IOException if the RRDTool file can‘t be read
+         */
         public Builder setRrdToolImporter(String externalPath) throws IOException {
             this.importer = new RrdToolReader(externalPath);
             this.externalPath = null;
@@ -217,6 +246,10 @@ public class RrdDb implements RrdUpdater<RrdDb>, Closeable {
             return this;
         }
 
+        /**
+         * @param rrdDef a {@link RrdDef} to a new rrd file.
+         * @return the same builder.
+         */
         public Builder setRrdDef(RrdDef rrdDef) {
             this.rrdDef = rrdDef;
             this.importer = null;
@@ -439,6 +472,32 @@ public class RrdDb implements RrdUpdater<RrdDb>, Closeable {
         }
     }
 
+    /**
+     * <p>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.</p>
+     *
+     * @param path Path to existing RRD.
+     * @return a {link RrdDb} opened with default settings
+     * @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);
+    }
+
+    /**
+     * <p>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.</p>
+     *
+     * @param uri URI to existing RRD.
+     * @return a {link RrdDb} opened with default settings
+     * @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);
+    }
+
     /**
      * <p>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.</p>
@@ -513,18 +572,6 @@ public class RrdDb implements RrdUpdater<RrdDb>, Closeable {
         this(path, null, false, null, null);
     }
 
-    /**
-     * <p>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.</p>
-     *
-     * @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);
-    }
-
     /**
      * <p>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.</p>
@@ -539,18 +586,6 @@ public class RrdDb implements RrdUpdater<RrdDb>, Closeable {
         this(null, uri, false, null, null);
     }
 
-    /**
-     * <p>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.</p>
-     *
-     * @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.
diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdDbPool.java b/apps/jrobin/java/src/org/rrd4j/core/RrdDbPool.java
index 292338162b..d09a690c08 100644
--- a/apps/jrobin/java/src/org/rrd4j/core/RrdDbPool.java
+++ b/apps/jrobin/java/src/org/rrd4j/core/RrdDbPool.java
@@ -1,15 +1,21 @@
 package org.rrd4j.core;
 
 import java.io.IOException;
+import java.lang.reflect.UndeclaredThrowableException;
 import java.net.URI;
 import java.util.HashSet;
+import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
 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.ExecutionException;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 /**
  * <p>This class should be used to synchronize access to RRD files
@@ -25,6 +31,12 @@ public class RrdDbPool {
         private RrdDbPoolSingletonHolder() {}
     }
 
+    private static class PoolFullException extends RuntimeException {
+        PoolFullException() {
+            super("", null, false, false);
+        }
+    }
+
     /**
      * 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.
@@ -33,10 +45,10 @@ public class RrdDbPool {
 
     /*
      * The RrdEntry stored in the pool can be of tree kind:
-     * - null, the URI is available, just for it and play
+     * - null, the URI is available, just take 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
+     *   meaning that some other thread is using it. Wait until the real entry is put back.
+     * - placeholder is false, this is the active entry pointing to a RrdDb. It's
      *   only used by the current thread.
      *
      */
@@ -44,29 +56,33 @@ public class RrdDbPool {
         RrdDb rrdDb = null;
         int count = 0;
         final CountDownLatch waitempty;
-        final CountDownLatch inuse;
+        final ReentrantReadWriteLock inuse;
+        final Lock lock;
         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);
-            }
+        RrdEntry(URI canonicalPath) throws InterruptedException {
+            placeholder = false;
+            uri = canonicalPath;
+            inuse = new ReentrantReadWriteLock();
+            lock = inuse.writeLock();
+            waitempty = new CountDownLatch(1);
+        }
+        RrdEntry(RrdEntry parent) throws InterruptedException {
+            assert ! parent.placeholder;
+            placeholder = true;
+            uri = parent.uri;
+            inuse = null;
+            lock = parent.inuse.readLock();
+            waitempty = null;
         }
         @Override
         public String toString() {
-            if (this.placeholder) {
-                return "RrdEntry [inuse=" + inuse.getCount()+ ", uri=" + uri + "]";
+            if (placeholder) {
+                return String.format("RrdEntry [placeholder, uri=%s]", uri);
             } else {
-                return "RrdEntry [rrdDb=" + rrdDb + ", count=" + count + ", uri=" + uri + "]";
+                return String.format("RrdEntry [count=%d, rrdDb=%s, uri%s]", count, rrdDb, uri);
             }
         }
-
     }
 
     /**
@@ -80,21 +96,37 @@ public class RrdDbPool {
         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 Semaphore usage = new Semaphore(maxCapacity);
+    private final ReentrantReadWriteLock.WriteLock usageWLock;
+    private final ReentrantReadWriteLock.ReadLock usageRLock;
+    private final Condition fullCondition;
+    // Needed because external threads can detect waiting condition
+    private final AtomicBoolean waitFull = new AtomicBoolean(false);
 
     private final ConcurrentMap<URI, RrdEntry> pool = new ConcurrentHashMap<>(INITIAL_CAPACITY);
 
-    private final RrdBackendFactory defaultFactory;
+    private RrdBackendFactory defaultFactory;
 
     /**
      * Constructor for RrdDbPool.
      * @since 3.5
      */
     public RrdDbPool() {
-        defaultFactory = RrdBackendFactory.getDefaultFactory();
+        this(RrdBackendFactory.getDefaultFactory());
+    }
+
+    /**
+     * Constructor for RrdDbPool.
+     * @param defaultFactory the default factory used when given simple path of a rrdDb.
+     * @since 3.6
+     */
+    public RrdDbPool(RrdBackendFactory defaultFactory) {
+        this.defaultFactory = defaultFactory;
+        ReentrantReadWriteLock usageLock = new ReentrantReadWriteLock(true);
+        usageWLock = usageLock.writeLock();
+        usageRLock = usageLock.readLock();
+        fullCondition = usageWLock.newCondition();
     }
 
     /**
@@ -103,7 +135,7 @@ public class RrdDbPool {
      * @return Number of currently open RRD files held in the pool.
      */
     public int getOpenFileCount() {
-        return usage.get();
+        return pool.size();
     }
 
     /**
@@ -113,9 +145,9 @@ public class RrdDbPool {
      */
     public URI[] getOpenUri() {
         //Direct toarray from keySet can fail
-        Set<URI> files = new HashSet<>();
-        files.addAll(pool.keySet());
-        return files.toArray(new URI[files.size()]);
+        Set<URI> uris = new HashSet<>(pool.size());
+        pool.forEach((k,v) -> uris.add(k));
+        return uris.toArray(new URI[uris.size()]);
     }
 
     /**
@@ -125,53 +157,84 @@ public class RrdDbPool {
      */
     public String[] getOpenFiles() {
         //Direct toarray from keySet can fail
-        Set<String> files = new HashSet<>();
-        for (RrdEntry i: pool.values()) {
-            files.add(i.rrdDb.getPath());
-        }
-        return files.toArray(new String[files.size()]);
+        Set<String> uris = new HashSet<>(pool.size());
+        pool.forEach((k,v) -> uris.add(k.getPath()));
+        return uris.toArray(new String[uris.size()]);
     }
 
     private RrdEntry getEntry(URI uri, boolean cancreate) throws InterruptedException {
         RrdEntry ref = null;
         try {
+            CompletableFuture<RrdEntry> holder = new CompletableFuture<>();
             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();
+                try {
+                    ref = pool.compute(uri, (u, e) -> {
+                        try {
+                            if (e == null) {
+                                if (cancreate) {
+                                    usageRLock.lockInterruptibly();
+                                    try {
+                                        if (! usage.tryAcquire()) {
+                                            throw new PoolFullException();
+                                        } else {
+                                            RrdEntry r = new RrdEntry(u);
+                                            holder.complete(r);
+                                            r.lock.lock();
+                                            return new RrdEntry(r);
+                                        }
+                                    } finally {
+                                        usageRLock.unlock();
+                                    }
+                                } else {
+                                    throw new IllegalStateException("Unknown URI in pool: " + u);
+                                }
+                            } else {
+                                if (e.placeholder) {
+                                    return e;
+                                } else {
+                                    e.lock.lock();
+                                    holder.complete(e);
+                                    return new RrdEntry(e);
+                                }
                             }
+                        } catch (InterruptedException ex) {
+                            holder.completeExceptionally(ex);
+                            return null;
                         }
+                    });
+                } catch (PoolFullException e) {
+                    ref = null;
+                    try {
+                        usageWLock.lockInterruptibly();
+                        waitFull.set(true);
+                        fullCondition.await();
+                    } catch (InterruptedException ex) {
+                        holder.completeExceptionally(ex);
+                        Thread.currentThread().interrupt();
                     } 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);
+                        if (usageWLock.isHeldByCurrentThread()) {
+                            waitFull.set(false);
+                            usageWLock.unlock();
+                        }
                     }
-                } else {
-                    // a place holder, wait for the using task to finish
-                    ref.inuse.await();
                 }
-            } while (ref != null && ref.placeholder);
-            return ref;
+                if (ref != null && !holder.isDone()) {
+                    // Wait for a signal from the active entry, it's available
+                    ref.lock.lockInterruptibly();
+                    ref.lock.unlock();
+                }
+            } while (! holder.isDone());
+            return holder.get();
+        } catch (ExecutionException e) {
+            InterruptedException ex = (InterruptedException) e.getCause();
+            Thread.currentThread().interrupt();
+            throw ex;
         } catch (InterruptedException | RuntimeException e) {
             // Oups we were interrupted, put everything back and go away
             passNext(ACTION.SWAP, ref);
-            Thread.currentThread().interrupt();
+            if (e instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
             throw e;
         }
     }
@@ -191,21 +254,26 @@ public class RrdDbPool {
             break;
         case DROP:
             o = pool.remove(e.uri);
-            if(usage.decrementAndGet() < maxCapacity) {
+            usage.release();
+            assert o == null || o.placeholder;
+            if (waitFull.get()) {
                 try {
-                    countLock.lockInterruptibly();
-                    full.signalAll();
-                    countLock.unlock();
+                    usageWLock.lockInterruptibly();
+                    fullCondition.signalAll();
                 } catch (InterruptedException ex) {
-                    Thread.currentThread().interrupt();
+                    throw new UndeclaredThrowableException(ex);
+                } finally {
+                    if (usageWLock.isHeldByCurrentThread()) {
+                        usageWLock.unlock();
+                    }
                 }
             }
             break;
         }
+        assert o != e : String.format("Same entry, action=%s, entry=%s\n", a, e);
+        assert o == null || ((e.placeholder && ! o.placeholder) || (o.placeholder && ! e.placeholder)) : String.format("Inconsistent entry, action=%s, in=%s out=%s\n", a, e, o);
         //task finished, waiting on a place holder can go on
-        if(o != null) {
-            o.inuse.countDown();
-        }  
+        e.lock.unlock();
     }
 
     /**
@@ -214,11 +282,12 @@ public class RrdDbPool {
      *
      * @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 a db 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
+        // They can happens in case of failures or interruptions at wrong place
         if (rrdDb == null) {
             return;
         }
@@ -229,23 +298,23 @@ public class RrdDbPool {
             ref = getEntry(dburi, false);
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
-            throw new IllegalStateException("release interrupted for " + rrdDb, e);
+            throw new IllegalStateException("Release interrupted for " + rrdDb.getPath(), e);
         }
         if (ref == null) {
-            return;
+            throw new IllegalStateException("Could not release [" + rrdDb.getPath() + "], not using pool for it");
+        }
+        if (ref.rrdDb == null) {
+            passNext(ACTION.DROP, ref);
+            throw new IllegalStateException("Could not release [" + rrdDb.getPath() + "], pool corruption");
         }
-
         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();
+                ref.rrdDb = null;
             } finally {
                 passNext(ACTION.DROP, ref);
                 //If someone is waiting for an empty entry, signal it
@@ -272,9 +341,7 @@ public class RrdDbPool {
      * @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);
     }
@@ -293,38 +360,12 @@ public class RrdDbPool {
      * @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
@@ -359,29 +400,31 @@ public class RrdDbPool {
      */
     private RrdEntry requestEmpty(URI uri) throws InterruptedException, IOException {
         RrdEntry ref = waitEmpty(uri);
-        ref.count = 1;
         return ref;
     }
 
-    /**
-     * <p>Requests a RrdDb reference for the given RRD file definition object.</p>
-     * <ul>
-     * <li>If the file with the path specified in the RrdDef object is already open,
-     * the method blocks until the file is closed.
-     * <li>If the file is not already open and the number of already open RRD files is less than
-     * {@link #INITIAL_CAPACITY}, a new RRD file will be created and a its RrdDb reference will be returned.
-     * If the file is not already open and the number of already open RRD files is equal to
-     * {@link #INITIAL_CAPACITY}, the method blocks until some RRD file is closed.
-     * </ul>
-     *
-     * @param rrdDef Definition of the RRD file to be created
-     * @return Reference to the newly created RRD file
-     * @throws java.io.IOException Thrown in case of I/O error
-     * @deprecated Use the {@link org.rrd4j.core.RrdDb.Builder} instead.
-     */
-    @Deprecated
-    public RrdDb requestRrdDb(RrdDef rrdDef) throws IOException {
-        return requestRrdDb(rrdDef, RrdBackendFactory.findFactory(rrdDef.getUri()));
+    RrdDb requestRrdDb(URI uri, RrdBackendFactory factory) throws IOException {
+        uri = factory.getCanonicalUri(uri);
+        RrdEntry ref = null;
+        try {
+            ref = getEntry(uri, true);
+
+            // 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;
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException("request interrupted for " + uri, e);
+        }
     }
 
     RrdDb requestRrdDb(RrdDef rrdDef, RrdBackendFactory backend) throws IOException {
@@ -390,6 +433,7 @@ public class RrdDbPool {
             URI uri = backend.getCanonicalUri(rrdDef.getUri());
             ref = requestEmpty(uri);
             ref.rrdDb = RrdDb.getBuilder().setRrdDef(rrdDef).setBackendFactory(backend).setPool(this).build();
+            ref.count = 1;
             return ref.rrdDb;
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
@@ -399,12 +443,54 @@ public class RrdDbPool {
             ref = null;
             throw e;
         } finally {
-            if (ref != null) {
-                passNext(ACTION.SWAP, ref);
-            }
+            passNext(ACTION.SWAP, ref);
+        }
+    }
+
+    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();
+            ref.count = 1;
+            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 {
+            passNext(ACTION.SWAP, ref);
         }
     }
 
+    RrdDb requestRrdDb(URI uri, RrdBackendFactory backend, DataImporter importer) throws IOException {
+        return requestRrdDb(RrdDb.getBuilder().setImporter(importer), uri, backend);
+    }
+
+    /**
+     * <p>Requests a RrdDb reference for the given RRD file definition object.</p>
+     * <ul>
+     * <li>If the file with the path specified in the RrdDef object is already open,
+     * the method blocks until the file is closed.
+     * <li>If the file is not already open and the number of already open RRD files is less than
+     * {@link #INITIAL_CAPACITY}, a new RRD file will be created and a its RrdDb reference will be returned.
+     * If the file is not already open and the number of already open RRD files is equal to
+     * {@link #INITIAL_CAPACITY}, the method blocks until some RRD file is closed.
+     * </ul>
+     *
+     * @param rrdDef Definition of the RRD file to be created
+     * @return Reference to the newly created RRD file
+     * @throws java.io.IOException Thrown in case of I/O error
+     */
+    public RrdDb requestRrdDb(RrdDef rrdDef) throws IOException {
+        return requestRrdDb(rrdDef, RrdBackendFactory.findFactory(rrdDef.getUri()));
+    }
+
     /**
      * <p>Requests a RrdDb reference for the given path. The file will be created from
      * external data (from XML dump or RRDTool's binary RRD file).</p>
@@ -416,21 +502,17 @@ public class RrdDbPool {
      * If the file is not already open and the number of already open RRD files is equal to
      * {@link #INITIAL_CAPACITY}, the method blocks until some RRD file is closed.
      * </ul>
-     * <p>The path is transformed internally to URI using the default factory, that is the reference that will
-     * be used elsewhere.</p>
+     * <p>The path is transformed internally to an URI using the default factory of the pool.</p>
      *
      * @param path       Path to RRD file which should be created
      * @param sourcePath Path to external data which is to be converted to Rrd4j's native RRD file format
      * @return Reference to the newly created RRD file
      * @throws java.io.IOException Thrown in case of I/O error
-     * @deprecated Use the {@link org.rrd4j.core.RrdDb.Builder} instead.
      */
-    @Deprecated
     public RrdDb requestRrdDb(String path, String sourcePath)
             throws IOException {
-        URI uri = RrdBackendFactory.getDefaultFactory().getUri(path);
-        RrdBackendFactory backend = RrdBackendFactory.getDefaultFactory();
-        return requestRrdDb(RrdDb.getBuilder().setExternalPath(sourcePath), uri, backend);
+        URI uri = defaultFactory.getUri(path);
+        return requestRrdDb(RrdDb.getBuilder().setExternalPath(sourcePath), uri, defaultFactory);
     }
 
     /**
@@ -451,56 +533,57 @@ public class RrdDbPool {
      * @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);
+        return requestRrdDb(RrdDb.getBuilder().setExternalPath(sourcePath), uri, RrdBackendFactory.findFactory(uri));
     }
 
-    private RrdDb requestRrdDb(RrdDb.Builder builder, URI uri, RrdBackendFactory backend)
-            throws IOException {
-        RrdEntry ref = null;
-        uri = backend.getCanonicalUri(uri);
+    /**
+     * Sets the default factory to use when obtaining rrdDb from simple path and not URI.
+     *
+     * @param defaultFactory The factory to used.
+     * @throws IllegalStateException if done will the pool is not empty or the thread was interrupted.
+     */
+    public void setDefaultFactory(RrdBackendFactory defaultFactory) {
         try {
-            ref = requestEmpty(uri);
-            ref.rrdDb = builder.setPath(uri).setBackendFactory(backend).setPool(this).build();
-            return ref.rrdDb;
+            usageWLock.lockInterruptibly();
+            if (usage.availablePermits() != maxCapacity) {
+                throw new IllegalStateException("Can only be done on a empty pool");
+            }
+            this.defaultFactory = defaultFactory;
         } 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;
+            throw new IllegalStateException("Factory not changed");
         } finally {
-            if (ref != null) {
-                passNext(ACTION.SWAP, ref);
+            if (usageWLock.isHeldByCurrentThread()) {
+                usageWLock.unlock();
             }
         }
     }
 
-    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.
+     * @throws IllegalStateException if done will the pool is not empty or the thread was interrupted.
      */
     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");
+            usageWLock.lockInterruptibly();
+            if (usage.availablePermits() != maxCapacity) {
+                throw new IllegalStateException("Can only be done on a empty pool");
             }
+            maxCapacity = newCapacity;
+            usage = new Semaphore(maxCapacity);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException("Resizing interrupted");
         } finally {
-            usage.set(oldUsage);
+            if (usageWLock.isHeldByCurrentThread()) {
+                usageWLock.unlock();
+            }
         }
-        maxCapacity = newCapacity;
     }
 
     /**
@@ -509,7 +592,17 @@ public class RrdDbPool {
      * @return maximum number of simultaneously open RRD files
      */
     public int getCapacity() {
-        return maxCapacity;
+        try {
+            usageRLock.lockInterruptibly();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException("Interrupted, can't get pool size");
+        }
+        try {
+            return maxCapacity;
+        } finally {
+            usageRLock.unlock();
+        }
     }
 
     /**
@@ -545,18 +638,12 @@ public class RrdDbPool {
         RrdEntry ref = null;
         try {
             ref = getEntry(uri, false);
-            if (ref == null)
-                return 0;
-            else {
-                return ref.count;
-            }
+            return Optional.ofNullable(ref).map(e -> e.count).orElse(0);
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
             throw new RuntimeException("getOpenCount interrupted", e);
         } finally {
-            if (ref != null) {
-                passNext(ACTION.SWAP, ref);
-            }
+            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
index 0bfcfb972b..8e130d41ac 100644
--- a/apps/jrobin/java/src/org/rrd4j/core/RrdDef.java
+++ b/apps/jrobin/java/src/org/rrd4j/core/RrdDef.java
@@ -44,7 +44,7 @@ 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).
@@ -631,37 +631,37 @@ public class RrdDef {
      * @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());
+        try (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.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();
     }
 
     /**
diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdFileBackendFactory.java b/apps/jrobin/java/src/org/rrd4j/core/RrdFileBackendFactory.java
index 0664a46a21..6ace4db600 100644
--- a/apps/jrobin/java/src/org/rrd4j/core/RrdFileBackendFactory.java
+++ b/apps/jrobin/java/src/org/rrd4j/core/RrdFileBackendFactory.java
@@ -1,8 +1,11 @@
 package org.rrd4j.core;
 
-import java.io.File;
+import java.io.IOError;
 import java.io.IOException;
 import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 
 /**
  * An abstract backend factory which is used to store RRD data to ordinary files on the disk.
@@ -20,7 +23,7 @@ public abstract class RrdFileBackendFactory extends RrdBackendFactory {
      */
     @Override
     protected boolean exists(String path) {
-        return Util.fileExists(path);
+        return Files.exists(Paths.get(path));
     }
 
     /** {@inheritDoc} */
@@ -37,25 +40,27 @@ public abstract class RrdFileBackendFactory extends RrdBackendFactory {
 
     @Override
     public URI getCanonicalUri(URI uri) {
+        // Resolve only parent, to avoid failing if the file is missing
+        Path file;
         try {
-            if (uri.isOpaque()) {
-                return new File(uri.getSchemeSpecificPart()).getCanonicalFile().toURI();
-            } else if (uri.isAbsolute()) {
-                return new File(uri).getCanonicalFile().toURI();
+            if (uri.isOpaque() || uri.getScheme() == null) {
+                file = Paths.get(uri.getSchemeSpecificPart());
             } else {
-                return new File(uri.getPath()).getCanonicalFile().toURI();
+                file = Paths.get(uri);
             }
-        } catch (IOException e) {
-            throw new IllegalArgumentException("can't get canonical URI from " + uri + ": " + e);
+            Path parent = file.getParent().toRealPath();
+            return parent.resolve(file.getFileName()).toUri();
+        } catch (IOError | IOException e) {
+            throw new IllegalArgumentException("can't get canonical URI from " + uri + ": " + e, 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);
+            return Paths.get(path).normalize().toUri();
+        } catch (IOError e) {
+            throw new IllegalArgumentException("can't get URI from path " + path + ": " + e, e);
         }
     }
 
@@ -64,7 +69,7 @@ public abstract class RrdFileBackendFactory extends RrdBackendFactory {
         if (uri.isOpaque()) {
             return uri.getSchemeSpecificPart();
         } else if (uri.isAbsolute()) {
-            return new File(uri).getPath();
+            return Paths.get(uri).normalize().toString();
         } else {
             return uri.getPath();
         }
diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdNioBackendFactory.java b/apps/jrobin/java/src/org/rrd4j/core/RrdNioBackendFactory.java
index bb7d476def..88e4c0e1ac 100644
--- a/apps/jrobin/java/src/org/rrd4j/core/RrdNioBackendFactory.java
+++ b/apps/jrobin/java/src/org/rrd4j/core/RrdNioBackendFactory.java
@@ -90,9 +90,9 @@ public class RrdNioBackendFactory extends RrdFileBackendFactory {
     }
 
     /**
-     * Creates a new RrdNioBackendFactory.
+     * Creates a new RrdNioBackendFactory, using a default {@link RrdSyncThreadPool}.
      *
-     * @param syncPeriod If syncPeriod is negative or 0, sync threads are disabled.
+     * @param syncPeriod the sync period, in seconds. If negative or 0, sync threads are disabled.
      */
     public RrdNioBackendFactory(int syncPeriod) {
         this(syncPeriod, syncPeriod > 0 ? DefaultSyncThreadPool.INSTANCE : null);
@@ -101,8 +101,8 @@ public class RrdNioBackendFactory extends RrdFileBackendFactory {
     /**
      * 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.
+     * @param syncPeriod the sync period, in seconds.
+     * @param syncPoolSize The number of threads to use to sync the mapped file to disk, if negative or 0, sync threads are disabled.
      */
     public RrdNioBackendFactory(int syncPeriod, int syncPoolSize) {
         this(syncPeriod, syncPoolSize > 0 ? new RrdSyncThreadPool(syncPoolSize) : null);
@@ -111,7 +111,7 @@ public class RrdNioBackendFactory extends RrdFileBackendFactory {
     /**
      * Creates a new RrdNioBackendFactory.
      *
-     * @param syncPeriod
+     * @param syncPeriod the sync period, in seconds.
      * @param syncThreadPool If null, disable background sync threads
      */
     public RrdNioBackendFactory(int syncPeriod, ScheduledExecutorService syncThreadPool) {
@@ -121,7 +121,7 @@ public class RrdNioBackendFactory extends RrdFileBackendFactory {
     /**
      * Creates a new RrdNioBackendFactory.
      *
-     * @param syncPeriod
+     * @param syncPeriod the sync period, in seconds.
      * @param syncThreadPool If null, disable background sync threads
      */
     public RrdNioBackendFactory(int syncPeriod, RrdSyncThreadPool syncThreadPool) {
diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdSafeFileBackendFactory.java b/apps/jrobin/java/src/org/rrd4j/core/RrdSafeFileBackendFactory.java
index f85040e846..9830b7241f 100644
--- a/apps/jrobin/java/src/org/rrd4j/core/RrdSafeFileBackendFactory.java
+++ b/apps/jrobin/java/src/org/rrd4j/core/RrdSafeFileBackendFactory.java
@@ -34,8 +34,8 @@ public class RrdSafeFileBackendFactory extends RrdRandomAccessFileBackendFactory
 
     /**
      * Generate a factory with custom lock settings
-     * @param lockWaitTime
-     * @param lockRetryPeriod
+     * @param lockWaitTime wait time in ms
+     * @param lockRetryPeriod retry period in ms
      */
     public RrdSafeFileBackendFactory(long lockWaitTime, long lockRetryPeriod) {
         this.lockWaitTime = lockWaitTime;
diff --git a/apps/jrobin/java/src/org/rrd4j/core/RrdToolkit.java b/apps/jrobin/java/src/org/rrd4j/core/RrdToolkit.java
index 270dbce625..0d2758f931 100644
--- a/apps/jrobin/java/src/org/rrd4j/core/RrdToolkit.java
+++ b/apps/jrobin/java/src/org/rrd4j/core/RrdToolkit.java
@@ -1,18 +1,18 @@
 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.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 
+import org.rrd4j.ConsolFun;
+
 /**
  * Class used to perform various complex operations on RRD files. Use an instance of the
  * RrdToolkit class to:
@@ -323,28 +323,14 @@ public class RrdToolkit {
 
     private static void copyFile(String sourcePath, String destPath, boolean saveBackup)
             throws IOException {
-        File source = new File(sourcePath);
-        File dest = new File(destPath);
+        
+        Path source = Paths.get(sourcePath);
+        Path destination = Paths.get(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);
-            }
+            Files.move(destination, Paths.get(backupPath), StandardCopyOption.REPLACE_EXISTING);
         }
+        Files.move(source, destination, StandardCopyOption.REPLACE_EXISTING);
     }
 
     private static String getBackupPath(String destPath) {
@@ -515,12 +501,6 @@ public class RrdToolkit {
         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
diff --git a/apps/jrobin/java/src/org/rrd4j/core/Util.java b/apps/jrobin/java/src/org/rrd4j/core/Util.java
index 60cf2e6406..efe04efa42 100644
--- a/apps/jrobin/java/src/org/rrd4j/core/Util.java
+++ b/apps/jrobin/java/src/org/rrd4j/core/Util.java
@@ -29,6 +29,7 @@ import java.text.NumberFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.regex.Pattern;
 
 /**
@@ -236,6 +237,7 @@ public class Util {
 
     /**
      * Returns timestamp (unix epoch) for the given year, month, day, hour and minute.
+     * <p>The date is resolved in the current time zone</p>
      *
      * @param year  Year
      * @param month Month (zero-based)
@@ -253,6 +255,7 @@ public class Util {
 
     /**
      * Returns timestamp (unix epoch) for the given year, month and day.
+     * <p>The date is resolved in the current time zone</p>
      *
      * @param year  Year
      * @param month Month (zero-based)
@@ -403,8 +406,8 @@ public class Util {
             root = Paths.get(getUserHomeDirectory(), RRD4J_DIR);
         }
         try {
-            Files.createDirectories(root);
-            return root.toAbsolutePath().toString() + File.separator;
+            root = Files.createDirectories(root.toAbsolutePath().normalize());
+            return root.toString() + File.separator;
         } catch (IOException e) {
             return null;
         }
@@ -731,7 +734,7 @@ public class Util {
      * @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();
+        return Paths.get(path).toRealPath().toString();
     }
 
     /**
@@ -739,9 +742,27 @@ public class Util {
      *
      * @param file File object representing file on the disk
      * @return Last modification time in seconds (without milliseconds)
+     * @deprecated use #getLastModifiedTime, that can throws exceptions if needed
      */
+    @Deprecated
     public static long getLastModified(String file) {
-        return (new File(file).lastModified() + 500L) / 1000L;
+        try {
+            return Files.getLastModifiedTime(Paths.get(file)).to(TimeUnit.SECONDS);
+        } catch (IOException e) {
+            // For compatibility with old API
+            return 0;
+        }
+    }
+
+    /**
+     * 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)
+     * @throws IOException 
+     */
+    public static long getLastModifiedTime(String file) throws IOException {
+        return Files.getLastModifiedTime(Paths.get(file)).to(TimeUnit.SECONDS);
     }
 
     /**
@@ -751,7 +772,7 @@ public class Util {
      * @return <code>true</code> if file exists, <code>false</code> otherwise
      */
     public static boolean fileExists(String filename) {
-        return new File(filename).exists();
+        return Files.exists(Paths.get(filename));
     }
 
     /**
diff --git a/apps/jrobin/java/src/org/rrd4j/core/XmlWriter.java b/apps/jrobin/java/src/org/rrd4j/core/XmlWriter.java
index f52fd31937..9b0ba28e53 100644
--- a/apps/jrobin/java/src/org/rrd4j/core/XmlWriter.java
+++ b/apps/jrobin/java/src/org/rrd4j/core/XmlWriter.java
@@ -15,17 +15,13 @@ import java.util.TimeZone;
 /**
  * Extremely simple utility class used to create XML documents.
  */
-public class XmlWriter {
+public class XmlWriter implements AutoCloseable {
     static final String INDENT_STR = "   ";
     private static final String STYLE = "style";
-    private static final ThreadLocal<SimpleDateFormat> ISOLIKE = new ThreadLocal<SimpleDateFormat>() {
-        @Override
-        protected SimpleDateFormat initialValue() {
-            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ", Locale.ENGLISH);
-            sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
-            return sdf;
-        }
-    };
+    private static final SimpleDateFormat ISOLIKE = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ", Locale.ENGLISH);
+    static {
+        ISOLIKE.setTimeZone(TimeZone.getTimeZone("UTC"));
+    }
 
     private final PrintWriter writer;
     private final StringBuilder indent = new StringBuilder("");
@@ -198,7 +194,9 @@ public class XmlWriter {
      */
     public void writeComment(Object comment) {
         if (comment instanceof Date) {
-            comment = ISOLIKE.get().format((Date) comment);
+            synchronized (ISOLIKE) {
+                comment = ISOLIKE.format((Date) comment);
+            }
         }
         writer.println(indent + "<!-- " + escape(comment.toString()) + " -->");
     }
@@ -207,4 +205,10 @@ public class XmlWriter {
         return s.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
     }
 
+    @Override
+    public void close() {
+        writer.flush();
+        writer.close();
+    }
+
 }
diff --git a/apps/jrobin/java/src/org/rrd4j/core/timespec/Epoch.java b/apps/jrobin/java/src/org/rrd4j/core/timespec/Epoch.java
index b5efe7c673..ec32810a1d 100644
--- a/apps/jrobin/java/src/org/rrd4j/core/timespec/Epoch.java
+++ b/apps/jrobin/java/src/org/rrd4j/core/timespec/Epoch.java
@@ -35,33 +35,34 @@ import java.util.Date;
  *
  */
 public class Epoch extends JFrame {
-    private static final String[] supportedFormats = {
+    private final String[] supportedFormats = {
             "MM/dd/yy HH:mm:ss", "dd.MM.yy HH:mm:ss", "yy-MM-dd HH:mm:ss", "MM/dd/yy HH:mm",
             "dd.MM.yy HH:mm", "yy-MM-dd HH:mm", "MM/dd/yy", "dd.MM.yy", "yy-MM-dd", "HH:mm MM/dd/yy",
             "HH:mm dd.MM.yy", "HH:mm yy-MM-dd", "HH:mm:ss MM/dd/yy", "HH:mm:ss dd.MM.yy", "HH:mm:ss yy-MM-dd"
     };
 
-    @SuppressWarnings("unchecked")
-    private static final ThreadLocal<SimpleDateFormat>[] parsers = new ThreadLocal[supportedFormats.length];
-    private static final String helpText;
+    private final SimpleDateFormat[] parsers = new SimpleDateFormat[supportedFormats.length];
+    private final String helpText;
 
-    private Timer timer = new Timer(1000, new ActionListener() {
+    private final Timer timer = new Timer(1000, new ActionListener() {
         public void actionPerformed(ActionEvent e) {
             showTimestamp();
         }
     });
 
-    static {
+    private final JLabel topLabel = new JLabel("Enter timestamp or readable date:");
+    private final JTextField inputField = new JTextField(25);
+    private final JButton convertButton = new JButton("Convert");
+    private final JButton helpButton = new JButton("Help");
+
+    private final SimpleDateFormat OUTPUT_DATE_FORMAT = new SimpleDateFormat("MM/dd/yy HH:mm:ss EEE");
+
+    private Epoch() {
+        super("Epoch");
         for (int i = 0; i < parsers.length; i++) {
-            final String format = supportedFormats[i];
-            parsers[i] = new ThreadLocal<SimpleDateFormat>() {
-                @Override
-                protected SimpleDateFormat initialValue() {
-                    SimpleDateFormat sdf = new SimpleDateFormat(format);
-                    sdf.setLenient(true);
-                    return sdf;
-                }
-            };
+            String format = supportedFormats[i];
+            parsers[i] = new SimpleDateFormat(format);
+            parsers[i].setLenient(true);
         }
         StringBuilder tooltipBuff = new StringBuilder("<html><b>Supported input formats:</b><br>");
         for (String supportedFormat : supportedFormats) {
@@ -69,24 +70,8 @@ public class Epoch extends JFrame {
         }
         tooltipBuff.append("<b>AT-style time specification</b><br>");
         tooltipBuff.append("timestamp<br><br>");
-        tooltipBuff.append("Copyright (c) 2013 The RRD4J Authors. Copyright (c) 2001-2005 Sasa Markovic and Ciaran Treanor. Copyright (c) 2013 The OpenNMS Group, Inc. Licensed under the Apache License, Version 2.0.</html>");
+        tooltipBuff.append("Copyright (c) 2013-2020 The RRD4J Authors. Copyright (c) 2001-2005 Sasa Markovic and Ciaran Treanor. Copyright (c) 2013 The OpenNMS Group, Inc. Licensed under the Apache License, Version 2.0.</html>");
         helpText = tooltipBuff.toString();
-    }
-
-    private JLabel topLabel = new JLabel("Enter timestamp or readable date:");
-    private JTextField inputField = new JTextField(25);
-    private JButton convertButton = new JButton("Convert");
-    private JButton helpButton = new JButton("Help");
-
-    private static final ThreadLocal<SimpleDateFormat> OUTPUT_DATE_FORMAT = new ThreadLocal<SimpleDateFormat>() {
-        @Override
-        protected SimpleDateFormat initialValue() {
-            return new SimpleDateFormat("MM/dd/yy HH:mm:ss EEE");
-        }
-    };
-
-    Epoch() {
-        super("Epoch");
         constructUI();
         timer.start();
     }
@@ -118,7 +103,7 @@ public class Epoch extends JFrame {
         setVisible(true);
     }
 
-    void centerOnScreen() {
+    private void centerOnScreen() {
         Toolkit t = Toolkit.getDefaultToolkit();
         Dimension screenSize = t.getScreenSize();
         Dimension frameSize = getPreferredSize();
@@ -153,14 +138,14 @@ public class Epoch extends JFrame {
         setTitle(timestamp + " seconds since epoch");
     }
 
-    void formatDate(Date date) {
-        inputField.setText(OUTPUT_DATE_FORMAT.get().format(date));
+    private synchronized void formatDate(Date date) {
+        inputField.setText(OUTPUT_DATE_FORMAT.format(date));
     }
 
-    private long parseDate(String time) {
-        for (ThreadLocal<SimpleDateFormat> parser : parsers) {
+    private synchronized long parseDate(String time) {
+        for (SimpleDateFormat parser : parsers) {
             try {
-                return Util.getTimestamp(parser.get().parse(time));
+                return Util.getTimestamp(parser.parse(time));
             }
             catch (ParseException e) {
             }
diff --git a/apps/jrobin/java/src/org/rrd4j/data/DataProcessor.java b/apps/jrobin/java/src/org/rrd4j/data/DataProcessor.java
index 7780f782c7..2871926d56 100644
--- a/apps/jrobin/java/src/org/rrd4j/data/DataProcessor.java
+++ b/apps/jrobin/java/src/org/rrd4j/data/DataProcessor.java
@@ -148,7 +148,7 @@ public class DataProcessor {
     /**
      * Defines the {@link org.rrd4j.core.RrdDbPool RrdDbPool} to use. If not defined, but {{@link #setPoolUsed(boolean)}
      * set to true, the default {@link RrdDbPool#getInstance()} will be used.
-     * @param pool
+     * @param pool an optional pool to use.
      */
     public void setPool(RrdDbPool pool) {
         this.pool = pool;
diff --git a/apps/jrobin/java/src/org/rrd4j/graph/ImageWorker.java b/apps/jrobin/java/src/org/rrd4j/graph/ImageWorker.java
index f3814a5ff9..37b757cfdf 100644
--- a/apps/jrobin/java/src/org/rrd4j/graph/ImageWorker.java
+++ b/apps/jrobin/java/src/org/rrd4j/graph/ImageWorker.java
@@ -3,17 +3,14 @@ package org.rrd4j.graph;
 import java.awt.Font;
 import java.awt.Graphics2D;
 import java.awt.Paint;
-import java.awt.Rectangle;
 import java.awt.RenderingHints;
 import java.awt.Stroke;
-import java.awt.TexturePaint;
 import java.awt.font.LineMetrics;
 import java.awt.geom.AffineTransform;
 import java.awt.image.BufferedImage;
 import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -28,8 +25,7 @@ import javax.imageio.stream.ImageOutputStream;
 
 class ImageWorker {
     private static final String DUMMY_TEXT = "Dummy";
-
-    static final int IMG_BUFFER_CAPACITY = 10000; // bytes
+    private static final int IMG_BUFFER_CAPACITY = 10000; // bytes
 
     private BufferedImage img;
     private Graphics2D g2d;
@@ -224,16 +220,9 @@ class ImageWorker {
         }
     }
 
-    /**
-     * <p>loadImage.</p>
-     *
-     * @param imageFile a {@link java.lang.String} object.
-     * @throws java.io.IOException if any.
-     */
-    public void loadImage(String imageFile) throws IOException {
-        BufferedImage wpImage = ImageIO.read(new File(imageFile));
-        TexturePaint paint = new TexturePaint(wpImage, new Rectangle(0, 0, wpImage.getWidth(), wpImage.getHeight()));
-        g2d.setPaint(paint);
-        g2d.fillRect(0, 0, wpImage.getWidth(), wpImage.getHeight());
+    void loadImage(RrdGraphDef.ImageSource imageSource, int x, int y, int w, int h) throws IOException {
+        BufferedImage wpImage = imageSource.apply(w, h).getSubimage(0, 0, w, h);
+        g2d.drawImage(wpImage, new AffineTransform(1f, 0f, 0f, 1f, x, y), null);
     }
+
 }
diff --git a/apps/jrobin/java/src/org/rrd4j/graph/Mapper.java b/apps/jrobin/java/src/org/rrd4j/graph/Mapper.java
index 4d168d4193..833ec0a231 100644
--- a/apps/jrobin/java/src/org/rrd4j/graph/Mapper.java
+++ b/apps/jrobin/java/src/org/rrd4j/graph/Mapper.java
@@ -1,8 +1,5 @@
 package org.rrd4j.graph;
 
-import org.rrd4j.graph.ImageParameters;
-import org.rrd4j.graph.RrdGraphDef;
-
 class Mapper {
     private final RrdGraphDef gdef;
     private final ImageParameters im;
diff --git a/apps/jrobin/java/src/org/rrd4j/graph/RrdGraph.java b/apps/jrobin/java/src/org/rrd4j/graph/RrdGraph.java
index 071bfbce93..058774d163 100644
--- a/apps/jrobin/java/src/org/rrd4j/graph/RrdGraph.java
+++ b/apps/jrobin/java/src/org/rrd4j/graph/RrdGraph.java
@@ -175,7 +175,7 @@ public class RrdGraph implements RrdGraphConstants {
 
     private void drawOverlay() throws IOException {
         if (gdef.overlayImage != null) {
-            worker.loadImage(gdef.overlayImage);
+            worker.loadImage(gdef.overlayImage, 0, 0, im.xgif, im.ygif);
         }
     }
 
@@ -393,7 +393,10 @@ public class RrdGraph implements RrdGraphConstants {
     private void drawBackground() throws IOException {
         worker.fillRect(0, 0, im.xgif, im.ygif, gdef.getColor(ElementsNames.back));
         if (gdef.backgroundImage != null) {
-            worker.loadImage(gdef.backgroundImage);
+            worker.loadImage(gdef.backgroundImage, 0, 0, im.xgif, im.ygif);
+        }
+        if (gdef.canvasImage != null) {
+            worker.loadImage(gdef.canvasImage, im.xorigin, im.yorigin - im.ysize, im.xsize, im.ysize);
         }
         worker.fillRect(im.xorigin, im.yorigin - im.ysize, im.xsize, im.ysize, gdef.getColor(ElementsNames.canvas));
     }
@@ -666,14 +669,14 @@ public class RrdGraph implements RrdGraphConstants {
         im.end = gdef.endTime;
     }
 
-    private boolean lazyCheck() {
+    private boolean lazyCheck() throws IOException {
         // redraw if lazy option is not set or file does not exist
         if (!gdef.lazy || !Util.fileExists(gdef.filename)) {
             return false; // 'false' means 'redraw'
         }
         // redraw if not enough time has passed
         long secPerPixel = (gdef.endTime - gdef.startTime) / gdef.width;
-        long elapsed = Util.getTimestamp() - Util.getLastModified(gdef.filename);
+        long elapsed = Util.getTimestamp() - Util.getLastModifiedTime(gdef.filename);
         return elapsed <= secPerPixel;
     }
 
diff --git a/apps/jrobin/java/src/org/rrd4j/graph/RrdGraphDef.java b/apps/jrobin/java/src/org/rrd4j/graph/RrdGraphDef.java
index 7e8446e822..edebe4c531 100644
--- a/apps/jrobin/java/src/org/rrd4j/graph/RrdGraphDef.java
+++ b/apps/jrobin/java/src/org/rrd4j/graph/RrdGraphDef.java
@@ -4,12 +4,18 @@ import java.awt.BasicStroke;
 import java.awt.Font;
 import java.awt.Paint;
 import java.awt.Stroke;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.List;
 import java.util.Locale;
 import java.util.TimeZone;
 
+import javax.imageio.ImageIO;
+
 import org.rrd4j.ConsolFun;
 import org.rrd4j.core.FetchData;
 import org.rrd4j.core.RrdBackendFactory;
@@ -47,6 +53,49 @@ import org.rrd4j.data.Variable;
  * the string to disable the auto justification.</p>
  */
 public class RrdGraphDef implements RrdGraphConstants {
+
+    /**
+     * <p>Implementations of this class can be used to generate image than can be
+     * layered on graph. The can be used for background image, a background image
+     * draw on canvas or an overlay image.</p>
+     * @author Fabrice Bacchella
+     *
+     */
+    public interface ImageSource {
+        /**
+         * A image of the required size that will be applied. If the generated image is too big, it will be clipped before being applied.
+         * @param w the width of the requested image
+         * @param h the high of the requested image
+         * @return an image to draw.
+         * @throws IOException
+         */
+        BufferedImage apply(int w, int h) throws IOException;
+    }
+
+    private static class FileImageSource implements ImageSource {
+        private final File imagesource;
+
+        FileImageSource(String imagesource) {
+            this.imagesource = new File(imagesource);
+        }
+
+        public BufferedImage apply(int w, int h) throws IOException {
+            return ImageIO.read(imagesource);
+        }
+    }
+
+    private static class UrlImageSource implements ImageSource {
+        private final URL imagesource;
+
+        private UrlImageSource(URL imagesource) {
+            this.imagesource = imagesource;
+        }
+
+        public BufferedImage apply(int w, int h) throws IOException {
+            return ImageIO.read(imagesource);
+        }
+    }
+
     boolean poolUsed = false; // ok
     boolean antiAliasing = false; // ok
     boolean textAntiAliasing = false; // ok
@@ -69,8 +118,9 @@ public class RrdGraphDef implements RrdGraphConstants {
     String imageInfo = null; // ok
     String imageFormat = DEFAULT_IMAGE_FORMAT; // ok
     float imageQuality = DEFAULT_IMAGE_QUALITY; // ok
-    String backgroundImage = null; // ok
-    String overlayImage = null; // ok
+    ImageSource backgroundImage = null; // ok
+    ImageSource canvasImage = null; // ok
+    ImageSource overlayImage = null; // ok
     String unit = null; // ok
     boolean lazy = false; // ok
     double minValue = Double.NaN; // ok
@@ -466,6 +516,7 @@ public class RrdGraphDef implements RrdGraphConstants {
 
     /**
      * Sets image format.
+     * ImageIO is used to save the image, so any supported format by ImageIO can be used, and it can be extended using https://github.com/geosolutions-it/imageio-ext.
      *
      * @param imageFormat Any value as return by {@link javax.imageio.ImageIO#getReaderFormatNames}
      */
@@ -474,22 +525,90 @@ public class RrdGraphDef implements RrdGraphConstants {
     }
 
     /**
-     * Sets background image - currently, only PNG images can be used as background.
+     * Sets background image.
+     * ImageIO is used to download, so any supported format by ImageIO can be used, and it can be extended using https://github.com/geosolutions-it/imageio-ext.
      *
      * @param backgroundImage Path to background image
      */
     public void setBackgroundImage(String backgroundImage) {
+        this.backgroundImage = new FileImageSource(backgroundImage);
+    }
+
+    /**
+     * Sets background image.
+     * ImageIO is used to download, so any supported format by ImageIO can be used, and it can be extended using https://github.com/geosolutions-it/imageio-ext.
+     *
+     * @param backgroundImageUrl URL to background image
+     */
+    public void setBackgroundImage(URL backgroundImageUrl) {
+        this.backgroundImage = new UrlImageSource(backgroundImageUrl);
+    }
+
+    /**
+     * Sets background image.
+     *
+     * @param backgroundImage An {@link ImageSource} that will provides a {@link BufferedImage}
+     */
+    public void setBackgroundImage(ImageSource backgroundImage) {
         this.backgroundImage = backgroundImage;
     }
 
     /**
-     * Sets overlay image - currently, only PNG images can be used as overlay. Overlay image is
-     * printed on the top of the image, once it is completely created.
+     * Sets canvas background image. Canvas image is printed on canvas area, under canvas color and plot.
+     * ImageIO is used to download, so any supported format by ImageIO can be used, and it can be extended using https://github.com/geosolutions-it/imageio-ext.
+     *
+     * @param canvasImage Path to canvas image
+     */
+    public void setCanvasImage(String canvasImage) {
+        this.canvasImage = new FileImageSource(canvasImage);
+    }
+
+    /**
+     * Sets canvas background image. Canvas image is printed on canvas area, under canvas color and plot.
+     * ImageIO is used to download, so any supported format by ImageIO can be used, and it can be extended using https://github.com/geosolutions-it/imageio-ext.
+     *
+     * @param canvasUrl URL to canvas image
+     */
+    public void setCanvasImage(URL canvasUrl) {
+        this.canvasImage = new UrlImageSource(canvasUrl);
+    }
+
+    /**
+     * Sets canvas background image. Canvas image is printed on canvas area, under canvas color and plot.
+     *
+     * @param canvasImageSource An {@link ImageSource} that will provides a {@link BufferedImage}
+     */
+    public void setCanvasImage(ImageSource canvasImageSource) {
+        this.canvasImage = canvasImageSource;
+    }
+
+    /**
+     * Sets overlay image. Overlay image is printed on the top of the image, once it is completely created.
+     * ImageIO is used to download, so any supported format by ImageIO can be used, and it can be extended using https://github.com/geosolutions-it/imageio-ext.
      *
      * @param overlayImage Path to overlay image
      */
     public void setOverlayImage(String overlayImage) {
-        this.overlayImage = overlayImage;
+        this.overlayImage = new FileImageSource(overlayImage);
+    }
+
+    /**
+     * Sets overlay image. Overlay image is printed on the top of the image, once it is completely created.
+     * ImageIO is used to download, so any supported format by ImageIO can be used, and it can be extended using https://github.com/geosolutions-it/imageio-ext.
+     *
+     * @param overlayImage URL to overlay image
+     */
+    public void setOverlayImage(URL overlayImage) {
+        this.overlayImage = new UrlImageSource(overlayImage);
+    }
+
+    /**
+     * Sets overlay image. Overlay image is printed on the top of the image, once it is completely created.
+     *
+     * @param overlayImageSource An {@link ImageSource} that will provides a {@link BufferedImage}
+     */
+    public void setOverlayImage(ImageSource overlayImageSource) {
+        this.overlayImage = overlayImageSource;
     }
 
     /**
@@ -598,8 +717,8 @@ public class RrdGraphDef implements RrdGraphConstants {
 
     /**
      * Overrides the colors for the standard elements of the graph.
-     * @param colorTag
-     * @param color
+     * @param colorTag The element to change color.
+     * @param color The color of the element.
      */
     public void setColor(ElementsNames colorTag, Paint color) {
         colors[colorTag.ordinal()] = color;
-- 
GitLab