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("<", "<").replaceAll(">", ">"); } + @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