的
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# binjr-core
|
||||
|
||||
[](https://search.maven.org/search?q=g:%22eu.binjr%22%20AND%20a:%22binjr-core%22)
|
||||
|
||||
This is the main module for the application. It is responsible for :
|
||||
* The presentation layer and interaction with the end-user.
|
||||
* The management and persistence of user sessions:
|
||||
- A single session is organised as a [Workspace](https://github.com/binjr/binjr/tree/master/binjr-core/src/main/java/eu/binjr/core/data/workspace/Workspace.java).
|
||||
- A workspace can hold an arbitrary number of [Worksheets](https://github.com/binjr/binjr/tree/master/binjr-core/src/main/java/eu/binjr/core/data/workspace/Worksheet.java) and [Sources](https://github.com/binjr/binjr/tree/master/binjr-core/src/main/java/eu/binjr/core/data/workspace/Source.java)
|
||||
- A worksheet displays data from time-series from any sources on one or more graphs, all sharing a single timeline.
|
||||
- A source is backed by a [DataAdapter](https://github.com/binjr/binjr/tree/master/binjr-core/src/main/java/eu/binjr/core/data/adapters/DataAdapter.java), a component which handles the actual communication to a data source.
|
||||
- A workspace can be saved (i.e. serialized to XML and written to a file) by the end-user at any time to be restored later on.
|
||||
* Loading and instantiating DataAdapters for the various supported data sources, packaged in other modules.
|
||||
* Exposing the [DataAdapter API](https://github.com/binjr/binjr/tree/master/binjr-core/src/main/java/eu/binjr/core/data/adapters) to other modules
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2018-2024 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
dependencies {
|
||||
api 'org.apache.logging.log4j:log4j-core:2.22.0'
|
||||
api 'org.apache.logging.log4j:log4j-jcl:2.22.0'
|
||||
implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.22.0'
|
||||
api 'org.controlsfx:controlsfx:11.2.1'
|
||||
implementation 'org.gillius:jfxutils:1.0'
|
||||
api 'com.google.code.gson:gson:2.10.1'
|
||||
api 'org.apache.commons:commons-csv:1.10.0'
|
||||
api 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.2'
|
||||
implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.5'
|
||||
implementation 'org.bouncycastle:bcpg-jdk18on:1.78.1'
|
||||
implementation 'org.reflections:reflections:0.9.12'
|
||||
api 'org.fxmisc.richtext:richtextfx:0.11.0'
|
||||
api 'org.apache.lucene:lucene-core:9.10.0'
|
||||
api 'org.apache.lucene:lucene-facet:9.10.0'
|
||||
implementation 'org.apache.lucene:lucene-queryparser:9.10.0'
|
||||
implementation 'org.apache.lucene:lucene-analysis-common:9.10.0'
|
||||
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
|
||||
// Only to prepare POM for Maven central upload
|
||||
compileOnlyApi "org.openjfx:javafx-base:${OPENJFX_VERSION}"
|
||||
compileOnlyApi "org.openjfx:javafx-graphics:${OPENJFX_VERSION}"
|
||||
compileOnlyApi "org.openjfx:javafx-controls:${OPENJFX_VERSION}"
|
||||
compileOnlyApi "org.openjfx:javafx-fxml:${OPENJFX_VERSION}"
|
||||
compileOnlyApi "org.openjfx:javafx-swing:${OPENJFX_VERSION}"
|
||||
}
|
||||
|
||||
|
||||
compileJava {
|
||||
doFirst {
|
||||
options.compilerArgs = [
|
||||
'--module-path', classpath.asPath,
|
||||
'--add-modules', 'javafx.controls,javafx.fxml,javafx.swing'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes(
|
||||
'Main-Class': 'eu.binjr.core.Bootstrap',
|
||||
'Specification-Title': project.name,
|
||||
'Specification-Version': project.version,
|
||||
'Implementation-Title': project.name,
|
||||
'Implementation-Version': project.version,
|
||||
'SplashScreen-Image': 'images / splashscreen.png',
|
||||
'Build-Number': BINJR_BUILD_NUMBER
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.auth;
|
||||
|
||||
/**
|
||||
* Defines an immutable set of credentials
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class CredentialsEntry {
|
||||
private final String login;
|
||||
private final char[] pwd;
|
||||
private final boolean filled;
|
||||
|
||||
/**
|
||||
* An empty credential set.
|
||||
* <p>This specific credential set should be used to intend that no credentials where explicitly provided</p>
|
||||
*/
|
||||
public static final CredentialsEntry EMPTY = new CredentialsEntry("", new char[0], false);
|
||||
|
||||
/**
|
||||
* A canceled credential set.
|
||||
* <p>This specific credential set should be used to intend that the query for explicit credentials was initiated but canceled</p>
|
||||
*/
|
||||
public static final CredentialsEntry CANCELLED = new CredentialsEntry("", new char[0], true);
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link CredentialsEntry} class
|
||||
*
|
||||
* @param login the principle's login
|
||||
* @param password the principle's password
|
||||
*/
|
||||
public CredentialsEntry(String login, char[] password) {
|
||||
this(login, password, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a deep clone of the provided {@link CredentialsEntry} instance.
|
||||
*
|
||||
* @param credentials the {@link CredentialsEntry} instance to copy.
|
||||
* @return a deep clone of the provided {@link CredentialsEntry} instance.
|
||||
*/
|
||||
public static CredentialsEntry copyOf(CredentialsEntry credentials) {
|
||||
return new CredentialsEntry(credentials.login, credentials.pwd, credentials.filled);
|
||||
}
|
||||
|
||||
|
||||
private CredentialsEntry(String login, char[] password, boolean filled) {
|
||||
this.login = login;
|
||||
this.pwd = password;
|
||||
this.filled = filled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the principle's password
|
||||
*
|
||||
* @return the password
|
||||
*/
|
||||
public char[] getPwd() {
|
||||
return pwd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the principle's login
|
||||
*
|
||||
* @return the principle's login
|
||||
*/
|
||||
public String getLogin() {
|
||||
return login;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current set of credentials have been filled-in, false otherwise.
|
||||
*
|
||||
* @return true if the current set of credentials have been filled-in, false otherwise.
|
||||
*/
|
||||
public boolean isFilled() {
|
||||
return filled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the array backing the principle's password property.
|
||||
*/
|
||||
public void clearPassword() {
|
||||
for (int i = 0; i < pwd.length; i++) {
|
||||
pwd[i] = '#';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2017-2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.auth;
|
||||
|
||||
import com.sun.security.auth.module.Krb5LoginModule;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import eu.binjr.core.dialogs.Dialogs;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.util.Pair;
|
||||
import org.controlsfx.dialog.LoginDialog;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.callback.*;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* An extension of the Krb5LoginModule that displays a JavaFX dialog to obtain credentials from the end-user if need be.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class JfxKrb5LoginModule extends Krb5LoginModule {
|
||||
private static final Logger logger = Logger.create(JfxKrb5LoginModule.class);
|
||||
private CredentialsEntry credentials = CredentialsEntry.EMPTY;
|
||||
|
||||
@Override
|
||||
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
|
||||
super.initialize(subject, callbacks -> {
|
||||
try {
|
||||
credentials = obtainCredentials(CredentialsEntry.copyOf(credentials));
|
||||
for (Callback callback : callbacks) {
|
||||
if (callback instanceof NameCallback nc) {
|
||||
nc.setName(credentials.getLogin());
|
||||
} else if (callback instanceof PasswordCallback pc) {
|
||||
pc.setPassword(credentials.getPwd());
|
||||
credentials.clearPassword();
|
||||
} else {
|
||||
throw new UnsupportedCallbackException(callback, "Unknown Callback");
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException | TimeoutException e) {
|
||||
logger.error("An exception occurred while retrieving credentials - " + e.getMessage());
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Stack trace", e);
|
||||
}
|
||||
}
|
||||
}, sharedState, options);
|
||||
}
|
||||
|
||||
private synchronized CredentialsEntry obtainCredentials(CredentialsEntry credentialsEntry) throws InterruptedException, TimeoutException {
|
||||
if (credentialsEntry.isFilled()) {
|
||||
return credentialsEntry;
|
||||
}
|
||||
AsyncResult<CredentialsEntry> future = new AsyncResult<>();
|
||||
Platform.runLater(() -> {
|
||||
LoginDialog dlg = new LoginDialog(null, null);
|
||||
dlg.setHeaderText("Enter login credentials");
|
||||
dlg.setTitle("Login");
|
||||
dlg.initStyle(StageStyle.UTILITY);
|
||||
Dialogs.setAlwaysOnTop(dlg);
|
||||
Optional<Pair<String, String>> res = dlg.showAndWait();
|
||||
if (res.isPresent()) {
|
||||
CredentialsEntry newCreds = new CredentialsEntry(dlg.getResult().getKey(), dlg.getResult().getValue().toCharArray());
|
||||
future.put(newCreds);
|
||||
} else {
|
||||
future.put(CredentialsEntry.CANCELLED);
|
||||
}
|
||||
});
|
||||
return future.get();
|
||||
}
|
||||
|
||||
private class AsyncResult<F> {
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
private F value;
|
||||
|
||||
public boolean isDone() {
|
||||
return latch.getCount() == 0;
|
||||
}
|
||||
|
||||
public F get() throws InterruptedException {
|
||||
latch.await();
|
||||
return value;
|
||||
}
|
||||
|
||||
public F get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
|
||||
if (latch.await(timeout, unit)) {
|
||||
return value;
|
||||
} else {
|
||||
throw new TimeoutException();
|
||||
}
|
||||
}
|
||||
|
||||
private void put(F result) {
|
||||
value = result;
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.cache;
|
||||
|
||||
/**
|
||||
* Indicates that the implementing class can be cached using {@link LRUMapCapacityBound} or {@link LRUMapSizeBound}
|
||||
*
|
||||
* @param <T> The type for cached values.
|
||||
*/
|
||||
public interface Cacheable<T> {
|
||||
/**
|
||||
* Returns the cached value
|
||||
*
|
||||
* @return the cached value/
|
||||
*/
|
||||
T getValue();
|
||||
|
||||
/**
|
||||
* Returns the size in bytes of the cached value.
|
||||
*
|
||||
* @return The size in bytes of the cached value.
|
||||
*/
|
||||
long getSize();
|
||||
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.cache;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Finite capacity map with a Least Recent Used eviction policy
|
||||
*
|
||||
* @param <K> type of keys
|
||||
* @param <V> type of values
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
@Deprecated
|
||||
public class LRUMapCapacityBound<K, V> extends LinkedHashMap<K, V> {
|
||||
private int cacheSize;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link LRUMapCapacityBound} class with the specified capacity
|
||||
*
|
||||
* @param capacity the maximum capacity for the {@link LRUMapCapacityBound}
|
||||
*/
|
||||
public LRUMapCapacityBound(int capacity) {
|
||||
super(16, 0.75f, true);
|
||||
this.cacheSize = capacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link LRUMapCapacityBound} class with the specified capacity and initial values
|
||||
*
|
||||
* @param capacity the maximum capacity for the {@link LRUMapCapacityBound}
|
||||
* @param values initial values to populate the {@link LRUMapCapacityBound}
|
||||
*/
|
||||
public LRUMapCapacityBound(int capacity, Map<? extends K, ? extends V> values) {
|
||||
this(capacity);
|
||||
putAll(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
|
||||
return size() > cacheSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.cache;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Map witch evicts the least recently used element when a maximum size of stored element is reached.
|
||||
*
|
||||
* <p><i><b>NB: </b>This implementation recalculates the combined size of all stored elements each time a new element is added,
|
||||
* so it is not well suited to pushing a large number of cached items at a high rate.</i></p>
|
||||
*
|
||||
* @param <K> type of keys
|
||||
* @param <V> type of values
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
@Deprecated
|
||||
public class LRUMapSizeBound<K, V extends Cacheable> extends LinkedHashMap<K, V> {
|
||||
private long maxSize;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link LRUMapSizeBound} class with the specified capacity
|
||||
*
|
||||
* @param maxSize the maximum capacity for the {@link LRUMapSizeBound}
|
||||
*/
|
||||
public LRUMapSizeBound(int maxSize) {
|
||||
super(16, 0.75f, true);
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link LRUMapSizeBound} class with the specified capacity and initial values
|
||||
*
|
||||
* @param maxSize the maximum capacity for the {@link LRUMapSizeBound}
|
||||
* @param values initial values to populate the {@link LRUMapSizeBound}
|
||||
*/
|
||||
public LRUMapSizeBound(int maxSize, Map<? extends K, ? extends V> values) {
|
||||
this(maxSize);
|
||||
putAll(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
|
||||
return this.values().stream().map(Cacheable::getSize).reduce(0L, Long::sum) > maxSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2020 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.colors;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* Utility methods to convert color encodings
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class ColorUtils {
|
||||
private static final Logger logger = Logger.create(ColorUtils.class);
|
||||
|
||||
public static String toRGBAString(Color color) {
|
||||
return toRGBAString(color, color.getOpacity());
|
||||
}
|
||||
|
||||
public static String toRGBAString(Color color, double alpha) {
|
||||
if (color == null) {
|
||||
throw new IllegalArgumentException("Argument color is null");
|
||||
}
|
||||
if (alpha > 1.0 || alpha < 0) {
|
||||
throw new IllegalArgumentException("alpha parameter value is out of bound: " + alpha);
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder("rgba(")
|
||||
.append(color.getRed())
|
||||
.append(",")
|
||||
.append(color.getGreen())
|
||||
.append(",")
|
||||
.append(color.getBlue())
|
||||
.append(",")
|
||||
.append(alpha)
|
||||
.append(")");
|
||||
|
||||
logger.debug(() -> "RGBA representation = " + sb.toString());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String toHex(Color color, String defaultHexValue) {
|
||||
if (defaultHexValue == null) {
|
||||
throw new IllegalArgumentException("Default color hex value cannot be null");
|
||||
}
|
||||
if (color == null) {
|
||||
return defaultHexValue;
|
||||
}
|
||||
return toHex(color);
|
||||
}
|
||||
|
||||
public static String toHex(Color color) {
|
||||
if (color == null) {
|
||||
throw new IllegalArgumentException("Argument color is null");
|
||||
}
|
||||
return toHex(color, color.getOpacity());
|
||||
}
|
||||
|
||||
public static String toHex(Color color, double alpha) {
|
||||
if (color == null) {
|
||||
throw new IllegalArgumentException("Argument color is null");
|
||||
}
|
||||
return String.format("#%02x%02x%02x%02x",
|
||||
Math.round(color.getRed() * 255),
|
||||
Math.round(color.getGreen() * 255),
|
||||
Math.round(color.getBlue() * 255),
|
||||
Math.round(alpha * 255));
|
||||
}
|
||||
|
||||
public static Color copy(Color color){
|
||||
return Color.color(color.getRed(),
|
||||
color.getGreen(),
|
||||
color.getBlue(),
|
||||
color.getOpacity());
|
||||
}
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
package eu.binjr.common.concurrent;
|
||||
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A generic resource manager for safely initializing, sharing and disposing
|
||||
* singletons amongst multiple consumer objects, on multiple threads.
|
||||
* <p>
|
||||
* The resource manager will guarantee that only one instance for each supplied
|
||||
* key is ever is initialized and returned when required.
|
||||
* </p>
|
||||
* <p>
|
||||
* Reference counting is used to ensure that the actual closing of a resource
|
||||
* only happens once all registered consumers have released it.
|
||||
* </p>
|
||||
*
|
||||
* @param <T> Resource type
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class CloseableResourceManager<T extends Closeable> {
|
||||
private static final Log logger = LogFactory.getLog(CloseableResourceManager.class);
|
||||
private final Map<String, ResourceHolder<T>> resources = new HashMap<>();
|
||||
private final ReadWriteLockHelper managerLock = new ReadWriteLockHelper(new ReentrantReadWriteLock());
|
||||
|
||||
/**
|
||||
* Resource holder Resource type
|
||||
*
|
||||
* @param <U> Resource type
|
||||
*/
|
||||
private final class ResourceHolder<U extends AutoCloseable> {
|
||||
private AtomicInteger referenceCount = new AtomicInteger(0);
|
||||
private volatile boolean closed;
|
||||
private U instance;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param instance Resource instance
|
||||
*/
|
||||
private ResourceHolder(U instance) {
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Resource instance
|
||||
*
|
||||
* @return Resource instance
|
||||
*/
|
||||
private U getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register resource
|
||||
*
|
||||
* @return Reference count
|
||||
*/
|
||||
private int register() {
|
||||
return referenceCount.incrementAndGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Release resource
|
||||
*
|
||||
* @return Reference count
|
||||
* @throws Exception exception
|
||||
*/
|
||||
private int release() throws Exception {
|
||||
int refLeft = referenceCount.decrementAndGet();
|
||||
if (refLeft < 1) {
|
||||
if (!closed) {
|
||||
try {
|
||||
instance.close();
|
||||
} finally {
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return refLeft;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a consumer with a resource, identified by the supplied key.
|
||||
* <p>
|
||||
* This method must only be called once per consumer (or the release methods
|
||||
* must be called accordingly), otherwise the resource may never be
|
||||
* released.
|
||||
* </p>
|
||||
*
|
||||
* @param key the key to identify the resource to register.
|
||||
* @param resourceFactory a factory method used to build a new instance of the resource
|
||||
* if one doesn't already exist.
|
||||
* @return the registered resource
|
||||
*/
|
||||
public T acquire(String key, Supplier<T> resourceFactory) {
|
||||
return managerLock.write().lock(() -> {
|
||||
var r = resources.computeIfAbsent(key, k -> new ResourceHolder<>(resourceFactory.get()));
|
||||
r.register();
|
||||
return r.getInstance();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a consumer with a resource, identified by the supplied key.
|
||||
* <p>
|
||||
* This method must only be called once per consumer (or the release methods
|
||||
* must be called accordingly), otherwise the resource may never be
|
||||
* released.
|
||||
* </p>
|
||||
*
|
||||
* @param key the key to identify the resource to register.
|
||||
* @param resource the resource to register
|
||||
* @return Reference count
|
||||
*/
|
||||
public int register(String key, T resource) {
|
||||
return managerLock.write().lock(() -> {
|
||||
var r = resources.computeIfAbsent(key, k -> new ResourceHolder<>(resource));
|
||||
return r.register();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the resource identified by the supplied key.
|
||||
* <p>
|
||||
* If all references to this resource are released, the resource is closed.
|
||||
* </p>
|
||||
* <p>
|
||||
* <b>Warning:</b> Registering a closed resources with the same key will
|
||||
* cause a new instance to be created.
|
||||
* </p>
|
||||
*
|
||||
* @param key the key to identify the resource to release.
|
||||
* @return the number of references left for the specified resource.
|
||||
* @throws Exception if an error occurs while closing the resource.
|
||||
*/
|
||||
public int release(String key) throws Exception {
|
||||
return managerLock.write().lock(() -> {
|
||||
ResourceHolder<T> r = resources.get(key);
|
||||
if (r != null) {
|
||||
try {
|
||||
return r.release();
|
||||
} finally {
|
||||
if (r.closed) {
|
||||
resources.remove(key);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warn("Trying to release a resource that is not registered.");
|
||||
return -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the release and subsequent closing of the specified resource,
|
||||
* regardless of how many references to it are still held by other
|
||||
* consumers.
|
||||
* <p>
|
||||
* Use with caution.
|
||||
* </p>
|
||||
*
|
||||
* @param key the key to identify the resource to close.
|
||||
* @throws Exception if an error occurs while closing the resource.
|
||||
*/
|
||||
public void forceReleaseAndClose(String key) throws Exception {
|
||||
managerLock.write().lock(() -> {
|
||||
ResourceHolder<T> r = resources.get(key);
|
||||
if (r != null) {
|
||||
while (r.release() > 0) {
|
||||
// Nothing
|
||||
}
|
||||
} else {
|
||||
logger.warn("Trying to close a resource that is not registered.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resource identified by the supplied key.
|
||||
*
|
||||
* @param key the key to identify the resource to release.
|
||||
* @return An {@link Optional} wrapping the resource identified by the supplied key.
|
||||
*/
|
||||
public Optional<T> get(String key) {
|
||||
return managerLock.read().lock(() -> {
|
||||
ResourceHolder<T> r = resources.get(key);
|
||||
if (r != null) {
|
||||
return Optional.of(r.getInstance());
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a resource identified by the provided key is registered,
|
||||
* false otherwise.
|
||||
*
|
||||
* @param key the key to identify the resource.
|
||||
* @return true if a resource identified by the provided key is registered,
|
||||
* false otherwise.
|
||||
*/
|
||||
public boolean isRegistered(String key) {
|
||||
return managerLock.read().lock(() -> resources.containsKey(key));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.concurrent;
|
||||
|
||||
import eu.binjr.common.function.*;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
|
||||
/**
|
||||
* An helper class that wraps a {@link ReadWriteLock} instance and provides methods to streamline usage and avoid common inattention mistakes,
|
||||
* such as locking one the lock and unlocking the other in the {@code finally} block.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class ReadWriteLockHelper {
|
||||
private final ReadWriteLock lock;
|
||||
private final LockHelper readLockHelper;
|
||||
private final LockHelper writeLockHelper;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the {@link ReadWriteLockHelper} class that wraps a new instance of {@link ReentrantReadWriteLock}
|
||||
*/
|
||||
public ReadWriteLockHelper() {
|
||||
this(new ReentrantReadWriteLock());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the {@link ReadWriteLockHelper} class for the provided lock.
|
||||
*
|
||||
* @param lock the {@link ReadWriteLock} instance to wrap.
|
||||
*/
|
||||
public ReadWriteLockHelper(ReadWriteLock lock) {
|
||||
this.lock = lock;
|
||||
this.readLockHelper = new LockHelper(lock.readLock());
|
||||
this.writeLockHelper = new LockHelper(lock.writeLock());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the underlying ReadWriteLock instance.
|
||||
*
|
||||
* @return the underlying ReadWriteLock instance.
|
||||
*/
|
||||
public ReadWriteLock getLock() {
|
||||
return lock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an helper providing methods to run lambdas within the context of the read lock.
|
||||
*
|
||||
* @return an helper providing methods to run lambdas within the context of the read lock.
|
||||
*/
|
||||
public LockHelper read() {
|
||||
return readLockHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an helper providing methods to run lambdas within the context of the write lock.
|
||||
*
|
||||
* @return an helper providing methods to run lambdas within the context of the write lock.
|
||||
*/
|
||||
public LockHelper write() {
|
||||
return writeLockHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* An helper providing methods to run lambdas within the context of supplied lock.
|
||||
*/
|
||||
public static class LockHelper {
|
||||
private final Lock lock;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param lock Lock
|
||||
*/
|
||||
private LockHelper(Lock lock) {
|
||||
this.lock = lock;
|
||||
}
|
||||
|
||||
//region *** lock method overloads ***
|
||||
|
||||
/**
|
||||
* Encapsulated the evaluation of the supplied {@link CheckedSupplier} within the boundaries of the lock.
|
||||
*
|
||||
* @param operation the {@link CheckedSupplier} to evaluate.
|
||||
* @param <R> the return type for the {@link CheckedSupplier}.
|
||||
* @param <E> the type of the exception thrown by the {@link CheckedSupplier}.
|
||||
* @return the return of the {@link CheckedSupplier}.
|
||||
* @throws E the exception thrown by the {@link CheckedSupplier}.
|
||||
*/
|
||||
public <R, E extends Exception> R lock(CheckedSupplier<R, E> operation) throws E {
|
||||
lock.lock();
|
||||
try {
|
||||
return operation.get();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulated the evaluation of the supplied {@link CheckedFunction} within the boundaries of the lock.
|
||||
*
|
||||
* @param operation the {@link CheckedFunction} to apply.
|
||||
* @param t the parameter to pass the {@link CheckedFunction}
|
||||
* @param <T> the type of he parameter to pass the {@link CheckedFunction}
|
||||
* @param <R> the return type for the {@link CheckedFunction}.
|
||||
* @param <E> the type of the exception thrown by the {@link CheckedFunction}.
|
||||
* @return the result of the {@link CheckedFunction}.
|
||||
* @throws E the exception thrown by the {@link CheckedFunction}.
|
||||
*/
|
||||
public <T, R, E extends Exception> R lock(CheckedFunction<T, R, E> operation, T t) throws E {
|
||||
lock.lock();
|
||||
try {
|
||||
return operation.apply(t);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulated the evaluation of the supplied {@link CheckedBiFunction} within the boundaries of the lock.
|
||||
*
|
||||
* @param operation the {@link CheckedBiFunction} to apply.
|
||||
* @param t the first parameter to pass the {@link CheckedBiFunction}.
|
||||
* @param u the second parameter to pass the {@link CheckedBiFunction}.
|
||||
* @param <T> the type of the first parameter of the {@link CheckedBiFunction}.
|
||||
* @param <U> the type of the first parameter of the {@link CheckedBiFunction}.
|
||||
* @param <R> the return type for the {@link CheckedBiFunction}.
|
||||
* @param <E> the type of the exception thrown by the {@link CheckedBiFunction}.
|
||||
* @return the result of the {@link CheckedFunction}.
|
||||
* @throws E the exception thrown by the {@link CheckedBiFunction}.
|
||||
*/
|
||||
public <T, U, R, E extends Exception> R lock(CheckedBiFunction<T, U, R, E> operation, T t, U u) throws E {
|
||||
lock.lock();
|
||||
try {
|
||||
return operation.apply(t, u);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulated the evaluation of the supplied {@link CheckedRunnable} within the boundaries of the lock.
|
||||
*
|
||||
* @param operation the {@link CheckedRunnable} to evaluate.
|
||||
* @param <E> the type of the exception thrown by the {@link CheckedRunnable}.
|
||||
* @throws E the exception thrown by the {@link CheckedRunnable}.
|
||||
*/
|
||||
public <E extends Exception> void lock(CheckedRunnable<E> operation) throws E {
|
||||
lock.lock();
|
||||
try {
|
||||
operation.run();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulated the evaluation of the supplied {@link CheckedConsumer} within the boundaries of the lock.
|
||||
*
|
||||
* @param operation the {@link CheckedConsumer} to evaluate.
|
||||
* @param t the parameter to pass the {@link CheckedConsumer}
|
||||
* @param <T> the type of the parameter to pass the {@link CheckedConsumer}
|
||||
* @param <E> the type of the exception thrown by the {@link CheckedConsumer}.
|
||||
* @throws E the exception thrown by the {@link CheckedConsumer}.
|
||||
*/
|
||||
public <T, E extends Exception> void lock(CheckedConsumer<T, E> operation, T t) throws E {
|
||||
lock.lock();
|
||||
try {
|
||||
operation.accept(t);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region *** tryLock method overloads ***
|
||||
|
||||
/**
|
||||
* Tries to acquire the lock and if successful, evaluates the supplied {@link CheckedSupplier} within the boundaries of the lock.
|
||||
*
|
||||
* @param operation the {@link CheckedSupplier} to evaluate.
|
||||
* @param <R> the return type for the {@link CheckedSupplier}.
|
||||
* @param <E> the type of the exception thrown by the {@link CheckedSupplier}.
|
||||
* @return An {@link Optional} that contains the return value of the {@link CheckedSupplier} if the lock could be acquired
|
||||
* @throws E the exception thrown by the {@link CheckedSupplier}.
|
||||
*/
|
||||
public <R, E extends Exception> Optional<R> tryLock(CheckedSupplier<R, E> operation) throws E {
|
||||
if (lock.tryLock()) {
|
||||
try {
|
||||
return Optional.ofNullable(operation.get());
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to acquire the lock and if successful, evaluates the supplied {@link CheckedFunction} within the boundaries of the lock.
|
||||
*
|
||||
* @param operation the {@link CheckedFunction} to apply.
|
||||
* @param t the parameter to pass the {@link CheckedFunction}
|
||||
* @param <T> the type of he parameter to pass the {@link CheckedFunction}
|
||||
* @param <R> the return type for the {@link CheckedFunction}.
|
||||
* @param <E> the type of the exception thrown by the {@link CheckedFunction}.
|
||||
* @return the result of the {@link CheckedFunction}.
|
||||
* @throws E the exception thrown by the {@link CheckedFunction}.
|
||||
*/
|
||||
public <T, R, E extends Exception> Optional<R> tryLock(CheckedFunction<T, R, E> operation, T t) throws E {
|
||||
if (lock.tryLock()) {
|
||||
try {
|
||||
return Optional.ofNullable(operation.apply(t));
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to acquire the lock and if successful, evaluates the supplied {@link CheckedBiFunction} within the boundaries of the lock.
|
||||
*
|
||||
* @param operation the {@link CheckedBiFunction} to apply.
|
||||
* @param t the first parameter to pass the {@link CheckedBiFunction}.
|
||||
* @param u the second parameter to pass the {@link CheckedBiFunction}.
|
||||
* @param <T> the type of the first parameter of the {@link CheckedBiFunction}.
|
||||
* @param <U> the type of the first parameter of the {@link CheckedBiFunction}.
|
||||
* @param <R> the return type for the {@link CheckedBiFunction}.
|
||||
* @param <E> the type of the exception thrown by the {@link CheckedBiFunction}.
|
||||
* @return the result of the {@link CheckedFunction}.
|
||||
* @throws E the exception thrown by the {@link CheckedBiFunction}.
|
||||
*/
|
||||
public <T, U, R, E extends Exception> Optional<R> tryLock(CheckedBiFunction<T, U, R, E> operation, T t, U u) throws E {
|
||||
if (lock.tryLock()) {
|
||||
try {
|
||||
return Optional.ofNullable(operation.apply(t, u));
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to acquire the lock and if successful, evaluates the supplied {@link CheckedRunnable} within the boundaries of the lock.
|
||||
*
|
||||
* @param operation the {@link CheckedRunnable} to evaluate.
|
||||
* @param <E> the type of the exception thrown by the {@link CheckedRunnable}.
|
||||
* @return true if the lock was acquired, false otherwise.
|
||||
* @throws E the exception thrown by the {@link CheckedRunnable}.
|
||||
*/
|
||||
public <E extends Exception> boolean tryLock(CheckedRunnable<E> operation) throws E {
|
||||
if (lock.tryLock()) {
|
||||
try {
|
||||
operation.run();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to acquire the lock and if successful, evaluates the supplied {@link CheckedConsumer} within the boundaries of the lock.
|
||||
*
|
||||
* @param operation the {@link CheckedConsumer} to evaluate.
|
||||
* @param t the parameter to pass the {@link CheckedConsumer}
|
||||
* @param <T> the type of the parameter to pass the {@link CheckedConsumer}
|
||||
* @param <E> the type of the exception thrown by the {@link CheckedConsumer}.
|
||||
* @return true if the lock was acquired, false otherwise.
|
||||
* @throws E the exception thrown by the {@link CheckedConsumer}.
|
||||
*/
|
||||
public <T, E extends Exception> boolean tryLock(CheckedConsumer<T, E> operation, T t) throws E {
|
||||
if (lock.tryLock()) {
|
||||
try {
|
||||
operation.accept(t);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright 2017-2020 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.diagnostic;
|
||||
|
||||
import eu.binjr.core.preferences.AppEnvironment;
|
||||
|
||||
import javax.management.JMX;
|
||||
import javax.management.MBeanServer;
|
||||
import javax.management.MalformedObjectNameException;
|
||||
import javax.management.ObjectName;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Defines methods for a diagnostic command.
|
||||
*/
|
||||
public interface DiagnosticCommand {
|
||||
DiagnosticCommand local = ((Supplier<DiagnosticCommand>) () -> {
|
||||
try {
|
||||
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
|
||||
ObjectName name = new ObjectName("com.sun.management", "type", "DiagnosticCommand");
|
||||
return JMX.newMBeanProxy(server, name, DiagnosticCommand.class);
|
||||
} catch (MalformedObjectNameException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}).get();
|
||||
|
||||
static String dumpVmSystemProperties() throws DiagnosticException {
|
||||
try {
|
||||
return local.vmSystemProperties();
|
||||
} catch (Throwable t) {
|
||||
throw new DiagnosticException("Failed to invoke diagnostic command VM.System_properties", t);
|
||||
}
|
||||
}
|
||||
|
||||
static String dumpThreadStacks() throws DiagnosticException {
|
||||
try {
|
||||
return local.threadPrint("-l");
|
||||
} catch (Throwable t) {
|
||||
throw new DiagnosticException("Failed to invoke diagnostic command Thread.print", t);
|
||||
}
|
||||
}
|
||||
|
||||
static String dumpClassHistogram() throws DiagnosticException {
|
||||
try {
|
||||
return local.gcClassHistogram();
|
||||
} catch (Throwable t) {
|
||||
throw new DiagnosticException("Failed to invoke diagnostic command GC.class_histogram", t);
|
||||
}
|
||||
}
|
||||
|
||||
static String dumpVmFlags() throws DiagnosticException {
|
||||
try {
|
||||
return local.vmFlags();
|
||||
} catch (Throwable t) {
|
||||
throw new DiagnosticException("Failed to invoke diagnostic command VM.flags", t);
|
||||
}
|
||||
}
|
||||
|
||||
static String dumpVmCommandLine() throws DiagnosticException {
|
||||
try {
|
||||
return local.vmCommandLine();
|
||||
} catch (Throwable t) {
|
||||
throw new DiagnosticException("Failed to invoke diagnostic command VM.command_line", t);
|
||||
}
|
||||
}
|
||||
|
||||
static String getHelp() throws DiagnosticException {
|
||||
try {
|
||||
return local.help();
|
||||
} catch (Throwable t) {
|
||||
throw new DiagnosticException("Failed to invoke diagnostic command help", t);
|
||||
}
|
||||
}
|
||||
|
||||
static void dumpHeap(Path path) throws DiagnosticException {
|
||||
switch (AppEnvironment.getInstance().getRunningJvm()) {
|
||||
case HOTSPOT:
|
||||
HotSpotDiagnosticHelper.dumpHeap(path);
|
||||
break;
|
||||
case OPENJ9:
|
||||
case UNSUPPORTED:
|
||||
default:
|
||||
throw new UnavailableDiagnosticFeatureException();
|
||||
}
|
||||
}
|
||||
|
||||
static Path getHeapDumpPath() throws DiagnosticException {
|
||||
switch (AppEnvironment.getInstance().getRunningJvm()) {
|
||||
case HOTSPOT:
|
||||
return HotSpotDiagnosticHelper.getHeapDumpPath();
|
||||
case OPENJ9:
|
||||
case UNSUPPORTED:
|
||||
default:
|
||||
throw new UnavailableDiagnosticFeatureException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void setHeapDumpPath(Path path) throws DiagnosticException {
|
||||
switch (AppEnvironment.getInstance().getRunningJvm()) {
|
||||
case HOTSPOT:
|
||||
HotSpotDiagnosticHelper.setHeapDumpPath(path);
|
||||
break;
|
||||
case OPENJ9:
|
||||
case UNSUPPORTED:
|
||||
default:
|
||||
throw new UnavailableDiagnosticFeatureException();
|
||||
}
|
||||
}
|
||||
|
||||
static boolean getHeapDumpOnOutOfMemoryError() throws DiagnosticException {
|
||||
switch (AppEnvironment.getInstance().getRunningJvm()) {
|
||||
case HOTSPOT:
|
||||
return HotSpotDiagnosticHelper.getHeapDumpOnOutOfMemoryError();
|
||||
case OPENJ9:
|
||||
case UNSUPPORTED:
|
||||
default:
|
||||
throw new UnavailableDiagnosticFeatureException();
|
||||
}
|
||||
}
|
||||
|
||||
static void setHeapDumpOnOutOfMemoryError(boolean value) throws DiagnosticException {
|
||||
switch (AppEnvironment.getInstance().getRunningJvm()) {
|
||||
case HOTSPOT:
|
||||
HotSpotDiagnosticHelper.setHeapDumpOnOutOfMemoryError(value);
|
||||
break;
|
||||
case OPENJ9:
|
||||
case UNSUPPORTED:
|
||||
default:
|
||||
throw new UnavailableDiagnosticFeatureException();
|
||||
}
|
||||
}
|
||||
|
||||
static String dumpVmOptions() throws DiagnosticException {
|
||||
switch (AppEnvironment.getInstance().getRunningJvm()) {
|
||||
case HOTSPOT:
|
||||
return HotSpotDiagnosticHelper.dumpVmOptions();
|
||||
case OPENJ9:
|
||||
case UNSUPPORTED:
|
||||
default:
|
||||
throw new UnavailableDiagnosticFeatureException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
String threadPrint(String... args);
|
||||
|
||||
String help(String... args);
|
||||
|
||||
String vmSystemProperties(String... args);
|
||||
|
||||
String gcClassHistogram(String... args);
|
||||
|
||||
String vmFlags(String... args);
|
||||
|
||||
String vmCommandLine(String... args);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.diagnostic;
|
||||
|
||||
/**
|
||||
* Signals that an error occurred while invoking a diagnostic command.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class DiagnosticException extends Exception {
|
||||
/**
|
||||
* Creates a new instance of the {@link DiagnosticException} class.
|
||||
*/
|
||||
public DiagnosticException() {
|
||||
super("An error occurred while invoking a diagnostic command.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the {@link DiagnosticException} class with the provided message.
|
||||
*
|
||||
* @param message the message of the exception.
|
||||
*/
|
||||
public DiagnosticException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the {@link DiagnosticException} class with the provided message and cause {@link Throwable}
|
||||
*
|
||||
* @param message the message of the exception.
|
||||
* @param cause the cause for the exception.
|
||||
*/
|
||||
public DiagnosticException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the {@link DiagnosticException} class with the provided cause {@link Throwable}
|
||||
*
|
||||
* @param cause the cause for the exception.
|
||||
*/
|
||||
public DiagnosticException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2019-2020 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.diagnostic;
|
||||
|
||||
import com.sun.management.HotSpotDiagnosticMXBean;
|
||||
import com.sun.management.VMOption;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Defines methods for HotSpot specific diagnistic commands.
|
||||
*/
|
||||
public interface HotSpotDiagnosticHelper {
|
||||
HotSpotDiagnosticMXBean HOTSPOT_DIAGNOSTIC = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
||||
|
||||
static String dumpVmOptions() throws DiagnosticException{
|
||||
return "Hotspot VM Options\n" + listOption()
|
||||
.stream()
|
||||
.map(vmOption -> String.format("%s=%s (%s)",
|
||||
vmOption.getName(),
|
||||
vmOption.getValue(),
|
||||
vmOption.getOrigin()))
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
static List<VMOption> listOption() throws DiagnosticException {
|
||||
try {
|
||||
return HOTSPOT_DIAGNOSTIC.getDiagnosticOptions();
|
||||
} catch (Throwable t) {
|
||||
throw new DiagnosticException("Failed to list VMOptions", t);
|
||||
}
|
||||
}
|
||||
|
||||
static void dumpHeap(Path path) throws DiagnosticException {
|
||||
try {
|
||||
HOTSPOT_DIAGNOSTIC.dumpHeap(path.toString(), false);
|
||||
} catch (Throwable t) {
|
||||
throw new DiagnosticException("Failed to get VMOption HeapDumpPath", t);
|
||||
}
|
||||
}
|
||||
|
||||
static Path getHeapDumpPath() throws DiagnosticException {
|
||||
try {
|
||||
return Path.of(HOTSPOT_DIAGNOSTIC.getVMOption("HeapDumpPath").getValue());
|
||||
} catch (Throwable t) {
|
||||
throw new DiagnosticException("Failed to get VMOption HeapDumpPath", t);
|
||||
}
|
||||
}
|
||||
|
||||
static void setHeapDumpPath(Path path) throws DiagnosticException {
|
||||
try {
|
||||
HOTSPOT_DIAGNOSTIC.setVMOption("HeapDumpPath", path.toString());
|
||||
} catch (Throwable t) {
|
||||
throw new DiagnosticException("Failed to set VMOption HeapDumpPath", t);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean getHeapDumpOnOutOfMemoryError() throws DiagnosticException {
|
||||
try {
|
||||
return Boolean.parseBoolean(HOTSPOT_DIAGNOSTIC.getVMOption("HeapDumpOnOutOfMemoryError").getValue());
|
||||
} catch (Throwable t) {
|
||||
throw new DiagnosticException("Failed to get VMOption HeapDumpOnOutOfMemoryError", t);
|
||||
}
|
||||
}
|
||||
|
||||
static void setHeapDumpOnOutOfMemoryError(boolean value) throws DiagnosticException {
|
||||
try {
|
||||
HOTSPOT_DIAGNOSTIC.setVMOption("HeapDumpOnOutOfMemoryError", Boolean.toString(value));
|
||||
} catch (Throwable t) {
|
||||
throw new DiagnosticException("Failed to set VMOption HeapDumpOnOutOfMemoryError", t);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2020 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.diagnostic;
|
||||
|
||||
/**
|
||||
* Signals that a diagnostic feature is not available for the current JVM implementation
|
||||
*/
|
||||
public class UnavailableDiagnosticFeatureException extends DiagnosticException {
|
||||
UnavailableDiagnosticFeatureException() {
|
||||
super("This diagnostic feature is not available for the current JVM implementation " +
|
||||
System.getProperty("java.vm.name"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2019 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.function;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* A functional interface equivalent to {@link BiConsumer}, whose method
|
||||
* throws a checked exception.
|
||||
*
|
||||
* @param <T> the first type of the input to the operation.
|
||||
* @param <U> the second type of the input to the operation.
|
||||
* @param <E> the type of checked exception thrown.
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CheckedBiConsumer<T, U, E extends Exception> {
|
||||
/**
|
||||
* Performs this operation on the given argument.
|
||||
*
|
||||
* @param t the first input argument
|
||||
* @param u the second input argument
|
||||
* @throws E a checked exception
|
||||
*/
|
||||
void accept(T t, U u) throws E;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.function;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* A functional interface equivalent to {@link BiFunction}, whose method
|
||||
* throws a checked exception.
|
||||
*
|
||||
* @param <T> the type of the first input to the function.
|
||||
* @param <U> the type of the second input to the function.
|
||||
* @param <R> the type of the result of the function.
|
||||
* @param <E> the type of checked exception thrown.
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CheckedBiFunction<T, U, R, E extends Exception> {
|
||||
/**
|
||||
* Applies this function to the given argument.
|
||||
*
|
||||
* @param t the first function argument
|
||||
* @param u the second function argument.
|
||||
* @return the function result
|
||||
* @throws E a checked exception
|
||||
*/
|
||||
R apply(T t, U u) throws E;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.function;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A functional interface equivalent to {@link Consumer}, whose method
|
||||
* throws a checked exception.
|
||||
*
|
||||
* @param <T> the type of the input to the operation.
|
||||
* @param <E> the type of checked exception thrown.
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CheckedConsumer<T, E extends Exception> {
|
||||
/**
|
||||
* Performs this operation on the given argument.
|
||||
*
|
||||
* @param t the input argument
|
||||
* @throws E a checked exception
|
||||
*/
|
||||
void accept(T t) throws E;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.function;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* A functional interface equivalent to {@link Function}, whose method
|
||||
* throws a checked exception.
|
||||
*
|
||||
* @param <T> the type of the input to the function.
|
||||
* @param <R> the type of the result of the function.
|
||||
* @param <E> the type of checked exception thrown.
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CheckedFunction<T, R, E extends Exception> {
|
||||
/**
|
||||
* Applies this function to the given argument.
|
||||
*
|
||||
* @param t the function argument
|
||||
* @return the function result
|
||||
* @throws E a checked exception
|
||||
*/
|
||||
R apply(T t) throws E;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.function;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A series of methods that wrap functional interfaces that throw checked
|
||||
* exceptions inside their standard (non throwing) counterparts, so they can be
|
||||
* used with streams or other classes expecting standards functional interfaces
|
||||
* and have them acting transparent to the exception thrown within the lambda.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public final class CheckedLambdas {
|
||||
/**
|
||||
* Override constructor
|
||||
*/
|
||||
private CheckedLambdas() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a {@link CheckedConsumer} inside a {@link Consumer} and rethrow the
|
||||
* original exception.
|
||||
*
|
||||
* @param consumer the {@link Consumer} to wrap.
|
||||
* @param <T> the type for the consumer's parameter.
|
||||
* @param <E> the type for the checked exception.
|
||||
* @return a {@link Consumer} instance.
|
||||
* @throws E the checked exception thrown in the lambda.
|
||||
*/
|
||||
public static <T, E extends Exception> Consumer<T> wrap(CheckedConsumer<T, E> consumer) throws E {
|
||||
return t -> {
|
||||
try {
|
||||
consumer.accept(t);
|
||||
} catch (Exception exception) {
|
||||
throw throwActualException(exception);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// public static <T, E extends Exception> Predicate<T> wrap(CheckedPredicate<T, E> predicate) throws E {
|
||||
// return t -> {
|
||||
// try {
|
||||
// return predicate.test(t);
|
||||
// } catch (Exception exception) {
|
||||
// throw throwActualException(exception);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
/**
|
||||
* Wraps a {@link CheckedRunnable} inside a {@link Runnable} and rethrow the
|
||||
* original exception.
|
||||
*
|
||||
* @param runnable the {@link Runnable} to wrap.
|
||||
* @param <E> the type for the checked exception.
|
||||
* @return a {@link Runnable} instance.
|
||||
* @throws E the checked exception thrown in the lambda.
|
||||
*/
|
||||
public static <E extends Exception> Runnable wrap(CheckedRunnable<E> runnable) throws E {
|
||||
return () -> {
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (Exception exception) {
|
||||
throw throwActualException(exception);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a {@link CheckedSupplier} inside a {@link Supplier} and rethrow the
|
||||
* original exception.
|
||||
*
|
||||
* @param supplier the {@link Supplier} to wrap.
|
||||
* @param <T> the type for the supplier's parameter.
|
||||
* @param <E> the type for the checked exception.
|
||||
* @return a {@link Supplier} instance.
|
||||
* @throws E the checked exception thrown in the lambda.
|
||||
*/
|
||||
public static <T, E extends Exception> Supplier<T> wrap(CheckedSupplier<T, E> supplier) throws E {
|
||||
return () -> {
|
||||
try {
|
||||
return supplier.get();
|
||||
} catch (Exception exception) {
|
||||
throw throwActualException(exception);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a {@link CheckedFunction} inside a {@link Function} and rethrow the
|
||||
* original exception.
|
||||
*
|
||||
* @param function the {@link Function} to wrap.
|
||||
* @param <T> the type for the function's parameter.
|
||||
* @param <R> the return type of the function.
|
||||
* @param <E> the type for the checked exception.
|
||||
* @return a {@link Function} instance.
|
||||
* @throws E the checked exception thrown in the lambda.
|
||||
*/
|
||||
public static <T, R, E extends Exception> Function<T, R> wrap(CheckedFunction<T, R, E> function) throws E {
|
||||
return t -> {
|
||||
try {
|
||||
return function.apply(t);
|
||||
} catch (Exception exception) {
|
||||
throw throwActualException(exception);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Forge runtime exception
|
||||
*
|
||||
* @param exception exception
|
||||
* @return Runtime exception
|
||||
* @throws E Exception
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <E extends Exception> RuntimeException throwActualException(Exception exception) throws E {
|
||||
throw (E) exception;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.function;
|
||||
|
||||
/**
|
||||
* A functional interface equivalent to {@link java.util.function.Predicate}, whose method
|
||||
* throws a checked exception.
|
||||
*
|
||||
* @param <T> the type of the input to the operation.
|
||||
* @param <E> the type of checked exception thrown.
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CheckedPredicate<T, E extends Exception> {
|
||||
/**
|
||||
* Performs this operation on the given argument.
|
||||
*
|
||||
* @param t the input argument
|
||||
* @return the result of the predicate resolution.
|
||||
* @throws E a checked exception
|
||||
*/
|
||||
boolean test(T t) throws E;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.function;
|
||||
|
||||
/**
|
||||
* A functional interface equivalent to {@link Runnable}, whose method
|
||||
* throws a checked exception.
|
||||
*
|
||||
* @param <E> the type of checked exception thrown.
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CheckedRunnable<E extends Exception> {
|
||||
/**
|
||||
* Evaluates the lambda.
|
||||
*
|
||||
* @throws E a checked exception
|
||||
*/
|
||||
void run() throws E;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.function;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A functional interface equivalent to {@link Supplier}, whose method
|
||||
* throws a checked exception.
|
||||
*
|
||||
* @param <T> the type of results supplied by this supplier
|
||||
* @param <E> the type of checked exception thrown.
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CheckedSupplier<T, E extends Exception> {
|
||||
/**
|
||||
* Gets a result.
|
||||
*
|
||||
* @return a result
|
||||
* @throws E a checked exception
|
||||
*/
|
||||
T get() throws E;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* Copyright 2017-2024 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.github;
|
||||
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import eu.binjr.common.io.IOUtils;
|
||||
import eu.binjr.common.io.ProxyConfiguration;
|
||||
import eu.binjr.common.io.SSLContextUtils;
|
||||
import eu.binjr.common.io.SSLCustomContextException;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import eu.binjr.core.preferences.UserPreferences;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.*;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* A series of helper methods to wrap some GitHub APIs
|
||||
* See: https://developer.github.com/v3/
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class GitHubApiHelper implements Closeable {
|
||||
private static final Logger logger = Logger.create(GitHubApiHelper.class);
|
||||
public static final String HTTPS_API_GITHUB_COM = "https://api.github.com";
|
||||
protected final HttpClient httpClient;
|
||||
private final URI apiEndpoint;
|
||||
protected String userCredentials;
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
private final static Type ghReleaseArrayType = new TypeToken<ArrayList<GithubRelease>>() {
|
||||
}.getType();
|
||||
private final static Type ghAssetArrayType = new TypeToken<ArrayList<GithubAsset>>() {
|
||||
}.getType();
|
||||
|
||||
private GitHubApiHelper() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
private GitHubApiHelper(URI apiEndpoint) {
|
||||
this(apiEndpoint, null, null, null, false);
|
||||
}
|
||||
|
||||
private GitHubApiHelper(URI apiEndpoint, ProxyConfiguration proxyConfig, String userName, String token, boolean useJvmCacerts) {
|
||||
this.apiEndpoint = Objects.requireNonNullElseGet(apiEndpoint, () -> URI.create(HTTPS_API_GITHUB_COM));
|
||||
HttpClient.Builder builder = null;
|
||||
|
||||
builder = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER));
|
||||
if (!useJvmCacerts) {
|
||||
try {
|
||||
builder.sslContext(SSLContextUtils.withPlatformKeystore());
|
||||
} catch (SSLCustomContextException e) {
|
||||
logger.error("Error creating SSL context for GitHub helper:" + e.getMessage());
|
||||
logger.debug("Stacktrace", e);
|
||||
}
|
||||
}
|
||||
if (proxyConfig != null && proxyConfig.enabled()) {
|
||||
try {
|
||||
var proxySelector = ProxySelector.of(InetSocketAddress.createUnresolved(proxyConfig.host(), proxyConfig.port()));
|
||||
if (proxyConfig.useAuth()) {
|
||||
builder.authenticator(new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
if (getRequestorType().equals(RequestorType.PROXY)) {
|
||||
return new PasswordAuthentication(proxyConfig.login(), proxyConfig.pwd());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
builder.proxy(proxySelector);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to setup http proxy: " + e.getMessage());
|
||||
logger.debug(() -> "Stack", e);
|
||||
}
|
||||
}
|
||||
setUserCredentials(userName, token);
|
||||
httpClient = builder.build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link GitHubApiHelper} class.
|
||||
*
|
||||
* @param apiEndpoint the URI that specifies the API endpoint.
|
||||
* @return a new instance of the {@link GitHubApiHelper} class.
|
||||
*/
|
||||
public static GitHubApiHelper of(URI apiEndpoint) {
|
||||
return new GitHubApiHelper(apiEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link GitHubApiHelper} class.
|
||||
*
|
||||
* @param apiEndpoint the URI that specifies the API endpoint.
|
||||
* @param proxyConfiguration Configuration for http proxy
|
||||
* @return a new instance of the {@link GitHubApiHelper} class.
|
||||
*/
|
||||
public static GitHubApiHelper of(URI apiEndpoint,
|
||||
ProxyConfiguration proxyConfiguration,
|
||||
String userName,
|
||||
String token,
|
||||
boolean useJvmCacerts) {
|
||||
return new GitHubApiHelper(apiEndpoint, proxyConfiguration, userName, token, useJvmCacerts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link GitHubApiHelper} class.
|
||||
*
|
||||
* @param apiEndpoint the URI that specifies the API endpoint.
|
||||
* @param proxyConfiguration Configuration for http proxy
|
||||
* @return a new instance of the {@link GitHubApiHelper} class.
|
||||
*/
|
||||
public static GitHubApiHelper of(URI apiEndpoint,
|
||||
ProxyConfiguration proxyConfiguration,
|
||||
String userName,
|
||||
String token) {
|
||||
return new GitHubApiHelper(apiEndpoint, proxyConfiguration, userName, token, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link GitHubApiHelper} class.
|
||||
*
|
||||
* @return a new instance of the {@link GitHubApiHelper} class.
|
||||
*/
|
||||
public static GitHubApiHelper createDefault() {
|
||||
return new GitHubApiHelper();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest release from the specified repository.
|
||||
*
|
||||
* @param owner the repository's owner
|
||||
* @param repo the repository's name
|
||||
* @return An {@link Optional} that contains the latest release if it could be found.
|
||||
* @throws IOException if an IO error occurs while communicating with GiHub
|
||||
* @throws URISyntaxException if the crafted URI is incorrect.
|
||||
*/
|
||||
public Optional<GithubRelease> getLatestRelease(String owner, String repo) throws IOException, URISyntaxException, InterruptedException {
|
||||
return getRelease(owner, repo, "latest");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest release from the specified repository.
|
||||
*
|
||||
* @param slug the repository's slug (owner/name)
|
||||
* @return An {@link Optional} that contains the latest release if it could be found.
|
||||
* @throws IOException if an IO error occurs while communicating with GiHub
|
||||
* @throws URISyntaxException if the crafted URI is incorrect.
|
||||
*/
|
||||
public Optional<GithubRelease> getLatestRelease(String slug) throws IOException, URISyntaxException, InterruptedException {
|
||||
return getRelease(slug, "latest");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specific release from the specified repository.
|
||||
*
|
||||
* @param owner the repository's owner
|
||||
* @param repo the repository's name
|
||||
* @param id the id of the release to retrieve
|
||||
* @return An {@link Optional} that contains the specified release if it could be found.
|
||||
* @throws IOException if an IO error occurs while communicating with GitHub.
|
||||
* @throws URISyntaxException if the crafted URI is incorrect.
|
||||
*/
|
||||
public Optional<GithubRelease> getRelease(String owner, String repo, String id) throws IOException, URISyntaxException, InterruptedException {
|
||||
return getRelease(owner + "/" + repo, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specific release from the specified repository.
|
||||
*
|
||||
* @param slug the repository's slug (owner/name)
|
||||
* @param id the id of the release to retrieve
|
||||
* @return An {@link Optional} that contains the specified release if it could be found.
|
||||
* @throws IOException if an IO error occurs while communicating with GitHub.
|
||||
* @throws URISyntaxException if the crafted URI is incorrect.
|
||||
*/
|
||||
public Optional<GithubRelease> getRelease(String slug, String id) throws IOException, URISyntaxException, InterruptedException {
|
||||
var requestUrl = URI.create(apiEndpoint + "/repos/" + slug + "/releases/" + id);
|
||||
logger.debug(() -> "requestUrl = " + requestUrl);
|
||||
var httpget = basicAuthGet(requestUrl);
|
||||
var response = httpClient.send(httpget, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != 200) {
|
||||
throw new IOException("Failed to get release from " +
|
||||
requestUrl +
|
||||
"(HTTP status: " + response.statusCode() + ")");
|
||||
}
|
||||
return Optional.ofNullable(GSON.fromJson(response.body(), GithubRelease.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all release from the specified repository.
|
||||
*
|
||||
* @param owner the repository's owner
|
||||
* @param repo the repository's name
|
||||
* @return a list of all release from the specified repository.
|
||||
* @throws IOException if an IO error occurs while communicating with GitHub.
|
||||
* @throws URISyntaxException if the crafted URI is incorrect.
|
||||
*/
|
||||
public List<GithubRelease> getAllReleases(String owner, String repo) throws IOException, URISyntaxException, InterruptedException {
|
||||
return getAllReleases(owner + "/" + repo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all release from the specified repository.
|
||||
*
|
||||
* @param slug the repository's slug (owner/name)
|
||||
* @return a list of all release from the specified repository.
|
||||
* @throws IOException if an IO error occurs while communicating with GitHub.
|
||||
* @throws URISyntaxException if the crafted URI is incorrect.
|
||||
*/
|
||||
public List<GithubRelease> getAllReleases(String slug) throws IOException, URISyntaxException, InterruptedException {
|
||||
var requestUrl = URI.create(apiEndpoint +
|
||||
"/repos/" + slug + "/releases?per_page=100");
|
||||
logger.debug(() -> "requestUrl = " + requestUrl);
|
||||
var httpget = basicAuthGet(requestUrl);
|
||||
var response = httpClient.send(httpget, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != 200) {
|
||||
throw new IOException("Failed to get release list from " +
|
||||
requestUrl +
|
||||
"(HTTP status: " + response.statusCode() + ")");
|
||||
}
|
||||
return GSON.fromJson(response.body(), ghReleaseArrayType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all {@link GithubAsset} instances associated to a a {@link GithubRelease} instance.
|
||||
*
|
||||
* @param release The {@link GithubRelease} instance to get the assets from.
|
||||
* @return a list of all {@link GithubAsset} instances associated to a a {@link GithubRelease} instance.
|
||||
* @throws URISyntaxException if the crafted URI is incorrect.
|
||||
* @throws IOException if an IO error occurs while communicating with GitHub.
|
||||
*/
|
||||
public List<GithubAsset> getAssets(GithubRelease release) throws URISyntaxException, IOException, InterruptedException {
|
||||
logger.debug(() -> "requestUrl = " + release.getAssetsUrl());
|
||||
var httpget = basicAuthGet(release.getAssetsUrl().toURI());
|
||||
var response = httpClient.send(httpget, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != 200) {
|
||||
throw new IOException("Failed to download asset list from " +
|
||||
release.getAssetsUrl() +
|
||||
"(HTTP status: " + response.statusCode() + ")");
|
||||
}
|
||||
return GSON.fromJson(response.body(), ghAssetArrayType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the byteArrayTuple of the specified github asset to a temporary location.
|
||||
*
|
||||
* @param asset the asset to download.
|
||||
* @return the {@link Path} where it was downloaded.
|
||||
* @throws IOException if an IO error occurs while attempting to download the file.
|
||||
* @throws URISyntaxException if the crafted URI is incorrect.
|
||||
*/
|
||||
public Path downloadAsset(GithubAsset asset) throws IOException, URISyntaxException, InterruptedException {
|
||||
return downloadAsset(asset, Files.createTempDirectory(UserPreferences.getInstance().temporaryFilesRoot.get(), "binjr-updates_"));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Download the byteArrayTuple of the specified github asset into the specified directory.
|
||||
*
|
||||
* @param asset the asset to download.
|
||||
* @param targetDir the target directory to download the asset to.
|
||||
* @return the {@link Path} where it was downloaded.
|
||||
* @throws IOException if an IO error occurs while attempting to download the file.
|
||||
* @throws URISyntaxException if the crafted URI is incorrect.
|
||||
*/
|
||||
public Path downloadAsset(GithubAsset asset, Path targetDir) throws IOException, URISyntaxException, InterruptedException {
|
||||
return downloadAsset(asset, targetDir, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the byteArrayTuple of the specified github asset into the specified directory.
|
||||
*
|
||||
* @param asset the asset to download.
|
||||
* @param targetDir the target directory to download the asset to.
|
||||
* @param progress A {@link DoubleProperty} used to report download progression.
|
||||
* @return the {@link Path} where it was downloaded.
|
||||
* @throws IOException if an IO error occurs while attempting to download the file.
|
||||
* @throws URISyntaxException if the crafted URI is incorrect.
|
||||
*/
|
||||
public Path downloadAsset(GithubAsset asset, Path targetDir, DoubleProperty progress) throws IOException, URISyntaxException, InterruptedException {
|
||||
if (!Files.isDirectory(targetDir)) {
|
||||
throw new NotDirectoryException(targetDir.toString());
|
||||
}
|
||||
var assetSize = asset.getSize();
|
||||
var get = HttpRequest.newBuilder()
|
||||
.uri(asset.getBrowserDownloadUrl().toURI())
|
||||
.build();
|
||||
Path target = targetDir.resolve(asset.getName());
|
||||
var response = httpClient.send(get, HttpResponse.BodyHandlers.ofInputStream());
|
||||
if (response.statusCode() != 200) {
|
||||
throw new IOException("Failed to download asset from " +
|
||||
asset.getBrowserDownloadUrl() +
|
||||
"(HTTP status: " + response.statusCode() + ")");
|
||||
}
|
||||
try (var in = response.body()) {
|
||||
if (progress == null) {
|
||||
Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
} else {
|
||||
try (var out = Files.newOutputStream(target, StandardOpenOption.CREATE_NEW)) {
|
||||
IOUtils.copyStreams(in, out, assetSize, progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the user credentials for api authentication.
|
||||
*
|
||||
* @param username the user name
|
||||
* @param token a github personal access token.
|
||||
*/
|
||||
public void setUserCredentials(String username, String token) {
|
||||
if (username != null && token != null && !username.trim().isEmpty()) {
|
||||
this.userCredentials = Base64.getEncoder().encodeToString((username + ":" + token).getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
this.userCredentials = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected HttpRequest basicAuthGet(URI requestUri) {
|
||||
var requestBuilder = HttpRequest.newBuilder()
|
||||
.GET()
|
||||
.uri(requestUri);
|
||||
if (userCredentials != null) {
|
||||
requestBuilder.header("Authorization", "Basic " + userCredentials);
|
||||
}
|
||||
return requestBuilder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
httpClient.close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2019 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.github;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* A data class that represents a GitHub release asset.
|
||||
*/
|
||||
public class GithubAsset {
|
||||
private URL url;
|
||||
private int id;
|
||||
@SerializedName("node_id")
|
||||
private String nodeId;
|
||||
private String name;
|
||||
private String label;
|
||||
private GithubUser uploader;
|
||||
@SerializedName("content_type")
|
||||
private String contentType;
|
||||
private String state;
|
||||
private int size;
|
||||
@SerializedName("download_count")
|
||||
private int downloadCount;
|
||||
@SerializedName("created_at")
|
||||
private Date createdAt;
|
||||
@SerializedName("updated_at")
|
||||
private Date updatedAt;
|
||||
@SerializedName("browser_download_url")
|
||||
private URL browserDownloadUrl;
|
||||
|
||||
public URL getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(URL url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getNodeId() {
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
public void setNodeId(String nodeId) {
|
||||
this.nodeId = nodeId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public GithubUser getUploader() {
|
||||
return uploader;
|
||||
}
|
||||
|
||||
public void setUploader(GithubUser uploader) {
|
||||
this.uploader = uploader;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public void setContentType(String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(int size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public int getDownloadCount() {
|
||||
return downloadCount;
|
||||
}
|
||||
|
||||
public void setDownloadCount(int downloadCount) {
|
||||
this.downloadCount = downloadCount;
|
||||
}
|
||||
|
||||
public Date getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Date createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Date getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Date updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public URL getBrowserDownloadUrl() {
|
||||
return browserDownloadUrl;
|
||||
}
|
||||
|
||||
public void setBrowserDownloadUrl(URL browserDownloadUrl) {
|
||||
this.browserDownloadUrl = browserDownloadUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GithubAsset{" +
|
||||
"name='" + name + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
/*
|
||||
* Copyright 2017-2020 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.github;
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import eu.binjr.common.version.Version;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* GitHub release POJO
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class GithubRelease {
|
||||
private static final Logger logger = Logger.create(GithubRelease.class);
|
||||
private URL url;
|
||||
@SerializedName("html_url")
|
||||
private URL htmlUrl;
|
||||
@SerializedName("assets_url")
|
||||
private URL assetsUrl;
|
||||
@SerializedName("upload_url")
|
||||
private URL uploadUrl;
|
||||
@SerializedName("tarball_url")
|
||||
private URL tarballUrl;
|
||||
@SerializedName("zipball_url")
|
||||
private URL zipballUrl;
|
||||
private long id;
|
||||
@SerializedName("tag_name")
|
||||
private String tagName;
|
||||
@SerializedName("target_commitish")
|
||||
private String targetCommitish;
|
||||
private String name;
|
||||
private String body;
|
||||
@SerializedName("draft")
|
||||
private boolean isDraft;
|
||||
@SerializedName("prerelease")
|
||||
private boolean isPrerelease;
|
||||
@SerializedName("created_at")
|
||||
private Date createdAt;
|
||||
@SerializedName("published_at")
|
||||
private Date publishedAt;
|
||||
private GithubUser author;
|
||||
@SerializedName("assets")
|
||||
private List<GithubAsset> assets;
|
||||
|
||||
/**
|
||||
* Returns the url associated to the release.
|
||||
*
|
||||
* @return the url associated to the release.
|
||||
*/
|
||||
public URL getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the url associated to the release.
|
||||
*
|
||||
* @param url the url associated to the release.
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setUrl(URL url) {
|
||||
this.url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url associated to the html page.
|
||||
*
|
||||
* @return htmlUrl the url associated to the html page.
|
||||
*/
|
||||
public URL getHtmlUrl() {
|
||||
return htmlUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the url associated to the html page.
|
||||
*
|
||||
* @param htmlUrl the url associated to the html page.
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setHtmlUrl(URL htmlUrl) {
|
||||
this.htmlUrl = htmlUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the assets url
|
||||
*
|
||||
* @return the assets url
|
||||
*/
|
||||
public URL getAssetsUrl() {
|
||||
return assetsUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the assets url
|
||||
*
|
||||
* @param assetsUrl the assets url
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setAssetsUrl(URL assetsUrl) {
|
||||
this.assetsUrl = assetsUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the upload url
|
||||
*
|
||||
* @return the upload url
|
||||
*/
|
||||
public URL getUploadUrl() {
|
||||
return uploadUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the upload url
|
||||
*
|
||||
* @param uploadUrl the upload url
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setUploadUrl(URL uploadUrl) {
|
||||
this.uploadUrl = uploadUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retuns the tarball url
|
||||
*
|
||||
* @return the tarball url
|
||||
*/
|
||||
public URL getTarballUrl() {
|
||||
return tarballUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tarball url
|
||||
*
|
||||
* @param tarballUrl the tarball url
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setTarballUrl(URL tarballUrl) {
|
||||
this.tarballUrl = tarballUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the zip url.
|
||||
*
|
||||
* @return zipballUrl the zip url.
|
||||
*/
|
||||
public URL getZipballUrl() {
|
||||
return zipballUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the zip url.
|
||||
*
|
||||
* @param zipballUrl the zip url.
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setZipballUrl(URL zipballUrl) {
|
||||
this.zipballUrl = zipballUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of the release
|
||||
*
|
||||
* @return id the id of the release
|
||||
*/
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the id of the release
|
||||
*
|
||||
* @param id the id of the release
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setId(long id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tag name.
|
||||
*
|
||||
* @return tagName the tag name.
|
||||
*/
|
||||
public String getTagName() {
|
||||
return tagName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tag name.
|
||||
*
|
||||
* @param tagName the tag name.
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setTagName(String tagName) {
|
||||
this.tagName = tagName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target commitish
|
||||
*
|
||||
* @return targetCommitish the target commitish
|
||||
*/
|
||||
public String getTargetCommitish() {
|
||||
return targetCommitish;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the target commitish
|
||||
*
|
||||
* @param targetCommitish the target commitish
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setTargetCommitish(String targetCommitish) {
|
||||
this.targetCommitish = targetCommitish;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the release.
|
||||
*
|
||||
* @return name the name of the release.
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the release.
|
||||
*
|
||||
* @param name the name of the release.
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the body of the release.
|
||||
*
|
||||
* @return body the body of the release.
|
||||
*/
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the body of the release.
|
||||
*
|
||||
* @param body the body of the release.
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setBody(String body) {
|
||||
this.body = body;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true is the release is a draft, false otherwise.
|
||||
*
|
||||
* @return isDraft true is the release is a draft, false otherwise.
|
||||
*/
|
||||
public boolean isDraft() {
|
||||
return isDraft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets to true is the release is a draft, false otherwise.
|
||||
*
|
||||
* @param isDraft true is the release is a draft, false otherwise.
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setDraft(boolean isDraft) {
|
||||
this.isDraft = isDraft;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the release is a pre-release, false otherwise.
|
||||
*
|
||||
* @return isPrerelease true is the release is a pre-release, false otherwise.
|
||||
*/
|
||||
public boolean isPrerelease() {
|
||||
return isPrerelease;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets to true if the release is a pre-release, false otherwise.
|
||||
*
|
||||
* @param isPrerelease true if the release is a pre-release, false otherwise.
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setPrerelease(boolean isPrerelease) {
|
||||
this.isPrerelease = isPrerelease;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the creation date.
|
||||
*
|
||||
* @return createdAt the creation date.
|
||||
*/
|
||||
public Date getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the creation date.
|
||||
*
|
||||
* @param createdAt the creation date.
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setCreatedAt(Date createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the publication date.
|
||||
*
|
||||
* @return publishedAt the publication date.
|
||||
*/
|
||||
public Date getPublishedAt() {
|
||||
return publishedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the publication date.
|
||||
*
|
||||
* @param publishedAt the publication date.
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setPublishedAt(Date publishedAt) {
|
||||
this.publishedAt = publishedAt;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the author of the release.
|
||||
*
|
||||
* @return author the author of the release.
|
||||
*/
|
||||
public GithubUser getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the author of the release.
|
||||
*
|
||||
* @param author the author of the release.
|
||||
* @return this release
|
||||
*/
|
||||
public GithubRelease setAuthor(GithubUser author) {
|
||||
this.author = author;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version of the release.
|
||||
*
|
||||
* @return the version of the release.
|
||||
*/
|
||||
public Version getVersion() {
|
||||
if (tagName == null) {
|
||||
return Version.emptyVersion;
|
||||
}
|
||||
try {
|
||||
return new Version(tagName.replaceAll("^v", ""));
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.error("Could not decode version number from tag: " + tagName, e);
|
||||
return Version.emptyVersion;
|
||||
}
|
||||
}
|
||||
|
||||
public List<GithubAsset> getAssets() {
|
||||
return assets;
|
||||
}
|
||||
|
||||
public void setAssets(List<GithubAsset> assets) {
|
||||
this.assets = assets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("GithubRelease{");
|
||||
sb.append("url='").append(url).append('\'');
|
||||
sb.append(", htmlUrl='").append(htmlUrl).append('\'');
|
||||
sb.append(", assetsUrl='").append(assetsUrl).append('\'');
|
||||
sb.append(", uploadUrl='").append(uploadUrl).append('\'');
|
||||
sb.append(", tarballUrl='").append(tarballUrl).append('\'');
|
||||
sb.append(", zipballUrl='").append(zipballUrl).append('\'');
|
||||
sb.append(", id=").append(id);
|
||||
sb.append(", tagName='").append(tagName).append('\'');
|
||||
sb.append(", targetCommitish='").append(targetCommitish).append('\'');
|
||||
sb.append(", name='").append(name).append('\'');
|
||||
sb.append(", body='").append(body).append('\'');
|
||||
sb.append(", isDraft=").append(isDraft);
|
||||
sb.append(", isPrerelease=").append(isPrerelease);
|
||||
sb.append(", createdAt=").append(createdAt);
|
||||
sb.append(", publishedAt=").append(publishedAt);
|
||||
sb.append(", author=").append(author);
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.github;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
/**
|
||||
* GitHub user POJO
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class GithubUser {
|
||||
private boolean hireable;
|
||||
@SerializedName("created_at")
|
||||
private Date createdAt;
|
||||
private int collaborators;
|
||||
@SerializedName("disk_usage")
|
||||
private int diskUsage;
|
||||
private int followers;
|
||||
private int following;
|
||||
private int id;
|
||||
@SerializedName("owned_private_repos")
|
||||
private int ownedPrivateRepos;
|
||||
@SerializedName("private_gists")
|
||||
private int privateGists;
|
||||
@SerializedName("public_gists")
|
||||
private int publicGists;
|
||||
@SerializedName("public_repos")
|
||||
private int publicRepos;
|
||||
@SerializedName("total_private_repos")
|
||||
private int totalPrivateRepos;
|
||||
@SerializedName("avatar_url")
|
||||
private String avatarUrl;
|
||||
private String bio;
|
||||
private String blog;
|
||||
private String company;
|
||||
private String email;
|
||||
@SerializedName("gravatar_id")
|
||||
private String gravatarId;
|
||||
@SerializedName("html_url")
|
||||
private String htmlUrl;
|
||||
private String location;
|
||||
private String login;
|
||||
private String name;
|
||||
private String type;
|
||||
private String url;
|
||||
|
||||
public boolean isHireable() {
|
||||
return hireable;
|
||||
}
|
||||
|
||||
public GithubUser setHireable(boolean hireable) {
|
||||
this.hireable = hireable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Date getCreatedAt() {
|
||||
return new Date(createdAt.getTime());
|
||||
}
|
||||
|
||||
public GithubUser setCreatedAt(Date createdAt) {
|
||||
this.createdAt = new Date(createdAt.getTime());
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getCollaborators() {
|
||||
return collaborators;
|
||||
}
|
||||
|
||||
public GithubUser setCollaborators(int collaborators) {
|
||||
this.collaborators = collaborators;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getDiskUsage() {
|
||||
return diskUsage;
|
||||
}
|
||||
|
||||
public GithubUser setDiskUsage(int diskUsage) {
|
||||
this.diskUsage = diskUsage;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getFollowers() {
|
||||
return followers;
|
||||
}
|
||||
|
||||
public GithubUser setFollowers(int followers) {
|
||||
this.followers = followers;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getFollowing() {
|
||||
return following;
|
||||
}
|
||||
|
||||
public GithubUser setFollowing(int following) {
|
||||
this.following = following;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public GithubUser setId(int id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getOwnedPrivateRepos() {
|
||||
return ownedPrivateRepos;
|
||||
}
|
||||
|
||||
public GithubUser setOwnedPrivateRepos(int ownedPrivateRepos) {
|
||||
this.ownedPrivateRepos = ownedPrivateRepos;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getPrivateGists() {
|
||||
return privateGists;
|
||||
}
|
||||
|
||||
public GithubUser setPrivateGists(int privateGists) {
|
||||
this.privateGists = privateGists;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getPublicGists() {
|
||||
return publicGists;
|
||||
}
|
||||
|
||||
public GithubUser setPublicGists(int publicGists) {
|
||||
this.publicGists = publicGists;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getPublicRepos() {
|
||||
return publicRepos;
|
||||
}
|
||||
|
||||
public GithubUser setPublicRepos(int publicRepos) {
|
||||
this.publicRepos = publicRepos;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getTotalPrivateRepos() {
|
||||
return totalPrivateRepos;
|
||||
}
|
||||
|
||||
public GithubUser setTotalPrivateRepos(int totalPrivateRepos) {
|
||||
this.totalPrivateRepos = totalPrivateRepos;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public GithubUser setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getBio() {
|
||||
return bio;
|
||||
}
|
||||
|
||||
public GithubUser setBio(String bio) {
|
||||
this.bio = bio;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getBlog() {
|
||||
return blog;
|
||||
}
|
||||
|
||||
public GithubUser setBlog(String blog) {
|
||||
this.blog = blog;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getCompany() {
|
||||
return company;
|
||||
}
|
||||
|
||||
public GithubUser setCompany(String company) {
|
||||
this.company = company;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public GithubUser setEmail(String email) {
|
||||
this.email = email;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public String getGravatarId() {
|
||||
return gravatarId;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public GithubUser setGravatarId(String gravatarId) {
|
||||
this.gravatarId = gravatarId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getHtmlUrl() {
|
||||
return htmlUrl;
|
||||
}
|
||||
|
||||
public GithubUser setHtmlUrl(String htmlUrl) {
|
||||
this.htmlUrl = htmlUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public GithubUser setLocation(String location) {
|
||||
this.location = location;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getLogin() {
|
||||
return login;
|
||||
}
|
||||
|
||||
public GithubUser setLogin(String login) {
|
||||
this.login = login;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public GithubUser setName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public GithubUser setType(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public GithubUser setUrl(String url) {
|
||||
this.url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("GithubUser{");
|
||||
sb.append("hireable=").append(hireable);
|
||||
sb.append(", createdAt=").append(createdAt);
|
||||
sb.append(", collaborators=").append(collaborators);
|
||||
sb.append(", diskUsage=").append(diskUsage);
|
||||
sb.append(", followers=").append(followers);
|
||||
sb.append(", following=").append(following);
|
||||
sb.append(", id=").append(id);
|
||||
sb.append(", ownedPrivateRepos=").append(ownedPrivateRepos);
|
||||
sb.append(", privateGists=").append(privateGists);
|
||||
sb.append(", publicGists=").append(publicGists);
|
||||
sb.append(", publicRepos=").append(publicRepos);
|
||||
sb.append(", totalPrivateRepos=").append(totalPrivateRepos);
|
||||
sb.append(", avatarUrl='").append(avatarUrl).append('\'');
|
||||
sb.append(", bio='").append(bio).append('\'');
|
||||
sb.append(", blog='").append(blog).append('\'');
|
||||
sb.append(", company='").append(company).append('\'');
|
||||
sb.append(", email='").append(email).append('\'');
|
||||
sb.append(", gravatarId='").append(gravatarId).append('\'');
|
||||
sb.append(", htmlUrl='").append(htmlUrl).append('\'');
|
||||
sb.append(", location='").append(location).append('\'');
|
||||
sb.append(", login='").append(login).append('\'');
|
||||
sb.append(", name='").append(name).append('\'');
|
||||
sb.append(", type='").append(type).append('\'');
|
||||
sb.append(", url='").append(url).append('\'');
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2022-2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.io;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import javax.crypto.*;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.*;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.util.Base64;
|
||||
|
||||
public class AesHelper {
|
||||
|
||||
private static final String AES = "AES";
|
||||
public static final String AES_GCM_NO_PADDING = "AES/GCM/NoPadding";
|
||||
private static final String PBKDF_2_WITH_HMAC_SHA_256 = "PBKDF2WithHmacSHA256";
|
||||
private static final int IV_LEN = 16;
|
||||
private static final Logger logger = LogManager.getLogger(AesHelper.class);
|
||||
|
||||
|
||||
public static String encrypt(String input, SecretKey key)
|
||||
throws GeneralSecurityException {
|
||||
var params = generateIv();
|
||||
Cipher cipher = Cipher.getInstance(AES_GCM_NO_PADDING);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, params);
|
||||
byte[] cipherText = cipher.doFinal(input.getBytes());
|
||||
return Base64.getEncoder().encodeToString(IOUtils.concat(params.getIV(), cipherText));
|
||||
}
|
||||
|
||||
public static String decrypt(String input, SecretKey key) throws GeneralSecurityException {
|
||||
var b = IOUtils.split(Base64.getDecoder().decode(input), IV_LEN);
|
||||
var params = new GCMParameterSpec(IV_LEN * 8, b.first());
|
||||
Cipher cipher = Cipher.getInstance(AES_GCM_NO_PADDING);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, params);
|
||||
byte[] plainText = cipher.doFinal(b.second());
|
||||
return new String(plainText);
|
||||
}
|
||||
|
||||
public static boolean validateKey(SecretKey key) {
|
||||
try {
|
||||
var params = generateIv();
|
||||
Cipher cipher = Cipher.getInstance(AES_GCM_NO_PADDING);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, params);
|
||||
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException |
|
||||
InvalidKeyException e) {
|
||||
logger.error("Cannot initialize Cipher with provided key:" + e.getMessage());
|
||||
logger.debug("Stack trace", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static SecretKey getKeyFromEncoded(String encoded) {
|
||||
byte[] buffer = Base64.getDecoder().decode(encoded);
|
||||
return new SecretKeySpec(buffer, AES);
|
||||
}
|
||||
|
||||
public static SecretKey generateKey(int n) throws NoSuchAlgorithmException {
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance(AES);
|
||||
keyGenerator.init(n);
|
||||
return keyGenerator.generateKey();
|
||||
}
|
||||
|
||||
public static String generateEncodedKey(int n) throws NoSuchAlgorithmException {
|
||||
return encodeSecretKey(generateKey(n));
|
||||
}
|
||||
|
||||
public static String encodeSecretKey(SecretKey secretKey) {
|
||||
return Base64.getEncoder().encodeToString(secretKey.getEncoded());
|
||||
}
|
||||
|
||||
public static SecretKey deriveKeyFromPassword(String password, String salt)
|
||||
throws NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
return deriveKeyFromPassword(password.toCharArray(), salt.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public static SecretKey deriveKeyFromPassword(char[] password, byte[] salt)
|
||||
throws NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF_2_WITH_HMAC_SHA_256);
|
||||
KeySpec spec = new PBEKeySpec(password, salt, 65536, 256);
|
||||
return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), AES);
|
||||
}
|
||||
|
||||
public static GCMParameterSpec generateIv() {
|
||||
byte[] iv = new byte[IV_LEN];
|
||||
new SecureRandom().nextBytes(iv);
|
||||
return new GCMParameterSpec(IV_LEN * 8, iv);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* Copyright 2020-2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.io;
|
||||
|
||||
import eu.binjr.common.function.CheckedLambdas;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Defines methods to help list entries in a file system, and obtain input streams on files it contains.
|
||||
* Roots for a {@link FileSystemBrowser} can be either folders in the default file system or zipfs compatible files
|
||||
* (typically .zip and .jar files).
|
||||
*/
|
||||
public class FileSystemBrowser implements Closeable {
|
||||
private static final Logger logger = Logger.create(FileSystemBrowser.class);
|
||||
private final Path fsRoot;
|
||||
private final FileSystem fs;
|
||||
|
||||
/**
|
||||
* Initialize a new browser from a single root
|
||||
*
|
||||
* @param fsRoot the root path for the browser
|
||||
* @throws IOException if an error occurs while initializing the browser.
|
||||
*/
|
||||
private FileSystemBrowser(Path fsRoot) throws IOException {
|
||||
this(Objects.requireNonNull(fsRoot).getFileSystem(), fsRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new browser from a list of roots and a {@link FileSystem}
|
||||
* NB: All roots must belong to the same FileSystem
|
||||
*
|
||||
* @param fs the FileSystem for the browser
|
||||
* @param fsRoot a list of root directories to browse
|
||||
* @throws IOException if an error occurs while initializing the browser.
|
||||
*/
|
||||
private FileSystemBrowser(FileSystem fs, Path fsRoot) throws IOException {
|
||||
Objects.requireNonNull(fsRoot);
|
||||
this.fs = fs;
|
||||
this.fsRoot = fsRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link FileSystemBrowser} instance for the provided path.
|
||||
* Valid path can be either a folder of the default file system or a zip.jar file.
|
||||
*
|
||||
* @param path the path to browse.
|
||||
* @return a new {@link FileSystemBrowser} instance for the provided path
|
||||
* @throws IOException if an error occurs while initializing the browser.
|
||||
*/
|
||||
public static FileSystemBrowser of(Path path) throws IOException {
|
||||
if (Files.isDirectory(path)) {
|
||||
return new FileSystemBrowser(path);
|
||||
}
|
||||
if (path.toString().toLowerCase(Locale.ROOT).endsWith(".zip") ||
|
||||
(path.toString().toLowerCase(Locale.ROOT).endsWith(".jar"))) {
|
||||
var fs = FileSystems.newFileSystem(path, (ClassLoader) null);
|
||||
for (var root : fs.getRootDirectories()) {
|
||||
// we only care about the first root in a zip file
|
||||
return new FileSystemBrowser(fs, root);
|
||||
}
|
||||
} else {
|
||||
// Return an instance that only lists the single file that was passed as root.
|
||||
return new FileSystemBrowser(path.getParent()){
|
||||
@Override
|
||||
public Collection<FileSystemEntry> listEntries(Predicate<Path> filter) throws IOException {
|
||||
return List.of(new FileSystemEntry(Files.isDirectory(path), path.getFileName(), Files.size(path)));
|
||||
}
|
||||
};
|
||||
}
|
||||
throw new UnsupportedOperationException("Unsupported operation: path is not a folder nor a zip file");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link FileSystemBrowser} instance for the provided URI and a root path inside the target file system
|
||||
* Valid URI can point to a zip or a jar file.
|
||||
*
|
||||
* @param uri the URI to browse the fs to browse
|
||||
* @param first the path string or initial part of the path string to browse
|
||||
* @param more additional strings to be joined to form the path string to browse
|
||||
* @return a new {@link FileSystemBrowser} instance
|
||||
* @throws IOException if an error occurs while initializing the browser.
|
||||
*/
|
||||
public static FileSystemBrowser of(URI uri, String first, String... more) throws IOException {
|
||||
return new FileSystemBrowser(FileSystems.newFileSystem(uri, Collections.emptyMap()).getPath(first, more));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link InputStream} for the file system entry identified by the provided path.
|
||||
* <p>
|
||||
* <b>NOTE:</b> It is the caller's responsibility to close the returned stream when no longer needed.
|
||||
* </p>
|
||||
*
|
||||
* @param path the path of the file system entry to get a stream for.
|
||||
* @return an {@link InputStream} for the file system entry identified by the provided path.
|
||||
* @throws IOException If no entry could be identified in the underlying file system for the provided path.
|
||||
*/
|
||||
public InputStream getData(String path) throws IOException {
|
||||
return Files.newInputStream(getRootDirectory().resolve(path), StandardOpenOption.READ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of {@link InputStream} for all file system entries matching the provided path predicate.
|
||||
* <p>
|
||||
* <b>NOTE:</b> It is the caller's responsibility to close the returned stream when no longer needed.
|
||||
* </p>
|
||||
*
|
||||
* @param filter a predicate that filters the file system entries to return a stream for.
|
||||
* @return a collection of {@link InputStream} for all file system entries matching the provided path predicate.
|
||||
* @throws IOException If no entry could be identified in the underlying file system for the provided path.
|
||||
*/
|
||||
public Collection<InputStream> getData(Predicate<Path> filter) throws IOException {
|
||||
var streams = new ArrayList<InputStream>();
|
||||
Files.walkFileTree(fsRoot, EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, new SimpleFileVisitor<>() {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
|
||||
var p = fsRoot.relativize(path);
|
||||
if (filter.test(p)) {
|
||||
streams.add(Files.newInputStream(fsRoot.resolve(p), StandardOpenOption.READ));
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
|
||||
logger.warn(() -> "An error occurred while accessing file " + file + ": " + exc.getMessage());
|
||||
logger.debug(() -> "Full stack", exc);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
return streams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of {@link FileSystemEntry} for all paths specified in the target file system
|
||||
*
|
||||
* @param paths list of paths to return a {@link FileSystemEntry} for.
|
||||
* @return a collection of {@link FileSystemEntry}.
|
||||
* @throws IOException If an error occurs while listing file system entries.
|
||||
*/
|
||||
public Collection<FileSystemEntry> listEntries(Collection<Path> paths) throws IOException {
|
||||
return paths.stream()
|
||||
.map(fsRoot::resolve)
|
||||
.filter(Files::exists)
|
||||
.map(CheckedLambdas.wrap(p -> {
|
||||
return new FileSystemEntry(Files.isDirectory(p), fsRoot.relativize(p), Files.size(fsRoot.resolve(p)));
|
||||
}))
|
||||
.sorted(FileSystemEntry::compareTo)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* * Returns a {@link FileSystemEntry} instance for the path specified in the target file system
|
||||
*
|
||||
* @param path path to return a {@link FileSystemEntry} for.
|
||||
* @return a {@link FileSystemEntry} instance for the path specified
|
||||
* @throws IOException If an error occurs while listing file system entries.
|
||||
*/
|
||||
public FileSystemEntry getEntry(String path) throws IOException {
|
||||
var p = getRootDirectory().resolve(path);
|
||||
return new FileSystemEntry(Files.isDirectory(p), p, Files.size(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of {@link FileSystemEntry} for all file system entries matching the provided path predicate.
|
||||
*
|
||||
* @param filter a predicate that filters the file system entries to return a {@link FileSystemEntry} for.
|
||||
* @return a collection of {@link FileSystemEntry} for all file system entries matching the provided path predicate.
|
||||
* @throws IOException If an error occurs while listing file system entries.
|
||||
*/
|
||||
public Collection<FileSystemEntry> listEntries(Predicate<Path> filter) throws IOException {
|
||||
var subEntries = new ArrayList<FileSystemEntry>();
|
||||
Files.walkFileTree(fsRoot, EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, new SimpleFileVisitor<>() {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
|
||||
var p = fsRoot.relativize(path);
|
||||
if (filter.test(p)) {
|
||||
subEntries.add(new FileSystemEntry(Files.isDirectory(p), p, Files.size(fsRoot.resolve(p))));
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
|
||||
logger.warn(() -> "An error occurred while accessing file " + file + ": " + exc.getMessage());
|
||||
logger.debug(() -> "Full stack", exc);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
return subEntries.stream().sorted(FileSystemEntry::compareTo).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of {@link FileSystemEntry} for all file system entries matching the provided
|
||||
* folder and files glob patterns.
|
||||
*
|
||||
* @param folderFilters a list of names of folders to inspect for content.
|
||||
* @param fileExtensionsFilters a list of file extensions patterns to inspect for content.
|
||||
* @return a collection of {@link FileSystemEntry} for all file system entries matching the provided path predicate.
|
||||
* @throws IOException If an error occurs while listing file system entries.
|
||||
*/
|
||||
public Collection<FileSystemEntry> listEntries(String[] folderFilters, String[] fileExtensionsFilters) throws IOException {
|
||||
return listEntries(path -> path.getFileName() != null &&
|
||||
Arrays.stream(folderFilters)
|
||||
.map(folder -> folder.equalsIgnoreCase("*") || path.startsWith(this.toInternalPath(folder)))
|
||||
.reduce(Boolean::logicalOr).orElse(false) &&
|
||||
Arrays.stream(fileExtensionsFilters)
|
||||
.map(f -> f.equalsIgnoreCase("*") ||
|
||||
f.equalsIgnoreCase("*.*") ||
|
||||
path.getFileName().toString().matches(("\\Q" + f + "\\E").replace("*", "\\E.*\\Q").replace("?", "\\E.\\Q")))
|
||||
.reduce(Boolean::logicalOr).orElse(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file system root directory for this {@link FileSystemBrowser} instance
|
||||
*
|
||||
* @return the file system root directory for this {@link FileSystemBrowser} instance
|
||||
*/
|
||||
public Path getRootDirectory() {
|
||||
return fsRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a path string, or a sequence of strings that when joined form a path string, to a {@link Path} valid
|
||||
* inside the browser's {@link FileSystem}
|
||||
*
|
||||
* @param first the path string or initial part of the path string
|
||||
* @param more additional strings to be joined to form the path string
|
||||
* @return the resulting {@code Path}
|
||||
* @throws InvalidPathException If the path string cannot be converted
|
||||
*/
|
||||
public Path toInternalPath(String first, String... more) {
|
||||
return this.fs.getPath(first, more);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (!this.fs.equals(FileSystems.getDefault())) {
|
||||
this.fs.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data class to carry selected properties describing file system entries browsed by {@link FileSystemBrowser}
|
||||
*/
|
||||
public static class FileSystemEntry implements Comparable<FileSystemEntry> {
|
||||
private final boolean directory;
|
||||
private final Path path;
|
||||
private final Long size;
|
||||
|
||||
public FileSystemEntry(boolean directory, Path path, Long size) {
|
||||
this.directory = directory;
|
||||
this.path = path;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public Long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(FileSystemEntry o) {
|
||||
var dirComp = Boolean.compare(this.directory, o.directory);
|
||||
if (dirComp != 0) {
|
||||
return dirComp;
|
||||
}
|
||||
|
||||
var rootComp = Boolean.compare(isRoot(this.path), isRoot(o.path));
|
||||
if (rootComp != 0) {
|
||||
return rootComp;
|
||||
}
|
||||
|
||||
var pathComp = this.path.toString().compareToIgnoreCase(o.path.toString());
|
||||
if (pathComp != 0) {
|
||||
return pathComp;
|
||||
}
|
||||
var depthComp = pathDepthFromRoot(o.path) - pathDepthFromRoot(this.path);
|
||||
if (depthComp != 0) {
|
||||
return depthComp;
|
||||
}
|
||||
return Long.compare(this.size, o.size);
|
||||
}
|
||||
|
||||
private boolean isRoot(Path path) {
|
||||
return path.getParent() == null || path.getParent().toString().equals("/");
|
||||
}
|
||||
|
||||
private int pathDepthFromRoot(Path path) {
|
||||
int depth = 0;
|
||||
Path parent = path.getParent();
|
||||
while (parent != null) {
|
||||
depth++;
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return path.hashCode() + Long.hashCode(size) + Boolean.hashCode(directory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
FileSystemEntry fse = (FileSystemEntry) obj;
|
||||
return path.equals(fse.path) &&
|
||||
size.equals(fse.size) &&
|
||||
directory == fse.directory;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright 2017-2022 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.io;
|
||||
|
||||
import eu.binjr.common.function.CheckedLambdas;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Utility methods to read, write and copy data from and across streams
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class IOUtils {
|
||||
private static final int DEFAULT_COPY_BUFFER_SIZE = 16384;
|
||||
private static final int EOF = -1;
|
||||
private static final Logger logger = Logger.create(IOUtils.class);
|
||||
|
||||
public static long copyChannels(ReadableByteChannel input, WritableByteChannel output) throws IOException {
|
||||
return copyChannels(input, output, DEFAULT_COPY_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
public static long copyChannels(ReadableByteChannel input, WritableByteChannel output, int bufferSize) throws IOException {
|
||||
Objects.requireNonNull(input, "Argument input must not be null");
|
||||
Objects.requireNonNull(output, "Argument output must not be null");
|
||||
ByteBuffer buffer = ByteBuffer.allocateDirect(bufferSize);
|
||||
long count = 0;
|
||||
while (input.read(buffer) != -1) {
|
||||
buffer.flip();
|
||||
count += output.write(buffer);
|
||||
buffer.compact();
|
||||
}
|
||||
buffer.flip();
|
||||
while (buffer.hasRemaining()) {
|
||||
count += output.write(buffer);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public static long copyStreams(InputStream input, OutputStream output) throws IOException {
|
||||
return copyStreams(input, output, DEFAULT_COPY_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
public static long copyStreams(InputStream input, OutputStream output, int bufferSize) throws IOException {
|
||||
Objects.requireNonNull(input, "Argument input must not be null");
|
||||
Objects.requireNonNull(output, "Argument output must not be null");
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
long count = 0;
|
||||
int n;
|
||||
while (EOF != (n = input.read(buffer))) {
|
||||
output.write(buffer, 0, n);
|
||||
count += n;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public static long copyStreams(InputStream input, OutputStream output,long size, DoubleProperty progress) throws IOException {
|
||||
Objects.requireNonNull(input, "Argument input must not be null");
|
||||
Objects.requireNonNull(output, "Argument output must not be null");
|
||||
if (size == 0){
|
||||
size = -1;
|
||||
}
|
||||
byte[] buffer = new byte[DEFAULT_COPY_BUFFER_SIZE];
|
||||
long count = 0;
|
||||
int n;
|
||||
while (EOF != (n = input.read(buffer))) {
|
||||
output.write(buffer, 0, n);
|
||||
count += n;
|
||||
progress.set(count / (double) size);
|
||||
}
|
||||
progress.set(1.0);
|
||||
return count;
|
||||
}
|
||||
|
||||
public static byte[] readToBuffer(InputStream input) throws IOException {
|
||||
Objects.requireNonNull(input, "Argument input must not be null");
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
long count = copyStreams(input, baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static long consumeStream(InputStream input) throws IOException {
|
||||
Objects.requireNonNull(input, "Argument input must not be null");
|
||||
byte[] buffer = new byte[DEFAULT_COPY_BUFFER_SIZE];
|
||||
long count = 0;
|
||||
int n;
|
||||
while (EOF != (n = input.read(buffer))) {
|
||||
count += n;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public static String readToString(InputStream in) throws IOException {
|
||||
return new String(readToBuffer(in));
|
||||
}
|
||||
|
||||
public static String readToString(InputStream in, String charset) throws IOException {
|
||||
return new String(readToBuffer(in), charset);
|
||||
}
|
||||
|
||||
public static <T extends AutoCloseable> void closeAll(Collection<T> collection) {
|
||||
closeAll(collection.stream());
|
||||
}
|
||||
|
||||
public static <T extends AutoCloseable> void closeAll(Collection<T> collection, BiConsumer<T, Exception> onError) {
|
||||
closeAll(collection.stream(), onError);
|
||||
}
|
||||
|
||||
public static <T extends AutoCloseable> void closeAll(Stream<T> stream) {
|
||||
closeAll(stream, (c, e) -> {
|
||||
logger.error("An error occurred while closing element" + c + ": " + e.getMessage());
|
||||
logger.debug("Stack Trace:", e);
|
||||
});
|
||||
}
|
||||
|
||||
public static <T extends AutoCloseable> void closeAll(Stream<T> stream, BiConsumer<T, Exception> onError) {
|
||||
Objects.requireNonNull(stream, "Argument collection must not be null");
|
||||
stream.forEach(closeable -> close(closeable, onError));
|
||||
}
|
||||
|
||||
public static <T extends AutoCloseable> void close(T closeable) {
|
||||
close(closeable, (t, e) -> {
|
||||
logger.error("An error occurred while closing " + t + ": " + e.getMessage());
|
||||
logger.debug("Stack Trace:", e);
|
||||
});
|
||||
}
|
||||
|
||||
public static <T extends AutoCloseable> void close(T closeable, BiConsumer<T, Exception> onError) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
logger.trace(() -> "Closing: " + closeable);
|
||||
closeable.close();
|
||||
} catch (Exception e) {
|
||||
onError.accept(closeable, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyDirectory(Path from, Path to, CopyOption... options) throws IOException {
|
||||
try (final Stream<Path> sources = Files.walk(from)) {
|
||||
sources.forEach(CheckedLambdas.wrap(src -> {
|
||||
final Path dest = to.resolve(from.relativize(src).toString());
|
||||
if (Files.isDirectory(src)) {
|
||||
if (Files.notExists(dest)) {
|
||||
logger.trace("Creating directory {}", dest);
|
||||
Files.createDirectories(dest);
|
||||
}
|
||||
} else {
|
||||
logger.trace("Extracting file {} to {}", src, dest);
|
||||
Files.copy(src, dest, options);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
public static void attemptDeleteTempPath(Path path) {
|
||||
try {
|
||||
logger.debug("Attempting to delete " + path);
|
||||
Files.walkFileTree(path, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) throws IOException {
|
||||
try {
|
||||
Files.delete(path);
|
||||
logger.trace("Deleted file: " + path);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Temporary file " + path + " could not be deleted: " + e.getMessage());
|
||||
logger.debug("Stack Trace:", e);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path directory, IOException ioException) throws IOException {
|
||||
try {
|
||||
Files.delete(directory);
|
||||
logger.trace("Deleted directory:" + path);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Temporary folder " + directory + " could not be deleted: " + e.getMessage());
|
||||
logger.debug("Stack Trace:", e);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
logger.warn("Temporary location " + path + " could not be deleted: " + e.getMessage());
|
||||
logger.debug("Stack Trace:", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] concat(byte[] a, byte[] b) {
|
||||
byte[] c = new byte[a.length + b.length];
|
||||
System.arraycopy(a, 0, c, 0, a.length);
|
||||
System.arraycopy(b, 0, c, a.length, b.length);
|
||||
return c;
|
||||
}
|
||||
|
||||
public static byteArrayTuple split(byte[] array, int len) {
|
||||
byte[] first = new byte[len];
|
||||
byte[] second = new byte[array.length - len];
|
||||
System.arraycopy(array, 0, first, 0, len);
|
||||
System.arraycopy(array, len, second, 0, second.length);
|
||||
return new byteArrayTuple(first, second);
|
||||
}
|
||||
|
||||
public record byteArrayTuple(byte[] first, byte[] second) {
|
||||
}
|
||||
|
||||
public static String sha256(String input) throws NoSuchAlgorithmException {
|
||||
MessageDigest msdDigest = MessageDigest.getInstance("SHA-256");
|
||||
msdDigest.update(input.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(msdDigest.digest());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.io;
|
||||
|
||||
public record ProxyConfiguration(Boolean enabled, String host, int port, boolean useAuth, String login, char[] pwd) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.io;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import java.io.IOException;
|
||||
import java.security.*;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.*;
|
||||
|
||||
public class SSLContextUtils {
|
||||
private static final Logger logger = Logger.create(SSLContextUtils.class);
|
||||
|
||||
public enum PlatformKeyStore {
|
||||
NONE("none"),
|
||||
WINDOWS_ROOT("Windows-ROOT"),
|
||||
WINDOWS_MY("Windows-MY"),
|
||||
MACOS_KEYCHAIN("KeychainStore");
|
||||
|
||||
private final String name;
|
||||
|
||||
PlatformKeyStore(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
public static SSLContext withPlatformKeystore() throws SSLCustomContextException {
|
||||
var OS_NAME = System.getProperty("os.name").toLowerCase(Locale.ROOT);
|
||||
if (OS_NAME.startsWith("windows")) {
|
||||
return withKeystore(PlatformKeyStore.WINDOWS_ROOT);
|
||||
}
|
||||
if (OS_NAME.startsWith("mac")) {
|
||||
return withKeystore(PlatformKeyStore.MACOS_KEYCHAIN);
|
||||
}
|
||||
return withKeystore(PlatformKeyStore.NONE);
|
||||
}
|
||||
|
||||
public static SSLContext withKeystore(PlatformKeyStore keyStore) throws SSLCustomContextException {
|
||||
try {
|
||||
var sslContext = SSLContext.getInstance("TLSv1.2");
|
||||
logger.debug(() -> "Using platform specific keystore: " + keyStore);
|
||||
TrustManager[] trustManagers = null;
|
||||
if (keyStore != PlatformKeyStore.NONE) {
|
||||
var tks = KeyStore.getInstance(keyStore.getName());
|
||||
tks.load(null, null);
|
||||
var tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmFactory.init(tks);
|
||||
trustManagers = tmFactory.getTrustManagers();
|
||||
}
|
||||
sslContext.init(null, trustManagers, null);
|
||||
return sslContext;
|
||||
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | KeyManagementException |
|
||||
IOException e) {
|
||||
throw new SSLCustomContextException("Failed to create custom SSL context: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.io;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SSLCustomContextException extends IOException {
|
||||
public SSLCustomContextException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public SSLCustomContextException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SSLCustomContextException(String message, Throwable inner) {
|
||||
super(message, inner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
* Copyright 2017-2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.bindings;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableSet;
|
||||
import javafx.collections.SetChangeListener;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.event.WeakEventHandler;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* A class that provide methods to centralize and register the attachment of listeners
|
||||
* and bindings onto {@link javafx.beans.Observable} instances.
|
||||
* <p>
|
||||
* This makes it possible to remove all listeners and bindings attached to to
|
||||
* registered {@link javafx.beans.Observable} instances in a determinist fashion (on invoking {@code close()})
|
||||
* and helps alleviate the potential for references leaks in some scenarios.
|
||||
* </p>
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class BindingManager implements AutoCloseable {
|
||||
private static final Logger logger = Logger.create(BindingManager.class);
|
||||
private final Map<ObservableValue, List<ChangeListener>> changeListeners = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private final Map<ObservableValue, List<InvalidationListener>> invalidationListeners = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private final Map<ObservableList, List<ListChangeListener>> listChangeListeners = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private final Map<ObservableList, List<InvalidationListener>> listInvalidationListeners = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private final Map<ObservableSet, List<SetChangeListener>> setChangeListeners = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private final Map<ObservableSet, List<InvalidationListener>> setInvalidationListeners= Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private final Map<Property<?>, ObservableValue> boundProperties = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private final Map<Property<?>, Property> bidirectionallyBoundProperties = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private final List<EventHandler<?>> registeredHandlers = Collections.synchronizedList(new ArrayList<>());
|
||||
private final AtomicBoolean closed = new AtomicBoolean(false);
|
||||
|
||||
|
||||
/**
|
||||
* Binds the specified {@link ObservableValue} onto the specified {@link Property} and registers the resulting binding.
|
||||
*
|
||||
* @param property the {@link Property} to bind
|
||||
* @param binding the {@link ObservableValue} to bind to the {@link Property}
|
||||
* @param <T> the type of {@link Property}
|
||||
* @param <U> the type of {@link ObservableValue}
|
||||
*/
|
||||
public <T, U extends T> void bind(Property<T> property, ObservableValue<U> binding) {
|
||||
Objects.requireNonNull(property, "property parameter cannot be null");
|
||||
Objects.requireNonNull(binding, "binding parameter cannot be null");
|
||||
logger.trace(() -> "Binding " + binding.toString() + " to " + property.toString());
|
||||
property.bind(binding);
|
||||
boundProperties.put(property, binding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbinds all registered bindings
|
||||
*/
|
||||
public void unbindAll() {
|
||||
boundProperties.forEach((property, binding) -> {
|
||||
logger.trace(() -> "Unbinding property " + property.toString());
|
||||
property.unbind();
|
||||
});
|
||||
boundProperties.clear();
|
||||
|
||||
bidirectionallyBoundProperties.forEach((property, binding) -> {
|
||||
logger.trace(() -> "Unbinding property " + property.toString() + " from " + binding.toString());
|
||||
property.unbindBidirectional(binding);
|
||||
});
|
||||
bidirectionallyBoundProperties.clear();
|
||||
}
|
||||
|
||||
|
||||
public <T> void bindBidirectional(Property<T> property, Property<T> binding) {
|
||||
Objects.requireNonNull(property, "property parameter cannot be null");
|
||||
Objects.requireNonNull(binding, "binding parameter cannot be null");
|
||||
logger.trace(() -> "Binding " + binding.toString() + " to " + property.toString());
|
||||
property.bindBidirectional(binding);
|
||||
bidirectionallyBoundProperties.put(property, binding);
|
||||
}
|
||||
|
||||
public <T> void unbindBidirectionnal(Property<T> property, Property<T> binding) {
|
||||
Objects.requireNonNull(property, "property parameter cannot be null");
|
||||
Objects.requireNonNull(binding, "binding parameter cannot be null");
|
||||
logger.trace(() -> "Unbinding " + binding.toString() + " from " + property.toString());
|
||||
property.unbindBidirectional(binding);
|
||||
bidirectionallyBoundProperties.remove(property, binding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a {@link ChangeListener} to an {@link ObservableValue} and registers the resulting binding.
|
||||
*
|
||||
* @param observable the {@link ObservableValue} to attach the listener to.
|
||||
* @param listener the {@link ChangeListener} to attach
|
||||
*/
|
||||
public void attachListener(ObservableValue<?> observable, ChangeListener<?> listener) {
|
||||
register(observable, listener, changeListeners, ObservableValue::addListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a {@link InvalidationListener} to an {@link ObservableValue} and registers the resulting binding.
|
||||
*
|
||||
* @param observable the {@link ObservableValue} to attach the listener to.
|
||||
* @param listener the {@link InvalidationListener} to attach
|
||||
*/
|
||||
public void attachListener(ObservableValue<?> observable, InvalidationListener listener) {
|
||||
register(observable, listener, invalidationListeners, ObservableValue::addListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a {@link ListChangeListener} to an {@link ObservableList} and registers the resulting binding.
|
||||
*
|
||||
* @param observable the {@link ObservableList} to attach the listener to.
|
||||
* @param listener the {@link ListChangeListener} to attach
|
||||
*/
|
||||
public void attachListener(ObservableList<?> observable, ListChangeListener listener) {
|
||||
register(observable, listener, listChangeListeners, ObservableList::addListener);
|
||||
}
|
||||
|
||||
public void attachListener(ObservableList<?> observable, InvalidationListener listener) {
|
||||
register(observable, listener, listInvalidationListeners, ObservableList::addListener);
|
||||
}
|
||||
|
||||
|
||||
public void attachListener(ObservableSet<?> observable, SetChangeListener listener){
|
||||
register(observable, listener, setChangeListeners, ObservableSet::addListener);
|
||||
}
|
||||
|
||||
public void attachListener(ObservableSet<?> observable, InvalidationListener listener){
|
||||
register(observable, listener, setInvalidationListeners, ObservableSet::addListener);
|
||||
}
|
||||
|
||||
public void detachListener(ObservableSet<?> observable, SetChangeListener listener) {
|
||||
unregister(observable, listener, setChangeListeners, ObservableSet::removeListener);
|
||||
}
|
||||
|
||||
public void detachListener(ObservableSet<?> observable, InvalidationListener listener) {
|
||||
unregister(observable, listener, setInvalidationListeners, ObservableSet::removeListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific {@link ChangeListener} from an {@link ObservableValue}.
|
||||
*
|
||||
* @param observable the {@link ObservableValue} to remove the listener from.
|
||||
* @param listener the {@link ChangeListener} to remove
|
||||
*/
|
||||
public void detachListener(ObservableValue<?> observable, ChangeListener listener) {
|
||||
unregister(observable, listener, changeListeners, ObservableValue::removeListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific {@link InvalidationListener} from an {@link ObservableValue}.
|
||||
*
|
||||
* @param observable the {@link ObservableValue} to remove the listener from.
|
||||
* @param listener the {@link InvalidationListener} to remove
|
||||
*/
|
||||
public void detachListener(ObservableValue<?> observable, InvalidationListener listener) {
|
||||
unregister(observable, listener, invalidationListeners, ObservableValue::removeListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific {@link ListChangeListener} from an {@link ObservableList}.
|
||||
*
|
||||
* @param observable the {@link ObservableList} to remove the listener from.
|
||||
* @param listener the {@link ListChangeListener} to remove
|
||||
*/
|
||||
public void detachListener(ObservableList<?> observable, ListChangeListener<?> listener) {
|
||||
unregister(observable, listener, listChangeListeners, ObservableList::removeListener);
|
||||
}
|
||||
|
||||
public void detachListener(ObservableList<?> observable, InvalidationListener listener) {
|
||||
unregister(observable, listener, listInvalidationListeners, ObservableList::removeListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove <u>all</u> {@link InvalidationListener} from an {@link ObservableValue}.
|
||||
*
|
||||
* @param observable the {@link ObservableValue} to remove all listeners from.
|
||||
*/
|
||||
public void detachAllInvalidationListeners(ObservableValue<?> observable) {
|
||||
unregister(observable, invalidationListeners, ObservableValue::removeListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove <u>all</u> {@link ChangeListener} from an {@link ObservableValue}.
|
||||
*
|
||||
* @param observable the {@link ObservableValue} to remove all listeners from.
|
||||
*/
|
||||
public void detachAllChangeListeners(ObservableValue<?> observable) {
|
||||
unregister(observable, changeListeners, ObservableValue::removeListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove <u>all</u> {@link ListChangeListener} from an {@link ObservableList}.
|
||||
*
|
||||
* @param observable the {@link ObservableList} to remove all listeners from.
|
||||
*/
|
||||
public void detachAllListChangeListeners(ObservableList<?> observable) {
|
||||
unregister(observable, listChangeListeners, ObservableList::removeListener);
|
||||
}
|
||||
|
||||
public void detachAllInvalidationListeners(ObservableList<?> observable) {
|
||||
unregister(observable, listInvalidationListeners, ObservableList::removeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
if (closed.compareAndSet(false, true)) {
|
||||
try {
|
||||
unregisterAll(listChangeListeners, ObservableList::removeListener);
|
||||
unregisterAll(listInvalidationListeners, ObservableList::removeListener);
|
||||
unregisterAll(invalidationListeners, ObservableValue::removeListener);
|
||||
unregisterAll(changeListeners, ObservableValue::removeListener);
|
||||
unregisterAll(setChangeListeners, ObservableSet::removeListener);
|
||||
unregisterAll(setInvalidationListeners, ObservableSet::removeListener);
|
||||
unbindAll();
|
||||
// Release strong refs to registered event handlers, so that their
|
||||
// weak counterpart may be collected.
|
||||
registeredHandlers.clear();
|
||||
} catch (Exception e) {
|
||||
logger.warn("An error occurred while closing BindingManager instance", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void suspend() {
|
||||
visitMap(listChangeListeners, ObservableList::removeListener);
|
||||
visitMap(listInvalidationListeners, ObservableList::removeListener);
|
||||
visitMap(invalidationListeners, ObservableValue::removeListener);
|
||||
visitMap(changeListeners, ObservableValue::removeListener);
|
||||
visitMap(setChangeListeners, ObservableSet::removeListener);
|
||||
visitMap(setInvalidationListeners, ObservableSet::removeListener);
|
||||
boundProperties.keySet().forEach(Property::unbind);
|
||||
}
|
||||
|
||||
public synchronized void resume() {
|
||||
visitMap(listChangeListeners, ObservableList::addListener);
|
||||
visitMap(listInvalidationListeners, ObservableList::addListener);
|
||||
visitMap(invalidationListeners, ObservableValue::addListener);
|
||||
visitMap(changeListeners, ObservableValue::addListener);
|
||||
visitMap(setChangeListeners, ObservableSet::addListener);
|
||||
visitMap(setInvalidationListeners, ObservableSet::addListener);
|
||||
boundProperties.forEach(Property::bind);
|
||||
}
|
||||
|
||||
|
||||
public synchronized void suspend(Property<?>... properties) {
|
||||
for (var p : properties) {
|
||||
if (!boundProperties.containsKey(p)) {
|
||||
throw new IllegalArgumentException("Property " + p.getName() + " is not managed by this instance of BindingManager");
|
||||
}
|
||||
p.unbind();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void resume(Property<?>... properties) {
|
||||
for (var p : properties) {
|
||||
if (!boundProperties.containsKey(p)) {
|
||||
throw new IllegalArgumentException("Property " + p.getName() + " is not managed by this instance of BindingManager");
|
||||
}
|
||||
p.bind(boundProperties.get(p));
|
||||
}
|
||||
}
|
||||
|
||||
public <T extends Event> WeakEventHandler<T> registerHandler(EventHandler<T> handler) {
|
||||
// Store strong ref to handler, so it doesn't get collected prematurely.
|
||||
registeredHandlers.add(handler);
|
||||
// wrap in WeakEventHandler
|
||||
return new WeakEventHandler<T>(handler);
|
||||
}
|
||||
|
||||
public <T extends Event> void unregisterHandler(EventHandler<T> handler) {
|
||||
// Remove strong ref to handler, so it can be collected.
|
||||
registeredHandlers.remove(handler);
|
||||
}
|
||||
|
||||
private <T, U> void register(T observable, U listener, Map<T, List<U>> map, BiConsumer<T, U> attachAction) {
|
||||
Objects.requireNonNull(observable, "observable parameter cannot be null");
|
||||
Objects.requireNonNull(listener, "listener parameter cannot be null");
|
||||
Objects.requireNonNull(map, "map parameter cannot be null");
|
||||
Objects.requireNonNull(attachAction, "attachAction parameter cannot be null");
|
||||
map.computeIfAbsent(observable, p -> new ArrayList<>()).add(listener);
|
||||
logger.trace(() -> "Attaching listener " + listener.toString() + " to observable " + observable.toString());
|
||||
attachAction.accept(observable, listener);
|
||||
}
|
||||
|
||||
private <T, U> void unregister(T key, U value, Map<T, List<U>> map, BiConsumer<T, U> unregisterAction) {
|
||||
Objects.requireNonNull(key, "key parameter cannot be null");
|
||||
Objects.requireNonNull(value, "value parameter cannot be null");
|
||||
Objects.requireNonNull(map, "map parameter cannot be null");
|
||||
Objects.requireNonNull(unregisterAction, "unregisterAction parameter cannot be null");
|
||||
List<U> listeners = map.get(key);
|
||||
if (listeners == null) {
|
||||
logger.debug(() -> "Object " + key.toString() + " is not managed by this BindingManager instance");
|
||||
return;
|
||||
}
|
||||
listeners.stream().filter(l -> l.equals(value)).findFirst().ifPresent(found -> map.get(key).remove(found));
|
||||
logger.trace(() -> "Unregistering " + value.toString() + " from " + key.toString());
|
||||
unregisterAction.accept(key, value);
|
||||
}
|
||||
|
||||
private <T, U> void unregister(T key, Map<T, List<U>> map, BiConsumer<T, U> unregisterAction) {
|
||||
Objects.requireNonNull(key, "key paramater cannot be null");
|
||||
Objects.requireNonNull(map, "map parameter cannot be null");
|
||||
Objects.requireNonNull(unregisterAction, "unregisterAction parameter cannot be null");
|
||||
List<U> l = map.get(key);
|
||||
if (l == null) {
|
||||
logger.debug(() -> "Object " + key.toString() + " is not managed by this BindingManager instance");
|
||||
return;
|
||||
}
|
||||
l.forEach(value -> {
|
||||
logger.trace(() -> "Unregistering " + value.toString() + " from " + key.toString());
|
||||
unregisterAction.accept(key, value);
|
||||
});
|
||||
map.remove(key);
|
||||
}
|
||||
|
||||
private <T, U> void unregisterAll(Map<T, List<U>> map, BiConsumer<T, U> unregisterAction) {
|
||||
Objects.requireNonNull(map, "map parameter cannot be null");
|
||||
Objects.requireNonNull(unregisterAction, "unregisterAction parameter cannot be null");
|
||||
map.forEach((k, vList) -> {
|
||||
vList.forEach(v -> {
|
||||
logger.trace(() -> "Unregistering " + v.toString() + " from " + k.toString());
|
||||
unregisterAction.accept(k, v);
|
||||
});
|
||||
});
|
||||
map.clear();
|
||||
}
|
||||
|
||||
private <T, U> void visitMap(Map<T, List<U>> map, BiConsumer<T, U> action) {
|
||||
Objects.requireNonNull(map, "map parameter cannot be null");
|
||||
Objects.requireNonNull(action, "action parameter cannot be null");
|
||||
map.forEach((observable, listeners) -> listeners.forEach(listener -> {
|
||||
logger.trace(() -> "visiting key " + listener.toString() + " value " + observable.toString());
|
||||
action.accept(observable, listener);
|
||||
}));
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2017-2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.charts;
|
||||
|
||||
import eu.binjr.common.text.BinaryPrefixFormatter;
|
||||
|
||||
/**
|
||||
* An implementation of {@link StableTicksAxis} that divide up large numbers by powers of 2 and apply binary unit prefixes
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class BinaryStableTicksAxis<T extends Number> extends StableTicksAxis<T> {
|
||||
public BinaryStableTicksAxis() {
|
||||
super(new BinaryPrefixFormatter(), 2, new double[]{1.0, 2.0, 4.0, 8.0, 16.0});
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2017-2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.charts;
|
||||
|
||||
import eu.binjr.common.text.MetricPrefixFormatter;
|
||||
|
||||
/**
|
||||
* An implementation of {@link StableTicksAxis} that divide up large numbers by powers of 10 and apply metric unit prefixes
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class MetricStableTicksAxis<T extends Number> extends StableTicksAxis<T> {
|
||||
public MetricStableTicksAxis() {
|
||||
super(new MetricPrefixFormatter(), 10, new double[]{1.0, 2.5, 5.0});
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2019-2020 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.charts;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.chart.Axis;
|
||||
import javafx.scene.chart.StackedAreaChart;
|
||||
import javafx.scene.chart.XYChart;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* A {@link StackedAreaChart} that support NaN values in series.
|
||||
*
|
||||
* @param <X> Type for the X axis
|
||||
* @param <Y> Type for the Y axis
|
||||
*/
|
||||
public class NaNStackedAreaChart<X, Y> extends StackedAreaChart<X, Y> {
|
||||
|
||||
|
||||
public NaNStackedAreaChart(Axis<X> xAxis, Axis<Y> yAxis) {
|
||||
super(xAxis, yAxis);
|
||||
}
|
||||
|
||||
public NaNStackedAreaChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X, Y>> data) {
|
||||
super(xAxis, yAxis, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void updateAxisRange() {
|
||||
// This override is necessary to update axis range based on cumulative Y value for the
|
||||
// Y axis instead of the normal way where max value in the data range is used.
|
||||
final Axis<X> xa = getXAxis();
|
||||
final Axis<Y> ya = getYAxis();
|
||||
if (xa.isAutoRanging()) {
|
||||
List<X> xData = new ArrayList<>();
|
||||
for (XYChart.Series<X, Y> series : getData()) {
|
||||
for (XYChart.Data<X, Y> data : series.getData()) {
|
||||
xData.add(data.getXValue());
|
||||
}
|
||||
}
|
||||
xa.invalidateRange(xData);
|
||||
}
|
||||
if (ya.isAutoRanging()) {
|
||||
double totalMinY = Double.MAX_VALUE;
|
||||
Iterator<XYChart.Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
|
||||
boolean first = true;
|
||||
NavigableMap<Double, Double> accum = new TreeMap<>();
|
||||
NavigableMap<Double, Double> prevAccum = new TreeMap<>();
|
||||
NavigableMap<Double, Double> currentValues = new TreeMap<>();
|
||||
while (seriesIterator.hasNext()) {
|
||||
currentValues.clear();
|
||||
XYChart.Series<X, Y> series = seriesIterator.next();
|
||||
for (XYChart.Data<X, Y> item : series.getData()) {
|
||||
if (item != null) {
|
||||
final double xv = xa.toNumericValue(item.getXValue());
|
||||
final double yv = Double.isNaN(ya.toNumericValue(item.getYValue())) ? 0.0 : ya.toNumericValue(item.getYValue());
|
||||
currentValues.put(xv, yv);
|
||||
if (first) {
|
||||
// On the first pass, just fill the map
|
||||
accum.put(xv, yv);
|
||||
// minimum is applicable only in the first series
|
||||
totalMinY = Math.min(totalMinY, yv);
|
||||
} else {
|
||||
if (prevAccum.containsKey(xv)) {
|
||||
accum.put(xv, prevAccum.get(xv) + yv);
|
||||
} else {
|
||||
// If the point wasn't yet in the previous (accumulated) series
|
||||
Map.Entry<Double, Double> he = prevAccum.higherEntry(xv);
|
||||
Map.Entry<Double, Double> le = prevAccum.lowerEntry(xv);
|
||||
if (he != null && le != null) {
|
||||
// If there's both point above and below this point, interpolate
|
||||
accum.put(xv, ((xv - le.getKey()) / (he.getKey() - le.getKey())) *
|
||||
(le.getValue() + he.getValue()) + yv);
|
||||
} else if (he != null) {
|
||||
// The point is before the first point in the previously accumulated series
|
||||
accum.put(xv, he.getValue() + yv);
|
||||
} else if (le != null) {
|
||||
// The point is after the last point in the previously accumulated series
|
||||
accum.put(xv, le.getValue() + yv);
|
||||
} else {
|
||||
// The previously accumulated series is empty
|
||||
accum.put(xv, yv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Now update all the keys that were in the previous series, but not in the new one
|
||||
for (Map.Entry<Double, Double> e : prevAccum.entrySet()) {
|
||||
if (accum.keySet().contains(e.getKey())) {
|
||||
continue;
|
||||
}
|
||||
Double k = e.getKey();
|
||||
final Double v = e.getValue();
|
||||
// Look at the values of the current series
|
||||
Map.Entry<Double, Double> he = currentValues.higherEntry(k);
|
||||
Map.Entry<Double, Double> le = currentValues.lowerEntry(k);
|
||||
if (he != null && le != null) {
|
||||
// Interpolate the for the point from current series and add the accumulated value
|
||||
accum.put(k, ((k - le.getKey()) / (he.getKey() - le.getKey())) *
|
||||
(le.getValue() + he.getValue()) + v);
|
||||
} else if (he != null) {
|
||||
// There accumulated value is before the first value in the current series
|
||||
accum.put(k, he.getValue() + v);
|
||||
} else if (le != null) {
|
||||
// There accumulated value is after the last value in the current series
|
||||
accum.put(k, le.getValue() + v);
|
||||
} else {
|
||||
// The current series are empty
|
||||
accum.put(k, v);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
prevAccum.clear();
|
||||
prevAccum.putAll(accum);
|
||||
accum.clear();
|
||||
first = (totalMinY == Double.MAX_VALUE); // If there was already some value in the series, we can consider as
|
||||
// being past the first series
|
||||
|
||||
}
|
||||
if (totalMinY != Double.MAX_VALUE) ya.invalidateRange(Arrays.asList(ya.toRealValue(totalMinY),
|
||||
ya.toRealValue(Collections.max(prevAccum.values()))));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2017-2020 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.charts;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import eu.binjr.common.logging.Profiler;
|
||||
import javafx.scene.chart.Axis;
|
||||
import javafx.scene.chart.StackedAreaChart;
|
||||
|
||||
/**
|
||||
* A {@link StackedAreaChart} that logs the execution time of the {@code layoutPlotChildren} method
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class ProfiledStackedAreaChart<X, Y> extends StackedAreaChart<X, Y> {
|
||||
private static final Logger logger = Logger.create(ProfiledStackedAreaChart.class);
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link ProfiledStackedAreaChart} class
|
||||
*
|
||||
* @param xAxis the x axis of the chart
|
||||
* @param yAxis the y axis of the chart
|
||||
*/
|
||||
public ProfiledStackedAreaChart(Axis<X> xAxis, Axis<Y> yAxis) {
|
||||
super(xAxis, yAxis);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutPlotChildren() {
|
||||
try (Profiler p = Profiler.start("Plotting MyStackedAreaChart " + this.getTitle(), logger::perf)) {
|
||||
super.layoutPlotChildren();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
/*
|
||||
* Copyright 2013 Jason Winnebeck
|
||||
* Copyright 2019-2022 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.charts;
|
||||
|
||||
import eu.binjr.common.text.PrefixFormatter;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Dimension2D;
|
||||
import javafx.geometry.Side;
|
||||
import javafx.scene.chart.Axis;
|
||||
import javafx.scene.chart.ValueAxis;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.util.Duration;
|
||||
import org.gillius.jfxutils.chart.AxisTickFormatter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The StableTicksAxis places tick marks at consistent (axis value rather than graphical) locations.
|
||||
*
|
||||
* @author Jason Winnebeck
|
||||
*/
|
||||
public class StableTicksAxis<T extends Number> extends ValueAxis<T> {
|
||||
private T dataMaxValue = (T) Double.valueOf(0);
|
||||
private T dataMinValue = (T) Double.valueOf(0);
|
||||
private final double[] dividers;
|
||||
private final int base;
|
||||
|
||||
public static class SelectableRegion extends Pane {
|
||||
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
|
||||
private final BooleanProperty selected = new BooleanPropertyBase(false) {
|
||||
public void invalidated() {
|
||||
pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return SelectableRegion.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "selected";
|
||||
}
|
||||
};
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected.get();
|
||||
}
|
||||
|
||||
public BooleanProperty selectedProperty() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected.set(selected);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Possible tick spacing at the 10^1 level. These numbers must be >= 1 and < 10.
|
||||
*/
|
||||
private static final int NUM_MINOR_TICKS = 3;
|
||||
public static final int BTN_WITDTH = 21;
|
||||
private final SelectableRegion selectionMarker = new SelectableRegion();
|
||||
private final BooleanProperty selectionMarkerVisible = selectionMarker.visibleProperty();
|
||||
private final BooleanProperty selected = selectionMarker.selectedProperty(); //new SimpleBooleanProperty(false);
|
||||
|
||||
private final Timeline animationTimeline = new Timeline();
|
||||
private final WritableValue<Double> scaleValue = new WritableValue<>() {
|
||||
@Override
|
||||
public Double getValue() {
|
||||
return getScale();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Double value) {
|
||||
setScale(value);
|
||||
}
|
||||
};
|
||||
|
||||
private AxisTickFormatter axisTickFormatter;
|
||||
private final SimpleDoubleProperty tickSpacing = new SimpleDoubleProperty(20);
|
||||
|
||||
private final StringProperty displayUnit = new SimpleStringProperty("");
|
||||
|
||||
private List<T> minorTicks;
|
||||
|
||||
/**
|
||||
* Amount of padding to add on the each end of the axis when auto ranging.
|
||||
*/
|
||||
private final DoubleProperty autoRangePadding = new SimpleDoubleProperty(0.1);
|
||||
|
||||
/**
|
||||
* If true, when auto-ranging, force 0 to be the min or max end of the range.
|
||||
*/
|
||||
private final BooleanProperty forceZeroInRange = new SimpleBooleanProperty(true);
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link StableTicksAxis} class.
|
||||
*
|
||||
* @param prefixFormatter the {@link PrefixFormatter} instance to use.
|
||||
* @param base the numerical base used to determine tick positions.
|
||||
* @param dividers a list of divider candidates.
|
||||
*/
|
||||
public StableTicksAxis(PrefixFormatter prefixFormatter, int base, double[] dividers) {
|
||||
super();
|
||||
this.base = base;
|
||||
this.dividers = dividers != null ? dividers : new double[]{1.0, 2.5, 5.0};
|
||||
getStyleClass().setAll("axis");
|
||||
|
||||
selectionMarker.getStyleClass().addAll("selection-marker", "drop-target");
|
||||
var states = selectionMarker.getPseudoClassStates();
|
||||
this.getChildren().add(selectionMarker);
|
||||
|
||||
this.axisTickFormatter = new AxisTickFormatter() {
|
||||
@Override
|
||||
public void setRange(double v, double v1, double v2) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(Number number) {
|
||||
return prefixFormatter.format(number.doubleValue()) + displayUnit.getValue();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the axis tick formatter.
|
||||
*
|
||||
* @return the axis tick formatter.
|
||||
*/
|
||||
public AxisTickFormatter getAxisTickFormatter() {
|
||||
return axisTickFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the axis tick formatter.
|
||||
*
|
||||
* @param axisTickFormatter the axis tick formatter.
|
||||
*/
|
||||
public void setAxisTickFormatter(AxisTickFormatter axisTickFormatter) {
|
||||
this.axisTickFormatter = axisTickFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Amount of padding to add on the each end of the axis when auto ranging.
|
||||
*
|
||||
* @return Amount of padding to add on the each end of the axis when auto ranging.
|
||||
*/
|
||||
public double getAutoRangePadding() {
|
||||
return autoRangePadding.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* The autoRangePadding property.
|
||||
*
|
||||
* @return the autoRangePadding property.
|
||||
*/
|
||||
public DoubleProperty autoRangePaddingProperty() {
|
||||
return autoRangePadding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Amount of padding to add on the each end of the axis when auto ranging.
|
||||
*
|
||||
* @param autoRangePadding Amount of padding to add on the each end of the axis when auto ranging.
|
||||
*/
|
||||
public void setAutoRangePadding(double autoRangePadding) {
|
||||
this.autoRangePadding.set(autoRangePadding);
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, when auto-ranging, force 0 to be the min or max end of the range.
|
||||
*
|
||||
* @return true if force 0 to be the min or max end of the range when auto-ranging, false otherwize.
|
||||
*/
|
||||
public boolean isForceZeroInRange() {
|
||||
return forceZeroInRange.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* The forceZeroInRange property
|
||||
*
|
||||
* @return the forceZeroInRange property
|
||||
*/
|
||||
public BooleanProperty forceZeroInRangeProperty() {
|
||||
return forceZeroInRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, when auto-ranging, force 0 to be the min or max end of the range.
|
||||
*
|
||||
* @param forceZeroInRange set to true, when auto-ranging, to force 0 to be the min or max end of the range.
|
||||
*/
|
||||
public void setForceZeroInRange(boolean forceZeroInRange) {
|
||||
this.forceZeroInRange.set(forceZeroInRange);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
Side side = getSide();
|
||||
if (side.isVertical()) {
|
||||
double xShift = side == Side.LEFT ? -10 : 0;
|
||||
double contentX = this.getLayoutX();
|
||||
double contentY = this.getLayoutY();
|
||||
double contentWidth = this.getWidth() + 10;//(xShift * -1);
|
||||
double contentHeight = this.getHeight();
|
||||
|
||||
// this.selectionMarker.setPrefWidth(contentWidth);
|
||||
this.selectionMarker.setPrefHeight(contentHeight);
|
||||
|
||||
selectionMarker.resizeRelocate(
|
||||
snapPositionX(xShift),
|
||||
snapPositionY(0),
|
||||
snapSizeX(contentWidth),
|
||||
snapSizeY(contentHeight));
|
||||
selectionMarker.toFront();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateRange(List<T> data) {
|
||||
// Calculate min and max value not taking NaN values into account
|
||||
dataMaxValue = data.stream()
|
||||
.filter(d -> d != null && !Double.isNaN(d.doubleValue()))
|
||||
.max(Comparator.comparingDouble(T::doubleValue))
|
||||
.orElse((T) Double.valueOf(getUpperBound()));
|
||||
dataMinValue = data.stream()
|
||||
.filter(d -> d != null && !Double.isNaN(d.doubleValue()))
|
||||
.min(Comparator.comparingDouble(T::doubleValue))
|
||||
.orElse((T) Double.valueOf(getLowerBound()));
|
||||
// Invoke super with an empty list so that requestLayout is called while avoiding iterating the sample list again.
|
||||
super.invalidateRange(Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Range autoRange(double minValue, double maxValue, double length, double labelSize) {
|
||||
// By fthevenet: Override the provided min and max values by those calculated by our override.
|
||||
minValue = dataMinValue.doubleValue();
|
||||
maxValue = dataMaxValue.doubleValue();
|
||||
//By dweil: if the range is very small, display it like a flat line, the scaling doesn't work very well at these
|
||||
//values. 1e-300 was chosen arbitrarily.
|
||||
if (Math.abs(minValue - maxValue) < 1e-300) {
|
||||
//Normally this is the case for all points with the same value
|
||||
minValue = minValue - 1;
|
||||
maxValue = maxValue + 1;
|
||||
|
||||
} else {
|
||||
//Add padding
|
||||
double delta = maxValue - minValue;
|
||||
double paddedMin = minValue - delta * autoRangePadding.get();
|
||||
//If we've crossed the 0 line, clamp to 0.
|
||||
//noinspection FloatingPointEquality
|
||||
if (Math.signum(paddedMin) != Math.signum(minValue)) {
|
||||
paddedMin = 0.0;
|
||||
}
|
||||
|
||||
double paddedMax = maxValue + delta * autoRangePadding.get();
|
||||
//If we've crossed the 0 line, clamp to 0.
|
||||
//noinspection FloatingPointEquality
|
||||
if (Math.signum(paddedMax) != Math.signum(maxValue)) {
|
||||
paddedMax = 0.0;
|
||||
}
|
||||
|
||||
minValue = paddedMin;
|
||||
maxValue = paddedMax;
|
||||
}
|
||||
|
||||
//Handle forcing zero into the range
|
||||
if (forceZeroInRange.get()) {
|
||||
if (minValue < 0 && maxValue < 0) {
|
||||
maxValue = 0;
|
||||
minValue -= -minValue * autoRangePadding.get();
|
||||
} else if (minValue > 0 && maxValue > 0) {
|
||||
minValue = 0;
|
||||
maxValue += maxValue * autoRangePadding.get();
|
||||
}
|
||||
}
|
||||
|
||||
Range ret = getRange(minValue, maxValue);
|
||||
return ret;
|
||||
}
|
||||
|
||||
private Range getRange(double minValue, double maxValue) {
|
||||
double length = getLength();
|
||||
double delta = maxValue - minValue;
|
||||
double scale = calculateNewScale(length, minValue, maxValue);
|
||||
|
||||
int maxTicks = Math.max(1, (int) (length / getTickSpacing()));
|
||||
|
||||
Range ret;
|
||||
ret = new Range(minValue, maxValue, calculateTickSpacing(delta, maxTicks), scale);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public double calculateTickSpacing(double delta, int maxTicks) {
|
||||
if (delta == 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
if (delta <= 0.0) {
|
||||
throw new IllegalArgumentException("delta must be positive");
|
||||
}
|
||||
if (maxTicks < 1) {
|
||||
throw new IllegalArgumentException("must be at least one tick");
|
||||
}
|
||||
int divider = 0;
|
||||
int factor = (int) (Math.log(delta) / Math.log(base));
|
||||
double numTicks = delta / (dividers[divider] * Math.pow(base, factor));
|
||||
//We don't have enough ticks, so increase ticks until we're over the limit, then back off once.
|
||||
if (numTicks < maxTicks) {
|
||||
while (numTicks < maxTicks) {
|
||||
//Move up
|
||||
--divider;
|
||||
if (divider < 0) {
|
||||
--factor;
|
||||
divider = dividers.length - 1;
|
||||
}
|
||||
|
||||
numTicks = delta / (dividers[divider] * Math.pow(base, factor));
|
||||
}
|
||||
|
||||
//Now back off once unless we hit exactly
|
||||
//noinspection FloatingPointEquality
|
||||
if (numTicks != maxTicks) {
|
||||
++divider;
|
||||
if (divider >= dividers.length) {
|
||||
++factor;
|
||||
divider = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//We have too many ticks or exactly max, so decrease until we're just under (or at) the limit.
|
||||
while (numTicks > maxTicks) {
|
||||
++divider;
|
||||
if (divider >= dividers.length) {
|
||||
++factor;
|
||||
divider = 0;
|
||||
}
|
||||
|
||||
numTicks = delta / (dividers[divider] * Math.pow(base, factor));
|
||||
}
|
||||
}
|
||||
return dividers[divider] * Math.pow(base, factor);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected List<T> calculateMinorTickMarks() {
|
||||
return minorTicks;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setRange(Object range, boolean animate) {
|
||||
Range rangeVal = (Range) range;
|
||||
if (animate) {
|
||||
animationTimeline.stop();
|
||||
ObservableList<KeyFrame> keyFrames = animationTimeline.getKeyFrames();
|
||||
keyFrames.setAll(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(currentLowerBound, getLowerBound()),
|
||||
new KeyValue(scaleValue, getScale())),
|
||||
new KeyFrame(Duration.millis(750),
|
||||
new KeyValue(currentLowerBound, rangeVal.low),
|
||||
new KeyValue(scaleValue, rangeVal.scale)));
|
||||
animationTimeline.play();
|
||||
|
||||
} else {
|
||||
currentLowerBound.set(rangeVal.low);
|
||||
setScale(rangeVal.scale);
|
||||
}
|
||||
setLowerBound(rangeVal.low);
|
||||
setUpperBound(rangeVal.high);
|
||||
|
||||
axisTickFormatter.setRange(rangeVal.low, rangeVal.high, rangeVal.tickSpacing);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Range getRange() {
|
||||
Range ret = getRange(getLowerBound(), getUpperBound());
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<T> calculateTickValues(double length, Object range) {
|
||||
Range rangeVal = (Range) range;
|
||||
//Use floor so we start generating ticks before the axis starts -- this is really only relevant
|
||||
//because of the minor ticks before the first visible major tick. We'll generate a first
|
||||
//invisible major tick but the ValueAxis seems to filter it out.
|
||||
double firstTick = Math.floor(rangeVal.low / rangeVal.tickSpacing) * rangeVal.tickSpacing;
|
||||
//Generate one more tick than we expect, for "overlap" to get minor ticks on both sides of the
|
||||
//first and last major tick.
|
||||
int numTicks = (int) (rangeVal.getDelta() / rangeVal.tickSpacing) + 1;
|
||||
List<T> ret = new ArrayList<>(numTicks + 1);
|
||||
minorTicks = new ArrayList<>((numTicks + 2) * NUM_MINOR_TICKS);
|
||||
double minorTickSpacing = rangeVal.tickSpacing / (NUM_MINOR_TICKS + 1);
|
||||
for (int i = 0; i <= numTicks; ++i) {
|
||||
double majorTick = firstTick + rangeVal.tickSpacing * i;
|
||||
ret.add((T) Double.valueOf(majorTick));
|
||||
for (int j = 1; j <= NUM_MINOR_TICKS; ++j) {
|
||||
minorTicks.add((T) Double.valueOf(majorTick + minorTickSpacing * j));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTickMarkLabel(T number) {
|
||||
return axisTickFormatter.format(number);
|
||||
}
|
||||
|
||||
private double getLength() {
|
||||
if (getSide().isHorizontal()) {
|
||||
return getWidth();
|
||||
} else {
|
||||
return getHeight();
|
||||
}
|
||||
}
|
||||
|
||||
private double getLabelSize() {
|
||||
Dimension2D dim = measureTickMarkLabelSize("-888.88E-88", getTickLabelRotation());
|
||||
if (getSide().isHorizontal()) {
|
||||
return dim.getWidth();
|
||||
} else {
|
||||
return dim.getHeight();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tick spacing value.
|
||||
*
|
||||
* @return the tick spacing value.
|
||||
*/
|
||||
public double getTickSpacing() {
|
||||
return tickSpacing.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* The tickSpacing property
|
||||
*
|
||||
* @return the tickSpacing property
|
||||
*/
|
||||
public SimpleDoubleProperty tickSpacingProperty() {
|
||||
return tickSpacing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of the space space in between ticks.
|
||||
*
|
||||
* @param tickSpacing the value of the space space in between ticks.
|
||||
*/
|
||||
public void setTickSpacing(double tickSpacing) {
|
||||
this.tickSpacing.set(tickSpacing);
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected.get();
|
||||
}
|
||||
|
||||
public void setSelected(boolean value) {
|
||||
selected.setValue(value);
|
||||
}
|
||||
|
||||
public BooleanProperty selectedProperty() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public boolean getSelectionMarkerVisible() {
|
||||
return selectionMarkerVisible.get();
|
||||
}
|
||||
|
||||
public BooleanProperty selectionMarkerVisibleProperty() {
|
||||
return selectionMarkerVisible;
|
||||
}
|
||||
|
||||
public void setSelectionMarkerVisible(boolean selectionMarkerVisible) {
|
||||
this.selectionMarkerVisible.set(selectionMarkerVisible);
|
||||
}
|
||||
|
||||
public SelectableRegion getSelectionMarker() {
|
||||
return selectionMarker;
|
||||
}
|
||||
|
||||
public String getDisplayUnit() {
|
||||
return displayUnit.get();
|
||||
}
|
||||
|
||||
public StringProperty displayUnitProperty() {
|
||||
return displayUnit;
|
||||
}
|
||||
|
||||
public void setDisplayUnit(String displayUnit) {
|
||||
this.displayUnit.set(displayUnit);
|
||||
}
|
||||
|
||||
|
||||
private static class Range {
|
||||
public final double low;
|
||||
public final double high;
|
||||
public final double tickSpacing;
|
||||
public final double scale;
|
||||
|
||||
private Range(double low, double high, double tickSpacing, double scale) {
|
||||
this.low = low;
|
||||
this.high = high;
|
||||
this.tickSpacing = tickSpacing;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public double getDelta() {
|
||||
return high - low;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Range{" +
|
||||
"low=" + low +
|
||||
", high=" + high +
|
||||
", tickSpacing=" + tickSpacing +
|
||||
", scale=" + scale +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
/*
|
||||
* Copyright 2016-2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.charts;
|
||||
|
||||
import eu.binjr.common.javafx.bindings.BindingManager;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.XYChart;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Line;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.shape.Shape;
|
||||
import javafx.scene.shape.StrokeType;
|
||||
import org.gillius.jfxutils.chart.XYChartInfo;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.gillius.jfxutils.JFXUtil.getXShift;
|
||||
import static org.gillius.jfxutils.JFXUtil.getYShift;
|
||||
|
||||
/**
|
||||
* Draws a crosshair on top of an {@link XYChart} and handles selection of a portion of the chart view.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class XYChartCrosshair<X, Y> {
|
||||
private static final Logger logger = Logger.create(XYChartCrosshair.class);
|
||||
private static final double SELECTION_OPACITY = 0.5;
|
||||
|
||||
private final Line horizontalMarker = new Line();
|
||||
private final Line verticalMarker = new Line();
|
||||
private final Label xAxisLabel;
|
||||
private final Label yAxisLabel;
|
||||
private final LinkedHashMap<XYChart<X, Y>, Function<Y, String>> charts;
|
||||
private final Function<X, String> xValuesFormatter;
|
||||
private final XYChartInfo chartInfo;
|
||||
private final BooleanProperty isSelecting = new SimpleBooleanProperty(false);
|
||||
private final Pane parent;
|
||||
private final Rectangle selection = new Rectangle(0, 0, 0, 0);
|
||||
private final BooleanProperty verticalMarkerVisible = new SimpleBooleanProperty();
|
||||
private final BooleanProperty horizontalMarkerVisible = new SimpleBooleanProperty();
|
||||
private final Map<XYChart<X, Y>, Property<Y>> currentYValues = new HashMap<>();
|
||||
private final Property<X> currentXValue = new SimpleObjectProperty<>();
|
||||
private final XYChart<X, Y> masterChart;
|
||||
private final BooleanProperty mouseOverChart = new SimpleBooleanProperty(false);
|
||||
private final BindingManager bindingManager = new BindingManager();
|
||||
private final BooleanProperty displayFullHeightMarker = new SimpleBooleanProperty(false);
|
||||
private Point2D selectionStart = new Point2D(-1, -1);
|
||||
private Point2D mousePosition = new Point2D(-1, -1);
|
||||
private Consumer<Map<XYChart<X, Y>, XYChartSelection<X, Y>>> selectionDoneEvent;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link XYChartCrosshair} class.
|
||||
*
|
||||
* @param charts a map of the {@link XYChart} to attach and their formatting function of the Y values.
|
||||
* @param parent the parent node of the chart
|
||||
* @param xValuesFormatter a function used to format the display of X values as strings
|
||||
*/
|
||||
public XYChartCrosshair(LinkedHashMap<XYChart<X, Y>, Function<Y, String>> charts, Pane parent, Function<X, String> xValuesFormatter) {
|
||||
this.charts = charts;
|
||||
applyStyle(this.verticalMarker);
|
||||
applyStyle(this.horizontalMarker);
|
||||
applyStyle(this.selection);
|
||||
this.xAxisLabel = newAxisLabel();
|
||||
this.yAxisLabel = newAxisLabel();
|
||||
this.parent = parent;
|
||||
parent.getChildren().addAll(xAxisLabel, yAxisLabel, verticalMarker, horizontalMarker, selection);
|
||||
this.xValuesFormatter = xValuesFormatter;
|
||||
masterChart = charts.keySet().stream().reduce((p, n) -> n).orElseThrow(() -> new IllegalStateException("Could not identify last element in chart linked hash map."));
|
||||
this.chartInfo = new XYChartInfo(masterChart, parent);
|
||||
masterChart.addEventHandler(MouseEvent.MOUSE_MOVED, bindingManager.registerHandler(this::handleMouseMoved));
|
||||
masterChart.addEventHandler(MouseEvent.MOUSE_DRAGGED, bindingManager.registerHandler(this::handleMouseMoved));
|
||||
masterChart.setOnMouseReleased(bindingManager.registerHandler(e -> {
|
||||
if (isSelecting.get()) {
|
||||
fireSelectionDoneEvent();
|
||||
drawVerticalMarker();
|
||||
drawHorizontalMarker();
|
||||
}
|
||||
isSelecting.set(false);
|
||||
}));
|
||||
isSelecting.addListener((observable, oldValue, newValue) -> {
|
||||
logger.debug(() -> "observable=" + observable + " oldValue=" + oldValue + " newValue=" + newValue);
|
||||
if (!oldValue && newValue) {
|
||||
selectionStart = new Point2D(verticalMarker.getStartX(), horizontalMarker.getStartY());
|
||||
}
|
||||
drawSelection();
|
||||
selection.setVisible(newValue);
|
||||
});
|
||||
horizontalMarkerVisible.addListener((observable, oldValue, newValue) -> {
|
||||
drawHorizontalMarker();
|
||||
if (!newValue && !verticalMarkerVisible.get()) {
|
||||
isSelecting.set(false);
|
||||
currentYValues.forEach((key, value) -> value.setValue(null));
|
||||
}
|
||||
});
|
||||
verticalMarkerVisible.addListener((observable, oldValue, newValue) -> {
|
||||
drawVerticalMarker();
|
||||
if (!newValue && !horizontalMarkerVisible.get()) {
|
||||
isSelecting.set(false);
|
||||
currentXValue.setValue(null);
|
||||
}
|
||||
});
|
||||
masterChart.setOnMouseExited(bindingManager.registerHandler(event -> mouseOverChart.set(false)));
|
||||
masterChart.setOnMouseEntered(bindingManager.registerHandler(event -> mouseOverChart.set(true)));
|
||||
bindingManager.bind(horizontalMarker.visibleProperty(), horizontalMarkerVisible.and(mouseOverChart));
|
||||
bindingManager.bind(yAxisLabel.visibleProperty(), horizontalMarkerVisible.and(mouseOverChart));
|
||||
bindingManager.bind(verticalMarker.visibleProperty(), verticalMarkerVisible.and(mouseOverChart));
|
||||
bindingManager.bind(xAxisLabel.visibleProperty(), verticalMarkerVisible.and(mouseOverChart));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the boolean property that tracks the visibility of the vertical marker of the crosshair
|
||||
*
|
||||
* @return the boolean property that tracks the visibility of the vertical marker of the crosshair
|
||||
*/
|
||||
public BooleanProperty verticalMarkerVisibleProperty() {
|
||||
return verticalMarkerVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the boolean property that tracks the visibility of the horizontal marker of the crosshair
|
||||
*
|
||||
* @return the boolean property that tracks the visibility of the horizontal marker of the crosshair
|
||||
*/
|
||||
public BooleanProperty horizontalMarkerVisibleProperty() {
|
||||
return horizontalMarkerVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the vertical marker is visible, false otherwise
|
||||
*
|
||||
* @return true if the vertical marker is visible, false otherwise
|
||||
*/
|
||||
public boolean isVerticalMarkerVisible() {
|
||||
return verticalMarkerVisible.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility of the vertical marker
|
||||
*
|
||||
* @param verticalMarkerVisible the visibility of the vertical marker
|
||||
*/
|
||||
public void setVerticalMarkerVisible(boolean verticalMarkerVisible) {
|
||||
this.verticalMarkerVisible.set(verticalMarkerVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the horizontal marker is visible, false otherwise
|
||||
*
|
||||
* @return true if the horizontal marker is visible, false otherwise
|
||||
*/
|
||||
public boolean isHorizontalMarkerVisible() {
|
||||
return horizontalMarkerVisible.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility of the horizontal marker
|
||||
*
|
||||
* @param horizontalMarkerVisible the visibility of the horizontal marker
|
||||
*/
|
||||
public void setHorizontalMarkerVisible(boolean horizontalMarkerVisible) {
|
||||
this.horizontalMarkerVisible.set(horizontalMarkerVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the action to be triggered when selection is complete
|
||||
*
|
||||
* @param action the action to be triggered when selection is complete
|
||||
*/
|
||||
public void onSelectionDone(Consumer<Map<XYChart<X, Y>, XYChartSelection<X, Y>>> action) {
|
||||
selectionDoneEvent = action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Y value for currently selected set of coordinates for the provided chart.
|
||||
*
|
||||
* @param chart The chart to retrieve the current Y value from.
|
||||
* @return the Y value for currently selected set of coordinates for the provided chart.
|
||||
*/
|
||||
public Y getCurrentYValue(XYChart<X, Y> chart) {
|
||||
return currentYValues.get(chart).getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retusn the X value for currently selected set of coordinates.
|
||||
*
|
||||
* @return the X value for currently selected set of coordinates.
|
||||
*/
|
||||
public X getCurrentXValue() {
|
||||
return currentXValue.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* The currentXValue property.
|
||||
*
|
||||
* @return the currentXValue property.
|
||||
*/
|
||||
public Property<X> currentXValueProperty() {
|
||||
return currentXValue;
|
||||
}
|
||||
|
||||
public boolean isMouseOverChart() {
|
||||
return mouseOverChart.get();
|
||||
}
|
||||
|
||||
public BooleanProperty mouseOverChartProperty() {
|
||||
return mouseOverChart;
|
||||
}
|
||||
|
||||
public boolean isDisplayFullHeightMarker() {
|
||||
return displayFullHeightMarker.get();
|
||||
}
|
||||
|
||||
public void setDisplayFullHeightMarker(boolean displayFullHeightMarker) {
|
||||
this.displayFullHeightMarker.set(displayFullHeightMarker);
|
||||
}
|
||||
|
||||
public BooleanProperty displayFullHeightMarkerProperty() {
|
||||
return displayFullHeightMarker;
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
logger.debug(() -> "Disposing XYChartCrossHair " + this);
|
||||
selectionDoneEvent = null;
|
||||
bindingManager.close();
|
||||
}
|
||||
|
||||
private void fireSelectionDoneEvent() {
|
||||
if (selectionDoneEvent != null && (selection.getWidth() > 0 && selection.getHeight() > 0)) {
|
||||
var s = new HashMap<XYChart<X, Y>, XYChartSelection<X, Y>>();
|
||||
var plotArea = chartInfo.getPlotArea();
|
||||
charts.forEach((c, f) -> {
|
||||
s.put(c, new XYChartSelection<>(
|
||||
getValueFromXcoord(selection.getX()),
|
||||
getValueFromXcoord(selection.getX() + selection.getWidth()),
|
||||
getValueFromYcoord(c, Math.min(plotArea.getMaxY(), (selection.getY() + selection.getHeight()))),
|
||||
getValueFromYcoord(c, Math.max(plotArea.getMinY(), (selection.getY()))),
|
||||
selection.getHeight() != plotArea.getHeight()));
|
||||
});
|
||||
selectionDoneEvent.accept(s);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawHorizontalMarker() {
|
||||
if (mousePosition.getY() < 0) {
|
||||
return;
|
||||
}
|
||||
var plotArea = chartInfo.getPlotArea();
|
||||
horizontalMarker.setStartX(plotArea.getMinX());
|
||||
horizontalMarker.setEndX(plotArea.getMaxX());
|
||||
horizontalMarker.setStartY(mousePosition.getY());
|
||||
horizontalMarker.setEndY(horizontalMarker.getStartY());
|
||||
yAxisLabel.setLayoutX(Math.min(parent.getWidth() - yAxisLabel.getWidth(), plotArea.getMaxX() + 5));
|
||||
yAxisLabel.setLayoutY(Math.min(mousePosition.getY() + 5, plotArea.getMaxY() - yAxisLabel.getHeight()));
|
||||
|
||||
StringBuilder yAxisText = new StringBuilder();
|
||||
charts.forEach((c, f) -> {
|
||||
currentYValues.computeIfAbsent(c, (k) -> new SimpleObjectProperty<Y>()).setValue(getValueFromYcoord(c, mousePosition.getY()));
|
||||
yAxisText.append(c.getYAxis().getLabel())
|
||||
.append(": ")
|
||||
.append(f.apply(currentYValues.get(c).getValue()))
|
||||
.append("\n");
|
||||
});
|
||||
yAxisLabel.setText(yAxisText.toString());
|
||||
}
|
||||
|
||||
private Y getValueFromYcoord(XYChart<X, Y> chart, double yPosition) {
|
||||
double yStart = chart.getYAxis().getLocalToParentTransform().getTy();
|
||||
double axisYRelativePosition = yPosition - getYShift(masterChart, parent) - (yStart * 1.5);
|
||||
return chart.getYAxis().getValueForDisplay(axisYRelativePosition);
|
||||
}
|
||||
|
||||
private X getValueFromXcoord(double xPosition) {
|
||||
double xStart = masterChart.getXAxis().getLocalToParentTransform().getTx();
|
||||
double axisXRelativeMousePosition = xPosition - getXShift(masterChart, parent) - xStart;
|
||||
return masterChart.getXAxis().getValueForDisplay(axisXRelativeMousePosition - 5);
|
||||
}
|
||||
|
||||
private void drawVerticalMarker() {
|
||||
if (mousePosition.getX() < 0) {
|
||||
return;
|
||||
}
|
||||
var plotArea = chartInfo.getPlotArea();
|
||||
verticalMarker.setStartX(mousePosition.getX());
|
||||
verticalMarker.setEndX(verticalMarker.getStartX());
|
||||
if (displayFullHeightMarker.getValue()) {
|
||||
verticalMarker.setStartY(2);
|
||||
verticalMarker.setEndY(parent.getHeight() - 2);
|
||||
} else {
|
||||
verticalMarker.setStartY(plotArea.getMinY());
|
||||
verticalMarker.setEndY(plotArea.getMaxY());
|
||||
}
|
||||
xAxisLabel.setLayoutY(plotArea.getMaxY() + 4);
|
||||
xAxisLabel.setLayoutX(Math.min(mousePosition.getX() + 4, plotArea.getMaxX() - xAxisLabel.getWidth()));
|
||||
currentXValue.setValue(getValueFromXcoord(mousePosition.getX()));
|
||||
xAxisLabel.setText(xValuesFormatter.apply(currentXValue.getValue()));
|
||||
}
|
||||
|
||||
|
||||
public static Point2D getShift(Node descendant, Region ancestor) {
|
||||
double retX = 0.0;
|
||||
double retY = 0.0;
|
||||
Node curr = descendant;
|
||||
while (curr != ancestor) {
|
||||
var t = curr.getLocalToParentTransform();
|
||||
retX += ancestor.snapSpaceX(t.getTx());
|
||||
retY += ancestor.snapSpaceX(t.getTy());
|
||||
curr = curr.getParent();
|
||||
if (curr == null)
|
||||
throw new IllegalArgumentException("'descendant' Node is not a descendant of 'ancestor");
|
||||
}
|
||||
|
||||
return new Point2D(retX, retY);
|
||||
}
|
||||
|
||||
private void handleMouseMoved(MouseEvent event) {
|
||||
Rectangle2D area = chartInfo.getPlotArea();
|
||||
var shift = getShift(masterChart, parent);
|
||||
double xPos = parent.snapSpaceX(event.getX()) + shift.getX();
|
||||
double yPos = parent.snapSpaceX(event.getY()) + shift.getY();
|
||||
mousePosition = new Point2D(Math.max(area.getMinX(), Math.min(area.getMaxX(), xPos)), Math.max(area.getMinY(), Math.min(area.getMaxY(), yPos)));
|
||||
if (horizontalMarkerVisible.get()) {
|
||||
drawHorizontalMarker();
|
||||
}
|
||||
if (verticalMarkerVisible.get()) {
|
||||
drawVerticalMarker();
|
||||
}
|
||||
if (event.isPrimaryButtonDown() && (verticalMarkerVisible.get() || horizontalMarkerVisible.get())) {
|
||||
isSelecting.set(true);
|
||||
drawSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawSelection() {
|
||||
if (selectionStart.getX() < 0 || selectionStart.getY() < 0) {
|
||||
return;
|
||||
}
|
||||
if (horizontalMarkerVisible.get()) {
|
||||
double height = horizontalMarker.getStartY() - selectionStart.getY();
|
||||
selection.setY(height < 0 ? horizontalMarker.getStartY() : selectionStart.getY());
|
||||
selection.setHeight(Math.abs(height));
|
||||
} else {
|
||||
selection.setY(verticalMarker.getStartY());
|
||||
selection.setHeight(verticalMarker.getEndY() - verticalMarker.getStartY());
|
||||
}
|
||||
|
||||
if (verticalMarkerVisible.get()) {
|
||||
double width = verticalMarker.getStartX() - selectionStart.getX();
|
||||
selection.setX(width < 0 ? verticalMarker.getStartX() : selectionStart.getX());
|
||||
selection.setWidth(Math.abs(width));
|
||||
} else {
|
||||
selection.setX(horizontalMarker.getStartX());
|
||||
selection.setWidth(horizontalMarker.getEndX() - horizontalMarker.getStartX());
|
||||
}
|
||||
}
|
||||
|
||||
private Label newAxisLabel() {
|
||||
Label label = new Label("");
|
||||
label.getStyleClass().add("crosshair-axis-label");
|
||||
label.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
|
||||
label.setMouseTransparent(true);
|
||||
label.setVisible(false);
|
||||
return label;
|
||||
}
|
||||
|
||||
private void applyStyle(Shape shape) {
|
||||
shape.setMouseTransparent(true);
|
||||
shape.setSmooth(false);
|
||||
shape.setStrokeWidth(1.0);
|
||||
shape.setVisible(false);
|
||||
shape.setStrokeType(StrokeType.CENTERED);
|
||||
shape.setStroke(Color.STEELBLUE);
|
||||
Color fillColor = Color.LIGHTSTEELBLUE;
|
||||
shape.setFill(new Color(
|
||||
fillColor.getRed(),
|
||||
fillColor.getGreen(),
|
||||
fillColor.getBlue(),
|
||||
SELECTION_OPACITY));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2016-2019 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.charts;
|
||||
|
||||
/**
|
||||
* An immutable representation of the selection of the portion of a chart.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class XYChartSelection<X, Y> {
|
||||
private static final int PRIME = 31;
|
||||
private final X startX;
|
||||
private final X endX;
|
||||
private final Y startY;
|
||||
private final Y endY;
|
||||
|
||||
private final boolean autoRangeY;
|
||||
|
||||
/**
|
||||
* Returns the lower bound on the X axis of the selection
|
||||
*
|
||||
* @return the lower bound on the X axis of the selection
|
||||
*/
|
||||
public X getStartX() {
|
||||
return startX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the upper bound on the X axis of the selection
|
||||
*
|
||||
* @return the upper bound on the X axis of the selection
|
||||
*/
|
||||
public X getEndX() {
|
||||
return endX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lower bound on the Y axis of the selection
|
||||
*
|
||||
* @return the lower bound on the Y axis of the selection
|
||||
*/
|
||||
public Y getStartY() {
|
||||
return startY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the upper bound on the Y axis of the selection
|
||||
*
|
||||
* @return the upper bound on the Y axis of the selection
|
||||
*/
|
||||
public Y getEndY() {
|
||||
return endY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true is auto range is enabled on the Y axis, false otherwise.
|
||||
*
|
||||
* @return true is auto range is enabled on the Y axis, false otherwise.
|
||||
*/
|
||||
public boolean isAutoRangeY() {
|
||||
return autoRangeY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor for the {@link XYChartSelection} class.
|
||||
*
|
||||
* @param selection the {@link XYChartSelection} to clone.
|
||||
*/
|
||||
public XYChartSelection(XYChartSelection<X, Y> selection) {
|
||||
this.startX = selection.getStartX();
|
||||
this.endX = selection.getEndX();
|
||||
this.startY = selection.getStartY();
|
||||
this.endY = selection.getEndY();
|
||||
this.autoRangeY = selection.isAutoRangeY();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link XYChartSelection} class
|
||||
*
|
||||
* @param startX the lower bound on the X axis of the selection
|
||||
* @param endX the upper bound on the X axis of the selection
|
||||
* @param startY the lower bound on the Y axis of the selection
|
||||
* @param endY the upper bound on the Y axis of the selection
|
||||
* @param autoRangeY set to true to adapt the range on the Y axis automatically
|
||||
*/
|
||||
public XYChartSelection(X startX, X endX, Y startY, Y endY, boolean autoRangeY) {
|
||||
this.startX = startX;
|
||||
this.endX = endX;
|
||||
this.startY = startY;
|
||||
this.endY = endY;
|
||||
this.autoRangeY = autoRangeY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 1;
|
||||
result = PRIME * result + ((this.getStartX() == null) ? 0 : this.getStartX().hashCode());
|
||||
result = PRIME * result + ((this.getEndX() == null) ? 0 : this.getEndX().hashCode());
|
||||
result = PRIME * result + ((this.getStartY() == null) ? 0 : this.getStartY().hashCode());
|
||||
result = PRIME * result + ((this.getEndY() == null) ? 0 : this.getEndY().hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
XYChartSelection other = (XYChartSelection) obj;
|
||||
if (!evaluatesEquality(this.getEndX(), other.getEndX())) {
|
||||
return false;
|
||||
}
|
||||
if (!evaluatesEquality(this.getStartX(), other.getStartX())) {
|
||||
return false;
|
||||
}
|
||||
if (!evaluatesEquality(this.getEndY(), other.getEndY())) {
|
||||
return false;
|
||||
}
|
||||
return evaluatesEquality(this.getStartY(), other.getStartY());
|
||||
}
|
||||
|
||||
private boolean evaluatesEquality(Object o1, Object o2) {
|
||||
if (o1 == null) {
|
||||
return o2 == null;
|
||||
} else return o1.equals(o2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Selection{startX=%s, endX=%s, startY=%s, endY=%s}", getStartX(), getEndX(), getStartY(), getEndY());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,607 @@
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2013, Christian Schudt
|
||||
* Copyright (c) 2016-2020, Frederic Thevenet
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.charts;
|
||||
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.chart.Axis;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An axis that displays date and time values.
|
||||
* Tick labels are usually automatically set and calculated depending on the range unless you explicitly {@linkplain #setTickLabelFormatter(StringConverter) set an formatter}.
|
||||
* You also have the chance to specify fix lower and upper bounds, otherwise they are calculated by your data.
|
||||
* This code is a straight forward adaptation of the original DateTimeAxis by Christian Schudt and Diego Cirujano
|
||||
* to use JAVA 8 {@link java.time.ZonedDateTime} instead of {@link java.util.Date}
|
||||
*
|
||||
* @author Christian Schudt
|
||||
* @author Diego Cirujano
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public final class ZonedDateTimeAxis extends Axis<ZonedDateTime> {
|
||||
/**
|
||||
* These property are used for animation.
|
||||
*/
|
||||
private final LongProperty currentLowerBound = new SimpleLongProperty(this, "currentLowerBound");
|
||||
|
||||
private final LongProperty currentUpperBound = new SimpleLongProperty(this, "currentUpperBound");
|
||||
|
||||
private final ObjectProperty<StringConverter<ZonedDateTime>> tickLabelFormatter = new ObjectPropertyBase<StringConverter<ZonedDateTime>>() {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
if (!isAutoRanging()) {
|
||||
invalidateRange();
|
||||
requestAxisLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return ZonedDateTimeAxis.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "tickLabelFormatter";
|
||||
}
|
||||
};
|
||||
|
||||
private final Property<ZoneId> zoneId;
|
||||
|
||||
/**
|
||||
* Stores the min and max date of the list of dates which is used.
|
||||
* If {@link #Axis:autoRanging} is true, these values are used as lower and upper bounds.
|
||||
*/
|
||||
private ZonedDateTime minDate, maxDate;
|
||||
|
||||
private ObjectProperty<ZonedDateTime> lowerBound = new ObjectPropertyBase<ZonedDateTime>() {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
if (!isAutoRanging()) {
|
||||
invalidateRange();
|
||||
requestAxisLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return ZonedDateTimeAxis.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "lowerBound";
|
||||
}
|
||||
};
|
||||
|
||||
private ObjectProperty<ZonedDateTime> upperBound = new ObjectPropertyBase<ZonedDateTime>() {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
if (!isAutoRanging()) {
|
||||
invalidateRange();
|
||||
requestAxisLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return ZonedDateTimeAxis.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "upperBound";
|
||||
}
|
||||
};
|
||||
|
||||
private Object currentAnimationID;
|
||||
|
||||
private Interval actualInterval = Interval.DECADE;
|
||||
private ZoneOffset zoneOffset = ZoneOffset.UTC;
|
||||
|
||||
/**
|
||||
* Default constructor. By default the lower and upper bound are calculated by the data.
|
||||
*/
|
||||
public ZonedDateTimeAxis() {
|
||||
this.zoneId = new SimpleObjectProperty<>(ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
public ZonedDateTimeAxis(ZoneId zoneId) {
|
||||
this.zoneId = new SimpleObjectProperty<>(zoneId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a date axis with fix lower and upper bounds.
|
||||
*
|
||||
* @param lowerBound The lower bound.
|
||||
* @param upperBound The upper bound.
|
||||
*/
|
||||
public ZonedDateTimeAxis(ZonedDateTime lowerBound, ZonedDateTime upperBound) {
|
||||
this(lowerBound.getZone());
|
||||
setAutoRanging(false);
|
||||
setLowerBound(lowerBound);
|
||||
setUpperBound(upperBound);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a date axis with a label and fix lower and upper bounds.
|
||||
*
|
||||
* @param axisLabel The label for the axis.
|
||||
* @param lowerBound The lower bound.
|
||||
* @param upperBound The upper bound.
|
||||
*/
|
||||
public ZonedDateTimeAxis(String axisLabel, ZonedDateTime lowerBound, ZonedDateTime upperBound) {
|
||||
this(lowerBound, upperBound);
|
||||
setLabel(axisLabel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateRange(List<ZonedDateTime> list) {
|
||||
super.invalidateRange(list);
|
||||
Collections.sort(list);
|
||||
if (list.isEmpty()) {
|
||||
minDate = maxDate = ZonedDateTime.now(zoneId.getValue());
|
||||
} else if (list.size() == 1) {
|
||||
minDate = maxDate = list.get(0);
|
||||
} else if (list.size() > 1) {
|
||||
minDate = list.get(0);
|
||||
maxDate = list.get(list.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object autoRange(double length) {
|
||||
if (isAutoRanging()) {
|
||||
return new Object[]{minDate, maxDate};
|
||||
} else {
|
||||
if (getLowerBound() == null || getUpperBound() == null) {
|
||||
throw new IllegalArgumentException("If autoRanging is false, a lower and upper bound must be set.");
|
||||
}
|
||||
return getRange();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setRange(Object range, boolean animating) {
|
||||
Object[] r = (Object[]) range;
|
||||
ZonedDateTime oldLowerBound = getLowerBound();
|
||||
ZonedDateTime oldUpperBound = getUpperBound();
|
||||
ZonedDateTime lower = (ZonedDateTime) r[0];
|
||||
ZonedDateTime upper = (ZonedDateTime) r[1];
|
||||
setLowerBound(lower);
|
||||
setUpperBound(upper);
|
||||
currentLowerBound.set(getLowerBound().toInstant().toEpochMilli());
|
||||
currentUpperBound.set(getUpperBound().toInstant().toEpochMilli());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getRange() {
|
||||
return new Object[]{getLowerBound(), getUpperBound()};
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getZeroPosition() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDisplayPosition(ZonedDateTime date) {
|
||||
final double length = getSide().isHorizontal() ? getWidth() : getHeight();
|
||||
|
||||
// Get the difference between the max and min date.
|
||||
double diff = currentUpperBound.get() - currentLowerBound.get();
|
||||
|
||||
// Get the actual range of the visible area.
|
||||
// The minimal date should start at the zero position, that's why we subtract it.
|
||||
double range = length - getZeroPosition();
|
||||
|
||||
// Then get the difference from the actual date to the min date and divide it by the total difference.
|
||||
// We get a value between 0 and 1, if the date is within the min and max date.
|
||||
double d = (date.toInstant().toEpochMilli() - currentLowerBound.get()) / diff;
|
||||
|
||||
// Multiply this percent value with the range and add the zero offset.
|
||||
if (getSide().isVertical()) {
|
||||
return getHeight() - d * range + getZeroPosition();
|
||||
} else {
|
||||
return d * range + getZeroPosition();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZonedDateTime getValueForDisplay(double displayPosition) {
|
||||
final double length = getSide().isHorizontal() ? getWidth() : getHeight();
|
||||
|
||||
// Get the difference between the max and min date.
|
||||
double diff = currentUpperBound.get() - currentLowerBound.get();
|
||||
|
||||
// Get the actual range of the visible area.
|
||||
// The minimal date should start at the zero position, that's why we subtract it.
|
||||
double range = length - getZeroPosition();
|
||||
|
||||
if (getSide().isVertical()) {
|
||||
long v = Math.round((displayPosition - getZeroPosition() - getHeight()) / -range * diff + currentLowerBound.get());
|
||||
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(v), zoneId.getValue());
|
||||
} else {
|
||||
long v = Math.round((displayPosition - getZeroPosition()) / range * diff + currentLowerBound.get());
|
||||
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(v), zoneId.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValueOnAxis(ZonedDateTime date) {
|
||||
return date.toInstant().toEpochMilli() > currentLowerBound.get() && date.toInstant().toEpochMilli() < currentUpperBound.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double toNumericValue(ZonedDateTime date) {
|
||||
return date.toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZonedDateTime toRealValue(double v) {
|
||||
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(Math.round(v)), zoneId.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ZonedDateTime> calculateTickValues(double v, Object range) {
|
||||
Object[] r = (Object[]) range;
|
||||
ZonedDateTime lower = (ZonedDateTime) r[0];
|
||||
ZonedDateTime upper = (ZonedDateTime) r[1];
|
||||
List<ZonedDateTime> dateList = new ArrayList<ZonedDateTime>();
|
||||
// The preferred gap which should be between two tick marks.
|
||||
double averageTickGap = 100;
|
||||
double averageTicks = v / averageTickGap;
|
||||
|
||||
// Starting with the greatest unit, add one of each calendar unit.
|
||||
int i = 0;
|
||||
while (i < Interval.values().length && dateList.size() <= averageTicks) {
|
||||
Interval interval = Interval.values()[i];
|
||||
ZonedDateTime currentLower = ZonedDateTime.from(lower);
|
||||
ZonedDateTime currentUpper = ZonedDateTime.from(upper);
|
||||
dateList.clear();
|
||||
actualInterval = interval;
|
||||
// Loop as long we exceeded the upper bound.
|
||||
while (currentLower.toInstant().toEpochMilli() <= currentUpper.toInstant().toEpochMilli()) {
|
||||
dateList.add(currentLower);
|
||||
currentLower = currentLower.plus(interval.amount, interval.unit);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
dateList.add(upper);
|
||||
|
||||
List<ZonedDateTime> evenDateList = makeDatesEven(dateList);
|
||||
// If there are at least three dates, check if the gap between the lower date and the second date is at least half the gap of the second and third date.
|
||||
// Do the same for the upper bound.
|
||||
// If gaps between dates are to small, remove one of them.
|
||||
// This can occur, e.g. if the lower bound is 25.12.2013 and years are shown. Then the next year shown would be 2014 (01.01.2014) which would be too narrow to 25.12.2013.
|
||||
if (evenDateList.size() > 2) {
|
||||
|
||||
ZonedDateTime secondDate = evenDateList.get(1);
|
||||
ZonedDateTime thirdDate = evenDateList.get(2);
|
||||
ZonedDateTime lastDate = evenDateList.get(dateList.size() - 2);
|
||||
ZonedDateTime previousLastDate = evenDateList.get(dateList.size() - 3);
|
||||
|
||||
// If the second date is too near by the lower bound, remove it.
|
||||
// if (secondDate.toInstant().toEpochMilli() - lower.toInstant().toEpochMilli() < (thirdDate.toInstant().toEpochMilli() - secondDate.toInstant().toEpochMilli()) / 2) {
|
||||
// evenDateList.remove(secondDate);
|
||||
// }
|
||||
|
||||
// If difference from the upper bound to the last date is less than the half of the difference of the previous two dates,
|
||||
// we better remove the last date, as it comes to close to the upper bound.
|
||||
if (upper.toInstant().toEpochMilli() - lastDate.toInstant().toEpochMilli() <
|
||||
(lastDate.toInstant().toEpochMilli() - previousLastDate.toInstant().toEpochMilli()) / 2) {
|
||||
evenDateList.remove(lastDate);
|
||||
}
|
||||
}
|
||||
|
||||
return evenDateList;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
if (!isAutoRanging()) {
|
||||
currentLowerBound.set(getLowerBound().toInstant().toEpochMilli());
|
||||
currentUpperBound.set(getUpperBound().toInstant().toEpochMilli());
|
||||
}
|
||||
super.layoutChildren();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTickMarkLabel(ZonedDateTime date) {
|
||||
StringConverter<ZonedDateTime> converter = getTickLabelFormatter();
|
||||
if (converter != null) {
|
||||
return converter.toString(date);
|
||||
}
|
||||
DateTimeFormatter formatter;
|
||||
if (actualInterval.unit == ChronoUnit.YEARS && date.getMonthValue() == 1 && date.getDayOfMonth() == 1) {
|
||||
formatter = DateTimeFormatter.ofPattern("yyyy");
|
||||
} else if (actualInterval.unit == ChronoUnit.MONTHS && date.getDayOfMonth() == 1) {
|
||||
formatter = DateTimeFormatter.ofPattern("MMM yy");
|
||||
} else {
|
||||
switch (actualInterval.unit) {
|
||||
case DAYS:
|
||||
case WEEKS:
|
||||
default:
|
||||
formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
|
||||
break;
|
||||
case HOURS:
|
||||
case MINUTES:
|
||||
formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
|
||||
break;
|
||||
case SECONDS:
|
||||
formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM);
|
||||
break;
|
||||
case MILLIS:
|
||||
formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return formatter.withZone(zoneId.getValue()).format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes dates even, in the sense of that years always begin in January, months always begin on the 1st and days always at midnight.
|
||||
*
|
||||
* @param dates The list of dates.
|
||||
* @return The new list of dates.
|
||||
*/
|
||||
private List<ZonedDateTime> makeDatesEven(List<ZonedDateTime> dates) {
|
||||
// If the dates contain more dates than just the lower and upper bounds, make the dates in between even.
|
||||
if (dates.size() > 2) {
|
||||
List<ZonedDateTime> evenDates = new ArrayList<ZonedDateTime>();
|
||||
|
||||
// For each unit, modify the date slightly by a few millis, to make sure they are different days.
|
||||
// This is because Axis stores each value and won't update the tick labels, if the value is already known.
|
||||
// This happens if you display days and then add a date many years in the future the tick label will still be displayed as day.
|
||||
for (int i = 0; i < dates.size(); i++) {
|
||||
ZonedDateTime date = dates.get(i);
|
||||
ZonedDateTime normalizedDate = date;
|
||||
boolean isFirstOrLast = i != 0 && i != dates.size() - 1;
|
||||
switch (actualInterval.unit) {
|
||||
case YEARS:
|
||||
normalizedDate = ZonedDateTime.of(
|
||||
date.getYear(),
|
||||
isFirstOrLast ? 1 : date.getMonthValue(),
|
||||
isFirstOrLast ? 1 : date.getDayOfMonth(),
|
||||
0, 0, 0, 6, dates.get(i).getZone());
|
||||
break;
|
||||
case MONTHS:
|
||||
normalizedDate = ZonedDateTime.of(
|
||||
date.getYear(),
|
||||
date.getMonthValue(),
|
||||
isFirstOrLast ? 1 : date.getDayOfMonth(),
|
||||
0, 0, 0, 5, dates.get(i).getZone());
|
||||
break;
|
||||
case WEEKS:
|
||||
normalizedDate = ZonedDateTime.of(
|
||||
date.getYear(),
|
||||
date.getMonthValue(),
|
||||
date.getDayOfMonth(),
|
||||
0, 0, 0, 4, dates.get(i).getZone());
|
||||
break;
|
||||
case DAYS:
|
||||
normalizedDate = ZonedDateTime.of(
|
||||
date.getYear(),
|
||||
date.getMonthValue(),
|
||||
date.getDayOfMonth(),
|
||||
0, 0, 0, 3, dates.get(i).getZone());
|
||||
break;
|
||||
case HOURS:
|
||||
normalizedDate = ZonedDateTime.of(
|
||||
date.getYear(),
|
||||
date.getMonthValue(),
|
||||
date.getDayOfMonth(),
|
||||
date.getHour(),
|
||||
isFirstOrLast ? 0 : date.getMinute(),
|
||||
isFirstOrLast ? 0 : date.getSecond(),
|
||||
2, dates.get(i).getZone());
|
||||
break;
|
||||
case MINUTES:
|
||||
normalizedDate = ZonedDateTime.of(
|
||||
date.getYear(),
|
||||
date.getMonthValue(),
|
||||
date.getDayOfMonth(),
|
||||
date.getHour(),
|
||||
date.getMinute(),
|
||||
isFirstOrLast ? 0 : date.getSecond(),
|
||||
1, dates.get(i).getZone());
|
||||
break;
|
||||
case SECONDS:
|
||||
normalizedDate = ZonedDateTime.of(
|
||||
date.getYear(),
|
||||
date.getMonthValue(),
|
||||
date.getDayOfMonth(),
|
||||
date.getHour(),
|
||||
date.getMinute(),
|
||||
date.getSecond(),
|
||||
1,
|
||||
dates.get(i).getZone());
|
||||
break;
|
||||
case MILLIS:
|
||||
normalizedDate = ZonedDateTime.of(
|
||||
date.getYear(),
|
||||
date.getMonthValue(),
|
||||
date.getDayOfMonth(),
|
||||
date.getHour(),
|
||||
date.getMinute(),
|
||||
date.getSecond(),
|
||||
(date.getNano() / 1000) * 1000,
|
||||
dates.get(i).getZone());
|
||||
break;
|
||||
}
|
||||
evenDates.add(normalizedDate);
|
||||
}
|
||||
return evenDates;
|
||||
} else {
|
||||
return dates;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the lower bound of the axis.
|
||||
*
|
||||
* @return The property.
|
||||
* @see #getLowerBound()
|
||||
*/
|
||||
public final ObjectProperty<ZonedDateTime> lowerBoundProperty() {
|
||||
return lowerBound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the lower bound of the axis.
|
||||
*
|
||||
* @return The lower bound.
|
||||
* @see #lowerBoundProperty()
|
||||
*/
|
||||
public final ZonedDateTime getLowerBound() {
|
||||
return lowerBound.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the lower bound of the axis.
|
||||
*
|
||||
* @param date The lower bound date.
|
||||
* @see #lowerBoundProperty()
|
||||
*/
|
||||
public final void setLowerBound(ZonedDateTime date) {
|
||||
lowerBound.set(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the upper bound of the axis.
|
||||
*
|
||||
* @return The property.
|
||||
* @see #getUpperBound() ()
|
||||
*/
|
||||
public final ObjectProperty<ZonedDateTime> upperBoundProperty() {
|
||||
return upperBound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the upper bound of the axis.
|
||||
*
|
||||
* @return The upper bound.
|
||||
* @see #upperBoundProperty()
|
||||
*/
|
||||
public final ZonedDateTime getUpperBound() {
|
||||
return upperBound.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the upper bound of the axis.
|
||||
*
|
||||
* @param date The upper bound date.
|
||||
* @see #upperBoundProperty() ()
|
||||
*/
|
||||
public final void setUpperBound(ZonedDateTime date) {
|
||||
upperBound.set(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tick label formatter for the ticks.
|
||||
*
|
||||
* @return The converter.
|
||||
*/
|
||||
public final StringConverter<ZonedDateTime> getTickLabelFormatter() {
|
||||
return tickLabelFormatter.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tick label formatter for the ticks.
|
||||
*
|
||||
* @param value The converter.
|
||||
*/
|
||||
public final void setTickLabelFormatter(StringConverter<ZonedDateTime> value) {
|
||||
tickLabelFormatter.setValue(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tick label formatter for the ticks.
|
||||
*
|
||||
* @return The property.
|
||||
*/
|
||||
public final ObjectProperty<StringConverter<ZonedDateTime>> tickLabelFormatterProperty() {
|
||||
return tickLabelFormatter;
|
||||
}
|
||||
|
||||
public ZoneId getZoneId() {
|
||||
return zoneId.getValue();
|
||||
}
|
||||
|
||||
public Property<ZoneId> zoneIdProperty() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The intervals, which are used for the tick labels. Beginning with the largest unit, the axis tries to calculate the tick values for this unit.
|
||||
* If a smaller unit is better suited for, that one is taken.
|
||||
*/
|
||||
private enum Interval {
|
||||
DECADE(ChronoUnit.YEARS, 10),
|
||||
YEAR(ChronoUnit.YEARS, 1),
|
||||
MONTH_6(ChronoUnit.MONTHS, 6),
|
||||
MONTH_3(ChronoUnit.MONTHS, 3),
|
||||
MONTH_1(ChronoUnit.MONTHS, 1),
|
||||
WEEK(ChronoUnit.WEEKS, 1),
|
||||
DAY(ChronoUnit.DAYS, 1),
|
||||
HOUR_12(ChronoUnit.HOURS, 12),
|
||||
HOUR_6(ChronoUnit.HOURS, 6),
|
||||
HOUR_3(ChronoUnit.HOURS, 3),
|
||||
HOUR_1(ChronoUnit.HOURS, 1),
|
||||
MINUTE_30(ChronoUnit.MINUTES, 30),
|
||||
MINUTE_10(ChronoUnit.MINUTES, 10),
|
||||
MINUTE_5(ChronoUnit.MINUTES, 5),
|
||||
MINUTE_1(ChronoUnit.MINUTES, 1),
|
||||
SECOND_15(ChronoUnit.SECONDS, 15),
|
||||
SECOND_5(ChronoUnit.SECONDS, 5),
|
||||
SECOND_1(ChronoUnit.SECONDS, 1),
|
||||
MILLISECOND_500(ChronoUnit.MILLIS, 500),
|
||||
MILLISECOND_100(ChronoUnit.MILLIS, 100),
|
||||
MILLISECOND_10(ChronoUnit.MILLIS, 10),
|
||||
MILLISECOND_1(ChronoUnit.MILLIS, 1);
|
||||
|
||||
private final int amount;
|
||||
|
||||
private final transient ChronoUnit unit;
|
||||
|
||||
Interval(ChronoUnit interval, int amount) {
|
||||
this.unit = interval;
|
||||
this.amount = amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2017-2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
import javafx.util.Callback;
|
||||
|
||||
/**
|
||||
* A table cell factory aiming to format cells containing decimal numbers.
|
||||
*
|
||||
* @param <S> The type of the TableView generic type
|
||||
* @param <T> The type of the item contained within the Cell
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class AlignedTableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
|
||||
|
||||
private TextAlignment alignment;
|
||||
|
||||
|
||||
/**
|
||||
* Gets the alignment of text in the cell
|
||||
*
|
||||
* @return the alignment of text in the cell
|
||||
*/
|
||||
public TextAlignment getAlignment() {
|
||||
return alignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the alignment of text in the cell
|
||||
*
|
||||
* @param alignment the alignment of text in the cell
|
||||
*/
|
||||
public void setAlignment(TextAlignment alignment) {
|
||||
this.alignment = alignment;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public TableCell<S, T> call(TableColumn<S, T> p) {
|
||||
TableCell<S, T> cell = new TableCell<S, T>() {
|
||||
|
||||
@Override
|
||||
public void updateItem(Object item, boolean empty) {
|
||||
if (item == getItem()) {
|
||||
return;
|
||||
}
|
||||
super.updateItem((T) item, empty);
|
||||
if (item == null) {
|
||||
super.setText(null);
|
||||
super.setGraphic(null);
|
||||
} else if (item instanceof Node node) {
|
||||
super.setText(null);
|
||||
super.setGraphic(node);
|
||||
} else {
|
||||
super.setText(item.toString());
|
||||
super.setGraphic(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
cell.setTextAlignment(alignment);
|
||||
switch (alignment) {
|
||||
case CENTER:
|
||||
cell.setAlignment(Pos.CENTER);
|
||||
break;
|
||||
case RIGHT:
|
||||
cell.setAlignment(Pos.CENTER_RIGHT);
|
||||
break;
|
||||
default:
|
||||
cell.setAlignment(Pos.CENTER_LEFT);
|
||||
break;
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright 2022 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import eu.binjr.core.dialogs.Dialogs;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class BinjrLoadingPane extends StackPane {
|
||||
|
||||
private static final PseudoClass LOADING_PSEUDO_CLASS = PseudoClass.getPseudoClass("loading");
|
||||
private static final int NB_FRAMES = 5;
|
||||
private final AtomicBoolean rendering = new AtomicBoolean(false);
|
||||
private final ObjectProperty<AnimationSize> animationSize = new SimpleObjectProperty<>(AnimationSize.LARGE);
|
||||
private final BooleanProperty loading = new BooleanPropertyBase(false) {
|
||||
public void invalidated() {
|
||||
pseudoClassStateChanged(LOADING_PSEUDO_CLASS, get());
|
||||
BinjrLoadingPane.this.imageView.setVisible(get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return BinjrLoadingPane.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "loading";
|
||||
}
|
||||
};
|
||||
|
||||
private final DoubleProperty targetFps;
|
||||
private final IntegerProperty initialDelayMs;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final ImageView imageView = new ImageView();
|
||||
private final Image[] frames = new Image[NB_FRAMES];
|
||||
private int frameIndex;
|
||||
|
||||
public BinjrLoadingPane() {
|
||||
this(2, 500);
|
||||
}
|
||||
|
||||
public BinjrLoadingPane(double targetFps, int initialDelayMs) {
|
||||
super();
|
||||
this.targetFps = new SimpleDoubleProperty(targetFps);
|
||||
this.initialDelayMs = new SimpleIntegerProperty(initialDelayMs);
|
||||
scheduler = Executors.newScheduledThreadPool(1, r -> {
|
||||
Thread thread = new Thread(r);
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
|
||||
animationSize.addListener((observable) -> {
|
||||
imageView.setFitWidth(animationSize.getValue().getSize());
|
||||
imageView.setFitHeight(animationSize.getValue().getSize());
|
||||
});
|
||||
|
||||
// Setup animation
|
||||
imageView.getStyleClass().add("binjr-logo-view");
|
||||
imageView.setFitWidth(animationSize.getValue().getSize());
|
||||
imageView.setFitHeight(animationSize.getValue().getSize());
|
||||
// imageView.setOpacity(.6);
|
||||
this.getChildren().add(imageView);
|
||||
this.frameIndex = 0;
|
||||
for (int i = 0; i < NB_FRAMES; i++) {
|
||||
this.frames[i] = new Image(Objects.requireNonNull(getClass().getResourceAsStream("/eu/binjr/images/loading_" + i + ".png")));
|
||||
}
|
||||
this.setAlignment(Pos.CENTER);
|
||||
this.visibleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
var delay = new PauseTransition(Duration.millis(this.initialDelayMs.get()));
|
||||
delay.setOnFinished((event) -> {
|
||||
// Only start animation if masker is still visible after delay
|
||||
if (this.isVisible()) {
|
||||
startAnimation();
|
||||
}
|
||||
});
|
||||
delay.playFromStart();
|
||||
}
|
||||
});
|
||||
startAnimation();
|
||||
this.getStyleClass().setAll("binjr-loading-pane");
|
||||
}
|
||||
|
||||
public double getTargetFps() {
|
||||
return this.targetFps.get();
|
||||
}
|
||||
|
||||
public DoubleProperty targetFpsProperty() {
|
||||
return this.targetFps;
|
||||
}
|
||||
|
||||
public void setTargetFps(double targetFps) {
|
||||
this.targetFps.set(targetFps);
|
||||
}
|
||||
|
||||
public int getInitialDelayMs() {
|
||||
return initialDelayMs.get();
|
||||
}
|
||||
|
||||
public IntegerProperty initialDelayMsProperty() {
|
||||
return initialDelayMs;
|
||||
}
|
||||
|
||||
public void setInitialDelayMs(int initialDelayMs) {
|
||||
this.initialDelayMs.set(initialDelayMs);
|
||||
}
|
||||
|
||||
private void render() {
|
||||
if (rendering.compareAndSet(false, true)) {
|
||||
try {
|
||||
Dialogs.runOnFXThread(() -> {
|
||||
imageView.setImage(frames[frameIndex]);
|
||||
if (++frameIndex >= NB_FRAMES) {
|
||||
frameIndex = 0;
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
rendering.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startAnimation() {
|
||||
this.loading.set(true);
|
||||
var task = scheduler.scheduleAtFixedRate(this::render, 0, Math.round(1000.0 / targetFps.get()), TimeUnit.MILLISECONDS);
|
||||
ChangeListener<Boolean> stopWhenNotVisible = new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||
if (!newValue) {
|
||||
BinjrLoadingPane.this.visibleProperty().removeListener(this);
|
||||
BinjrLoadingPane.this.loading.set(false);
|
||||
task.cancel(true);
|
||||
frameIndex = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
this.visibleProperty().addListener(stopWhenNotVisible);
|
||||
}
|
||||
|
||||
public void setAnimationSize(AnimationSize value){
|
||||
this.animationSize.setValue(value);
|
||||
}
|
||||
|
||||
public AnimationSize getAnimationSize() {
|
||||
return animationSize.getValue();
|
||||
}
|
||||
|
||||
public Property<AnimationSize> animationSizeProperty() {
|
||||
return animationSize;
|
||||
}
|
||||
|
||||
public enum AnimationSize {
|
||||
SMALL(32),
|
||||
MEDIUM(64.0),
|
||||
LARGE(128.0),
|
||||
XL(256.0);
|
||||
|
||||
private final double size;
|
||||
|
||||
AnimationSize(double size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public double getSize() {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import eu.binjr.common.javafx.bindings.BindingManager;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* A {@link TableCell} implementation that shows a {@link ColorPicker}
|
||||
*
|
||||
* @param <T> The type of the TableView generic type
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class ColorTableCell<T> extends TableCell<T, Color> {
|
||||
private final ColorPicker colorPicker;
|
||||
|
||||
public ColorTableCell(TableColumn<T, Color> column, BindingManager bindingManager) {
|
||||
colorPicker = new ColorPicker();
|
||||
colorPicker.getStyleClass().add("button");
|
||||
colorPicker.getStyleClass().add("borderless-color-picker");
|
||||
bindingManager.bind(colorPicker.editableProperty(), column.editableProperty());
|
||||
bindingManager.bind(colorPicker.disableProperty(), column.editableProperty().not());
|
||||
colorPicker.setOnShowing(bindingManager.registerHandler(event -> {
|
||||
TableView<T> tableView = getTableView();
|
||||
tableView.getSelectionModel().clearSelection();
|
||||
tableView.getSelectionModel().select(getTableRow().getIndex());
|
||||
tableView.edit(tableView.getSelectionModel().getSelectedIndex(), column);
|
||||
}));
|
||||
colorPicker.setOnAction(bindingManager.registerHandler(event -> {
|
||||
if (isEditing()) {
|
||||
commitEdit(colorPicker.getValue());
|
||||
}
|
||||
}));
|
||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Color item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
setText(null);
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
colorPicker.setValue(item);
|
||||
setGraphic(this.colorPicker);
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TreeCell;
|
||||
import javafx.util.Callback;
|
||||
|
||||
/**
|
||||
* An implementation of {@link TreeCell} with a context menu attached
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class ContextMenuTableViewCell<S, T> extends TableCell<S, T> {
|
||||
|
||||
public static <S, T> Callback<TableColumn<S, T>, TableCell<S, T>> forTableColumn(ContextMenu contextMenu) {
|
||||
return forTableColumn(contextMenu, null);
|
||||
}
|
||||
|
||||
public static <S, T> Callback<TableColumn<S, T>, TableCell<S, T>> forTableColumn(final ContextMenu contextMenu, final Callback<TableColumn<S, T>, TableCell<S, T>> cellFactory) {
|
||||
return column -> {
|
||||
TableCell<S, T> cell;
|
||||
if (cellFactory == null) {
|
||||
cell = new TableCell<S, T>();
|
||||
cell.itemProperty().addListener((observable, oldValue, newValue) -> {
|
||||
cell.setText(newValue == null ? null : newValue.toString());
|
||||
});
|
||||
} else {
|
||||
cell = cellFactory.call(column);
|
||||
}
|
||||
cell.setContextMenu(contextMenu);
|
||||
return cell;
|
||||
};
|
||||
}
|
||||
|
||||
public ContextMenuTableViewCell(ContextMenu contextMenu) {
|
||||
setContextMenu(contextMenu);
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.control.TreeView;
|
||||
import javafx.util.Callback;
|
||||
|
||||
/**
|
||||
* An implementation of {@link TreeCell} with a context menu attached
|
||||
*/
|
||||
public class ContextMenuTreeViewCell<T> extends TreeCell<T> {
|
||||
|
||||
public static <T> Callback<TreeView<T>, TreeCell<T>> forTreeView(ContextMenu contextMenu) {
|
||||
return forTreeView(contextMenu, null);
|
||||
}
|
||||
|
||||
public static <T> Callback<TreeView<T>, TreeCell<T>> forTreeView(final ContextMenu contextMenu, final Callback<TreeView<T>, TreeCell<T>> cellFactory) {
|
||||
return treeView -> {
|
||||
TreeCell<T> cell;
|
||||
if (cellFactory == null) {
|
||||
cell = new TreeCell<T>();
|
||||
cell.itemProperty().addListener((observable, oldValue, newValue) -> cell.setText(newValue == null ? null : newValue.toString()));
|
||||
} else {
|
||||
cell = cellFactory.call(treeView);
|
||||
}
|
||||
cell.setContextMenu(contextMenu);
|
||||
|
||||
return cell;
|
||||
};
|
||||
}
|
||||
|
||||
public ContextMenuTreeViewCell(ContextMenu contextMenu) {
|
||||
setContextMenu(contextMenu);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2017-2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import eu.binjr.core.data.async.AsyncTaskManager;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.util.Duration;
|
||||
|
||||
/**
|
||||
* Schedule a tasks to start after the specified delay
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class DelayedAction {
|
||||
private final Task<Object> delayedTask;
|
||||
|
||||
public DelayedAction(Runnable action, Duration delay) {
|
||||
|
||||
delayedTask = new Task<>() {
|
||||
@Override
|
||||
protected Object call() throws Exception {
|
||||
Thread.sleep(((long) delay.toMillis()));
|
||||
return null;
|
||||
}
|
||||
};
|
||||
delayedTask.setOnSucceeded(event -> action.run());
|
||||
|
||||
}
|
||||
|
||||
public void submit() {
|
||||
AsyncTaskManager.getInstance().submit(delayedTask);
|
||||
}
|
||||
|
||||
public static void run(Runnable action, Duration delay) {
|
||||
new DelayedAction(action, delay).submit();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* Copyright 2017-2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import javafx.animation.*;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Side;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.util.Duration;
|
||||
|
||||
/**
|
||||
* An {@link AnchorPane} that can slide in or out from a side of its attached window.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class DrawerPane extends AnchorPane {
|
||||
private static PseudoClass EXPANDED_PSEUDO_CLASS = PseudoClass.getPseudoClass("expanded");
|
||||
private DoubleProperty collapsedWidth = new SimpleDoubleProperty(48);
|
||||
private DoubleProperty expandedWidth = new SimpleDoubleProperty(200);
|
||||
private IntegerProperty animationDuration = new SimpleIntegerProperty(50);
|
||||
private Property<PaneAnimation> animation = new SimpleObjectProperty<>(PaneAnimation.NONE);
|
||||
private Timeline showTimeline;
|
||||
private Timeline hideTimeline;
|
||||
private DoubleProperty commandBarWidth = new SimpleDoubleProperty(0.2);
|
||||
private Property<Side> side = new SimpleObjectProperty<>(Side.LEFT);
|
||||
private Property<Node> sibling = new SimpleObjectProperty<>();
|
||||
|
||||
public enum PaneAnimation {
|
||||
NONE,
|
||||
GROW,
|
||||
SLIDE
|
||||
}
|
||||
|
||||
private BooleanProperty expanded = new BooleanPropertyBase(false) {
|
||||
public void invalidated() {
|
||||
pseudoClassStateChanged(EXPANDED_PSEUDO_CLASS, get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return DrawerPane.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "expanded";
|
||||
}
|
||||
};
|
||||
|
||||
public DrawerPane() {
|
||||
expanded.addListener((observable, oldValue, newValue) -> {
|
||||
switch (animation.getValue()) {
|
||||
case NONE:
|
||||
movePane(newValue);
|
||||
break;
|
||||
case GROW:
|
||||
if (newValue)
|
||||
growPane();
|
||||
else
|
||||
shrinkPane();
|
||||
break;
|
||||
case SLIDE:
|
||||
slidePane(newValue);
|
||||
break;
|
||||
}
|
||||
});
|
||||
commandBarWidth.addListener((observable, oldValue, newValue) -> {
|
||||
doCommandBarResize(newValue.doubleValue());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void movePane(boolean show) {
|
||||
Double value = show ? expandedWidth.getValue() : collapsedWidth.getValue();
|
||||
if (side.getValue().isVertical()) {
|
||||
this.setTranslateX(value);
|
||||
} else {
|
||||
this.setTranslateY(value);
|
||||
}
|
||||
switch (side.getValue()) {
|
||||
case LEFT:
|
||||
this.setTranslateX(value);
|
||||
break;
|
||||
case RIGHT:
|
||||
this.setTranslateX(-1 * value);
|
||||
break;
|
||||
case TOP:
|
||||
this.setTranslateY(value);
|
||||
break;
|
||||
case BOTTOM:
|
||||
this.setTranslateY(-1 * value);
|
||||
break;
|
||||
}
|
||||
anchorNode(sibling.getValue(), value);
|
||||
}
|
||||
|
||||
private void growPane() {
|
||||
if (hideTimeline != null) {
|
||||
hideTimeline.stop();
|
||||
}
|
||||
if (showTimeline != null && showTimeline.getStatus() == Animation.Status.RUNNING) {
|
||||
return;
|
||||
}
|
||||
Duration duration = Duration.millis(animationDuration.getValue());
|
||||
KeyFrame keyFrame = new KeyFrame(duration, new KeyValue(commandBarWidth, expandedWidth.getValue()));
|
||||
showTimeline = new Timeline(keyFrame);
|
||||
if (sibling.getValue() != null) {
|
||||
showTimeline.setOnFinished(event -> new DelayedAction(
|
||||
() -> anchorNode(sibling.getValue(), expandedWidth.getValue()),
|
||||
Duration.millis(50)).submit());
|
||||
}
|
||||
showTimeline.play();
|
||||
this.expanded.setValue(true);
|
||||
}
|
||||
|
||||
private void shrinkPane() {
|
||||
if (showTimeline != null) {
|
||||
showTimeline.stop();
|
||||
}
|
||||
if (hideTimeline != null && hideTimeline.getStatus() == Animation.Status.RUNNING) {
|
||||
return;
|
||||
}
|
||||
if (commandBarWidth.get() <= collapsedWidth.getValue()) {
|
||||
return;
|
||||
}
|
||||
Duration duration = Duration.millis(animationDuration.getValue());
|
||||
hideTimeline = new Timeline(new KeyFrame(duration, new KeyValue(commandBarWidth, collapsedWidth.getValue())));
|
||||
anchorNode(sibling.getValue(), collapsedWidth.getValue());
|
||||
hideTimeline.play();
|
||||
this.expanded.setValue(false);
|
||||
}
|
||||
|
||||
|
||||
private void doCommandBarResize(double v) {
|
||||
if (side.getValue().isVertical()) {
|
||||
this.setMinWidth(v);
|
||||
} else {
|
||||
this.setMinHeight(v);
|
||||
}
|
||||
}
|
||||
|
||||
private void slidePane(boolean show) {
|
||||
Double value = show ? expandedWidth.getValue() : collapsedWidth.getValue();
|
||||
TranslateTransition transition = new TranslateTransition(new Duration(animationDuration.get()), this);
|
||||
switch (side.getValue()) {
|
||||
case LEFT:
|
||||
transition.setToX(value);
|
||||
break;
|
||||
case RIGHT:
|
||||
transition.setToX(-1 * value);
|
||||
break;
|
||||
case TOP:
|
||||
transition.setToY(value);
|
||||
break;
|
||||
case BOTTOM:
|
||||
transition.setToY(-1 * value);
|
||||
break;
|
||||
}
|
||||
transition.play();
|
||||
if (show) {
|
||||
transition.setOnFinished(event -> new DelayedAction(() -> anchorNode(sibling.getValue(), value), Duration.millis(50)).submit());
|
||||
} else {
|
||||
anchorNode(sibling.getValue(), value);
|
||||
}
|
||||
}
|
||||
|
||||
private void anchorNode(Node node, double distance) {
|
||||
if (node != null) {
|
||||
switch (getSide()) {
|
||||
case LEFT:
|
||||
AnchorPane.setLeftAnchor(node, distance);
|
||||
break;
|
||||
case BOTTOM:
|
||||
AnchorPane.setBottomAnchor(node, distance);
|
||||
break;
|
||||
case RIGHT:
|
||||
AnchorPane.setRightAnchor(node, distance);
|
||||
break;
|
||||
case TOP:
|
||||
AnchorPane.setTopAnchor(node, distance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Side getSide() {
|
||||
return side.getValue();
|
||||
}
|
||||
|
||||
public Property<Side> sideProperty() {
|
||||
return side;
|
||||
}
|
||||
|
||||
public void setSide(Side side) {
|
||||
this.side.setValue(side);
|
||||
}
|
||||
|
||||
public double getCollapsedWidth() {
|
||||
return collapsedWidth.get();
|
||||
}
|
||||
|
||||
public DoubleProperty collapsedWidthProperty() {
|
||||
return collapsedWidth;
|
||||
}
|
||||
|
||||
public void setCollapsedWidth(double collapsedWidth) {
|
||||
this.collapsedWidth.set(collapsedWidth);
|
||||
}
|
||||
|
||||
public double getExpandedWidth() {
|
||||
return expandedWidth.get();
|
||||
}
|
||||
|
||||
public DoubleProperty expandedWidthProperty() {
|
||||
return expandedWidth;
|
||||
}
|
||||
|
||||
public void setExpandedWidth(double expandedWidth) {
|
||||
this.expandedWidth.set(expandedWidth);
|
||||
}
|
||||
|
||||
public int getAnimationDuration() {
|
||||
return animationDuration.get();
|
||||
}
|
||||
|
||||
public IntegerProperty animationDurationProperty() {
|
||||
return animationDuration;
|
||||
}
|
||||
|
||||
public void setAnimationDuration(int animationDuration) {
|
||||
this.animationDuration.set(animationDuration);
|
||||
}
|
||||
|
||||
public PaneAnimation getAnimation() {
|
||||
return animation.getValue();
|
||||
}
|
||||
|
||||
public Property<PaneAnimation> animationProperty() {
|
||||
return animation;
|
||||
}
|
||||
|
||||
public void setAnimation(PaneAnimation animation) {
|
||||
this.animation.setValue(animation);
|
||||
}
|
||||
|
||||
public Node getSibling() {
|
||||
return sibling.getValue();
|
||||
}
|
||||
|
||||
public Property<Node> siblingProperty() {
|
||||
return sibling;
|
||||
}
|
||||
|
||||
public void setSibling(Node sibling) {
|
||||
this.sibling.setValue(sibling);
|
||||
}
|
||||
|
||||
public boolean isExpanded() {
|
||||
return expanded.get();
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty expandedProperty() {
|
||||
return ReadOnlyBooleanProperty.readOnlyBooleanProperty(expanded);
|
||||
}
|
||||
|
||||
public void expand() {
|
||||
this.expanded.set(true);
|
||||
}
|
||||
|
||||
public void collapse() {
|
||||
this.expanded.set(false);
|
||||
}
|
||||
|
||||
public void toggle() {
|
||||
this.expanded.set(!expanded.getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2017-2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import com.sun.istack.Nullable;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
/**
|
||||
* A Tab control that can be renamed on double-click
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class EditableTab extends Tab {
|
||||
private final Label titleLabel;
|
||||
|
||||
private final BooleanProperty editable = new SimpleBooleanProperty(false);
|
||||
|
||||
public String getName() {
|
||||
return titleLabel.textProperty().getValue();
|
||||
}
|
||||
|
||||
private final TextField textField = new TextField();
|
||||
|
||||
public Property<String> nameProperty() {
|
||||
return titleLabel.textProperty();
|
||||
}
|
||||
|
||||
public void setName(String tabName) {
|
||||
titleLabel.textProperty().setValue(tabName);
|
||||
}
|
||||
|
||||
public EditableTab(String text) {
|
||||
this(text, (ButtonBase) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link EditableTab} instance.
|
||||
*
|
||||
* @param text the title for the tab.
|
||||
* @param buttons A custom {@link Button} instance used to close the tab
|
||||
*/
|
||||
public EditableTab(String text, @Nullable ButtonBase... buttons) {
|
||||
this(null, text, buttons);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link EditableTab} instance.
|
||||
*
|
||||
* @param text the title for the tab.
|
||||
* @param buttons A custom {@link Button} instance used to close the tab
|
||||
* @param graphic A node used as an icon oon the tab
|
||||
*/
|
||||
public EditableTab(Node graphic, String text, @Nullable ButtonBase... buttons) {
|
||||
super();
|
||||
titleLabel = new Label(text);
|
||||
titleLabel.setPadding(new javafx.geometry.Insets(0, 4, 0, 4));
|
||||
var toolbar = new HBox();
|
||||
toolbar.setAlignment(Pos.CENTER_LEFT);
|
||||
if (graphic != null) {
|
||||
toolbar.getChildren().add(graphic);
|
||||
}
|
||||
toolbar.getChildren().add(titleLabel);
|
||||
if (buttons != null) {
|
||||
setClosable(false);
|
||||
toolbar.getChildren().addAll(buttons);
|
||||
}
|
||||
|
||||
// We need to wrap the hbox used for layout into a Label
|
||||
// otherwise the tabs names in the overflow menu is empty.
|
||||
var wrappingLabel = new Label();
|
||||
wrappingLabel.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
wrappingLabel.textProperty().bind(titleLabel.textProperty());
|
||||
wrappingLabel.setGraphic(toolbar);
|
||||
setGraphic(wrappingLabel);
|
||||
|
||||
editable.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
textField.setText(titleLabel.getText());
|
||||
setGraphic(textField);
|
||||
textField.selectAll();
|
||||
textField.requestFocus();
|
||||
} else {
|
||||
if (!textField.getText().isEmpty()) {
|
||||
titleLabel.setText(textField.getText());
|
||||
}
|
||||
setGraphic(wrappingLabel);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
titleLabel.setOnMouseClicked(event -> {
|
||||
if (event.getClickCount() == 2) {
|
||||
editable.setValue(true);
|
||||
}
|
||||
});
|
||||
|
||||
textField.setOnAction(event -> {
|
||||
editable.setValue(false);
|
||||
});
|
||||
|
||||
textField.focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (!newValue) {
|
||||
editable.setValue(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the tab
|
||||
*
|
||||
* @param text the new name for the tab.
|
||||
*/
|
||||
public void rename(String text) {
|
||||
titleLabel.setText(text);
|
||||
}
|
||||
|
||||
public boolean isEditable() {
|
||||
return editable.get();
|
||||
}
|
||||
|
||||
public BooleanProperty editableProperty() {
|
||||
return editable;
|
||||
}
|
||||
|
||||
public void setEditable(boolean editable) {
|
||||
if (editable) {
|
||||
this.textField.requestFocus();
|
||||
}
|
||||
this.editable.set(editable);
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import eu.binjr.common.javafx.bindings.BindingManager;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.skin.PaginationSkin;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
|
||||
|
||||
public class EnhancedPagination extends Pagination {
|
||||
@Override
|
||||
protected Skin<Pagination> createDefaultSkin() {
|
||||
return new EnhancedPaginationSkin(this);
|
||||
}
|
||||
|
||||
public static class EnhancedPaginationSkin extends PaginationSkin {
|
||||
private final HBox controlBox;
|
||||
private final Button previousPageButton;
|
||||
private final Button nextPageButton;
|
||||
private final Button firstPageButton;
|
||||
private final Button lastPageButton;
|
||||
private final Label pageInformation;
|
||||
private final Button jumpToPageButton;
|
||||
private final BindingManager bindingManager = new BindingManager();
|
||||
|
||||
public EnhancedPaginationSkin(Pagination pagination) {
|
||||
super(pagination);
|
||||
controlBox = (HBox) lookup(pagination, ".control-box");
|
||||
previousPageButton = (Button) lookup(pagination, ".left-arrow-button");
|
||||
nextPageButton = (Button) lookup(pagination, ".right-arrow-button");
|
||||
pageInformation = (Label) lookup(pagination, ".page-information");
|
||||
pageInformation.setVisible(false);
|
||||
pageInformation.setManaged(false);
|
||||
|
||||
firstPageButton = new ToolButtonBuilder<Button>(bindingManager)
|
||||
.setText("First")
|
||||
.setTooltip("Jump to first page")
|
||||
.setHeight(previousPageButton.getHeight())
|
||||
.setStyleClass("dialog-button")
|
||||
.setIconStyleClass("rewind-icon")
|
||||
.bind(Node::disableProperty, previousPageButton.disableProperty())
|
||||
.setAction(event -> pagination.setCurrentPageIndex(0))
|
||||
.build(Button::new);
|
||||
|
||||
lastPageButton = new ToolButtonBuilder<Button>(bindingManager)
|
||||
.setText("Last")
|
||||
.setTooltip("Jump to last page")
|
||||
.setHeight(nextPageButton.getHeight())
|
||||
.setStyleClass("dialog-button")
|
||||
.setIconStyleClass("fast-forward-icon")
|
||||
.bind(Node::disableProperty, nextPageButton.disableProperty())
|
||||
.setAction(event -> pagination.setCurrentPageIndex(pagination.getPageCount()))
|
||||
.build(Button::new);
|
||||
|
||||
var input = new TextField();
|
||||
HBox.setHgrow(input, Priority.NEVER);
|
||||
var label = new Label("Jump to:");
|
||||
label.setMinWidth(55);
|
||||
HBox.setHgrow(label, Priority.ALWAYS);
|
||||
var hbox = new HBox();
|
||||
hbox.setSpacing(10);
|
||||
hbox.getChildren().addAll(label, input);
|
||||
hbox.setAlignment(Pos.CENTER_LEFT);
|
||||
hbox.getStyleClass().addAll("pagination-popup");
|
||||
hbox.setPadding(new Insets(5.0, 5.0, 5.0, 5.0));
|
||||
hbox.setPrefSize(135, 40);
|
||||
var popup = new PopupControl();
|
||||
popup.setAutoHide(true);
|
||||
popup.getScene().setRoot(hbox);
|
||||
input.setOnAction(bindingManager.registerHandler(event -> {
|
||||
try {
|
||||
int targetPageIndex = Math.max(Math.min(Integer.parseInt(input.getText()) - 1, pagination.getPageCount()), 0);
|
||||
pagination.setCurrentPageIndex(targetPageIndex);
|
||||
} catch (NumberFormatException e) {
|
||||
// Ignore badly formatted numerical input
|
||||
} finally {
|
||||
popup.hide();
|
||||
}
|
||||
}));
|
||||
jumpToPageButton = new ToolButtonBuilder<Button>(bindingManager)
|
||||
.setText("Jump")
|
||||
.setTooltip("Jump to page")
|
||||
.setHeight(nextPageButton.getHeight())
|
||||
.setStyleClass("dialog-button")
|
||||
.setIconStyleClass("share-icon")
|
||||
.setAction(actionEvent -> {
|
||||
Node owner = (Node) actionEvent.getSource();
|
||||
Bounds bounds = owner.localToScreen(owner.getBoundsInLocal());
|
||||
popup.show(owner.getScene().getWindow(), bounds.getMinX(), bounds.getMinY() - 45);
|
||||
input.setText(Integer.toString(pagination.getCurrentPageIndex() + 1));
|
||||
input.selectAll();
|
||||
}).build(Button::new);
|
||||
|
||||
bindingManager.attachListener(this.controlBox.getChildren(), (ListChangeListener<Node>) c -> {
|
||||
while (c.next()) {
|
||||
if (c.wasAdded() && !c.wasRemoved() && c.getAddedSize() == 1 && c.getAddedSubList().get(0) == nextPageButton) {
|
||||
addCustomNodes();
|
||||
}
|
||||
}
|
||||
});
|
||||
addCustomNodes();
|
||||
}
|
||||
|
||||
private Node lookup(Control parent, String key) {
|
||||
var node = parent.lookup(key);
|
||||
if (node == null) {
|
||||
throw new IllegalStateException("Failed to find a child node for lookup " + key);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
protected void addCustomNodes() {
|
||||
if (firstPageButton.getParent() != controlBox) {
|
||||
controlBox.getChildren().add(0, firstPageButton);
|
||||
controlBox.getChildren().addAll(lastPageButton, jumpToPageButton);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
bindingManager.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright 2019-2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import eu.binjr.common.preferences.ObfuscatedString;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.control.PropertySheet;
|
||||
import org.controlsfx.property.editor.DefaultPropertyEditorFactory;
|
||||
import org.controlsfx.property.editor.PropertyEditor;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class ExtendedPropertyEditorFactory extends DefaultPropertyEditorFactory {
|
||||
private static final Logger logger = Logger.create(ExtendedPropertyEditorFactory.class);
|
||||
private static final Gson gson = new Gson();
|
||||
private final ObfuscatedString.Obfuscator obfuscator;
|
||||
|
||||
public ExtendedPropertyEditorFactory() {
|
||||
this.obfuscator = null;
|
||||
}
|
||||
|
||||
public ExtendedPropertyEditorFactory(ObfuscatedString.Obfuscator obfuscator) {
|
||||
this.obfuscator = obfuscator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyEditor<?> call(PropertySheet.Item item) {
|
||||
var editor = super.call(item);
|
||||
if (editor != null) {
|
||||
return editor;
|
||||
}
|
||||
if (Path.class.isAssignableFrom(item.getType())) {
|
||||
return new FormattedPropertyEditor<Path>(item, new TextFormatter<>(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Path object) {
|
||||
if (object == null) {
|
||||
return "";
|
||||
}
|
||||
return object.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path fromString(String string) {
|
||||
return Path.of(string);
|
||||
}
|
||||
}), (node, path) -> {
|
||||
DirectoryChooser fileChooser = new DirectoryChooser();
|
||||
fileChooser.setTitle("Select Folder");
|
||||
try {
|
||||
Path pluginPath = path.toRealPath();
|
||||
if (Files.isDirectory(pluginPath)) {
|
||||
fileChooser.setInitialDirectory(pluginPath.toFile());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.debug("Could not initialize working dir for DirectoryChooser", e);
|
||||
}
|
||||
File folder = fileChooser.showDialog(NodeUtils.getStage(node));
|
||||
if (folder != null) {
|
||||
return Optional.of(folder.toPath());
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
if (String[].class.isAssignableFrom(item.getType())) {
|
||||
return new FormattedPropertyEditor<String[]>(item, new TextFormatter<>(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(String[] object) {
|
||||
if (object == null) {
|
||||
object = new String[0];
|
||||
}
|
||||
return gson.toJson(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] fromString(String string) {
|
||||
var res = gson.fromJson(string, String[].class);
|
||||
return res != null ? res : new String[0];
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.obfuscator != null && ObfuscatedString.class.isAssignableFrom(item.getType())) {
|
||||
return new PasswordPropertyEditor<ObfuscatedString>(item, new TextFormatter<>(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(ObfuscatedString object) {
|
||||
if (object == null) {
|
||||
return "";
|
||||
}
|
||||
return object.toPlainText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObfuscatedString fromString(String string) {
|
||||
return obfuscator.fromPlainText(string);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (Duration.class.isAssignableFrom(item.getType())) {
|
||||
return new FormattedPropertyEditor<Duration>(item, new TextFormatter<>(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Duration object) {
|
||||
if (object == null) {
|
||||
return "";
|
||||
}
|
||||
return String.format("%d", (long) object.toMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration fromString(String string) {
|
||||
return Duration.millis(Long.parseLong(string));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (URI.class.isAssignableFrom(item.getType())) {
|
||||
return new FormattedPropertyEditor<URI>(item, new TextFormatter<>(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(URI object) {
|
||||
if (object == null) {
|
||||
return "";
|
||||
}
|
||||
return object.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI fromString(String string) {
|
||||
return URI.create(string);
|
||||
}
|
||||
}), s -> {
|
||||
try {
|
||||
new URL(s);
|
||||
return new TextFieldValidator.ValidationStatus(true, "");
|
||||
} catch (MalformedURLException e) {
|
||||
return new TextFieldValidator.ValidationStatus(false, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class PasswordPropertyEditor<T> implements PropertyEditor<T> {
|
||||
private final PasswordField textField;
|
||||
private final TextFormatter<T> textFormatter;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public PasswordPropertyEditor(PropertySheet.Item item, TextFormatter<T> textFormatter) {
|
||||
textField = new PasswordField();
|
||||
this.textFormatter = textFormatter;
|
||||
textField.setTextFormatter(textFormatter);
|
||||
item.getObservableValue().ifPresent(itemValue -> {
|
||||
itemValue.addListener((observable, oldValue, newValue) -> textFormatter.setValue((T) newValue));
|
||||
});
|
||||
textFormatter.setValue((T) item.getValue());
|
||||
textFormatter.valueProperty().addListener((observable, oldValue, newValue) -> item.setValue(newValue));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getEditor() {
|
||||
return textField;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getValue() {
|
||||
return textFormatter.getValueConverter().fromString(textField.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(T value) {
|
||||
textField.setText(textFormatter.getValueConverter().toString(value));
|
||||
}
|
||||
}
|
||||
|
||||
public static class FormattedPropertyEditor<T> implements PropertyEditor<T> {
|
||||
private final HBox editor;
|
||||
private final TextField textField;
|
||||
private final TextFormatter<T> textFormatter;
|
||||
|
||||
FormattedPropertyEditor(PropertySheet.Item item, TextFormatter<T> textFormatter) {
|
||||
this(item, textFormatter, null, null);
|
||||
}
|
||||
|
||||
FormattedPropertyEditor(PropertySheet.Item item, TextFormatter<T> textFormatter, Function<String, TextFieldValidator.ValidationStatus> validator) {
|
||||
this(item, textFormatter, null, validator);
|
||||
}
|
||||
|
||||
FormattedPropertyEditor(PropertySheet.Item item, TextFormatter<T> textFormatter, BiFunction<Node, T, Optional<T>> editAction) {
|
||||
this(item, textFormatter, editAction, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
FormattedPropertyEditor(PropertySheet.Item item,
|
||||
TextFormatter<T> textFormatter,
|
||||
BiFunction<Node, T, Optional<T>> editAction,
|
||||
Function<String, TextFieldValidator.ValidationStatus> validator) {
|
||||
this.textFormatter = textFormatter;
|
||||
editor = new HBox();
|
||||
editor.setSpacing(5);
|
||||
AnchorPane.setLeftAnchor(editor, 0.0);
|
||||
AnchorPane.setRightAnchor(editor, 0.0);
|
||||
textField = new TextField();
|
||||
textField.setTextFormatter(textFormatter);
|
||||
item.getObservableValue().ifPresent(itemValue -> {
|
||||
itemValue.addListener((observable, oldValue, newValue) -> textFormatter.setValue((T) newValue));
|
||||
});
|
||||
textFormatter.setValue((T) item.getValue());
|
||||
textFormatter.valueProperty().addListener((observable, oldValue, newValue) -> item.setValue(newValue));
|
||||
|
||||
HBox.setHgrow(textField, Priority.ALWAYS);
|
||||
editor.getChildren().add(textField);
|
||||
if (editAction != null) {
|
||||
var editButton = new Button("...");
|
||||
editButton.setOnAction(event -> {
|
||||
editAction.apply(getEditor(), textFormatter.getValue()).ifPresent(textFormatter::setValue);
|
||||
});
|
||||
editor.getChildren().add(editButton);
|
||||
}
|
||||
if (validator != null) {
|
||||
textField.setOnAction(event -> TextFieldValidator.validate(textField, true, validator));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getEditor() {
|
||||
return editor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getValue() {
|
||||
return textFormatter.getValueConverter().fromString(textField.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(T value) {
|
||||
textField.setText(textFormatter.getValueConverter().toString(value));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import eu.binjr.common.javafx.bindings.BindingManager;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* A {@link TableCell} implementation that shows an icon if the bound property is true
|
||||
*
|
||||
* @param <T> The type of the TableView generic type
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class IconTableCell<T> extends TableCell<T, Boolean> {
|
||||
private final Node icon;
|
||||
|
||||
public IconTableCell(Node icon) {
|
||||
this.icon = icon;
|
||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Boolean item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
setText(null);
|
||||
if (empty || !item) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setGraphic(icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import eu.binjr.core.preferences.UserPreferences;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class LabelWithInlineHelp extends HBox {
|
||||
|
||||
private final Label label;
|
||||
|
||||
private final StringProperty inlineHelp = new SimpleStringProperty();
|
||||
|
||||
public LabelWithInlineHelp() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
public LabelWithInlineHelp(String label) {
|
||||
this(label, null);
|
||||
}
|
||||
|
||||
public LabelWithInlineHelp(String labelText, String inlineHelpText) {
|
||||
var helpPopup = new Tooltip();
|
||||
helpPopup.textProperty().bind(inlineHelpProperty());
|
||||
helpPopup.setAutoHide(true);
|
||||
helpPopup.setHideOnEscape(true);
|
||||
var tooltip = new Tooltip();
|
||||
tooltip.textProperty().bind(inlineHelpProperty());
|
||||
ButtonBase helpButton = new ToolButtonBuilder<>()
|
||||
.setStyleClass("dialog-button")
|
||||
.setHeight(20.0)
|
||||
.setWidth(20.0)
|
||||
.setIconStyleClass("help-small-icon")
|
||||
.build(ToggleButton::new);
|
||||
helpButton.setTooltip(tooltip);
|
||||
((ToggleButton) helpButton).selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
Bounds bounds = helpButton.localToScreen(helpButton.getBoundsInLocal());
|
||||
helpPopup.show(helpButton, bounds.getMaxX(), bounds.getMinY());
|
||||
helpButton.setTooltip(null);
|
||||
} else {
|
||||
helpPopup.hide();
|
||||
helpButton.setTooltip(tooltip);
|
||||
}
|
||||
});
|
||||
helpPopup.showingProperty().addListener((observable, oldValue, newValue) -> {
|
||||
((ToggleButton) helpButton).setSelected(newValue);
|
||||
});
|
||||
helpButton.setAlignment(Pos.TOP_RIGHT);
|
||||
helpButton.visibleProperty().bind(UserPreferences.getInstance().showInlineHelpButtons.property());
|
||||
helpButton.managedProperty().bind(UserPreferences.getInstance().showInlineHelpButtons.property());
|
||||
label = new Label();
|
||||
label.setAlignment(Pos.TOP_LEFT);
|
||||
label.setWrapText(true);
|
||||
var spacer = new Pane();
|
||||
HBox.setHgrow(spacer, Priority.SOMETIMES);
|
||||
this.getChildren().addAll(label, spacer, helpButton);
|
||||
if (labelText != null) {
|
||||
label.setText(labelText);
|
||||
}
|
||||
if (inlineHelpText != null) {
|
||||
inlineHelp.set(inlineHelpText);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public String getInlineHelp() {
|
||||
return inlineHelp.get();
|
||||
}
|
||||
|
||||
public StringProperty inlineHelpProperty() {
|
||||
return inlineHelp;
|
||||
}
|
||||
|
||||
public void setInlineHelp(String inlineHelp) {
|
||||
this.inlineHelp.set(inlineHelp);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return label.getText();
|
||||
}
|
||||
|
||||
public StringProperty textProperty() {
|
||||
return label.textProperty();
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.label.setText(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2022 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import eu.binjr.core.dialogs.Dialogs;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.transform.Rotate;
|
||||
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class LoadingPane extends StackPane {
|
||||
private static final double HEIGHT = 64.0;
|
||||
private static final double WIDTH = 64.0;
|
||||
private final Canvas canvas;
|
||||
private final AtomicBoolean rendering = new AtomicBoolean(false);
|
||||
|
||||
private final DoubleProperty targetFps;
|
||||
private final DoubleProperty rotationSpeed;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final GraphicsContext context2D;
|
||||
private double currentAngle;
|
||||
private final Rotate transform;
|
||||
private final Image image;
|
||||
private double angularStep;
|
||||
|
||||
public LoadingPane() {
|
||||
this(30, 90);
|
||||
}
|
||||
|
||||
public LoadingPane(double targetFps, double rotationSpeed) {
|
||||
super();
|
||||
this.targetFps = new SimpleDoubleProperty(targetFps);
|
||||
this.rotationSpeed = new SimpleDoubleProperty(rotationSpeed);
|
||||
|
||||
scheduler = Executors.newScheduledThreadPool(1, r -> {
|
||||
Thread thread = new Thread(r);
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
|
||||
InvalidationListener computeAngularStep = (observable) -> this.angularStep = this.rotationSpeed.get() / this.targetFps.get();
|
||||
|
||||
// Setup animation
|
||||
this.canvas = new Canvas(WIDTH, HEIGHT);
|
||||
this.context2D = canvas.getGraphicsContext2D();
|
||||
this.currentAngle = 0;
|
||||
this.transform = new Rotate(0, WIDTH / 2, HEIGHT / 2);
|
||||
this.image = new Image(Objects.requireNonNull(getClass().getResourceAsStream("/eu/binjr/images/loading_64.png")));
|
||||
this.targetFps.addListener(computeAngularStep);
|
||||
|
||||
this.rotationSpeed.addListener(computeAngularStep);
|
||||
computeAngularStep.invalidated(null);
|
||||
|
||||
this.setAlignment(Pos.CENTER);
|
||||
this.visibleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
startSpinning();
|
||||
}
|
||||
});
|
||||
this.getChildren().add(canvas);
|
||||
startSpinning();
|
||||
this.getStyleClass().setAll("canvas-masker-pane");
|
||||
}
|
||||
|
||||
|
||||
public double getTargetFps() {
|
||||
return this.targetFps.get();
|
||||
}
|
||||
|
||||
public DoubleProperty targetFpsProperty() {
|
||||
return this.targetFps;
|
||||
}
|
||||
|
||||
public void setTargetFps(double targetFps) {
|
||||
this.targetFps.set(targetFps);
|
||||
}
|
||||
|
||||
public double getRotationSpeed() {
|
||||
return rotationSpeed.get();
|
||||
}
|
||||
|
||||
public DoubleProperty rotationSpeedProperty() {
|
||||
return rotationSpeed;
|
||||
}
|
||||
|
||||
public void setRotationSpeed(double rotationSpeed) {
|
||||
this.rotationSpeed.set(rotationSpeed);
|
||||
}
|
||||
|
||||
|
||||
private void render() {
|
||||
if (rendering.compareAndSet(false, true)) {
|
||||
try {
|
||||
Dialogs.runOnFXThread(() -> {
|
||||
context2D.clearRect(0, 0, WIDTH, HEIGHT);
|
||||
transform.setAngle(currentAngle);
|
||||
context2D.setTransform(
|
||||
transform.getMxx(),
|
||||
transform.getMyx(),
|
||||
transform.getMxy(),
|
||||
transform.getMyy(),
|
||||
transform.getTx(),
|
||||
transform.getTy());
|
||||
context2D.drawImage(image, 0, 0);
|
||||
currentAngle += angularStep;
|
||||
});
|
||||
} finally {
|
||||
rendering.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startSpinning() {
|
||||
var task = scheduler.scheduleAtFixedRate(this::render, 0, Math.round(1000.0 / targetFps.get()), TimeUnit.MILLISECONDS);
|
||||
ChangeListener<Boolean> stopWhenNotVisible = new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||
if (!newValue) {
|
||||
LoadingPane.this.visibleProperty().removeListener(this);
|
||||
task.cancel(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.visibleProperty().addListener(stopWhenNotVisible);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2019 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.SnapshotParameters;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.transform.Transform;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
/**
|
||||
* Defines a collection of helper methods that work with @{@link Node}
|
||||
*/
|
||||
public final class NodeUtils {
|
||||
|
||||
/**
|
||||
* Takes a snapshot of the provided node and render an image using the same output scale as it containing stage.
|
||||
*
|
||||
* @param node the node to take a snapshot of.
|
||||
* @return the rendered image.
|
||||
*/
|
||||
public static WritableImage scaledSnapshot(Node node) {
|
||||
return scaledSnapshot(node, Color.TRANSPARENT, getOutputScaleX(node), getOutputScaleY(node));
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a snapshot of the provided node and render an image using the specified output scale.
|
||||
*
|
||||
* @param node the node to take a snapshot of.
|
||||
* @param fillColor the fill color.
|
||||
* @return the rendered image.
|
||||
*/
|
||||
public static WritableImage scaledSnapshot(Node node, Paint fillColor) {
|
||||
return scaledSnapshot(node, fillColor, getOutputScaleX(node), getOutputScaleY(node));
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a snapshot of the provided node and render an image using the specified output scale.
|
||||
*
|
||||
* @param node the node to take a snapshot of.
|
||||
* @param fillColor the fill color.
|
||||
* @param scaleX the X output scale to use.
|
||||
* @param scaleY the Y output scale to use.
|
||||
* @return the rendered image.
|
||||
*/
|
||||
public static WritableImage scaledSnapshot(Node node, Paint fillColor, double scaleX, double scaleY) {
|
||||
SnapshotParameters spa = new SnapshotParameters();
|
||||
spa.setFill(fillColor);
|
||||
spa.setTransform(Transform.scale(scaleX, scaleY));
|
||||
return node.snapshot(spa, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the vertical output scale of the {@link Stage} that contains the provided {@link Node}
|
||||
*
|
||||
* @param node the node contained by the stage to get the output scale for.
|
||||
* @return the vertical output scale
|
||||
*/
|
||||
public static double getOutputScaleX(Node node) {
|
||||
var stage = getStage(node);
|
||||
return stage == null ? Screen.getPrimary().getOutputScaleX() : stage.getOutputScaleX();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the horizontal output scale of the {@link Stage} that contains the provided {@link Node}
|
||||
*
|
||||
* @param node the node contained by the stage to get the output scale for.
|
||||
* @return the horizontal output scale.
|
||||
*/
|
||||
public static double getOutputScaleY(Node node) {
|
||||
var stage = getStage(node);
|
||||
return stage == null ? Screen.getPrimary().getOutputScaleY() : stage.getOutputScaleY();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Stage} instance to which the provided {@link Node} is attached
|
||||
*
|
||||
* @param node the node to get the stage for.
|
||||
* @return the {@link Stage} instance to which the provided {@link Node} is attached
|
||||
*/
|
||||
public static Stage getStage(Node node) {
|
||||
if (node != null && node.getScene() != null) {
|
||||
return (Stage) node.getScene().getWindow();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2017-2022 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import eu.binjr.common.javafx.bindings.BindingManager;
|
||||
import eu.binjr.core.data.indexes.parser.profile.BuiltInParsingProfile;
|
||||
import eu.binjr.core.data.indexes.parser.profile.ParsingProfile;
|
||||
import eu.binjr.core.dialogs.LogParsingProfileDialog;
|
||||
import javafx.scene.control.*;
|
||||
|
||||
/**
|
||||
* A {@link TableCell} implementation that shows a {@link Button}
|
||||
*
|
||||
* @param <T> The type of the TableView generic type
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class ParsingProfileCell<T> extends TableCell<T, ParsingProfile> {
|
||||
private final ButtonBase button;
|
||||
private ParsingProfile current;
|
||||
|
||||
public ParsingProfileCell(TableColumn<T, ParsingProfile> column,
|
||||
BindingManager bindingManager,
|
||||
Runnable onEdit) {
|
||||
button = new ToolButtonBuilder<>(bindingManager)
|
||||
.setText("")
|
||||
.setTooltip("Edit")
|
||||
.setStyleClass("dialog-button")
|
||||
.setIconStyleClass("settings-icon", "small-icon")
|
||||
.setFocusTraversable(false)
|
||||
.setAction(event -> {
|
||||
// getTableView().getSelectionModel().select(getTableRow().getIndex());
|
||||
var dlg = new LogParsingProfileDialog(NodeUtils.getStage(getTableView()), current);
|
||||
dlg.showAndWait().ifPresent(selection -> {
|
||||
getTableView().edit(getTableRow().getIndex(), column);
|
||||
commitEdit(selection);
|
||||
onEdit.run();
|
||||
});
|
||||
})
|
||||
.build(Button::new);
|
||||
setContentDisplay(ContentDisplay.LEFT);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void updateItem(ParsingProfile item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
setText("");
|
||||
} else {
|
||||
if (item != null) {
|
||||
setText(item.toString());
|
||||
current = item;
|
||||
if (item != BuiltInParsingProfile.NONE) {
|
||||
setGraphic(button);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import eu.binjr.core.data.indexes.IndexingStatus;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.TableCell;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link TableCell} implementation that shows an icon if the bound property is true
|
||||
*
|
||||
* @param <T> The type of the TableView generic type
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class StatusIconTableCell<T> extends TableCell<T, IndexingStatus> {
|
||||
private final Map<IndexingStatus, Node> iconMap;
|
||||
|
||||
public StatusIconTableCell(Map<IndexingStatus, Node> icons) {
|
||||
this.iconMap = icons;
|
||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(IndexingStatus item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
setText(null);
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(item.name());
|
||||
setGraphic(iconMap.getOrDefault(item, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.control.TreeView;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class StylableTreeItem {
|
||||
private final String label;
|
||||
private final List<PseudoClass> pseudoClasses;
|
||||
|
||||
public StylableTreeItem(String label, String... pseudoClasses) {
|
||||
this.label = label;
|
||||
this.pseudoClasses = Stream.of(pseudoClasses).map(PseudoClass::getPseudoClass).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public List<PseudoClass> getPseudoClasses() {
|
||||
return pseudoClasses;
|
||||
}
|
||||
|
||||
public static <T extends StylableTreeItem> void setCellFactory(TreeView<T> treeView){
|
||||
treeView.setCellFactory(tv -> new TreeCell<T>() {
|
||||
private final Set<PseudoClass> pseudoClassesSet = new HashSet<>();
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
textProperty().unbind();
|
||||
|
||||
pseudoClassesSet.forEach(pc -> pseudoClassStateChanged(pc, false));
|
||||
if (empty) {
|
||||
setText("");
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(item.getLabel());
|
||||
setGraphic(getTreeItem().getGraphic());
|
||||
List<PseudoClass> itemPC = item.getPseudoClasses();
|
||||
if (itemPC != null) {
|
||||
itemPC.forEach(pc->{
|
||||
pseudoClassStateChanged(pc, true);
|
||||
pseudoClassesSet.add(pc);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2019 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.scene.control.TableView;
|
||||
|
||||
public final class TableViewUtils {
|
||||
//FIXME try and retrieve the actual scrollbar width at runtime
|
||||
private static final double SCROLL_BAR_WIDTH = 20.0;
|
||||
|
||||
public static void autoFillTableWidthWithLastColumn(TableView<?> table) {
|
||||
autoFillTableWidthWithColumn(table, table.getColumns().size() - 1);
|
||||
}
|
||||
|
||||
public static void autoFillTableWidthWithColumn(TableView<?> table, int columnIdx) {
|
||||
if (columnIdx > 0) {
|
||||
ReadOnlyDoubleProperty[] widthProperties = new ReadOnlyDoubleProperty[table.getColumns().size()];
|
||||
for (int i = 0; i < table.getColumns().size(); i++) {
|
||||
if (i != columnIdx) {
|
||||
widthProperties[i] = table.getColumns().get(i).widthProperty();
|
||||
}
|
||||
}
|
||||
|
||||
widthProperties[columnIdx] = table.widthProperty();
|
||||
table.getColumns().get(columnIdx).prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
|
||||
double width = widthProperties[columnIdx].getValue() - SCROLL_BAR_WIDTH;
|
||||
for (int i = 0; i < table.getColumns().size(); i++) {
|
||||
if (i != columnIdx) {
|
||||
width -= widthProperties[i].getValue();
|
||||
}
|
||||
}
|
||||
return Math.max(width, 100.0);
|
||||
}, widthProperties));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
/*
|
||||
* Copyright 2017-2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
|
||||
import eu.binjr.common.javafx.bindings.BindingManager;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.stage.WindowEvent;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
|
||||
/**
|
||||
* A TabPane container with a button to add a new tab that also supports tearing away tabs into a separate window.
|
||||
* <p>It relies on the {@link TabPaneManager} class to keep track of all tabs spread over many windows and {@link TabPane} instances</p>
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class TearableTabPane extends TabPane implements AutoCloseable {
|
||||
private static final PseudoClass HOVER_PSEUDO_CLASS = PseudoClass.getPseudoClass("hover");
|
||||
private static final Logger logger = Logger.create(TearableTabPane.class);
|
||||
private boolean tearable;
|
||||
private boolean reorderable;
|
||||
private Function<ActionEvent, Optional<Tab>> newTabFactory = (e) -> Optional.of(new Tab());
|
||||
private final Map<Tab, TabState> tearableTabMap = new HashMap<>();
|
||||
private final TabPaneManager manager;
|
||||
private EventHandler<ActionEvent> onAddNewTab;
|
||||
private EventHandler<WindowEvent> onOpenNewWindow;
|
||||
private EventHandler<WindowEvent> onClosingWindow;
|
||||
private final BindingManager bindingManager = new BindingManager();
|
||||
private final Property<StageStyle> detachedStageStyle;
|
||||
private final ReadOnlyBooleanWrapper empty = new ReadOnlyBooleanWrapper(true);
|
||||
private ContextMenu newTabContextMenu;
|
||||
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link TearableTabPane} class.
|
||||
*/
|
||||
public TearableTabPane() {
|
||||
this(new TabPaneManager(), false, false, StageStyle.DECORATED, (Tab[]) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link TearableTabPane} class.
|
||||
*
|
||||
* @param manager the {@link TabPaneManager} manager instance
|
||||
* @param reorderable true if tabs can be reordered, false otherwise.
|
||||
* @param tearable true if tabs are teared away from the pane, false otherwise.
|
||||
* @param style the {@link StageStyle} for the detached tab window.
|
||||
* @param tabs tabs to attached to the TabPane.
|
||||
*/
|
||||
public TearableTabPane(TabPaneManager manager, boolean reorderable, boolean tearable, StageStyle style, Tab... tabs) {
|
||||
super(tabs);
|
||||
this.manager = manager;
|
||||
this.tearable = tearable;
|
||||
this.reorderable = reorderable;
|
||||
this.detachedStageStyle = new SimpleObjectProperty<>(style);
|
||||
bindingManager.attachListener(this.getSelectionModel().selectedItemProperty(), (ChangeListener<Tab>)
|
||||
(observable, oldValue, newValue) -> this.manager.setSelectedTab(newValue)
|
||||
);
|
||||
|
||||
|
||||
bindingManager.attachListener(this.getTabs(), (ListChangeListener<Tab>) c -> {
|
||||
while (c.next()) {
|
||||
if (c.wasAdded()) {
|
||||
for (Tab newTab : c.getAddedSubList()) {
|
||||
this.tearableTabMap.put(newTab, new TabState(true));
|
||||
this.manager.addTab(newTab, this);
|
||||
Node target = newTab.getGraphic();
|
||||
if (target.getOnDragEntered() == null) {
|
||||
target.setOnDragEntered(bindingManager.registerHandler(event -> {
|
||||
if (event.getDragboard().hasContent(manager.getDragAndDropFormat())) {
|
||||
findTabHeader(newTab).ifPresent(node -> node.pseudoClassStateChanged(HOVER_PSEUDO_CLASS, true));
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (target.getOnDragExited() == null) {
|
||||
target.setOnDragExited(bindingManager.registerHandler(event -> {
|
||||
if (event.getDragboard().hasContent(manager.getDragAndDropFormat())) {
|
||||
findTabHeader(newTab).ifPresent(node -> node.pseudoClassStateChanged(HOVER_PSEUDO_CLASS, false));
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (target.getOnDragDropped() == null) {
|
||||
target.setOnDragDropped(bindingManager.registerHandler(event -> {
|
||||
Dragboard db = event.getDragboard();
|
||||
if (db.hasContent(manager.getDragAndDropFormat())) {
|
||||
logger.trace(() -> "setOnDragDropped fired");
|
||||
if (manager.completeDragAndDrop()) {
|
||||
String id = (String) db.getContent(manager.getDragAndDropFormat());
|
||||
Tab draggedTab = manager.getTab(id);
|
||||
if (draggedTab != null) {
|
||||
TearableTabPane draggedTabPane = (TearableTabPane) manager.getTabPane(draggedTab);
|
||||
if (draggedTabPane.isReorderable()) {
|
||||
manager.setMovingTab(true);
|
||||
try {
|
||||
var draggedIndex = draggedTabPane.getTabs().indexOf(draggedTab);
|
||||
var droppedTabPane = manager.getTabPane(newTab);
|
||||
var dropIndex = droppedTabPane.getTabs().indexOf(newTab);
|
||||
if (!newTab.equals(draggedTab)) {
|
||||
draggedTabPane.getTabs().remove(draggedIndex);
|
||||
droppedTabPane.getTabs().add(dropIndex, draggedTab);
|
||||
droppedTabPane.getSelectionModel().clearAndSelect(dropIndex);
|
||||
}
|
||||
} finally {
|
||||
manager.setMovingTab(false);
|
||||
}
|
||||
} else {
|
||||
logger.debug(() -> "Tabs on this pane cannot be reordered");
|
||||
}
|
||||
} else {
|
||||
logger.debug(() -> "Failed to retrieve targetTab with id " + (id != null ? id : "null"));
|
||||
}
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (c.wasRemoved()) {
|
||||
for (Tab t : c.getRemoved()) {
|
||||
this.tearableTabMap.remove(t);
|
||||
this.manager.removeTab(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.trace(() -> "Tearable tabs in tab pane: " +
|
||||
tearableTabMap.keySet().stream()
|
||||
.map(tab -> tab.getText() == null ? tab.toString() : tab.getText())
|
||||
.reduce((s, s2) -> s + " " + s2).orElse("null"));
|
||||
});
|
||||
this.setOnDragDetected(bindingManager.registerHandler((MouseEvent event) -> {
|
||||
if (!this.tearable) {
|
||||
return;
|
||||
}
|
||||
if (event.getSource() instanceof TabPane) {
|
||||
Tab currentTab = this.getSelectionModel().getSelectedItem();
|
||||
if (currentTab != null) {
|
||||
Dragboard db = this.startDragAndDrop(TransferMode.MOVE);
|
||||
ClipboardContent clipboardContent = new ClipboardContent();
|
||||
clipboardContent.put(manager.getDragAndDropFormat(), manager.getId(currentTab));
|
||||
db.setDragView(NodeUtils.scaledSnapshot(currentTab.getContent()));
|
||||
db.setContent(clipboardContent);
|
||||
manager.startDragAndDrop();
|
||||
}
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
));
|
||||
this.setOnDragOver(bindingManager.registerHandler(event -> {
|
||||
if (!this.tearable) {
|
||||
return;
|
||||
}
|
||||
Dragboard db = event.getDragboard();
|
||||
if (db.hasContent(manager.getDragAndDropFormat())) {
|
||||
String id = (String) db.getContent(manager.getDragAndDropFormat());
|
||||
Tab t = manager.getTab(id);
|
||||
event.acceptTransferModes(TransferMode.MOVE);
|
||||
event.consume();
|
||||
}
|
||||
}));
|
||||
this.setOnDragDone(bindingManager.registerHandler(
|
||||
(DragEvent event) -> {
|
||||
if (!this.tearable || event.isDropCompleted()) {
|
||||
return;
|
||||
}
|
||||
Dragboard db = event.getDragboard();
|
||||
if (manager.completeDragAndDrop() && db.hasContent(manager.getDragAndDropFormat())) {
|
||||
String id = (String) db.getContent(manager.getDragAndDropFormat());
|
||||
logger.trace(() -> "setOnDragDone fired");
|
||||
Tab t = manager.getTab(id);
|
||||
manager.setMovingTab(true);
|
||||
try {
|
||||
tearOffTab(t);
|
||||
} finally {
|
||||
manager.setMovingTab(false);
|
||||
}
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
));
|
||||
this.setOnDragDropped(bindingManager.registerHandler(event -> {
|
||||
Dragboard db = event.getDragboard();
|
||||
if (db.hasContent(manager.getDragAndDropFormat())) {
|
||||
logger.trace(() -> "setOnDragDropped fired");
|
||||
if (manager.completeDragAndDrop()) {
|
||||
String id = (String) db.getContent(manager.getDragAndDropFormat());
|
||||
Tab t = manager.getTab(id);
|
||||
if (t != null) {
|
||||
TabPane p = manager.getTabPane(t);
|
||||
if (!this.equals(p)) {
|
||||
manager.setMovingTab(true);
|
||||
try {
|
||||
p.getTabs().remove(t);
|
||||
this.getTabs().add(t);
|
||||
this.getSelectionModel().select(t);
|
||||
bringStageToFront();
|
||||
} finally {
|
||||
manager.setMovingTab(false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug(() -> "Failed to retrieve tab with id " + (id != null ? id : "null"));
|
||||
}
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
}));
|
||||
Platform.runLater(() -> {
|
||||
positionNewTabButton();
|
||||
Stage stage = (Stage) this.getScene().getWindow();
|
||||
bindingManager.attachListener(stage.focusedProperty(),
|
||||
(ChangeListener<Boolean>) (observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
manager.setSelectedTab(this.getSelectionModel().getSelectedItem());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Prepare to change the button on screen position if the tearableTabMap side changes
|
||||
bindingManager.attachListener(sideProperty(),
|
||||
(observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
positionNewTabButton();
|
||||
}
|
||||
});
|
||||
|
||||
bindingManager.attachListener(this.getTabs(), ((InvalidationListener) observable -> {
|
||||
empty.setValue(this.getTabs().size() == 0);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
public void detachTab(Tab t) {
|
||||
Objects.requireNonNull(t, "Tab to detach cannot be null");
|
||||
logger.trace(() -> "Detaching tab " + t.getId() + " " + t.getText());
|
||||
manager.setMovingTab(true);
|
||||
try {
|
||||
tearOffTab(t);
|
||||
} finally {
|
||||
manager.setMovingTab(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the factory for creating new tabs
|
||||
*
|
||||
* @return the factory for creating new tabs
|
||||
*/
|
||||
public Function<ActionEvent, Optional<Tab>> getNewTabFactory() {
|
||||
return newTabFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the factory for creating new tabs
|
||||
*
|
||||
* @param newTabFactory the factory for creating new tabs
|
||||
*/
|
||||
public void setNewTabFactory(Function<ActionEvent, Optional<Tab>> newTabFactory) {
|
||||
this.newTabFactory = newTabFactory;
|
||||
}
|
||||
|
||||
public void setNewTabContextMenu(ContextMenu menu) {
|
||||
this.newTabContextMenu = menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the action that should be fired on the addition of a tab to the pane.
|
||||
*
|
||||
* @param onAddNewTab the actions that should be fired on the addition of a tab to the pane.
|
||||
*/
|
||||
public void setOnAddNewTab(EventHandler<ActionEvent> onAddNewTab) {
|
||||
this.onAddNewTab = onAddNewTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if tabs can be teared away from the pane, false otherwise.
|
||||
*
|
||||
* @return true true if tabs can be teared away from the pane, false otherwise.
|
||||
*/
|
||||
public boolean isTearable() {
|
||||
return tearable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true if tabs can be teared away from the pane, false otherwise.
|
||||
*
|
||||
* @param tearable true if tabs can be teared away from the pane, false otherwise.
|
||||
*/
|
||||
public void setTearable(boolean tearable) {
|
||||
this.tearable = tearable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if tabs can be reordered, false otherwise.
|
||||
*
|
||||
* @return true if tabs can be reordered, false otherwise.
|
||||
*/
|
||||
public boolean isReorderable() {
|
||||
return reorderable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true if tabs can be reordered, false otherwise.
|
||||
*
|
||||
* @param reorderable true if tabs can be reordered, false otherwise.
|
||||
*/
|
||||
public void setReorderable(boolean reorderable) {
|
||||
this.reorderable = reorderable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tab currently selected.
|
||||
*
|
||||
* @return the tab currently selected.
|
||||
*/
|
||||
public Tab getSelectedTab() {
|
||||
return manager.getSelectedTab();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pane containing the tab currently selected.
|
||||
*
|
||||
* @return the pane containing the tab currently selected.
|
||||
*/
|
||||
public TabPane getSelectedTabPane() {
|
||||
if (manager.getSelectedTab() == null || manager.tabToPaneMap.get(manager.getSelectedTab()) == null) {
|
||||
return this;
|
||||
}
|
||||
return manager.tabToPaneMap.get(getSelectedTab());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all tabs, across of panes sharing the same {@link TabPaneManager}
|
||||
*
|
||||
* @return a list of all tabs, across of panes sharing the same {@link TabPaneManager}
|
||||
*/
|
||||
public ObservableList<Tab> getGlobalTabs() {
|
||||
return manager.getGlobalTabList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the list of tabs.
|
||||
*/
|
||||
public void clearAllTabs() {
|
||||
manager.clearAllTabs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the action to be fired on opening a new window to host tabs.
|
||||
*
|
||||
* @param action the action to be fired on opening a new window to host tabs.
|
||||
*/
|
||||
public void setOnOpenNewWindow(EventHandler<WindowEvent> action) {
|
||||
this.onOpenNewWindow = action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the action to be fired on closing a window hosting tabs.
|
||||
*
|
||||
* @param action the action to be fired on closing a window hosting tabs.
|
||||
*/
|
||||
public void setOnClosingWindow(EventHandler<WindowEvent> action) {
|
||||
this.onClosingWindow = action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the generated {@link DataFormat} used to identify drag and drop operations across panes sharing the same {@link TabPaneManager}
|
||||
*
|
||||
* @return the generated {@link DataFormat} used to identify drag and drop operations across panes sharing the same {@link TabPaneManager}
|
||||
*/
|
||||
public DataFormat getDataFormat() {
|
||||
return manager.dragAndDropFormat;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return empty.get();
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty emptyProperty() {
|
||||
return empty.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private void positionNewTabButton() {
|
||||
Pane tabHeaderBg = (Pane) this.lookup(".tab-header-background");
|
||||
if (tabHeaderBg == null) {
|
||||
// TabPane is not ready
|
||||
return;
|
||||
}
|
||||
Pane tabHeaderArea = (Pane) this.lookup(".tab-header-area");
|
||||
logger.debug("tabHeaderArea.getHeight() = " + tabHeaderArea.getHeight());
|
||||
Button newTabButton = (Button) tabHeaderBg.lookup("#newTabButton");
|
||||
|
||||
// Remove the button if it was already present
|
||||
if (newTabButton != null) {
|
||||
tabHeaderBg.getChildren().remove(newTabButton);
|
||||
}
|
||||
newTabButton = new Button();
|
||||
newTabButton.visibleProperty().bind(manager.newTabButtonVisible);
|
||||
newTabButton.setId("newTabButton");
|
||||
newTabButton.setFocusTraversable(false);
|
||||
Pane headersRegion = (Pane) this.lookup(".headers-region");
|
||||
Region headerArea = (Region) this.lookup(".tab-header-area");
|
||||
|
||||
logger.debug("headersRegion.getHeight() = " + headersRegion.getHeight());
|
||||
logger.debug("headersRegion.getPrefHeight = " + headersRegion.getPrefHeight());
|
||||
newTabButton.getStyleClass().add("add-tab-button");
|
||||
SVGPath icon = new SVGPath();
|
||||
icon.setContent("m 31.25,54.09375 0,2.4375 -2.46875,0 0,0.375 2.46875,0 0,2.46875 0.375,0 0,-2.46875 2.46875,0 0,-0.375 -2.46875,0 0,-2.4375 -0.375,0 z");
|
||||
icon.getStyleClass().add("add-tab-button-icon");
|
||||
newTabButton.setGraphic(icon);
|
||||
newTabButton.setAlignment(Pos.CENTER);
|
||||
if (newTabContextMenu != null) {
|
||||
newTabButton.setContextMenu(newTabContextMenu);
|
||||
}
|
||||
if (onAddNewTab != null) {
|
||||
newTabButton.setOnAction(bindingManager.registerHandler(onAddNewTab));
|
||||
} else {
|
||||
newTabButton.setOnAction(bindingManager.registerHandler(event -> {
|
||||
newTabFactory.apply(event).ifPresent(newTab -> {
|
||||
getTabs().add(newTab);
|
||||
this.getSelectionModel().select(newTab);
|
||||
});
|
||||
}));
|
||||
}
|
||||
tabHeaderBg.getChildren().add(newTabButton);
|
||||
StackPane.setAlignment(newTabButton, Pos.CENTER_LEFT);
|
||||
switch (getSide()) {
|
||||
case TOP, BOTTOM -> newTabButton.translateXProperty().bind(
|
||||
headersRegion.widthProperty()
|
||||
.add(Bindings.createDoubleBinding(() -> headerArea.getInsets().getLeft(), headerArea.insetsProperty()))
|
||||
);
|
||||
case LEFT, RIGHT -> newTabButton.translateXProperty().bind(
|
||||
tabHeaderBg.widthProperty()
|
||||
.subtract(headersRegion.widthProperty())
|
||||
.subtract(newTabButton.widthProperty())
|
||||
.subtract(Bindings.createDoubleBinding(() -> headerArea.getInsets().getTop(), headerArea.insetsProperty()))
|
||||
);
|
||||
default -> throw new IllegalStateException("Invalid value for side enum");
|
||||
}
|
||||
}
|
||||
|
||||
private void bringStageToFront() {
|
||||
if (this.getScene() != null) {
|
||||
Stage stage = (Stage) this.getScene().getWindow();
|
||||
if (stage != null) {
|
||||
stage.toFront();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Node> findTabHeader(Tab t) {
|
||||
Styleable styleable = t.getGraphic();
|
||||
while (styleable != null && !styleable.getStyleClass().contains("tab")) {
|
||||
styleable = styleable.getStyleableParent();
|
||||
}
|
||||
if (styleable instanceof Node node) {
|
||||
return Optional.of(node);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void tearOffTab(Tab tab) {
|
||||
TearableTabPane detachedTabPane = new TearableTabPane(this.manager, isReorderable(), true, this.getDetachedStageStyle());
|
||||
detachedTabPane.setId("tearableTabPane");
|
||||
detachedTabPane.setOnOpenNewWindow(this.onOpenNewWindow);
|
||||
detachedTabPane.setNewTabFactory(this.getNewTabFactory());
|
||||
this.getTabs().remove(tab);
|
||||
detachedTabPane.getTabs().add(tab);
|
||||
Pane root = new AnchorPane(detachedTabPane);
|
||||
AnchorPane.setBottomAnchor(detachedTabPane, 0.0);
|
||||
AnchorPane.setLeftAnchor(detachedTabPane, 0.0);
|
||||
AnchorPane.setRightAnchor(detachedTabPane, 0.0);
|
||||
AnchorPane.setTopAnchor(detachedTabPane, 0.0);
|
||||
final Scene scene = new Scene(root, root.getPrefWidth(), root.getPrefHeight());
|
||||
Stage stage = new Stage();
|
||||
stage.setScene(scene);
|
||||
Point p = MouseInfo.getPointerInfo().getLocation();
|
||||
stage.setX(p.getX());
|
||||
stage.setY(p.getY());
|
||||
detachedTabPane.getTabs().addListener((ListChangeListener<Tab>) c -> {
|
||||
if (c.getList().size() == 0) {
|
||||
if (onClosingWindow != null) {
|
||||
onClosingWindow.handle(new WindowEvent(stage, WindowEvent.WINDOW_CLOSE_REQUEST));
|
||||
}
|
||||
stage.close();
|
||||
detachedTabPane.close();
|
||||
}
|
||||
});
|
||||
if (onOpenNewWindow != null) {
|
||||
onOpenNewWindow.handle(new WindowEvent(stage, WindowEvent.WINDOW_SHOWING));
|
||||
}
|
||||
stage.initStyle(this.getDetachedStageStyle());
|
||||
stage.show();
|
||||
detachedTabPane.getSelectionModel().select(tab);
|
||||
stage.setOnCloseRequest(bindingManager.registerHandler(event -> {
|
||||
detachedTabPane.getTabs().removeAll(detachedTabPane.getTabs());
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
getTabs().forEach(tab -> tab.setContextMenu(null));
|
||||
logger.trace(() -> "Closing down TearableTabPane instance");
|
||||
bindingManager.close();
|
||||
}
|
||||
|
||||
public StageStyle getDetachedStageStyle() {
|
||||
return detachedStageStyle.getValue();
|
||||
}
|
||||
|
||||
public Property<StageStyle> detachedStageStyleProperty() {
|
||||
return detachedStageStyle;
|
||||
}
|
||||
|
||||
public void setDetachedStageStyle(StageStyle detachedStageStyle) {
|
||||
this.detachedStageStyle.setValue(detachedStageStyle);
|
||||
}
|
||||
|
||||
public boolean isNewTabButtonVisible() {
|
||||
return manager.newTabButtonVisible.get();
|
||||
}
|
||||
|
||||
public BooleanProperty newTabButtonVisibleProperty() {
|
||||
return manager.newTabButtonVisible;
|
||||
}
|
||||
|
||||
public void setNewTabButtonVisible(boolean newTabButtonVisible) {
|
||||
manager.newTabButtonVisible.set(newTabButtonVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the state of a single tab
|
||||
*/
|
||||
private record TabState(boolean attached) {
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that represents the state of the tabs across all TabPane windows
|
||||
*/
|
||||
protected static class TabPaneManager {
|
||||
private final ObservableMap<Tab, TabPane> tabToPaneMap;
|
||||
private final Map<String, Tab> idToTabMap;
|
||||
private final ObservableList<Tab> globalTabList;
|
||||
private final DataFormat dragAndDropFormat;
|
||||
private final AtomicBoolean dndComplete;
|
||||
private Tab selectedTab;
|
||||
private boolean movingTab;
|
||||
private final BooleanProperty newTabButtonVisible = new SimpleBooleanProperty(true);
|
||||
|
||||
|
||||
public TabPaneManager() {
|
||||
tabToPaneMap = FXCollections.observableMap(new HashMap<>());
|
||||
idToTabMap = new HashMap<>();
|
||||
globalTabList = FXCollections.observableList(new ArrayList<>());
|
||||
dragAndDropFormat = new DataFormat(UUID.randomUUID().toString());
|
||||
dndComplete = new AtomicBoolean(true);
|
||||
|
||||
}
|
||||
|
||||
public void startDragAndDrop() {
|
||||
dndComplete.set(false);
|
||||
}
|
||||
|
||||
public boolean completeDragAndDrop() {
|
||||
return dndComplete.compareAndSet(false, true);
|
||||
}
|
||||
|
||||
public void addTab(Tab tab, TabPane pane) {
|
||||
idToTabMap.put(getId(tab), tab);
|
||||
tabToPaneMap.put(tab, pane);
|
||||
if (!movingTab) {
|
||||
globalTabList.add(tab);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeTab(Tab tab) {
|
||||
logger.trace(() -> "Removing tab " + tab.getText() + " (movingTab=" + movingTab + ")");
|
||||
idToTabMap.remove(getId(tab));
|
||||
tabToPaneMap.remove(tab);
|
||||
if (!movingTab) {
|
||||
globalTabList.remove(tab);
|
||||
}
|
||||
}
|
||||
|
||||
public TabPane getTabPane(Tab tab) {
|
||||
return tabToPaneMap.get(tab);
|
||||
}
|
||||
|
||||
public String getId(Tab tab) {
|
||||
return Integer.toString(tab.hashCode());
|
||||
}
|
||||
|
||||
public Tab getTab(String id) {
|
||||
return idToTabMap.get(id);
|
||||
}
|
||||
|
||||
public DataFormat getDragAndDropFormat() {
|
||||
return dragAndDropFormat;
|
||||
}
|
||||
|
||||
private ObservableList<Tab> getGlobalTabList() {
|
||||
return globalTabList;
|
||||
}
|
||||
|
||||
public Tab getSelectedTab() {
|
||||
return selectedTab;
|
||||
}
|
||||
|
||||
public void setSelectedTab(Tab selectedTab) {
|
||||
this.selectedTab = selectedTab;
|
||||
logger.trace(() -> "Selected Tab: " + ((selectedTab == null) ? "null" : selectedTab + " " + getId(selectedTab) + " " + tabToPaneMap.get(selectedTab)));
|
||||
}
|
||||
|
||||
public void setMovingTab(boolean movingTab) {
|
||||
this.movingTab = movingTab;
|
||||
}
|
||||
|
||||
public void clearAllTabs() {
|
||||
tabToPaneMap.values()
|
||||
.stream()
|
||||
.distinct()
|
||||
.toList()
|
||||
.forEach(p -> p.getTabs().clear());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2021-2022 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import eu.binjr.common.colors.ColorUtils;
|
||||
import eu.binjr.common.javafx.bindings.BindingManager;
|
||||
import eu.binjr.core.dialogs.Dialogs;
|
||||
import eu.binjr.core.preferences.UserPreferences;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.TextField;
|
||||
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class TextFieldValidator {
|
||||
|
||||
public static void succeed(TextField textField) {
|
||||
textField.setStyle("");
|
||||
}
|
||||
|
||||
public static void fail(TextField textField,
|
||||
boolean autoReset,
|
||||
ObservableValue<?>... resetProperties) {
|
||||
fail(textField, null, autoReset, resetProperties);
|
||||
}
|
||||
|
||||
public static void fail(TextField textField, String reason,
|
||||
boolean autoReset,
|
||||
ObservableValue<?>... resetProperties) {
|
||||
textField.setStyle(String.format("-fx-background-color: %s;",
|
||||
ColorUtils.toHex(UserPreferences.getInstance().invalidInputColor.get())));
|
||||
if (reason != null) {
|
||||
Dialogs.notifyError("Invalid input", reason, Pos.BOTTOM_RIGHT, textField);
|
||||
}
|
||||
if (autoReset) {
|
||||
BindingManager manager = new BindingManager();
|
||||
InvalidationListener autoResetListener = obs -> {
|
||||
succeed(textField);
|
||||
manager.close();
|
||||
};
|
||||
manager.attachListener(textField.textProperty(), autoResetListener);
|
||||
if (resetProperties != null) {
|
||||
for (ObservableValue<?> p : resetProperties) {
|
||||
manager.attachListener(p, autoResetListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean validate(TextField textField,
|
||||
boolean autoReset,
|
||||
Predicate<String> validator,
|
||||
ObservableValue<?>... resetProperties) {
|
||||
if (validator.test(textField.getText())) {
|
||||
succeed(textField);
|
||||
return true;
|
||||
} else {
|
||||
fail(textField, autoReset, resetProperties);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean validate(TextField textField,
|
||||
boolean autoReset,
|
||||
Function<String, ValidationStatus> validator,
|
||||
ObservableValue<?>... resetProperties) {
|
||||
var status = validator.apply(textField.getText());
|
||||
if (status.isValid()) {
|
||||
succeed(textField);
|
||||
return true;
|
||||
} else {
|
||||
fail(textField, status.reason, autoReset, resetProperties);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public record ValidationStatus(boolean isValid, String reason) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2019 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import jakarta.xml.bind.annotation.XmlAccessType;
|
||||
import jakarta.xml.bind.annotation.XmlAccessorType;
|
||||
import javafx.scene.input.DataFormat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Objects;
|
||||
|
||||
@JsonAdapter(TimeRange.Adapter.class)
|
||||
@XmlAccessorType(XmlAccessType.PROPERTY)
|
||||
public class TimeRange {
|
||||
public static final DataFormat TIME_RANGE_DATA_FORMAT = new DataFormat(TimeRange.class.getCanonicalName());
|
||||
private static final String DELIMITER = " ";
|
||||
private final ZonedDateTime beginning;
|
||||
private final ZonedDateTime end;
|
||||
private final ZoneId zoneId;
|
||||
|
||||
private TimeRange(ZonedDateTime beginning, ZonedDateTime end) {
|
||||
Objects.requireNonNull(beginning, "Parameter 'beginning' must not be null");
|
||||
Objects.requireNonNull(end, "Parameter 'end' must not be null");
|
||||
this.zoneId = beginning.getZone();
|
||||
this.beginning = beginning;
|
||||
this.end = end.withZoneSameInstant(zoneId);
|
||||
}
|
||||
|
||||
public static TimeRange of(TimeRange range) {
|
||||
return new TimeRange(range.getBeginning(), range.getEnd());
|
||||
}
|
||||
|
||||
public static TimeRange of(ZonedDateTime beginning, ZonedDateTime end) {
|
||||
return new TimeRange(beginning, end);
|
||||
}
|
||||
|
||||
public static TimeRange last24Hours() {
|
||||
var end = ZonedDateTime.now();
|
||||
return new TimeRange(end.minusHours(24), end);
|
||||
}
|
||||
|
||||
public static TimeRange deSerialize(String valueStr) {
|
||||
String[] s = valueStr.split(DELIMITER);
|
||||
if (s.length != 2) {
|
||||
throw new IllegalArgumentException("Could not parse provided string as a TimeRange");
|
||||
}
|
||||
return TimeRange.of(DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(s[0], ZonedDateTime::from), DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(s[1], ZonedDateTime::from));
|
||||
}
|
||||
|
||||
public ZonedDateTime getBeginning() {
|
||||
return beginning;
|
||||
}
|
||||
|
||||
public ZonedDateTime getEnd() {
|
||||
return end;
|
||||
}
|
||||
|
||||
public Duration getDuration() {
|
||||
return Duration.between(beginning, end);
|
||||
}
|
||||
|
||||
public ZoneId getZoneId() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
public boolean isNegative() {
|
||||
return getDuration().isNegative();
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
return DateTimeFormatter.ISO_ZONED_DATE_TIME.format(beginning) + DELIMITER + DateTimeFormatter.ISO_ZONED_DATE_TIME.format(end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return beginning.hashCode() + end.hashCode() + zoneId.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
var other = (TimeRange) obj;
|
||||
return beginning.equals(other.beginning) &&
|
||||
end.equals(other.end) &&
|
||||
zoneId.equals(other.zoneId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuffer sb = new StringBuffer("TimeRange{");
|
||||
sb.append("beginning=").append(beginning);
|
||||
sb.append(", end=").append(end);
|
||||
sb.append(", zoneId=").append(zoneId);
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static class Adapter extends TypeAdapter<TimeRange> {
|
||||
@Override
|
||||
public void write(final JsonWriter jsonWriter, final TimeRange value) throws IOException {
|
||||
jsonWriter.value(value.serialize());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeRange read(final JsonReader jsonReader) throws IOException {
|
||||
String value = jsonReader.nextString();
|
||||
return TimeRange.deSerialize(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
/*
|
||||
* Copyright 2017-2020 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import eu.binjr.common.javafx.bindings.BindingManager;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import eu.binjr.core.dialogs.Dialogs;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.control.textfield.AutoCompletionBinding;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.time.*;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TimeRangePicker extends HBox {
|
||||
private static final Logger logger = Logger.create(TimeRangePicker.class);
|
||||
private final ToggleButton timeRangeLabel;
|
||||
private final ButtonBase previousIntervalBtn;
|
||||
private final ButtonBase nextIntervalBtn;
|
||||
private final ButtonBase resetIntervalBtn;
|
||||
|
||||
|
||||
private TimeRangePickerController timeRangePickerController;
|
||||
private final PopupControl popup;
|
||||
private final Property<ZoneId> zoneId;
|
||||
private final Property<TimeRange> timeRange = new SimpleObjectProperty<>(TimeRange.of(ZonedDateTime.now(), ZonedDateTime.now()));
|
||||
private final ObjectProperty<TimeRange> selectedRange = new SimpleObjectProperty<>(TimeRange.of(ZonedDateTime.now().minusHours(1), ZonedDateTime.now()));
|
||||
private final BindingManager bindingManager = new BindingManager();
|
||||
private final ObjectProperty<Supplier<TimeRange>> onResetInterval = new SimpleObjectProperty<>();
|
||||
|
||||
public void setReferenceEndDateSupplier(Supplier<ZonedDateTime> referenceEndDateSupplier) {
|
||||
this.referenceEndDateSupplier = referenceEndDateSupplier;
|
||||
}
|
||||
|
||||
private Supplier<ZonedDateTime> referenceEndDateSupplier;
|
||||
|
||||
public TimeRangePicker() throws IOException {
|
||||
super();
|
||||
this.previousIntervalBtn = new ToolButtonBuilder<>()
|
||||
.setStyleClass("dialog-button")
|
||||
.setHeight(USE_COMPUTED_SIZE)
|
||||
.setWidth(20.0)
|
||||
.setIconStyleClass("left-arrow-icon")
|
||||
.setAction(event -> {
|
||||
timeRangePickerController.stepBy(
|
||||
Duration.between(timeRangePickerController.endDate.getDateTimeValue(),
|
||||
timeRangePickerController.startDate.getDateTimeValue()));
|
||||
event.consume();
|
||||
})
|
||||
.setTooltip("Step Back")
|
||||
.build(Button::new);
|
||||
previousIntervalBtn.setMaxHeight(Double.MAX_VALUE);
|
||||
this.nextIntervalBtn = new ToolButtonBuilder<>()
|
||||
.setHeight(USE_COMPUTED_SIZE)
|
||||
.setWidth(20)
|
||||
.setStyleClass("dialog-button")
|
||||
.setIconStyleClass("right-arrow-icon")
|
||||
.setAction(event -> {
|
||||
timeRangePickerController.stepBy(
|
||||
Duration.between(timeRangePickerController.startDate.getDateTimeValue(),
|
||||
timeRangePickerController.endDate.getDateTimeValue()));
|
||||
event.consume();
|
||||
})
|
||||
.setTooltip("Step Forward")
|
||||
.build(Button::new);
|
||||
nextIntervalBtn.setMaxHeight(Double.MAX_VALUE);
|
||||
this.resetIntervalBtn = new ToolButtonBuilder<>()
|
||||
.setHeight(USE_COMPUTED_SIZE)
|
||||
.setWidth(40.0)
|
||||
.setStyleClass("dialog-button")
|
||||
.setAction(event -> {
|
||||
if (getOnResetInterval() != null) {
|
||||
var range = getOnResetInterval().get();
|
||||
timeRangePickerController.applyNewTimeRange.accept(range.getBeginning(), range.getEnd());
|
||||
}
|
||||
event.consume();
|
||||
})
|
||||
.setIconStyleClass("reset-clock-icon", "medium-icon")
|
||||
.setTooltip("Reset Time Range")
|
||||
.build(Button::new);
|
||||
resetIntervalBtn.setMaxHeight(Double.MAX_VALUE);
|
||||
timeRangeLabel = new ToggleButton();
|
||||
var tooltip = new Tooltip("Edit the time range");
|
||||
tooltip.setShowDelay(javafx.util.Duration.millis(500));
|
||||
timeRangeLabel.setTooltip(tooltip);
|
||||
timeRangeLabel.setMaxHeight(Double.MAX_VALUE);
|
||||
this.setAlignment(Pos.CENTER);
|
||||
this.getChildren().addAll(
|
||||
previousIntervalBtn,
|
||||
timeRangeLabel,
|
||||
resetIntervalBtn,
|
||||
nextIntervalBtn);
|
||||
|
||||
timeRangeLabel.setGraphic(ToolButtonBuilder.makeIconNode(Pos.CENTER, "combo-box-arrow-icon", "small-icon"));
|
||||
timeRangeLabel.setContentDisplay(ContentDisplay.RIGHT);
|
||||
timeRangeLabel.setGraphicTextGap(8);
|
||||
timeRangeLabel.setPadding(new Insets(0, 10, 0, 10));
|
||||
FXMLLoader loader = new FXMLLoader(getClass().getResource("/eu/binjr/views/TimeRangePickerView.fxml"));
|
||||
this.timeRangePickerController = new TimeRangePickerController();
|
||||
loader.setController(timeRangePickerController);
|
||||
Pane timeRangePickerPane = loader.load();
|
||||
popup = new PopupControl();
|
||||
popup.setAutoHide(true);
|
||||
popup.getScene().setRoot(timeRangePickerPane);
|
||||
popup.showingProperty().addListener((observable, oldValue, newValue) -> {
|
||||
timeRangeLabel.setSelected(newValue);
|
||||
});
|
||||
|
||||
this.timeRangeLabel.setOnAction(actionEvent -> {
|
||||
Node owner = (Node) actionEvent.getSource();
|
||||
Bounds bounds = owner.localToScreen(owner.getBoundsInLocal());
|
||||
this.popup.show(owner.getScene().getWindow(), bounds.getMinX(), bounds.getMaxY());
|
||||
});
|
||||
|
||||
timeRangePickerController.applyNewTimeRange = (beginning, end) -> {
|
||||
this.selectedRange.setValue(TimeRange.of(beginning, end));
|
||||
};
|
||||
|
||||
timeRangePickerController.startDate.setDateTimeValue(selectedRange.getValue().getBeginning());
|
||||
timeRangePickerController.endDate.setDateTimeValue(selectedRange.getValue().getEnd());
|
||||
zoneId = timeRangePickerController.endDate.zoneIdProperty();
|
||||
bindingManager.bind(timeRangePickerController.startDate.zoneIdProperty(), zoneId);
|
||||
bindingManager.bindBidirectional(timeRangePickerController.zoneIdProperty(), zoneId);
|
||||
bindingManager.attachListener(zoneId, (observable, oldValue, newValue) -> updateText());
|
||||
bindingManager.attachListener(selectedRange, (ChangeListener<TimeRange>) (observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
bindingManager.suspend();
|
||||
try {
|
||||
zoneId.setValue(newValue.getZoneId());
|
||||
timeRangePickerController.startDate.setDateTimeValue(newValue.getBeginning());
|
||||
timeRangePickerController.endDate.setDateTimeValue(newValue.getEnd());
|
||||
} finally {
|
||||
bindingManager.resume();
|
||||
updateText();
|
||||
}
|
||||
}
|
||||
});
|
||||
bindingManager.attachListener(timeRangePickerController.startDate.dateTimeValueProperty(),
|
||||
(ChangeListener<ZonedDateTime>) (observable, oldValue, newValue) -> {
|
||||
if (newValue != null && timeRangePickerController.endDate.getDateTimeValue() != null) {
|
||||
TimeRange newRange = TimeRange.of(newValue, timeRangePickerController.endDate.getDateTimeValue());
|
||||
if (newRange.isNegative()) {
|
||||
TimeRange oldRange = TimeRange.of(oldValue, timeRangePickerController.endDate.getDateTimeValue());
|
||||
this.selectedRange.setValue(TimeRange.of(newValue, newValue.plus(oldRange.getDuration())));
|
||||
} else {
|
||||
this.selectedRange.setValue(newRange);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bindingManager.attachListener(timeRangePickerController.endDate.dateTimeValueProperty(),
|
||||
(ChangeListener<ZonedDateTime>) (observable, oldValue, newValue) -> {
|
||||
if (newValue != null && timeRangePickerController.startDate.getDateTimeValue() != null) {
|
||||
TimeRange newRange = TimeRange.of(timeRangePickerController.startDate.getDateTimeValue(), newValue);
|
||||
if (newRange.isNegative()) {
|
||||
TimeRange oldRange = TimeRange.of(timeRangePickerController.startDate.getDateTimeValue(), oldValue);
|
||||
this.selectedRange.setValue(TimeRange.of(newValue.minus(oldRange.getDuration()), newValue));
|
||||
} else {
|
||||
this.selectedRange.setValue(newRange);
|
||||
}
|
||||
}
|
||||
});
|
||||
referenceEndDateSupplier = () -> ZonedDateTime.now(zoneId.getValue());
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return timeRangeLabel.getText();
|
||||
}
|
||||
|
||||
public StringProperty textProperty() {
|
||||
return timeRangeLabel.textProperty();
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
timeRangeLabel.setText(text);
|
||||
}
|
||||
|
||||
public ZoneId getZoneId() {
|
||||
return zoneId.getValue();
|
||||
}
|
||||
|
||||
public Property<ZoneId> zoneIdProperty() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
bindingManager.close();
|
||||
}
|
||||
|
||||
private void updateText() {
|
||||
Dialogs.runOnFXThread(() -> {
|
||||
this.timeRange.setValue(TimeRange.of(timeRangePickerController.startDate.getDateTimeValue(), timeRangePickerController.endDate.getDateTimeValue()));
|
||||
timeRangeLabel.setText(String.format("From %s to %s (%s)",
|
||||
timeRangePickerController.startDate.getDateTimeValue().format(timeRangePickerController.startDate.getFormatter()),
|
||||
timeRangePickerController.endDate.getDateTimeValue().format(timeRangePickerController.endDate.getFormatter()),
|
||||
getZoneId().toString()));
|
||||
});
|
||||
}
|
||||
|
||||
public Supplier<TimeRange> getOnResetInterval() {
|
||||
return onResetInterval.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Supplier<TimeRange>> onResetIntervalProperty() {
|
||||
return onResetInterval;
|
||||
}
|
||||
|
||||
public void setOnResetInterval(Supplier<TimeRange> onResetInterval) {
|
||||
this.onResetInterval.set(onResetInterval);
|
||||
}
|
||||
|
||||
public TimeRange getSelectedRange() {
|
||||
return selectedRange.getValue();
|
||||
}
|
||||
|
||||
public Property<TimeRange> selectedRangeProperty() {
|
||||
return selectedRange;
|
||||
}
|
||||
|
||||
public void initSelectedRange(TimeRange selectedRange) {
|
||||
this.selectedRange.setValue(selectedRange);
|
||||
}
|
||||
|
||||
public void updateSelectedRange(TimeRange newValue) {
|
||||
bindingManager.suspend();
|
||||
try {
|
||||
logger.trace(() -> "updateRangeBeginning -> " + newValue.toString());
|
||||
timeRangePickerController.startDate.setDateTimeValue(newValue.getBeginning());
|
||||
timeRangePickerController.endDate.setDateTimeValue(newValue.getEnd());
|
||||
this.selectedRange.setValue(TimeRange.of(newValue.getBeginning(), newValue.getEnd()));
|
||||
zoneId.setValue(newValue.getZoneId());
|
||||
updateText();
|
||||
} finally {
|
||||
bindingManager.resume();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public TimeRange getTimeRange() {
|
||||
return timeRange.getValue();
|
||||
}
|
||||
|
||||
public Property<TimeRange> timeRangeProperty() {
|
||||
return timeRange;
|
||||
}
|
||||
|
||||
private void onZoneIdChanged(ObservableValue<? extends ZoneId> observable, ZoneId oldValue, ZoneId newValue) {
|
||||
updateText();
|
||||
}
|
||||
|
||||
public void setOnSelectedRangeChanged(ChangeListener<TimeRange> timeRangeChangeListener) {
|
||||
bindingManager.attachListener(selectedRange, timeRangeChangeListener);
|
||||
}
|
||||
|
||||
|
||||
public Boolean isTimeRangeLinked() {
|
||||
return timeRangePickerController.linkTimeRangeButton.isSelected();
|
||||
}
|
||||
|
||||
public Property<Boolean> timeRangeLinkedProperty() {
|
||||
return timeRangePickerController.linkTimeRangeButton.selectedProperty();
|
||||
}
|
||||
|
||||
public void setTimeRangeLinked(Boolean timeRangeLinked) {
|
||||
timeRangePickerController.linkTimeRangeButton.setSelected(timeRangeLinked);
|
||||
}
|
||||
|
||||
|
||||
private class TimeRangePickerController {
|
||||
|
||||
@FXML
|
||||
private ResourceBundle resources;
|
||||
|
||||
@FXML
|
||||
private URL location;
|
||||
|
||||
@FXML
|
||||
private AnchorPane root;
|
||||
|
||||
@FXML
|
||||
private Button previousIntervalBtn;
|
||||
|
||||
@FXML
|
||||
private ZonedDateTimePicker startDate;
|
||||
|
||||
@FXML
|
||||
private ZonedDateTimePicker endDate;
|
||||
|
||||
@FXML
|
||||
private Button nextIntervalBtn;
|
||||
|
||||
@FXML
|
||||
private ComboBox<String> timezoneField;
|
||||
|
||||
@FXML
|
||||
private Button last6Hours;
|
||||
@FXML
|
||||
private Button last3Hours;
|
||||
@FXML
|
||||
private Button last24Hours;
|
||||
@FXML
|
||||
private Button last7Days;
|
||||
@FXML
|
||||
private Button last15Days;
|
||||
@FXML
|
||||
private Button last30Days;
|
||||
@FXML
|
||||
private Button last90Days;
|
||||
@FXML
|
||||
private Button last15Minutes;
|
||||
@FXML
|
||||
private Button last30Minutes;
|
||||
@FXML
|
||||
private Button last60Minutes;
|
||||
@FXML
|
||||
private Button last12Hours;
|
||||
@FXML
|
||||
private Button last90Minutes;
|
||||
@FXML
|
||||
private Button today;
|
||||
@FXML
|
||||
private Button yesterday;
|
||||
@FXML
|
||||
private Button thisWeek;
|
||||
@FXML
|
||||
private Button lastWeek;
|
||||
@FXML
|
||||
private Button minus1Hour;
|
||||
@FXML
|
||||
private Button plus1Hour;
|
||||
@FXML
|
||||
private Button minus24Hours;
|
||||
@FXML
|
||||
private Button plus24Hours;
|
||||
|
||||
@FXML
|
||||
private Button pasteTimeRangeButton;
|
||||
@FXML
|
||||
private Button copyTimeRangeButton;
|
||||
@FXML
|
||||
private ToggleButton linkTimeRangeButton;
|
||||
|
||||
private TextFormatter<ZoneId> formatter;
|
||||
private AutoCompletionBinding<String> autoCompletionBinding;
|
||||
|
||||
private BiConsumer<ZonedDateTime, ZonedDateTime> applyNewTimeRange = (start, end) -> {
|
||||
startDate.dateTimeValueProperty().setValue(start);
|
||||
endDate.dateTimeValueProperty().setValue(end);
|
||||
};
|
||||
|
||||
private void stepBy(Duration intervalDuration) {
|
||||
applyNewTimeRange.accept(startDate.getDateTimeValue().plus(intervalDuration), endDate.getDateTimeValue().plus(intervalDuration));
|
||||
}
|
||||
|
||||
private void last(Duration duration) {
|
||||
ZonedDateTime end = referenceEndDateSupplier.get();
|
||||
applyNewTimeRange.accept(end.minus(duration), end);
|
||||
// hide popup
|
||||
popup.hide();
|
||||
}
|
||||
|
||||
private void forward(Duration duration) {
|
||||
shift(duration, ZonedDateTime::plus);
|
||||
}
|
||||
|
||||
private void backward(Duration duration) {
|
||||
shift(duration, ZonedDateTime::minus);
|
||||
}
|
||||
|
||||
private void shift(Duration duration, BiFunction<ZonedDateTime, Duration, ZonedDateTime> move) {
|
||||
var ref = TimeRange.of(selectedRange.get());
|
||||
applyNewTimeRange.accept(
|
||||
move.apply(ref.getBeginning(), duration),
|
||||
move.apply(ref.getEnd(), duration));
|
||||
// hide popup
|
||||
popup.hide();
|
||||
}
|
||||
|
||||
private void updateAutoCompletionBinding() {
|
||||
if (autoCompletionBinding != null) {
|
||||
autoCompletionBinding.dispose();
|
||||
}
|
||||
autoCompletionBinding = TextFields.bindAutoCompletion(timezoneField.getEditor(),
|
||||
ZoneId.getAvailableZoneIds().stream().sorted().collect(Collectors.toList()));
|
||||
autoCompletionBinding.setPrefWidth(200);
|
||||
}
|
||||
|
||||
@FXML
|
||||
void initialize() {
|
||||
formatter = new TextFormatter<ZoneId>(new StringConverter<ZoneId>() {
|
||||
@Override
|
||||
public String toString(ZoneId object) {
|
||||
if (object == null) {
|
||||
return "null";
|
||||
}
|
||||
return object.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZoneId fromString(String string) {
|
||||
return ZoneId.of(string);
|
||||
}
|
||||
});
|
||||
updateAutoCompletionBinding();
|
||||
timezoneField.getEditor().setTextFormatter(formatter);
|
||||
timezoneField.setItems(FXCollections.observableArrayList(ZoneId.getAvailableZoneIds().stream().sorted().collect(Collectors.toList())));
|
||||
timezoneField.showingProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
autoCompletionBinding.dispose();
|
||||
} else {
|
||||
updateAutoCompletionBinding();
|
||||
}
|
||||
});
|
||||
nextIntervalBtn.setOnAction(event -> {
|
||||
stepBy(Duration.between(startDate.getDateTimeValue(), endDate.getDateTimeValue()));
|
||||
});
|
||||
previousIntervalBtn.setOnAction(event -> {
|
||||
stepBy(Duration.between(endDate.getDateTimeValue(), startDate.getDateTimeValue()));
|
||||
});
|
||||
last3Hours.setOnAction(event -> last(Duration.of(3, ChronoUnit.HOURS)));
|
||||
last6Hours.setOnAction(event -> last(Duration.of(6, ChronoUnit.HOURS)));
|
||||
last12Hours.setOnAction(event -> last(Duration.of(12, ChronoUnit.HOURS)));
|
||||
last24Hours.setOnAction(event -> last(Duration.of(24, ChronoUnit.HOURS)));
|
||||
last7Days.setOnAction(event -> last(Duration.of(7, ChronoUnit.DAYS)));
|
||||
last15Days.setOnAction(event -> last(Duration.of(15, ChronoUnit.DAYS)));
|
||||
last30Days.setOnAction(event -> last(Duration.of(30, ChronoUnit.DAYS)));
|
||||
last90Days.setOnAction(event -> last(Duration.of(90, ChronoUnit.DAYS)));
|
||||
last15Minutes.setOnAction(event -> last(Duration.of(15, ChronoUnit.MINUTES)));
|
||||
last30Minutes.setOnAction(event -> last(Duration.of(30, ChronoUnit.MINUTES)));
|
||||
last60Minutes.setOnAction(event -> last(Duration.of(60, ChronoUnit.MINUTES)));
|
||||
last90Minutes.setOnAction(event -> last(Duration.of(90, ChronoUnit.MINUTES)));
|
||||
today.setOnAction(event -> {
|
||||
LocalDate today = LocalDate.now(zoneId.getValue());
|
||||
applyNewTimeRange.accept(
|
||||
ZonedDateTime.of(today, LocalTime.MIDNIGHT, zoneId.getValue()),
|
||||
ZonedDateTime.of(today.plusDays(1), LocalTime.MIDNIGHT, zoneId.getValue()));
|
||||
// hide popup
|
||||
popup.hide();
|
||||
});
|
||||
yesterday.setOnAction(event -> {
|
||||
LocalDate today = LocalDate.now(zoneId.getValue());
|
||||
applyNewTimeRange.accept(
|
||||
ZonedDateTime.of(today.minusDays(1), LocalTime.MIDNIGHT, zoneId.getValue()),
|
||||
ZonedDateTime.of(today, LocalTime.MIDNIGHT, zoneId.getValue()));
|
||||
// hide popup
|
||||
popup.hide();
|
||||
});
|
||||
thisWeek.setOnAction(event -> {
|
||||
LocalDate refDay = LocalDate.now(zoneId.getValue());
|
||||
int n = refDay.getDayOfWeek().getValue();
|
||||
applyNewTimeRange.accept(
|
||||
ZonedDateTime.of(refDay.minusDays(n - 1), LocalTime.MIDNIGHT, zoneId.getValue()),
|
||||
ZonedDateTime.of(refDay.plusDays(8 - n), LocalTime.MIDNIGHT, zoneId.getValue()));
|
||||
// hide popup
|
||||
popup.hide();
|
||||
});
|
||||
lastWeek.setOnAction(event -> {
|
||||
LocalDate refDay = LocalDate.now(zoneId.getValue()).minusWeeks(1);
|
||||
int n = refDay.getDayOfWeek().getValue();
|
||||
applyNewTimeRange.accept(
|
||||
ZonedDateTime.of(refDay.minusDays(n - 1), LocalTime.MIDNIGHT, zoneId.getValue()),
|
||||
ZonedDateTime.of(refDay.plusDays(8 - n), LocalTime.MIDNIGHT, zoneId.getValue()));
|
||||
// hide popup
|
||||
popup.hide();
|
||||
});
|
||||
minus1Hour.setOnAction(event -> backward(Duration.ofHours(1)));
|
||||
plus1Hour.setOnAction(event -> forward(Duration.ofHours(1)));
|
||||
minus24Hours.setOnAction(event -> backward(Duration.ofHours(24)));
|
||||
plus24Hours.setOnAction(event -> forward(Duration.ofHours(24)));
|
||||
copyTimeRangeButton.setOnAction(event -> {
|
||||
try {
|
||||
final ClipboardContent content = new ClipboardContent();
|
||||
content.put(TimeRange.TIME_RANGE_DATA_FORMAT, timeRange.getValue().serialize());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to copy time range to clipboard", e);
|
||||
}
|
||||
});
|
||||
pasteTimeRangeButton.setOnAction(event -> {
|
||||
try {
|
||||
if (Clipboard.getSystemClipboard().hasContent(TimeRange.TIME_RANGE_DATA_FORMAT)) {
|
||||
String content = (String) Clipboard.getSystemClipboard().getContent(TimeRange.TIME_RANGE_DATA_FORMAT);
|
||||
TimeRange range = TimeRange.deSerialize(content);
|
||||
zoneId.setValue(range.getBeginning().getZone());
|
||||
selectedRange.setValue(range);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to paste range output from clipboard", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Property<ZoneId> zoneIdProperty() {
|
||||
return formatter.valueProperty();
|
||||
}
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2019-2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import eu.binjr.common.colors.ColorUtils;
|
||||
import eu.binjr.common.javafx.bindings.BindingManager;
|
||||
import eu.binjr.core.preferences.UserPreferences;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ToolButtonBuilder<T extends ButtonBase> {
|
||||
|
||||
private final BindingManager bindingManager;
|
||||
private double height = 20.0;
|
||||
private double width = 20.0;
|
||||
private String text = "";
|
||||
private String tooltip = null;
|
||||
private List<String> styleClass = new ArrayList<>();
|
||||
private List<String> iconStyleClass = new ArrayList<>();
|
||||
private EventHandler<ActionEvent> action = null;
|
||||
private final List<Consumer<T>> bindings = new ArrayList<>();
|
||||
private boolean focusTraversable = true;
|
||||
private ContentDisplay contentDisplay = ContentDisplay.GRAPHIC_ONLY;
|
||||
private String iconColorString = null;
|
||||
|
||||
public ToolButtonBuilder() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public ToolButtonBuilder(BindingManager bindingManager) {
|
||||
this.bindingManager = bindingManager;
|
||||
}
|
||||
|
||||
private T buildButton(T btn) {
|
||||
btn.setText(text);
|
||||
if (tooltip != null) {
|
||||
btn.setTooltip(makeTooltip(tooltip));
|
||||
}
|
||||
btn.setPrefHeight(height);
|
||||
btn.setMaxHeight(height);
|
||||
btn.setMinHeight(height);
|
||||
btn.setPrefWidth(width);
|
||||
btn.setMaxWidth(width);
|
||||
btn.setMinWidth(width);
|
||||
btn.setFocusTraversable(focusTraversable);
|
||||
if (styleClass != null) {
|
||||
btn.getStyleClass().addAll(styleClass);
|
||||
}
|
||||
if (iconStyleClass != null && !iconStyleClass.isEmpty()) {
|
||||
Region icon = new Region();
|
||||
icon.getStyleClass().addAll(iconStyleClass);
|
||||
if (iconColorString != null) {
|
||||
icon.setStyle("-fx-background-color:" + iconColorString + ";");
|
||||
}
|
||||
btn.setGraphic(icon);
|
||||
btn.setAlignment(Pos.CENTER);
|
||||
btn.setContentDisplay(contentDisplay);
|
||||
} else {
|
||||
btn.setContentDisplay(ContentDisplay.TEXT_ONLY);
|
||||
}
|
||||
|
||||
if (action != null) {
|
||||
btn.setOnAction(bindingManager != null ? bindingManager.registerHandler(action) : action);
|
||||
}
|
||||
bindings.forEach(buttonBaseConsumer -> buttonBaseConsumer.accept(btn));
|
||||
return btn;
|
||||
}
|
||||
|
||||
public ToolButtonBuilder<T> setHeight(double height) {
|
||||
this.height = height;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolButtonBuilder<T> setWidth(double width) {
|
||||
this.width = width;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolButtonBuilder<T> setFocusTraversable(boolean focusTraversable) {
|
||||
this.focusTraversable = focusTraversable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolButtonBuilder<T> setText(String text) {
|
||||
this.text = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolButtonBuilder<T> setTooltip(String tooltip) {
|
||||
this.tooltip = tooltip;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolButtonBuilder<T> setStyleClass(String... styleClass) {
|
||||
this.styleClass = new ArrayList<>(Arrays.asList(styleClass));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolButtonBuilder<T> setIconStyleClass(String... iconStyleClass) {
|
||||
this.iconStyleClass = new ArrayList<>(Arrays.asList(iconStyleClass));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolButtonBuilder<T> setIconColor(Color color) {
|
||||
this.iconColorString = ColorUtils.toHex(color);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolButtonBuilder<T> setContentDisplay(ContentDisplay contentDisplay) {
|
||||
this.contentDisplay = contentDisplay;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolButtonBuilder<T> setAction(EventHandler<ActionEvent> action) {
|
||||
this.action = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
public <R> ToolButtonBuilder<T> bindBidirectionnal(Function<T, Property<R>> mapper, Property<R> property) {
|
||||
bindings.add(t -> {
|
||||
if (bindingManager != null) {
|
||||
bindingManager.bindBidirectional(mapper.apply(t), property);
|
||||
} else {
|
||||
mapper.apply(t).bindBidirectional(property);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public <R, U extends R> ToolButtonBuilder<T> bind(Function<T, Property<R>> mapper, ObservableValue<U> observableValue) {
|
||||
bindings.add(buttonBase -> {
|
||||
if (bindingManager != null) {
|
||||
bindingManager.bind(mapper.apply(buttonBase), observableValue);
|
||||
} else {
|
||||
mapper.apply(buttonBase).bind(observableValue);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public T build(Supplier<T> generator) {
|
||||
return buildButton(generator.get());
|
||||
}
|
||||
|
||||
public static Node makeIconNode(Pos position, String... iconStyles) {
|
||||
return makeIconNode(position, 0, 0, iconStyles);
|
||||
}
|
||||
|
||||
public static Node makeIconNode(Pos position, double width, double height, String... iconStyles) {
|
||||
Region r = new Region();
|
||||
r.setMinSize(width, height);
|
||||
r.getStyleClass().addAll(iconStyles);
|
||||
HBox box = new HBox(r);
|
||||
box.setAlignment(position);
|
||||
box.getStyleClass().add("icon-container");
|
||||
return box;
|
||||
}
|
||||
|
||||
private Tooltip makeTooltip(String text) {
|
||||
var tooltip = new Tooltip(text);
|
||||
var delayProp = UserPreferences.getInstance().tooltipShowDelayMs.property();
|
||||
tooltip.showDelayProperty().bind(Bindings.createObjectBinding(() -> Duration.millis(delayProp.getValue().doubleValue()), delayProp));
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.TreeItem;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Helper methods to walk JavaFX TreeViews
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class TreeViewUtils {
|
||||
|
||||
public enum ExpandDirection {
|
||||
UP,
|
||||
DOWN,
|
||||
BOTH
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first tree item that matches the provided predicate.
|
||||
*
|
||||
* @param currentTreeItem the node in the tree where to start searching.
|
||||
* @param predicate the predicate onto witch the search is based.
|
||||
* @param <T> the type for the tree item
|
||||
* @return an {@link Optional} encapsulating the {@link TreeItem} instance matching the predicate.
|
||||
*/
|
||||
public static <T> Optional<TreeItem<T>> findFirstInTree(TreeItem<T> currentTreeItem, Predicate<TreeItem<T>> predicate) {
|
||||
if (predicate.test(currentTreeItem)) {
|
||||
return Optional.of(currentTreeItem);
|
||||
}
|
||||
if (!currentTreeItem.isLeaf()) {
|
||||
for (TreeItem<T> item : currentTreeItem.getChildren()) {
|
||||
Optional<TreeItem<T>> res = findFirstInTree(item, predicate);
|
||||
if (res.isPresent()) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all tree items that match the provided predicate.
|
||||
*
|
||||
* @param currentTreeItem the node in the tree where to start searching.
|
||||
* @param predicate the predicate onto witch the search is based.
|
||||
* @param <T> the type for the tree item
|
||||
* @return a list of {@link Optional} encapsulating the {@link TreeItem} instances matching the predicate.
|
||||
*/
|
||||
public static <T> List<TreeItem<T>> findAllInTree(TreeItem<T> currentTreeItem, Predicate<TreeItem<T>> predicate) {
|
||||
return findAllInTree(currentTreeItem, predicate, new ArrayList<>());
|
||||
}
|
||||
|
||||
private static <T> List<TreeItem<T>> findAllInTree(TreeItem<T> currentTreeItem, Predicate<TreeItem<T>> predicate, List<TreeItem<T>> found) {
|
||||
if (predicate.test(currentTreeItem)) {
|
||||
found.add(currentTreeItem);
|
||||
}
|
||||
if (!currentTreeItem.isLeaf()) {
|
||||
for (TreeItem<T> item : currentTreeItem.getChildren()) {
|
||||
findAllInTree(item, predicate, found);
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
public static <T> List<T> flattenLeaves(TreeItem<T> branch) {
|
||||
return flattenLeaves(branch, false);
|
||||
}
|
||||
|
||||
public static <T> List<T> flattenLeaves(TreeItem<T> branch, boolean expand) {
|
||||
List<T> leaves = new ArrayList<>();
|
||||
flattenLeaves(branch, leaves, expand);
|
||||
return leaves;
|
||||
}
|
||||
|
||||
private static <T> void flattenLeaves(TreeItem<T> branch, List<T> leaves, boolean expand) {
|
||||
if (expand) {
|
||||
branch.setExpanded(true);
|
||||
}
|
||||
if (!branch.isLeaf()) {
|
||||
for (TreeItem<T> t : branch.getChildren()) {
|
||||
flattenLeaves(t, leaves, expand);
|
||||
}
|
||||
} else {
|
||||
leaves.add(branch.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> List<TreeItem<T>> splitAboveLeaves(TreeItem<T> branch) {
|
||||
return splitAboveLeaves(branch, false);
|
||||
}
|
||||
|
||||
public static <T> List<TreeItem<T>> splitAboveLeaves(TreeItem<T> branch, boolean expand) {
|
||||
List<TreeItem<T>> level1Items = new ArrayList<>();
|
||||
if (branch.isLeaf()) {
|
||||
level1Items.add(branch);
|
||||
} else {
|
||||
splitAboveLeaves(branch, level1Items, expand);
|
||||
}
|
||||
return level1Items;
|
||||
}
|
||||
|
||||
//FIXME: The following implementation assumes that a leaf node cannot have a sibling which isn't also a leaf.
|
||||
private static <T> void splitAboveLeaves(TreeItem<T> branch, List<TreeItem<T>> level1Items, boolean expand) {
|
||||
if (expand) {
|
||||
branch.setExpanded(true);
|
||||
}
|
||||
if (!branch.isLeaf() && branch.getChildren().get(0).isLeaf()) {
|
||||
level1Items.add(branch);
|
||||
} else {
|
||||
branch.setExpanded(true);
|
||||
for (TreeItem<T> t : branch.getChildren()) {
|
||||
splitAboveLeaves(t, level1Items, expand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> void expandBranch(TreeItem<T> branch, ExpandDirection direction) {
|
||||
if (EnumSet.of(ExpandDirection.BOTH, ExpandDirection.UP).contains(direction)) {
|
||||
climbUpFromBranch(branch, true);
|
||||
}
|
||||
if (EnumSet.of(ExpandDirection.BOTH, ExpandDirection.DOWN).contains(direction)) {
|
||||
climbDownFromBranch(branch, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> void collapseBranch(TreeItem<T> branch, ExpandDirection direction) {
|
||||
if (EnumSet.of(ExpandDirection.BOTH, ExpandDirection.UP).contains(direction)) {
|
||||
climbUpFromBranch(branch, false);
|
||||
}
|
||||
if (EnumSet.of(ExpandDirection.BOTH, ExpandDirection.DOWN).contains(direction)) {
|
||||
climbDownFromBranch(branch, false);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> void sortFromBranch(TreeItem<T> branch) {
|
||||
if (!branch.isLeaf()){
|
||||
branch.getChildren().forEach(TreeViewUtils::sortFromBranch);
|
||||
}
|
||||
FXCollections.sort(branch.getChildren(),
|
||||
(o1, o2) -> {
|
||||
Objects.requireNonNull(o1);
|
||||
Objects.requireNonNull(o2);
|
||||
if (o1.isLeaf() && !o2.isLeaf()) {
|
||||
return 1;
|
||||
}
|
||||
if (!o1.isLeaf() && o2.isLeaf()) {
|
||||
return -1;
|
||||
}
|
||||
return o1.getValue().toString().compareToIgnoreCase(o2.getValue().toString());
|
||||
});
|
||||
}
|
||||
|
||||
private static <T> void climbUpFromBranch(TreeItem<T> branch, boolean expanded) {
|
||||
if (branch == null) {
|
||||
return;
|
||||
}
|
||||
branch.setExpanded(expanded);
|
||||
climbUpFromBranch(branch.getParent(), expanded);
|
||||
}
|
||||
|
||||
private static <T> void climbDownFromBranch(TreeItem<T> branch, boolean expanded) {
|
||||
if (branch == null) {
|
||||
return;
|
||||
}
|
||||
branch.setExpanded(expanded);
|
||||
if (branch.getChildren() != null) {
|
||||
for (TreeItem<?> item : branch.getChildren()) {
|
||||
climbDownFromBranch(item, expanded);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2016-2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.controls;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.scene.control.DatePicker;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
|
||||
/**
|
||||
* A date and time picker control that works with {@link java.time.ZonedDateTime}
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class ZonedDateTimePicker extends DatePicker {
|
||||
private static final Logger logger = Logger.create(ZonedDateTimePicker.class);
|
||||
private final Property<ZoneId> zoneId;
|
||||
private final ObjectProperty<ZonedDateTime> dateTimeValue;//
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link ZonedDateTimePicker} class with the system's default timezone
|
||||
*/
|
||||
public ZonedDateTimePicker() {
|
||||
this(ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
public ZonedDateTimePicker(ZoneId zoneId) {
|
||||
this(new SimpleObjectProperty<>(zoneId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link ZonedDateTimePicker} class with the provided timezone
|
||||
*
|
||||
* @param zoneIdProperty the timezone id to use in the control
|
||||
*/
|
||||
public ZonedDateTimePicker(SimpleObjectProperty<ZoneId> zoneIdProperty) {
|
||||
this.zoneId = zoneIdProperty;
|
||||
dateTimeValue = new SimpleObjectProperty<>(ZonedDateTime.now(zoneIdProperty.get()));
|
||||
getStyleClass().add("datetime-picker");
|
||||
setConverter(new StringConverter<LocalDate>() {
|
||||
public String toString(LocalDate object) {
|
||||
ZonedDateTime value = getDateTimeValue();
|
||||
return (value != null) ? value.format(getFormatter()) : "";
|
||||
}
|
||||
|
||||
public LocalDate fromString(String stringValue) {
|
||||
if (stringValue == null || stringValue.isEmpty()) {
|
||||
dateTimeValue.set(null);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
dateTimeValue.set(ZonedDateTime.parse(stringValue, getFormatter()));
|
||||
logger.trace(() -> "zonedId for dateTimeValue=" + dateTimeValue.get().getZone());
|
||||
} catch (Exception ex) {
|
||||
logger.debug("Error parsing date", ex);
|
||||
throw ex;
|
||||
}
|
||||
return dateTimeValue.get().toLocalDate();
|
||||
}
|
||||
});
|
||||
|
||||
ChangeListener<LocalDate> localDateChangeListener = (observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
if (dateTimeValue.get() == null) {
|
||||
dateTimeValue.set(ZonedDateTime.of(newValue, LocalTime.now(), getZoneId()));
|
||||
} else {
|
||||
LocalTime time = dateTimeValue.get().toLocalTime();
|
||||
dateTimeValue.set(ZonedDateTime.of(newValue, time, getZoneId()));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
valueProperty().addListener(localDateChangeListener);
|
||||
|
||||
ChangeListener<ZoneId> zoneIdChangeListener = (observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
dateTimeValue.setValue(dateTimeValue.get().withZoneSameInstant(newValue));
|
||||
}
|
||||
};
|
||||
|
||||
dateTimeValue.addListener((observable, oldValue, newValue) -> {
|
||||
valueProperty().removeListener(localDateChangeListener);
|
||||
setValue(null);
|
||||
setValue(newValue == null ? null : newValue.toLocalDate());
|
||||
valueProperty().addListener(localDateChangeListener);
|
||||
});
|
||||
|
||||
getEditor().focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (!newValue) {
|
||||
KeyEvent ke = new KeyEvent(KeyEvent.KEY_RELEASED,
|
||||
KeyCode.ENTER.toString(), KeyCode.ENTER.toString(),
|
||||
KeyCode.ENTER, false, false, false, false);
|
||||
getEditor().getParent().fireEvent(ke);
|
||||
}
|
||||
});
|
||||
|
||||
this.zoneId.addListener(zoneIdChangeListener);
|
||||
}
|
||||
|
||||
DateTimeFormatter getFormatter() {
|
||||
return DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.MEDIUM).withZone(getZoneId());
|
||||
}
|
||||
|
||||
//region [Properties]
|
||||
|
||||
/**
|
||||
* Gets the selected date/time value of the control.
|
||||
*
|
||||
* @return the selected date/time value of the control.
|
||||
*/
|
||||
public ZonedDateTime getDateTimeValue() {
|
||||
return dateTimeValue.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selected date/time value of the control.
|
||||
*
|
||||
* @param dateTimeValue the selected date/time value of the control.
|
||||
*/
|
||||
public void setDateTimeValue(ZonedDateTime dateTimeValue) {
|
||||
this.dateTimeValue.set(dateTimeValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value property for the selected date/time
|
||||
*
|
||||
* @return the value property for the selected date/time
|
||||
*/
|
||||
public ObjectProperty<ZonedDateTime> dateTimeValueProperty() {
|
||||
return dateTimeValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ZoneId} of the date picker
|
||||
*
|
||||
* @return the {@link ZoneId} of the date picker
|
||||
*/
|
||||
public ZoneId getZoneId() {
|
||||
return zoneId.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* The zoneId property
|
||||
*
|
||||
* @return the zoneId property
|
||||
*/
|
||||
public Property<ZoneId> zoneIdProperty() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ZoneId} of the date picker
|
||||
*
|
||||
* @param zoneId the {@link ZoneId} of the date picker
|
||||
*/
|
||||
public void setZoneId(ZoneId zoneId) {
|
||||
this.zoneId.setValue(zoneId);
|
||||
}
|
||||
|
||||
//endregion
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright 2020-2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.richtext;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import org.fxmisc.richtext.model.StyleSpans;
|
||||
import org.fxmisc.richtext.model.StyleSpansBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
public class CodeAreaHighlighter {
|
||||
private static final Logger logger = Logger.create(CodeAreaHighlighter.class);
|
||||
private static final Pattern XML_TAG = Pattern.compile(
|
||||
"(?<ELEMENT>(</?\\??\\h*)([\\w:\\-_]+)([^<>]*)(\\h*/?\\??>))" +
|
||||
"|(?<COMMENT><!--[^<>]+-->)");
|
||||
|
||||
private static final Pattern LOGS_SEVERITY = Pattern.compile(
|
||||
"(?i)\\[TRACE|DEBUG|PERF|NOTE|INFO|WARN|ERROR|FATAL\\s?\\]");
|
||||
private static final Pattern ATTRIBUTES = Pattern.compile("([\\w:]+\\h*)(=)(\\h*\"[^\"]*\")");
|
||||
|
||||
private static final int GROUP_OPEN_BRACKET = 2;
|
||||
private static final int GROUP_ELEMENT_NAME = 3;
|
||||
private static final int GROUP_ATTRIBUTES_SECTION = 4;
|
||||
private static final int GROUP_CLOSE_BRACKET = 5;
|
||||
private static final int GROUP_ATTRIBUTE_NAME = 1;
|
||||
private static final int GROUP_EQUAL_SYMBOL = 2;
|
||||
private static final int GROUP_ATTRIBUTE_VALUE = 3;
|
||||
public static final String SEARCH_RESULT_HIGHLIGHT = "search-result-highlight";
|
||||
|
||||
public static class SearchHitRange {
|
||||
private final int start;
|
||||
private final int end;
|
||||
|
||||
private SearchHitRange(int start, int end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public int getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
public int getEnd() {
|
||||
return end;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SearchHighlightResults {
|
||||
private final List<SearchHitRange> searchHitRanges;
|
||||
private final StyleSpans<Collection<String>> styleSpans;
|
||||
|
||||
private SearchHighlightResults(List<SearchHitRange> searchHitRanges, StyleSpans<Collection<String>> styleSpans) {
|
||||
this.searchHitRanges = searchHitRanges;
|
||||
this.styleSpans = styleSpans;
|
||||
}
|
||||
|
||||
public List<SearchHitRange> getSearchHitRanges() {
|
||||
return searchHitRanges;
|
||||
}
|
||||
|
||||
public StyleSpans<Collection<String>> getStyleSpans() {
|
||||
return styleSpans;
|
||||
}
|
||||
}
|
||||
|
||||
public static SearchHighlightResults computeSearchHitsHighlighting(String text,
|
||||
String searchText,
|
||||
boolean matchCase,
|
||||
boolean regEx) throws HighlightPatternException {
|
||||
StringBuilder searchExpression = new StringBuilder();
|
||||
if (!regEx) {
|
||||
searchText.codePoints().forEachOrdered(value -> {
|
||||
var c = Character.toString(value);
|
||||
String regExSpecialChars = "\\^$.|?*+()[]{}";
|
||||
if (regExSpecialChars.contains(c)) {
|
||||
searchExpression.append("\\");
|
||||
}
|
||||
searchExpression.append(c);
|
||||
});
|
||||
} else {
|
||||
searchExpression.append(searchText);
|
||||
}
|
||||
if (!matchCase) {
|
||||
searchExpression.insert(0, "(?i)");
|
||||
}
|
||||
List<SearchHitRange> hits = new ArrayList<>();
|
||||
int lastKwEnd = 0;
|
||||
StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();
|
||||
try {
|
||||
var searchPattern = Pattern.compile(searchExpression.toString());
|
||||
if (searchText != null && !searchText.isEmpty()) {
|
||||
Matcher searchMatcher = searchPattern.matcher(text);
|
||||
while (searchMatcher.find()) {
|
||||
spansBuilder.add(Collections.emptyList(), searchMatcher.start() - lastKwEnd);
|
||||
spansBuilder.add(Collections.singleton(SEARCH_RESULT_HIGHLIGHT), searchMatcher.end() - searchMatcher.start());
|
||||
hits.add(new SearchHitRange(searchMatcher.start(), searchMatcher.end()));
|
||||
lastKwEnd = searchMatcher.end();
|
||||
}
|
||||
}
|
||||
} catch (PatternSyntaxException e) {
|
||||
throw new HighlightPatternException(e);
|
||||
}
|
||||
spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
|
||||
return new SearchHighlightResults(hits, spansBuilder.create());
|
||||
}
|
||||
|
||||
public static StyleSpans<Collection<String>> computeLogsSyntaxHighlighting(String text) {
|
||||
Matcher matcher = LOGS_SEVERITY.matcher(text);
|
||||
int lastKwEnd = 0;
|
||||
StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();
|
||||
while (matcher.find()) {
|
||||
spansBuilder.add(Collections.emptyList(), matcher.start() - lastKwEnd);
|
||||
spansBuilder.add(Collections.singleton("comment"), matcher.end() - matcher.start());
|
||||
lastKwEnd = matcher.end();
|
||||
}
|
||||
spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
|
||||
return spansBuilder.create();
|
||||
}
|
||||
|
||||
public static StyleSpans<Collection<String>> computeXmlSyntaxHighlighting(String text) {
|
||||
Matcher matcher = XML_TAG.matcher(text);
|
||||
int lastKwEnd = 0;
|
||||
StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();
|
||||
while (matcher.find()) {
|
||||
spansBuilder.add(Collections.emptyList(), matcher.start() - lastKwEnd);
|
||||
if (matcher.group("COMMENT") != null) {
|
||||
spansBuilder.add(Collections.singleton("comment"), matcher.end() - matcher.start());
|
||||
} else {
|
||||
if (matcher.group("ELEMENT") != null) {
|
||||
String attributesText = matcher.group(GROUP_ATTRIBUTES_SECTION);
|
||||
spansBuilder.add(Collections.singleton("tagmark"), matcher.end(GROUP_OPEN_BRACKET) - matcher.start(GROUP_OPEN_BRACKET));
|
||||
spansBuilder.add(Collections.singleton("anytag"), matcher.end(GROUP_ELEMENT_NAME) - matcher.end(GROUP_OPEN_BRACKET));
|
||||
if (!attributesText.isEmpty()) {
|
||||
lastKwEnd = 0;
|
||||
Matcher amatcher = ATTRIBUTES.matcher(attributesText);
|
||||
while (amatcher.find()) {
|
||||
spansBuilder.add(Collections.emptyList(), amatcher.start() - lastKwEnd);
|
||||
spansBuilder.add(Collections.singleton("attribute"), amatcher.end(GROUP_ATTRIBUTE_NAME) - amatcher.start(GROUP_ATTRIBUTE_NAME));
|
||||
spansBuilder.add(Collections.singleton("tagmark"), amatcher.end(GROUP_EQUAL_SYMBOL) - amatcher.end(GROUP_ATTRIBUTE_NAME));
|
||||
spansBuilder.add(Collections.singleton("avalue"), amatcher.end(GROUP_ATTRIBUTE_VALUE) - amatcher.end(GROUP_EQUAL_SYMBOL));
|
||||
lastKwEnd = amatcher.end();
|
||||
}
|
||||
if (attributesText.length() > lastKwEnd)
|
||||
spansBuilder.add(Collections.emptyList(), attributesText.length() - lastKwEnd);
|
||||
}
|
||||
lastKwEnd = matcher.end(GROUP_ATTRIBUTES_SECTION);
|
||||
spansBuilder.add(Collections.singleton("tagmark"), matcher.end(GROUP_CLOSE_BRACKET) - lastKwEnd);
|
||||
}
|
||||
}
|
||||
lastKwEnd = matcher.end();
|
||||
}
|
||||
spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
|
||||
return spansBuilder.create();
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.javafx.richtext;
|
||||
|
||||
public class HighlightPatternException extends Exception {
|
||||
public HighlightPatternException() {
|
||||
super("Invalid Pattern for highlight expression");
|
||||
}
|
||||
|
||||
public HighlightPatternException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public HighlightPatternException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public HighlightPatternException(Throwable cause) {
|
||||
super("Invalid Pattern for highlight expression", cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2022 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.json.adapters;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
public class LocaleJsonAdapter extends TypeAdapter<Locale> {
|
||||
@Override
|
||||
public void write(JsonWriter jsonWriter, Locale locale) throws IOException {
|
||||
jsonWriter.value(locale.toLanguageTag());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Locale read(JsonReader jsonReader) throws IOException {
|
||||
return Locale.forLanguageTag(jsonReader.nextString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2022 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.json.adapters;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class PatternJsonAdapter extends TypeAdapter<Pattern> {
|
||||
@Override
|
||||
public void write(JsonWriter out, Pattern value) throws IOException {
|
||||
out.value(value.pattern());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pattern read(final JsonReader jsonReader) throws IOException {
|
||||
var pattern = jsonReader.nextString();
|
||||
return Pattern.compile(pattern);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2019 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.logging;
|
||||
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public enum Log4j2Level {
|
||||
OFF(Level.OFF),
|
||||
FATAL(Level.FATAL),
|
||||
ERROR(Level.ERROR),
|
||||
WARN(Level.WARN),
|
||||
INFO(Level.INFO),
|
||||
DEBUG(Level.DEBUG),
|
||||
TRACE(Level.TRACE),
|
||||
ALL(Level.ALL);
|
||||
|
||||
private final Level level;
|
||||
|
||||
Log4j2Level(Level level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public Level getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public static Log4j2Level valueOf(String value, Log4j2Level defaultValue) {
|
||||
if (value != null &&
|
||||
Arrays.stream(Log4j2Level.values()).map(Enum::toString).anyMatch(s -> s.equalsIgnoreCase(value))) {
|
||||
return Log4j2Level.valueOf(value);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static Log4j2Level valueOf(Level value){
|
||||
return valueOf(value, Log4j2Level.INFO);
|
||||
}
|
||||
|
||||
public static Log4j2Level valueOf(Level value, Log4j2Level defaultValue) {
|
||||
return Arrays.stream(Log4j2Level.values())
|
||||
.filter(l -> l.getLevel().equals(value))
|
||||
.findFirst()
|
||||
.orElse(defaultValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,811 @@
|
||||
package eu.binjr.common.logging;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Marker;
|
||||
import org.apache.logging.log4j.message.Message;
|
||||
import org.apache.logging.log4j.message.MessageFactory;
|
||||
import org.apache.logging.log4j.spi.AbstractLogger;
|
||||
import org.apache.logging.log4j.spi.ExtendedLoggerWrapper;
|
||||
import org.apache.logging.log4j.util.MessageSupplier;
|
||||
import org.apache.logging.log4j.util.Supplier;
|
||||
|
||||
/**
|
||||
* Extended Logger interface with convenience methods for
|
||||
* the PROFILE custom log level.
|
||||
* <p>Compatible with Log4j 2.6 or higher.</p>
|
||||
*/
|
||||
public final class Logger extends ExtendedLoggerWrapper {
|
||||
private static final long serialVersionUID = 1888347051960200L;
|
||||
private final ExtendedLoggerWrapper logger;
|
||||
|
||||
private static final String FQCN = Logger.class.getName();
|
||||
public static final Level PERF = Level.forName("PERF", 450);
|
||||
|
||||
private Logger(final org.apache.logging.log4j.Logger logger) {
|
||||
super((AbstractLogger) logger, logger.getName(), logger.getMessageFactory());
|
||||
this.logger = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a custom Logger with the name of the calling class.
|
||||
*
|
||||
* @return The custom Logger for the calling class.
|
||||
*/
|
||||
public static Logger create() {
|
||||
final org.apache.logging.log4j.Logger wrapped = LogManager.getLogger();
|
||||
return new Logger(wrapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a custom Logger using the fully qualified name of the Class as
|
||||
* the Logger name.
|
||||
*
|
||||
* @param loggerName The Class whose name should be used as the Logger name.
|
||||
* If null it will default to the calling class.
|
||||
* @return The custom Logger.
|
||||
*/
|
||||
public static Logger create(final Class<?> loggerName) {
|
||||
final org.apache.logging.log4j.Logger wrapped = LogManager.getLogger(loggerName);
|
||||
return new Logger(wrapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a custom Logger using the fully qualified name of the Class as
|
||||
* the Logger name.
|
||||
*
|
||||
* @param loggerName The Class whose name should be used as the Logger name.
|
||||
* If null it will default to the calling class.
|
||||
* @param messageFactory The message factory is used only when creating a
|
||||
* logger, subsequent use does not change the logger but will log
|
||||
* a warning if mismatched.
|
||||
* @return The custom Logger.
|
||||
*/
|
||||
public static Logger create(final Class<?> loggerName, final MessageFactory messageFactory) {
|
||||
final org.apache.logging.log4j.Logger wrapped = LogManager.getLogger(loggerName, messageFactory);
|
||||
return new Logger(wrapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a custom Logger using the fully qualified class name of the value
|
||||
* as the Logger name.
|
||||
*
|
||||
* @param value The value whose class name should be used as the Logger
|
||||
* name. If null the name of the calling class will be used as
|
||||
* the logger name.
|
||||
* @return The custom Logger.
|
||||
*/
|
||||
public static Logger create(final Object value) {
|
||||
final org.apache.logging.log4j.Logger wrapped = LogManager.getLogger(value);
|
||||
return new Logger(wrapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a custom Logger using the fully qualified class name of the value
|
||||
* as the Logger name.
|
||||
*
|
||||
* @param value The value whose class name should be used as the Logger
|
||||
* name. If null the name of the calling class will be used as
|
||||
* the logger name.
|
||||
* @param messageFactory The message factory is used only when creating a
|
||||
* logger, subsequent use does not change the logger but will log
|
||||
* a warning if mismatched.
|
||||
* @return The custom Logger.
|
||||
*/
|
||||
public static Logger create(final Object value, final MessageFactory messageFactory) {
|
||||
final org.apache.logging.log4j.Logger wrapped = LogManager.getLogger(value, messageFactory);
|
||||
return new Logger(wrapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a custom Logger with the specified name.
|
||||
*
|
||||
* @param name The logger name. If null the name of the calling class will
|
||||
* be used.
|
||||
* @return The custom Logger.
|
||||
*/
|
||||
public static Logger create(final String name) {
|
||||
final org.apache.logging.log4j.Logger wrapped = LogManager.getLogger(name);
|
||||
return new Logger(wrapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a custom Logger with the specified name.
|
||||
*
|
||||
* @param name The logger name. If null the name of the calling class will
|
||||
* be used.
|
||||
* @param messageFactory The message factory is used only when creating a
|
||||
* logger, subsequent use does not change the logger but will log
|
||||
* a warning if mismatched.
|
||||
* @return The custom Logger.
|
||||
*/
|
||||
public static Logger create(final String name, final MessageFactory messageFactory) {
|
||||
final org.apache.logging.log4j.Logger wrapped = LogManager.getLogger(name, messageFactory);
|
||||
return new Logger(wrapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with the specific Marker at the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param msg the message string to be logged
|
||||
*/
|
||||
public void perf(final Marker marker, final Message msg) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, msg, (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with the specific Marker at the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param msg the message string to be logged
|
||||
* @param t A Throwable or null.
|
||||
*/
|
||||
public void perf(final Marker marker, final Message msg, final Throwable t) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, msg, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message object with the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message object to log.
|
||||
*/
|
||||
public void perf(final Marker marker, final Object message) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message CharSequence with the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message CharSequence to log.
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final Marker marker, final CharSequence message) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message at the {@code PROFILE} level including the stack trace of
|
||||
* the {@link Throwable} {@code t} passed as parameter.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message to log.
|
||||
* @param t the exception to log, including its stack trace.
|
||||
*/
|
||||
public void perf(final Marker marker, final Object message, final Throwable t) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message at the {@code PROFILE} level including the stack trace of
|
||||
* the {@link Throwable} {@code t} passed as parameter.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the CharSequence to log.
|
||||
* @param t the exception to log, including its stack trace.
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final Marker marker, final CharSequence message, final Throwable t) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message object with the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message object to log.
|
||||
*/
|
||||
public void perf(final Marker marker, final String message) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param params parameters to the message.
|
||||
* @see #getMessageFactory()
|
||||
*/
|
||||
public void perf(final Marker marker, final String message, final Object... params) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final Marker marker, final String message, final Object p0) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, p0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final Marker marker, final String message, final Object p0, final Object p1) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, p0, p1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final Marker marker, final String message, final Object p0, final Object p1, final Object p2) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, p0, p1, p2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @param p3 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final Marker marker, final String message, final Object p0, final Object p1, final Object p2,
|
||||
final Object p3) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, p0, p1, p2, p3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @param p3 parameter to the message.
|
||||
* @param p4 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final Marker marker, final String message, final Object p0, final Object p1, final Object p2,
|
||||
final Object p3, final Object p4) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, p0, p1, p2, p3, p4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @param p3 parameter to the message.
|
||||
* @param p4 parameter to the message.
|
||||
* @param p5 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final Marker marker, final String message, final Object p0, final Object p1, final Object p2,
|
||||
final Object p3, final Object p4, final Object p5) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, p0, p1, p2, p3, p4, p5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @param p3 parameter to the message.
|
||||
* @param p4 parameter to the message.
|
||||
* @param p5 parameter to the message.
|
||||
* @param p6 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final Marker marker, final String message, final Object p0, final Object p1, final Object p2,
|
||||
final Object p3, final Object p4, final Object p5, final Object p6) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, p0, p1, p2, p3, p4, p5, p6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @param p3 parameter to the message.
|
||||
* @param p4 parameter to the message.
|
||||
* @param p5 parameter to the message.
|
||||
* @param p6 parameter to the message.
|
||||
* @param p7 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final Marker marker, final String message, final Object p0, final Object p1, final Object p2,
|
||||
final Object p3, final Object p4, final Object p5, final Object p6,
|
||||
final Object p7) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, p0, p1, p2, p3, p4, p5, p6, p7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @param p3 parameter to the message.
|
||||
* @param p4 parameter to the message.
|
||||
* @param p5 parameter to the message.
|
||||
* @param p6 parameter to the message.
|
||||
* @param p7 parameter to the message.
|
||||
* @param p8 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final Marker marker, final String message, final Object p0, final Object p1, final Object p2,
|
||||
final Object p3, final Object p4, final Object p5, final Object p6,
|
||||
final Object p7, final Object p8) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, p0, p1, p2, p3, p4, p5, p6, p7, p8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @param p3 parameter to the message.
|
||||
* @param p4 parameter to the message.
|
||||
* @param p5 parameter to the message.
|
||||
* @param p6 parameter to the message.
|
||||
* @param p7 parameter to the message.
|
||||
* @param p8 parameter to the message.
|
||||
* @param p9 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final Marker marker, final String message, final Object p0, final Object p1, final Object p2,
|
||||
final Object p3, final Object p4, final Object p5, final Object p6,
|
||||
final Object p7, final Object p8, final Object p9) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message at the {@code PROFILE} level including the stack trace of
|
||||
* the {@link Throwable} {@code t} passed as parameter.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message to log.
|
||||
* @param t the exception to log, including its stack trace.
|
||||
*/
|
||||
public void perf(final Marker marker, final String message, final Throwable t) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the specified Message at the {@code PROFILE} level.
|
||||
*
|
||||
* @param msg the message string to be logged
|
||||
*/
|
||||
public void perf(final Message msg) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, msg, (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the specified Message at the {@code PROFILE} level.
|
||||
*
|
||||
* @param msg the message string to be logged
|
||||
* @param t A Throwable or null.
|
||||
*/
|
||||
public void perf(final Message msg, final Throwable t) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, msg, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message object with the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message object to log.
|
||||
*/
|
||||
public void perf(final Object message) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message at the {@code PROFILE} level including the stack trace of
|
||||
* the {@link Throwable} {@code t} passed as parameter.
|
||||
*
|
||||
* @param message the message to log.
|
||||
* @param t the exception to log, including its stack trace.
|
||||
*/
|
||||
public void perf(final Object message, final Throwable t) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message CharSequence with the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message CharSequence to log.
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final CharSequence message) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a CharSequence at the {@code PROFILE} level including the stack trace of
|
||||
* the {@link Throwable} {@code t} passed as parameter.
|
||||
*
|
||||
* @param message the CharSequence to log.
|
||||
* @param t the exception to log, including its stack trace.
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final CharSequence message, final Throwable t) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message object with the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message object to log.
|
||||
*/
|
||||
public void perf(final String message) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param params parameters to the message.
|
||||
* @see #getMessageFactory()
|
||||
*/
|
||||
public void perf(final String message, final Object... params) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final String message, final Object p0) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, p0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final String message, final Object p0, final Object p1) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, p0, p1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final String message, final Object p0, final Object p1, final Object p2) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, p0, p1, p2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @param p3 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final String message, final Object p0, final Object p1, final Object p2,
|
||||
final Object p3) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, p0, p1, p2, p3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @param p3 parameter to the message.
|
||||
* @param p4 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final String message, final Object p0, final Object p1, final Object p2,
|
||||
final Object p3, final Object p4) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, p0, p1, p2, p3, p4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @param p3 parameter to the message.
|
||||
* @param p4 parameter to the message.
|
||||
* @param p5 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final String message, final Object p0, final Object p1, final Object p2,
|
||||
final Object p3, final Object p4, final Object p5) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, p0, p1, p2, p3, p4, p5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @param p3 parameter to the message.
|
||||
* @param p4 parameter to the message.
|
||||
* @param p5 parameter to the message.
|
||||
* @param p6 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final String message, final Object p0, final Object p1, final Object p2,
|
||||
final Object p3, final Object p4, final Object p5, final Object p6) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, p0, p1, p2, p3, p4, p5, p6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @param p3 parameter to the message.
|
||||
* @param p4 parameter to the message.
|
||||
* @param p5 parameter to the message.
|
||||
* @param p6 parameter to the message.
|
||||
* @param p7 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final String message, final Object p0, final Object p1, final Object p2,
|
||||
final Object p3, final Object p4, final Object p5, final Object p6,
|
||||
final Object p7) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, p0, p1, p2, p3, p4, p5, p6, p7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @param p3 parameter to the message.
|
||||
* @param p4 parameter to the message.
|
||||
* @param p5 parameter to the message.
|
||||
* @param p6 parameter to the message.
|
||||
* @param p7 parameter to the message.
|
||||
* @param p8 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final String message, final Object p0, final Object p1, final Object p2,
|
||||
final Object p3, final Object p4, final Object p5, final Object p6,
|
||||
final Object p7, final Object p8) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, p0, p1, p2, p3, p4, p5, p6, p7, p8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters at the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param p0 parameter to the message.
|
||||
* @param p1 parameter to the message.
|
||||
* @param p2 parameter to the message.
|
||||
* @param p3 parameter to the message.
|
||||
* @param p4 parameter to the message.
|
||||
* @param p5 parameter to the message.
|
||||
* @param p6 parameter to the message.
|
||||
* @param p7 parameter to the message.
|
||||
* @param p8 parameter to the message.
|
||||
* @param p9 parameter to the message.
|
||||
* @see #getMessageFactory()
|
||||
* @since Log4j-2.6
|
||||
*/
|
||||
public void perf(final String message, final Object p0, final Object p1, final Object p2,
|
||||
final Object p3, final Object p4, final Object p5, final Object p6,
|
||||
final Object p7, final Object p8, final Object p9) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message at the {@code PROFILE} level including the stack trace of
|
||||
* the {@link Throwable} {@code t} passed as parameter.
|
||||
*
|
||||
* @param message the message to log.
|
||||
* @param t the exception to log, including its stack trace.
|
||||
*/
|
||||
public void perf(final String message, final Throwable t) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message which is only to be constructed if the logging level is the {@code PROFILE}level.
|
||||
*
|
||||
* @param msgSupplier A function, which when called, produces the desired log message;
|
||||
* the format depends on the message factory.
|
||||
* @since Log4j-2.4
|
||||
*/
|
||||
public void perf(final Supplier<?> msgSupplier) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, msgSupplier, (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message (only to be constructed if the logging level is the {@code PROFILE}
|
||||
* level) including the stack trace of the {@link Throwable} <code>t</code> passed as parameter.
|
||||
*
|
||||
* @param msgSupplier A function, which when called, produces the desired log message;
|
||||
* the format depends on the message factory.
|
||||
* @param t the exception to log, including its stack trace.
|
||||
* @since Log4j-2.4
|
||||
*/
|
||||
public void perf(final Supplier<?> msgSupplier, final Throwable t) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, msgSupplier, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message which is only to be constructed if the logging level is the
|
||||
* {@code PROFILE} level with the specified Marker.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param msgSupplier A function, which when called, produces the desired log message;
|
||||
* the format depends on the message factory.
|
||||
* @since Log4j-2.4
|
||||
*/
|
||||
public void perf(final Marker marker, final Supplier<?> msgSupplier) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, msgSupplier, (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters which are only to be constructed if the logging level is the
|
||||
* {@code PROFILE} level.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
|
||||
* @since Log4j-2.4
|
||||
*/
|
||||
public void perf(final Marker marker, final String message, final Supplier<?>... paramSuppliers) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, message, paramSuppliers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message (only to be constructed if the logging level is the {@code PROFILE}
|
||||
* level) with the specified Marker and including the stack trace of the {@link Throwable}
|
||||
* <code>t</code> passed as parameter.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param msgSupplier A function, which when called, produces the desired log message;
|
||||
* the format depends on the message factory.
|
||||
* @param t A Throwable or null.
|
||||
* @since Log4j-2.4
|
||||
*/
|
||||
public void perf(final Marker marker, final Supplier<?> msgSupplier, final Throwable t) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, msgSupplier, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message with parameters which are only to be constructed if the logging level is
|
||||
* the {@code PROFILE} level.
|
||||
*
|
||||
* @param message the message to log; the format depends on the message factory.
|
||||
* @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
|
||||
* @since Log4j-2.4
|
||||
*/
|
||||
public void perf(final String message, final Supplier<?>... paramSuppliers) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, message, paramSuppliers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message which is only to be constructed if the logging level is the
|
||||
* {@code PROFILE} level with the specified Marker. The {@code MessageSupplier} may or may
|
||||
* not use the {@link MessageFactory} to construct the {@code Message}.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param msgSupplier A function, which when called, produces the desired log message.
|
||||
* @since Log4j-2.4
|
||||
*/
|
||||
public void perf(final Marker marker, final MessageSupplier msgSupplier) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, msgSupplier, (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message (only to be constructed if the logging level is the {@code PROFILE}
|
||||
* level) with the specified Marker and including the stack trace of the {@link Throwable}
|
||||
* <code>t</code> passed as parameter. The {@code MessageSupplier} may or may not use the
|
||||
* {@link MessageFactory} to construct the {@code Message}.
|
||||
*
|
||||
* @param marker the marker data specific to this log statement
|
||||
* @param msgSupplier A function, which when called, produces the desired log message.
|
||||
* @param t A Throwable or null.
|
||||
* @since Log4j-2.4
|
||||
*/
|
||||
public void perf(final Marker marker, final MessageSupplier msgSupplier, final Throwable t) {
|
||||
logger.logIfEnabled(FQCN, PERF, marker, msgSupplier, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message which is only to be constructed if the logging level is the
|
||||
* {@code PROFILE} level. The {@code MessageSupplier} may or may not use the
|
||||
* {@link MessageFactory} to construct the {@code Message}.
|
||||
*
|
||||
* @param msgSupplier A function, which when called, produces the desired log message.
|
||||
* @since Log4j-2.4
|
||||
*/
|
||||
public void perf(final MessageSupplier msgSupplier) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, msgSupplier, (Throwable) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message (only to be constructed if the logging level is the {@code PROFILE}
|
||||
* level) including the stack trace of the {@link Throwable} <code>t</code> passed as parameter.
|
||||
* The {@code MessageSupplier} may or may not use the {@link MessageFactory} to construct the
|
||||
* {@code Message}.
|
||||
*
|
||||
* @param msgSupplier A function, which when called, produces the desired log message.
|
||||
* @param t the exception to log, including its stack trace.
|
||||
* @since Log4j-2.4
|
||||
*/
|
||||
public void perf(final MessageSupplier msgSupplier, final Throwable t) {
|
||||
logger.logIfEnabled(FQCN, PERF, null, msgSupplier, t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2019-2020 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.logging;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class LoggingOutputStream extends OutputStream {
|
||||
|
||||
/**
|
||||
* Default number of bytes in the buffer.
|
||||
*/
|
||||
private static final int DEFAULT_BUFFER_LENGTH = 2048;
|
||||
|
||||
/**
|
||||
* Indicates stream state.
|
||||
*/
|
||||
private boolean hasBeenClosed = false;
|
||||
|
||||
/**
|
||||
* Internal buffer where data is stored.
|
||||
*/
|
||||
private byte[] buf;
|
||||
|
||||
/**
|
||||
* The number of valid bytes in the buffer.
|
||||
*/
|
||||
private int count;
|
||||
|
||||
/**
|
||||
* Remembers the size of the buffer.
|
||||
*/
|
||||
private int curBufLength;
|
||||
|
||||
/**
|
||||
* The logger to write to.
|
||||
*/
|
||||
private Logger log;
|
||||
|
||||
/**
|
||||
* The log level.
|
||||
*/
|
||||
private Level level;
|
||||
|
||||
/**
|
||||
* Creates the Logging instance to flush to the given logger.
|
||||
*
|
||||
* @param log the Logger to write to
|
||||
* @param level the log level
|
||||
* @throws IllegalArgumentException in case if one of arguments is null.
|
||||
*/
|
||||
public LoggingOutputStream(final Logger log,
|
||||
final Level level)
|
||||
throws IllegalArgumentException {
|
||||
if (log == null || level == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Logger or log level must be not null");
|
||||
}
|
||||
this.log = log;
|
||||
this.level = level;
|
||||
curBufLength = DEFAULT_BUFFER_LENGTH;
|
||||
buf = new byte[curBufLength];
|
||||
count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the specified byte to this output stream.
|
||||
*
|
||||
* @param b the byte to write
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
public void write(final int b) throws IOException {
|
||||
if (hasBeenClosed) {
|
||||
throw new IOException("The stream has been closed.");
|
||||
}
|
||||
// don't log nulls
|
||||
if (b == 0) {
|
||||
return;
|
||||
}
|
||||
// would this be writing past the buffer?
|
||||
if (count == curBufLength) {
|
||||
// grow the buffer
|
||||
final int newBufLength = curBufLength +
|
||||
DEFAULT_BUFFER_LENGTH;
|
||||
final byte[] newBuf = new byte[newBufLength];
|
||||
System.arraycopy(buf, 0, newBuf, 0, curBufLength);
|
||||
buf = newBuf;
|
||||
curBufLength = newBufLength;
|
||||
}
|
||||
|
||||
buf[count] = (byte) b;
|
||||
count++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes this output stream and forces any buffered output
|
||||
* bytes to be written out.
|
||||
*/
|
||||
public void flush() {
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
final byte[] bytes = new byte[count];
|
||||
System.arraycopy(buf, 0, bytes, 0, count);
|
||||
String str = new String(bytes);
|
||||
if (!str.isBlank()) {
|
||||
log.log(level, str);
|
||||
}
|
||||
count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this output stream and releases any system resources
|
||||
* associated with this stream.
|
||||
*/
|
||||
public void close() {
|
||||
flush();
|
||||
hasBeenClosed = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
* Copyright 2016-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.logging;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A utility class that measures and reports the execution time of a portion of
|
||||
* code.
|
||||
* <p>
|
||||
* A time interval is measured from the creation of the object to its closing.
|
||||
* It implements {@code AutoCloseable} and can be used in conjunction with a
|
||||
* try-with-resource statement to measure the amount of time totalTime from
|
||||
* entering to exiting the try block.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public final class Profiler implements AutoCloseable {
|
||||
private final Elapsed elapsed;
|
||||
private final OutputDelegate writeCallback;
|
||||
private final AtomicBoolean closed = new AtomicBoolean(false);
|
||||
private final long startTime;
|
||||
private final long thresholdMs;
|
||||
|
||||
private Profiler(Elapsed elapsed, OutputDelegate writeCallback, long thresholdMs) {
|
||||
this.elapsed = elapsed;
|
||||
this.writeCallback = writeCallback;
|
||||
this.thresholdMs = thresholdMs;
|
||||
this.startTime = System.nanoTime();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the {@link Profiler} class.
|
||||
*
|
||||
* @param message The message associated to the perf totalTime.
|
||||
* @param writeCallback The callback that will be invoked to log the results of the
|
||||
* totalTime.
|
||||
* @return The new instance of the Profiler class
|
||||
*/
|
||||
public static Profiler start(String message, OutputDelegate writeCallback) {
|
||||
return new Profiler(new Elapsed(message), writeCallback, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the {@link Profiler} class.
|
||||
*
|
||||
* @param messageSupplier The lambda responsible for suppling the message associated to the perf totalTime..
|
||||
* @param writeCallback The callback that will be invoked to log the results of totalTime.
|
||||
* @return The new instance of the Profiler class
|
||||
*/
|
||||
public static Profiler start(Supplier<String> messageSupplier, OutputDelegate writeCallback) {
|
||||
return new Profiler(new Elapsed(messageSupplier), writeCallback, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the {@link Profiler} class.
|
||||
*
|
||||
* @param message The message associated to the perf totalTime.
|
||||
* @param writeCallback The callback that will be invoked to log the results of the
|
||||
* totalTime.
|
||||
* @param threshold If the elapsed time is greater or equal to that threshold
|
||||
* value (in ms) the message is logged otherwise it isn't.
|
||||
* @return The new instance of the Profiler class
|
||||
*/
|
||||
public static Profiler start(String message, OutputDelegate writeCallback, long threshold) {
|
||||
return new Profiler(new Elapsed(message), writeCallback, threshold);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the {@link Profiler} class.
|
||||
*
|
||||
* @param messageSupplier The lambda responsible for supplying the message associated to the perf totalTime.
|
||||
* @param writeCallback The callback that will be invoked to log the results of totalTime.
|
||||
* @param threshold If the elapsed time is greater or equal to that threshold
|
||||
* value (in ms) the message is logged otherwise it isn't.
|
||||
* @return The new instance of the Profiler class
|
||||
*/
|
||||
public static Profiler start(Supplier<String> messageSupplier, OutputDelegate writeCallback, long threshold) {
|
||||
return new Profiler(new Elapsed(messageSupplier), writeCallback, threshold);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the {@link Profiler} class.
|
||||
*
|
||||
* @param message The message associated to the perf totalTime.
|
||||
* @return The newly created Profiler instance.
|
||||
*/
|
||||
public static Profiler start(String message) {
|
||||
return new Profiler(new Elapsed(message), null, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the {@link Profiler} class.
|
||||
*
|
||||
* @param messageSupplier The lambda responsible for supplying the message associated to the perf totalTime.
|
||||
* @return The newly created Profiler instance.
|
||||
*/
|
||||
public static Profiler start(Supplier<String> messageSupplier) {
|
||||
return new Profiler(new Elapsed(messageSupplier), null, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the {@link Profiler} class.
|
||||
*
|
||||
* @param message The message associated to the perf totalTime.
|
||||
* @param threshold If the elapsed time is greater or equal to that threshold
|
||||
* value (in ms) the message is logged otherwise it isn't.
|
||||
* @return The newly created Profiler instance.
|
||||
*/
|
||||
public static Profiler start(String message, long threshold) {
|
||||
return new Profiler(new Elapsed(message), null, threshold);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the {@link Profiler} class.
|
||||
*
|
||||
* @param messageSupplier The lambda responsible for supplying the message associated to the perf totalTime.
|
||||
* @param threshold If the elapsed time is greater or equal to that threshold
|
||||
* value (in ms) the message is logged otherwise it isn't.
|
||||
* @return The newly created Profiler instance.
|
||||
*/
|
||||
public static Profiler start(Supplier<String> messageSupplier, long threshold) {
|
||||
return new Profiler(new Elapsed(messageSupplier), null, threshold);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the {@link Profiler} class.
|
||||
*
|
||||
* @param writeCallback The callback that will be invoked to log the results of the
|
||||
* totalTime.
|
||||
* @return The new instance of the Profiler class.
|
||||
*/
|
||||
public static Profiler start(OutputDelegate writeCallback) {
|
||||
return new Profiler(new Elapsed(""), writeCallback, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the {@link Profiler} class.
|
||||
*
|
||||
* @param writeCallback The callback that will be invoked to log the results of the
|
||||
* totalTime.
|
||||
* @param threshold If the elapsed time is greater or equal to that threshold
|
||||
* value (in ms) the message is logged otherwise it isn't.
|
||||
* @return The new instance of the Profiler class.
|
||||
*/
|
||||
public static Profiler start(OutputDelegate writeCallback, long threshold) {
|
||||
return new Profiler(new Elapsed(""), writeCallback, threshold);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the {@link Profiler} class.
|
||||
*
|
||||
* @param elapsed A Elapsed object that will be used to store the results of the
|
||||
* totalTime.
|
||||
* @return The newly created Profiler instance.
|
||||
*/
|
||||
public static Profiler start(Elapsed elapsed) {
|
||||
return new Profiler(elapsed, null, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the {@link Profiler} class.
|
||||
*
|
||||
* @param elapsed A Elapsed object that will be used to store the results of the
|
||||
* totalTime.
|
||||
* @param threshold If the elapsed time is greater or equal to that threshold
|
||||
* value (in ms) the message is logged otherwise it isn't.
|
||||
* @return The newly created Profiler instance.
|
||||
*/
|
||||
public static Profiler start(Elapsed elapsed, long threshold) {
|
||||
return new Profiler(elapsed, null, threshold);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (closed.compareAndSet(false, true)) {
|
||||
long stopTime = System.nanoTime();
|
||||
this.elapsed.nanoSec += stopTime - this.startTime;
|
||||
if (writeCallback != null) {
|
||||
if (this.elapsed.getMillis() >= thresholdMs) {
|
||||
writeCallback.invoke(this.elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A functional interface that represents the action to output the measured
|
||||
* interval (for compatibility with < 1.8)
|
||||
*/
|
||||
public interface OutputDelegate {
|
||||
/**
|
||||
* Invoke
|
||||
*
|
||||
* @param e Elapsed
|
||||
*/
|
||||
void invoke(Elapsed e);
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that encapsulate the interval measured by a {@link Profiler}
|
||||
* object.
|
||||
*/
|
||||
public static class Elapsed {
|
||||
private Supplier<String> messageSupplier;
|
||||
private long nanoSec;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link Elapsed} class.
|
||||
*/
|
||||
public Elapsed() {
|
||||
this("", 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link Elapsed} class with the
|
||||
* specified message.
|
||||
*
|
||||
* @param message The specified message
|
||||
*/
|
||||
public Elapsed(String message) {
|
||||
this(message, 0);
|
||||
}
|
||||
|
||||
public Elapsed(Supplier<String> messageSupplier) {
|
||||
this(messageSupplier, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link Elapsed} class with the
|
||||
* specified message and initial value for the totalTime time.
|
||||
*
|
||||
* @param intialValue the initial value for the totalTime time.
|
||||
* @param message The specified message
|
||||
*/
|
||||
public Elapsed(String message, long intialValue) {
|
||||
this(() -> message, intialValue);
|
||||
}
|
||||
|
||||
public Elapsed(Supplier<String> messageSupplier, long intialValue) {
|
||||
this.nanoSec = intialValue;
|
||||
this.setMessageSupplier(messageSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Elapsed}'s message.
|
||||
*
|
||||
* @return the perf totalTime's message.
|
||||
*/
|
||||
public String getMessage() {
|
||||
if (messageSupplier != null) {
|
||||
return messageSupplier.get();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Elapsed}'s message.
|
||||
*
|
||||
* @param messageSupplier the perf totalTime's message.
|
||||
*/
|
||||
public void setMessageSupplier(Supplier<String> messageSupplier) {
|
||||
this.messageSupplier = messageSupplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Elapsed} time in ns.
|
||||
*
|
||||
* @return the totalTime time in ns.
|
||||
*/
|
||||
public long getNanos() {
|
||||
return nanoSec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Elapsed} time in μs.
|
||||
*
|
||||
* @return the totalTime time in μs.
|
||||
*/
|
||||
public long getMicros() {
|
||||
return Math.round(nanoSec * Math.pow(10, -3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Elapsed} time in ms.
|
||||
*
|
||||
* @return the totalTime time in ms.
|
||||
*/
|
||||
public long getMillis() {
|
||||
return Math.round(nanoSec * Math.pow(10, -6));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Elapsed} time in s.
|
||||
*
|
||||
* @return the totalTime time in s.
|
||||
*/
|
||||
public long getSeconds() {
|
||||
return Math.round(nanoSec * Math.pow(10, -9));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.getMessage() + ": " + getMillis() + " ms";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the profiler, composed ot the
|
||||
* message and the time interval in ns.
|
||||
*
|
||||
* @return a string representation of the profiler, composed ot the
|
||||
* message and the time interval in ns.
|
||||
*/
|
||||
public String toNanoString() {
|
||||
return this.getMessage() + ": " + getNanos() + " ns";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the profiler, composed ot the
|
||||
* message and the time interval in μs.
|
||||
*
|
||||
* @return a string representation of the profiler, composed ot the
|
||||
* message and the time interval in μs.
|
||||
*/
|
||||
public String toMicroString() {
|
||||
return this.getMessage() + ": " + getMicros() + " μs";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the profiler, composed ot the
|
||||
* message and the time interval in ms.
|
||||
*
|
||||
* @return a string representation of the profiler, composed ot the
|
||||
* message and the time interval in ms.
|
||||
*/
|
||||
public String toMilliString() {
|
||||
return this.getMessage() + ": " + getMillis() + " ms";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the profiler, composed ot the
|
||||
* message and the time interval in s.
|
||||
*
|
||||
* @return a string representation of the profiler, composed ot the
|
||||
* message and the time interval in s.
|
||||
*/
|
||||
public String toSecondString() {
|
||||
return this.getMessage() + ": " + getSeconds() + " s";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2017-2022 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.logging;
|
||||
|
||||
import eu.binjr.core.dialogs.Dialogs;
|
||||
import eu.binjr.core.preferences.AppEnvironment;
|
||||
import eu.binjr.core.preferences.UserPreferences;
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.core.Filter;
|
||||
import org.apache.logging.log4j.core.Layout;
|
||||
import org.apache.logging.log4j.core.LogEvent;
|
||||
import org.apache.logging.log4j.core.appender.AbstractAppender;
|
||||
import org.apache.logging.log4j.core.config.Property;
|
||||
import org.apache.logging.log4j.core.config.plugins.Plugin;
|
||||
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
|
||||
import org.apache.logging.log4j.core.config.plugins.PluginElement;
|
||||
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
|
||||
import org.apache.logging.log4j.core.layout.PatternLayout;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* TextFlowAppender for Log4j 2
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
@Plugin(
|
||||
name = "TextFlowAppender",
|
||||
category = "Core",
|
||||
elementType = "appender",
|
||||
printObject = true)
|
||||
public class TextFlowAppender extends AbstractAppender {
|
||||
private final Lock renderTextLock = new ReentrantLock();
|
||||
private final Map<Level, String> logColors = new HashMap<>();
|
||||
private final String defaultColor = "log-info";
|
||||
private final LogBuffer logBuffer = new LogBuffer();
|
||||
private Consumer<Collection<Log>> renderTextDelegate;
|
||||
|
||||
protected TextFlowAppender(String name, Filter filter,
|
||||
Layout<? extends Serializable> layout,
|
||||
final boolean ignoreExceptions) {
|
||||
super(name, filter, layout, ignoreExceptions, Property.EMPTY_ARRAY);
|
||||
logColors.put(Level.TRACE, "trace");
|
||||
logColors.put(Level.DEBUG, "debug");
|
||||
logColors.put(Logger.PERF, "perf");
|
||||
logColors.put(Level.INFO, "info");
|
||||
logColors.put(Level.WARN, "warn");
|
||||
logColors.put(Level.ERROR, "error");
|
||||
logColors.put(Level.FATAL, "fatal");
|
||||
Timer timer = new Timer(true);
|
||||
timer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (AppEnvironment.getInstance().isDebugMode()) {
|
||||
refreshTextFlow();
|
||||
}
|
||||
}
|
||||
}, 500, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method. Log4j will parse the configuration and call this factory
|
||||
* method to construct the appender with
|
||||
* the configured attributes.
|
||||
*
|
||||
* @param name Name of appender
|
||||
* @param layout Log layout of appender
|
||||
* @param filter Filter for appender
|
||||
* @return The TextFlowAppender
|
||||
*/
|
||||
@PluginFactory
|
||||
public static TextFlowAppender createAppender(
|
||||
@PluginAttribute("name") String name,
|
||||
@PluginElement("Layout") Layout<? extends Serializable> layout,
|
||||
@PluginElement("Filter") final Filter filter) {
|
||||
if (name == null) {
|
||||
LOGGER.error("No name provided for TextFlowAppender");
|
||||
return null;
|
||||
}
|
||||
if (layout == null) {
|
||||
layout = PatternLayout.createDefaultLayout();
|
||||
}
|
||||
return new TextFlowAppender(name, filter, layout, true);
|
||||
}
|
||||
|
||||
public void setRenderTextDelegate(Consumer<Collection<Log>> delegate) {
|
||||
this.renderTextDelegate = delegate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the circular buffer used by the appender
|
||||
*/
|
||||
public void clearBuffer() {
|
||||
renderTextLock.lock();
|
||||
try {
|
||||
logBuffer.clear();
|
||||
} finally {
|
||||
renderTextLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is where the appender does the work.
|
||||
*
|
||||
* @param event Log event with log data
|
||||
*/
|
||||
@Override
|
||||
public void append(LogEvent event) {
|
||||
renderTextLock.lock();
|
||||
try {
|
||||
new String(getLayout().toByteArray(event), StandardCharsets.UTF_8).lines().forEach(
|
||||
message -> {
|
||||
Log log = new Log(message, logColors.getOrDefault(event.getLevel(), defaultColor));
|
||||
logBuffer.push(log);
|
||||
});
|
||||
} finally {
|
||||
renderTextLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshTextFlow() {
|
||||
Dialogs.runOnFXThread(() -> {
|
||||
if (renderTextLock.tryLock()) {
|
||||
try {
|
||||
if (renderTextDelegate != null && logBuffer.isDirty()) {
|
||||
logBuffer.clean();
|
||||
renderTextDelegate.accept(logBuffer.getLogs());
|
||||
}
|
||||
} finally {
|
||||
renderTextLock.unlock();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public record Log(String message, String styleClass) {
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public String getStyleClass() {
|
||||
return styleClass;
|
||||
}
|
||||
}
|
||||
|
||||
private static class LogBuffer {
|
||||
private volatile boolean dirty;
|
||||
private long lineCounter = 0;
|
||||
private final LinkedHashMap<Long, Log> buffer = new LinkedHashMap<>() {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<Long, Log> eldest) {
|
||||
return size() > UserPreferences.getInstance().consoleMaxLineCapacity.get().intValue();
|
||||
}
|
||||
};
|
||||
|
||||
public void clear() {
|
||||
dirty = true;
|
||||
buffer.clear();
|
||||
lineCounter = 0;
|
||||
}
|
||||
|
||||
public void clean() {
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
public Log push(Log log) {
|
||||
dirty = true;
|
||||
return buffer.put(lineCounter++, log);
|
||||
}
|
||||
|
||||
public boolean isDirty() {
|
||||
return dirty;
|
||||
}
|
||||
|
||||
public Collection<Log> getLogs() {
|
||||
return buffer.values();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2020 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.navigation;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Wraps a stack to record user navigation steps.
|
||||
*/
|
||||
public class NavigationHistory<T> {
|
||||
private static final Logger logger = Logger.create(NavigationHistory.class);
|
||||
private final HistoryQueue<T> forward = new HistoryQueue<>();
|
||||
private final HistoryQueue<T> backward = new HistoryQueue<>();
|
||||
|
||||
public T getHead() {
|
||||
return head.getValue();
|
||||
}
|
||||
|
||||
public Property<T> headProperty() {
|
||||
return head;
|
||||
}
|
||||
|
||||
private final Property<T> head = new SimpleObjectProperty<>();
|
||||
private T previousItem;
|
||||
|
||||
public HistoryQueue<T> forward() {
|
||||
return forward;
|
||||
}
|
||||
|
||||
public HistoryQueue<T> backward() {
|
||||
return backward;
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
forward.clear();
|
||||
backward.clear();
|
||||
}
|
||||
|
||||
public synchronized void setHead(T item, boolean save) {
|
||||
if (save) {
|
||||
logger.debug((() -> "Saving to history: item=" + item.toString() + " previous=" + previousItem.toString()));
|
||||
this.backward.push(previousItem);
|
||||
this.forward.clear();
|
||||
}
|
||||
previousItem = item;
|
||||
head.setValue(item);
|
||||
}
|
||||
|
||||
public Optional<T> getPrevious() {
|
||||
return restoreSelectionFromHistory(backward, forward);
|
||||
}
|
||||
|
||||
public Optional<T> getNext() {
|
||||
return restoreSelectionFromHistory(forward, backward);
|
||||
}
|
||||
|
||||
public synchronized String dump() {
|
||||
return "Backward navigation history:\n" + backward.dump() +
|
||||
"Forward navigation history:\n" + forward.dump();
|
||||
}
|
||||
|
||||
private synchronized Optional<T> restoreSelectionFromHistory(HistoryQueue<T> history, HistoryQueue<T> toHistory) {
|
||||
if (!history.isEmpty()) {
|
||||
toHistory.push(getHead());
|
||||
return Optional.of(history.pop());
|
||||
} else {
|
||||
logger.debug(() -> "NavigationHistory is empty: nothing to go back to.");
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public class HistoryQueue<T> {
|
||||
|
||||
private final Deque<T> stack = new ArrayDeque<>();
|
||||
|
||||
private final SimpleBooleanProperty empty = new SimpleBooleanProperty(true);
|
||||
|
||||
|
||||
private void push(T state) {
|
||||
if (state == null) {
|
||||
logger.warn(() -> "Trying to push null state into backwardHistory");
|
||||
return;
|
||||
}
|
||||
empty.set(false);
|
||||
this.stack.push(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the history
|
||||
*/
|
||||
private void clear() {
|
||||
this.stack.clear();
|
||||
empty.set(true);
|
||||
}
|
||||
|
||||
|
||||
private T pop() {
|
||||
T r = this.stack.pop();
|
||||
empty.set(stack.isEmpty());
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
public SimpleBooleanProperty emptyProperty() {
|
||||
return empty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the underlying stack is empty, false otherwise.
|
||||
*
|
||||
* @return true if the underlying stack is empty, false otherwise.
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return empty.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps the content of the stack as a string
|
||||
*
|
||||
* @return the content of the stack as a string
|
||||
*/
|
||||
private String dump() {
|
||||
final StringBuilder sb = new StringBuilder("");
|
||||
AtomicInteger pos = new AtomicInteger(0);
|
||||
if (this.isEmpty()) {
|
||||
sb.append(" { empty }");
|
||||
} else {
|
||||
stack.forEach(h -> sb.append(pos.incrementAndGet()).append(" ->").append(h.toString()).append("\n"));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2020 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.navigation;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class RingIterator<E> implements Iterator<E> {
|
||||
private final List<E> wrappedList;
|
||||
private final int lastIdx;
|
||||
private int idx = -1;
|
||||
|
||||
private RingIterator(List<E> wrappedList) {
|
||||
this.wrappedList = wrappedList;
|
||||
lastIdx = wrappedList.size() - 1;
|
||||
}
|
||||
|
||||
public static <T> RingIterator<T> of(List<T> list) {
|
||||
return new RingIterator<>(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return lastIdx > -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public E next() {
|
||||
return wrappedList.get(nextIndex());
|
||||
}
|
||||
|
||||
public boolean hasPrevious() {
|
||||
return lastIdx > -1;
|
||||
}
|
||||
|
||||
public E previous() {
|
||||
return wrappedList.get(previousIndex());
|
||||
}
|
||||
|
||||
public int nextIndex() {
|
||||
idx = (idx == lastIdx) ? 0 : idx + 1;
|
||||
return idx;
|
||||
}
|
||||
|
||||
public int previousIndex() {
|
||||
idx = (idx <= 0) ? lastIdx : idx - 1;
|
||||
return idx;
|
||||
}
|
||||
|
||||
public int peekCurrentIndex() {
|
||||
return idx;
|
||||
}
|
||||
|
||||
public int peekLastIndex() {
|
||||
return lastIdx;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2019-2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.plugins;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* A collection of helper methods to work with {@link ServiceLoader}
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public final class ServiceLoaderHelper {
|
||||
private static final Logger logger = Logger.create(ServiceLoaderHelper.class);
|
||||
|
||||
/**
|
||||
* A helper method to load and return service implementations from the classpath and external jars
|
||||
*
|
||||
* @param clazz the type of service to load and return
|
||||
* @param loadedServices a set of services to witch the loaded services should be added.
|
||||
* @param <T> the type of service to load and return
|
||||
*/
|
||||
public static <T> void loadFromClasspath(Class<T> clazz, Set<T> loadedServices) {
|
||||
// Load plugins from classpath
|
||||
loadFromServiceLoader(ServiceLoader.load(clazz), loadedServices);
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to load and return service implementations from the classpath and/or external jars
|
||||
*
|
||||
* @param clazz the type of service to load and return
|
||||
* @param loadedServices a set of services to witch the loaded services should be added.
|
||||
* @param externalLocations file system paths specifying where to look for external jar to load services from
|
||||
* @param <T> the type of service to load and return
|
||||
* @return the class loader used to load jars from the external paths
|
||||
*/
|
||||
public static <T> ClassLoader loadFromPaths(Class<T> clazz, Set<T> loadedServices, Collection<Path> externalLocations) {
|
||||
return loadFromPaths(clazz, loadedServices, externalLocations.toArray(Path[]::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to load and return service implementations from the classpath and/or external jars
|
||||
*
|
||||
* @param clazz the type of service to load and return
|
||||
* @param loadedServices a set of services to witch the loaded services should be added.
|
||||
* @param externalLocations file system paths specifying where to look for external jar to load services from
|
||||
* @param <T> the type of service to load and return
|
||||
* @return the class loader used to load jars from the external paths
|
||||
*/
|
||||
public static <T> ClassLoader loadFromPaths(Class<T> clazz, Set<T> loadedServices, Path... externalLocations) {
|
||||
Objects.requireNonNull(externalLocations);
|
||||
//Load plugin from external folder
|
||||
List<URL> urls = new ArrayList<>();
|
||||
for (var externalLocation : externalLocations) {
|
||||
if (externalLocation != null && Files.exists(externalLocation)) {
|
||||
logger.info(() -> "Looking for services of type " + clazz.getName() + " in " + externalLocation);
|
||||
PathMatcher jarMatcher = FileSystems.getDefault().getPathMatcher("glob:**.jar");
|
||||
try {
|
||||
Files.walkFileTree(externalLocation,
|
||||
EnumSet.noneOf(FileVisitOption.class),
|
||||
1,
|
||||
new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
if (jarMatcher.matches(file)) {
|
||||
logger.debug(() -> "Inspecting " + file.getFileName() +
|
||||
" for " + clazz.getName() + " service implementations");
|
||||
urls.add(file.toUri().toURL());
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
logger.error("Error while scanning for services: " + e.getMessage());
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Stack trace", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warn("External location " + externalLocation + " does not exist.");
|
||||
}
|
||||
}
|
||||
var ucl = new URLClassLoader(urls.toArray(URL[]::new));
|
||||
loadFromServiceLoader(ServiceLoader.load(clazz, ucl), loadedServices);
|
||||
return ucl;
|
||||
}
|
||||
|
||||
private static <T> void loadFromServiceLoader(ServiceLoader<T> sl, Set<T> loadedServices) {
|
||||
for (Iterator<T> iterator = sl.iterator(); iterator.hasNext(); ) {
|
||||
try {
|
||||
T res = iterator.next();
|
||||
loadedServices.add(res);
|
||||
logger.debug(() -> "Successfully registered resource " + res.toString() + " from external JAR.");
|
||||
} catch (ServiceConfigurationError sce) {
|
||||
logger.error("Failed to load resource", sce);
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error while loading resource", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.preferences;
|
||||
|
||||
import eu.binjr.common.io.AesHelper;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class AesKeyring extends ObservablePreferenceFactory {
|
||||
private static final Logger logger = Logger.create(AesKeyring.class);
|
||||
private static final String UNINITIALIZED = "uninitialized";
|
||||
|
||||
protected AesKeyring(String storeKey) {
|
||||
super(storeKey);
|
||||
}
|
||||
|
||||
private SecretKey createDefaultKey() {
|
||||
try {
|
||||
return AesHelper.generateKey(256);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
logger.fatal("Failed to generated key: " + e.getMessage());
|
||||
logger.debug("Stack Trace", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public ObservablePreference<SecretKey> secretKeyPreference(String storePath) {
|
||||
var p = new ObservablePreference<>(SecretKey.class, storePath, null, backingStore) {
|
||||
@Override
|
||||
protected Property<SecretKey> makeProperty(SecretKey value) {
|
||||
return new SimpleObjectProperty<>(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SecretKey loadFromBackend() {
|
||||
var encoded = getBackingStore().get(getKey(), UNINITIALIZED);
|
||||
SecretKey decoded;
|
||||
if (!UNINITIALIZED.equals(encoded)) {
|
||||
try {
|
||||
decoded = AesHelper.getKeyFromEncoded(encoded);
|
||||
return decoded;
|
||||
} catch (Exception e){
|
||||
logger.debug("Stack trace", e);
|
||||
logger.warn("Failed to decode key for " + storePath + ": a new key will be initialized");
|
||||
}
|
||||
} else {
|
||||
logger.debug("Initializing new key instance " + storePath);
|
||||
}
|
||||
// Key is uninitialized or invalid: generating a new one.
|
||||
decoded = createDefaultKey();
|
||||
getBackingStore().put(getKey(), AesHelper.encodeSecretKey(decoded));
|
||||
return decoded;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(SecretKey value) {
|
||||
getBackingStore().put(getKey(), AesHelper.encodeSecretKey(value));
|
||||
}
|
||||
};
|
||||
storedItems.put(p.getKey(), p);
|
||||
return p;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.preferences;
|
||||
|
||||
import eu.binjr.common.io.AesHelper;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class AesStringObfuscator extends ObfuscatedString.Obfuscator {
|
||||
private static final Logger logger = LogManager.getLogger(AesStringObfuscator.class);
|
||||
private final SecretKey secretKey;
|
||||
|
||||
public AesStringObfuscator(SecretKey secretKey) {
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
protected String deObfuscateString(String obfuscated) {
|
||||
try {
|
||||
if (obfuscated.isEmpty()) {
|
||||
return obfuscated;
|
||||
}
|
||||
return AesHelper.decrypt(obfuscated, this.secretKey);
|
||||
} catch (Exception e) {
|
||||
logger.error("Error while attempting to de-obfuscate string: " + e.getMessage());
|
||||
logger.debug(() -> "Stack trace", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
protected String obfuscateString(String clearText) {
|
||||
try {
|
||||
if (clearText.isEmpty()) {
|
||||
return clearText;
|
||||
}
|
||||
return AesHelper.encrypt(clearText, this.secretKey);
|
||||
} catch (Exception e) {
|
||||
logger.error("Error while attempting to obfuscate string: " + e.getMessage());
|
||||
logger.debug(() -> "Stack trace", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Licence:
|
||||
* CC0 1.0 Universal (CC0 1.0)
|
||||
* Public Domain Dedication
|
||||
*
|
||||
* The person who associated a work with this deed has dedicated
|
||||
* the work to the public domain by waiving all of his or her rights
|
||||
* to the work worldwide under copyright law, including all related
|
||||
* and neighboring rights, to the extent allowed by law.
|
||||
*
|
||||
*/
|
||||
|
||||
package eu.binjr.common.preferences;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.prefs.AbstractPreferences;
|
||||
import java.util.prefs.BackingStoreException;
|
||||
|
||||
/**
|
||||
* Preferences implementation that stores to a user-defined file.
|
||||
*
|
||||
* @author David Croft (<a href="http://www.davidc.net">www.davidc.net</a>)
|
||||
*/
|
||||
public class FilePreferences extends AbstractPreferences {
|
||||
private static final Logger logger = Logger.create(FilePreferences.class);
|
||||
private final File backingFile;
|
||||
private Map<String, String> root;
|
||||
private Map<String, FilePreferences> children;
|
||||
private boolean isRemoved = false;
|
||||
|
||||
public FilePreferences(File backingFile, AbstractPreferences parent, String name) {
|
||||
super(parent, name);
|
||||
this.backingFile = backingFile;
|
||||
|
||||
logger.trace("Instantiating node " + name);
|
||||
|
||||
root = new TreeMap<>();
|
||||
children = new TreeMap<>();
|
||||
|
||||
try {
|
||||
sync();
|
||||
} catch (BackingStoreException e) {
|
||||
logger.error("Unable to sync on creation of node " + name, e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void putSpi(String key, String value) {
|
||||
root.put(key, value);
|
||||
try {
|
||||
flush();
|
||||
} catch (BackingStoreException e) {
|
||||
logger.error("Unable to flush after putting " + key, e);
|
||||
}
|
||||
}
|
||||
|
||||
protected String getSpi(String key) {
|
||||
return root.get(key);
|
||||
}
|
||||
|
||||
protected void removeSpi(String key) {
|
||||
root.remove(key);
|
||||
try {
|
||||
flush();
|
||||
} catch (BackingStoreException e) {
|
||||
logger.error("Unable to flush after removing " + key, e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void removeNodeSpi() throws BackingStoreException {
|
||||
isRemoved = true;
|
||||
flush();
|
||||
}
|
||||
|
||||
protected String[] keysSpi() throws BackingStoreException {
|
||||
return root.keySet().toArray(new String[root.keySet().size()]);
|
||||
}
|
||||
|
||||
protected String[] childrenNamesSpi() throws BackingStoreException {
|
||||
return children.keySet().toArray(new String[children.keySet().size()]);
|
||||
}
|
||||
|
||||
protected FilePreferences childSpi(String name) {
|
||||
FilePreferences child = children.get(name);
|
||||
if (child == null || child.isRemoved()) {
|
||||
child = new FilePreferences(this.backingFile, this, name);
|
||||
children.put(name, child);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
|
||||
protected void syncSpi() throws BackingStoreException {
|
||||
if (isRemoved()){
|
||||
return;
|
||||
}
|
||||
|
||||
if (!backingFile.exists()) {
|
||||
return;
|
||||
}
|
||||
synchronized (backingFile) {
|
||||
Properties p = new Properties();
|
||||
try {
|
||||
p.load(new FileInputStream(backingFile));
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
getPath(sb);
|
||||
String path = sb.toString();
|
||||
|
||||
final Enumeration<?> pnen = p.propertyNames();
|
||||
while (pnen.hasMoreElements()) {
|
||||
String propKey = (String) pnen.nextElement();
|
||||
if (propKey.startsWith(path)) {
|
||||
String subKey = propKey.substring(path.length());
|
||||
// Only load immediate descendants
|
||||
if (subKey.indexOf('.') == -1) {
|
||||
root.put(subKey, p.getProperty(propKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new BackingStoreException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void getPath(StringBuilder sb) {
|
||||
final FilePreferences parent = (FilePreferences) parent();
|
||||
if (parent == null) return;
|
||||
|
||||
parent.getPath(sb);
|
||||
sb.append(name()).append('.');
|
||||
}
|
||||
|
||||
protected void flushSpi() throws BackingStoreException {
|
||||
synchronized (backingFile) {
|
||||
Properties p = new Properties();
|
||||
try {
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
getPath(sb);
|
||||
String path = sb.toString();
|
||||
|
||||
if (backingFile.exists()) {
|
||||
p.load(new FileInputStream(backingFile));
|
||||
|
||||
List<String> toRemove = new ArrayList<String>();
|
||||
|
||||
// Make a list of all direct children of this node to be removed
|
||||
final Enumeration<?> pnen = p.propertyNames();
|
||||
while (pnen.hasMoreElements()) {
|
||||
String propKey = (String) pnen.nextElement();
|
||||
if (propKey.startsWith(path)) {
|
||||
String subKey = propKey.substring(path.length());
|
||||
// Only do immediate descendants
|
||||
if (subKey.indexOf('.') == -1) {
|
||||
toRemove.add(propKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove them now that the enumeration is done with
|
||||
for (String propKey : toRemove) {
|
||||
p.remove(propKey);
|
||||
}
|
||||
}
|
||||
|
||||
// If this node hasn't been removed, add back in any values
|
||||
if (!isRemoved) {
|
||||
for (String s : root.keySet()) {
|
||||
p.setProperty(path + s, root.get(s));
|
||||
}
|
||||
}
|
||||
|
||||
p.store(new FileOutputStream(backingFile), "FilePreferences");
|
||||
} catch (IOException e) {
|
||||
throw new BackingStoreException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2019-2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.preferences;
|
||||
|
||||
import eu.binjr.common.concurrent.ReadWriteLockHelper;
|
||||
import eu.binjr.common.logging.Logger;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* Manages access to a persistent, Last In First Out, auto de-duplicating collection, whose main purpose is to
|
||||
* handle "most recently used" item lists in UI applications.
|
||||
*
|
||||
* @param <T> The type of item to collect
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public abstract class MostRecentlyUsedList<T> implements ReloadableItemStore.Reloadable {
|
||||
private static final Logger logger = Logger.create(MostRecentlyUsedList.class);
|
||||
private final int capacity;
|
||||
private final String key;
|
||||
private final Preferences backingStore;
|
||||
ReadWriteLockHelper monitor = new ReadWriteLockHelper(new ReentrantReadWriteLock());
|
||||
private final Deque<T> mruList;
|
||||
private Consumer<T> onItemEvicted;
|
||||
|
||||
public void setOnItemEvicted(Consumer<T> onItemEvicted) {
|
||||
this.onItemEvicted = onItemEvicted;
|
||||
}
|
||||
|
||||
protected Preferences getBackingStore() {
|
||||
return backingStore;
|
||||
}
|
||||
|
||||
protected String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
protected MostRecentlyUsedList(String key, int capacity, Preferences backingStore) {
|
||||
this.key = key;
|
||||
this.backingStore = backingStore;
|
||||
this.capacity = capacity;
|
||||
mruList = failSafeLoad();
|
||||
}
|
||||
|
||||
protected abstract boolean validate(T value);
|
||||
|
||||
/**
|
||||
* Push an item to the top of the stack.
|
||||
* <p>
|
||||
* If the value is already present in the inner stack, it is removed from its current location and replaced on top.
|
||||
* <p>
|
||||
* If the defined capacity has been reached, the tail of the stack is culled.
|
||||
*
|
||||
* @param value the item to push on the stack.
|
||||
*/
|
||||
public void push(T value) {
|
||||
if (validate(value)) {
|
||||
monitor.write().lock(() -> {
|
||||
mruList.remove(value);
|
||||
mruList.push(value);
|
||||
if (mruList.size() > capacity) {
|
||||
var evicted = mruList.pollLast();
|
||||
if (onItemEvicted != null && evicted != null) {
|
||||
onItemEvicted.accept(evicted);
|
||||
}
|
||||
}
|
||||
failSafeSave();
|
||||
});
|
||||
} else {
|
||||
logger.debug("Invalid value to push in " + key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true is the list contains the specified value, false otherwise.
|
||||
*
|
||||
* @param value the value whose presence is to be tested
|
||||
* @return true is the list contains the specified value, false otherwise
|
||||
*/
|
||||
public boolean contains(T value) {
|
||||
return monitor.read().lock(() -> mruList.contains(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a value from the most recently used list.
|
||||
*
|
||||
* @param value the value to remove
|
||||
*/
|
||||
public void remove(T value) {
|
||||
monitor.write().lock(() -> {
|
||||
mruList.remove(value);
|
||||
failSafeSave();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the head of the stack, i.e. the last pushed item.
|
||||
*
|
||||
* @return the head of the stack
|
||||
*/
|
||||
public Optional<T> peek() {
|
||||
var mru = monitor.read().lock(mruList::peek);
|
||||
return mru != null ? Optional.of(mru) : Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload() {
|
||||
monitor.write().lock(() -> {
|
||||
mruList.clear();
|
||||
mruList.addAll(failSafeLoad());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the current items on the stack.
|
||||
*
|
||||
* @return all the current items on the stack.
|
||||
*/
|
||||
public Collection<T> getAll() {
|
||||
return monitor.read().lock(() -> List.copyOf(mruList));
|
||||
}
|
||||
|
||||
private void failSafeSave() {
|
||||
try {
|
||||
getBackingStore().node(getKey()).clear();
|
||||
int i = 0;
|
||||
for (T t : getAll()) {
|
||||
saveToBackend(i++, t);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.error("Failed to save preference " + key + " to backend: " + t.getMessage(), t);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void saveToBackend(int index, T value);
|
||||
|
||||
private Deque<T> failSafeLoad() {
|
||||
var mru = new LinkedList<T>();
|
||||
try {
|
||||
for (int i = 0; i < capacity; i++) {
|
||||
var o = loadFromBackend(i);
|
||||
if (o.isPresent()) {
|
||||
mru.add(o.get());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.error("Failed to load preference " + key + " from backend: " + t.getMessage(), t);
|
||||
}
|
||||
return mru;
|
||||
}
|
||||
|
||||
protected abstract Optional<T> loadFromBackend(int index);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2019-2020 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.preferences;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import javafx.beans.property.Property;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* A factory to create and manage preferences exposed as {@link Property} and backed by {@link Preferences}
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class MruFactory extends ReloadableItemStore<MostRecentlyUsedList<?>> {
|
||||
private static final Logger logger = Logger.create(MruFactory.class);
|
||||
|
||||
public MruFactory(String backingStoreKey) {
|
||||
super(backingStoreKey);
|
||||
}
|
||||
|
||||
public MostRecentlyUsedList<String> stringMostRecentlyUsedList(String key, int capacity) {
|
||||
var mru = new MostRecentlyUsedList<String>(key, capacity, backingStore) {
|
||||
@Override
|
||||
protected boolean validate(String value) {
|
||||
return (value != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(int index, String value) {
|
||||
getBackingStore().node(getKey()).put("value_" + index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<String> loadFromBackend(int index) {
|
||||
var p = getBackingStore().node(getKey()).get("value_" + index, null);
|
||||
if (p != null) {
|
||||
return Optional.of(p);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
};
|
||||
storedItems.put(key, mru);
|
||||
return mru;
|
||||
}
|
||||
|
||||
public MostRecentlyUsedList<URI> uriMostRecentlyUsedList(String key, int capacity) {
|
||||
var mru = new MostRecentlyUsedList<URI>(key, capacity, backingStore) {
|
||||
@Override
|
||||
protected boolean validate(URI value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(int index, URI value) {
|
||||
getBackingStore().node(getKey()).put("value_" + index, value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<URI> loadFromBackend(int index) {
|
||||
var p = getBackingStore().node(getKey()).get("value_" + index, "");
|
||||
if (!p.isEmpty()) {
|
||||
try {
|
||||
return Optional.of(new URI(p));
|
||||
} catch (URISyntaxException e) {
|
||||
logger.debug(() -> "Error reloading URI: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
};
|
||||
storedItems.put(key, mru);
|
||||
return mru;
|
||||
}
|
||||
|
||||
public MostRecentlyUsedList<Path> pathMostRecentlyUsedList(String key, int capacity, boolean mustBeDirectory) {
|
||||
var mru = new MostRecentlyUsedList<Path>(key, capacity, backingStore) {
|
||||
@Override
|
||||
protected boolean validate(Path value) {
|
||||
try {
|
||||
if (value != null && value.toRealPath(LinkOption.NOFOLLOW_LINKS) != null
|
||||
&& (!mustBeDirectory || value.toFile().isDirectory())) {
|
||||
return true;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.debug(() -> "Cannot insert into most recently used list: " + e.getMessage(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(int index, Path value) {
|
||||
getBackingStore().node(getKey()).put("value_" + index, value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<Path> loadFromBackend(int index) {
|
||||
var p = getBackingStore().node(getKey()).get("value_" + index, "");
|
||||
if (!p.isEmpty()) {
|
||||
return Optional.of(Path.of(p));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
};
|
||||
storedItems.put(key, mru);
|
||||
return mru;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2022-2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.preferences;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Encapsulates a String and obfuscates its content.
|
||||
* <p>
|
||||
* <b>Remark:</b> if an empty string is used to initialize the {@link ObfuscatedString}, then it will not be obfuscated (i.e.
|
||||
* its obfuscated value will also be an empty string).
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class ObfuscatedString {
|
||||
private static final Logger logger = LogManager.getLogger(ObfuscatedString.class);
|
||||
|
||||
private final String obfuscated;
|
||||
private final Obfuscator obfuscator;
|
||||
|
||||
private ObfuscatedString(String obfuscated, Obfuscator obfuscator) {
|
||||
Objects.requireNonNull(obfuscated, "Cannot create an ObfuscatedString instance from a null value");
|
||||
Objects.requireNonNull(obfuscator, "factory cannot be null");
|
||||
this.obfuscator = obfuscator;
|
||||
this.obfuscated = obfuscated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "************";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plain text representation of the encapsulated string
|
||||
*
|
||||
* @return the plain text representation of the encapsulated string
|
||||
*/
|
||||
public String toPlainText() {
|
||||
return obfuscator.deObfuscateString(obfuscated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the obfuscated representation of the encapsulated string
|
||||
*
|
||||
* @return the obfuscated representation of the encapsulated string
|
||||
*/
|
||||
public String getObfuscated() {
|
||||
return obfuscated;
|
||||
}
|
||||
|
||||
public abstract static class Obfuscator {
|
||||
|
||||
/**
|
||||
* Returns an {@link ObfuscatedString} instance that stores the value of the provided String.
|
||||
* <p>
|
||||
* <b>Remark:</b> if an empty string is used to initialize the {@link ObfuscatedString}, then it will not be obfuscated (i.e.
|
||||
* its obfuscated value will also be an empty string).
|
||||
*
|
||||
* @param plainText The value stored by the {@link ObfuscatedString} instance.
|
||||
* @return an {@link ObfuscatedString} instance that stores the value of the provided String.
|
||||
*/
|
||||
public ObfuscatedString fromPlainText(String plainText) {
|
||||
return fromObfuscatedText(obfuscateString(plainText));
|
||||
}
|
||||
|
||||
public ObfuscatedString fromObfuscatedText(String obfuscated) {
|
||||
return new ObfuscatedString(obfuscated, this);
|
||||
}
|
||||
|
||||
|
||||
protected abstract String deObfuscateString(String obfuscated);
|
||||
|
||||
protected abstract String obfuscateString(String clearText);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2019-2020 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.preferences;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import org.controlsfx.control.PropertySheet;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* An abstract class that defines a preference that can be accessed via a {@link Property} and is persisted as
|
||||
* user {@link Preferences}
|
||||
*
|
||||
* @param <T> The type of the inner value of the preference
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public abstract class ObservablePreference<T> implements ReloadableItemStore.Reloadable {
|
||||
private static final Logger logger = Logger.create(ObservablePreference.class);
|
||||
private final String key;
|
||||
private final T defaultValue;
|
||||
private final Preferences backingStore;
|
||||
private final Property<T> backingProperty;
|
||||
private final Class<T> innerType;
|
||||
|
||||
public ObservablePreference(Class<T> innerType, String key, T defaultValue, Preferences backingStore) {
|
||||
this.backingStore = backingStore;
|
||||
this.key = key;
|
||||
this.defaultValue = defaultValue;
|
||||
this.innerType = innerType;
|
||||
backingProperty = makeProperty(failSafeLoad());
|
||||
backingProperty.addListener((observable, oldValue, newValue) -> failSafeSave(newValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload() {
|
||||
backingProperty.setValue(failSafeLoad());
|
||||
}
|
||||
|
||||
public T get() {
|
||||
return backingProperty.getValue();
|
||||
}
|
||||
|
||||
public Property<T> property() {
|
||||
return backingProperty;
|
||||
}
|
||||
|
||||
public void set(T property) {
|
||||
this.backingProperty.setValue(property);
|
||||
}
|
||||
|
||||
protected abstract Property<T> makeProperty(T value);
|
||||
|
||||
protected abstract T loadFromBackend();
|
||||
|
||||
protected abstract void saveToBackend(T value);
|
||||
|
||||
protected Preferences getBackingStore() {
|
||||
return backingStore;
|
||||
}
|
||||
|
||||
protected String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
protected T getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
protected Class<T> getInnerType() {
|
||||
return innerType;
|
||||
}
|
||||
|
||||
private void failSafeSave(T value) {
|
||||
try {
|
||||
saveToBackend(value);
|
||||
} catch (Throwable t) {
|
||||
logger.error("Failed to save preference " + key + " to backend: " + t.getMessage(), t);
|
||||
}
|
||||
}
|
||||
|
||||
private T failSafeLoad() {
|
||||
try {
|
||||
return loadFromBackend();
|
||||
} catch (Throwable t) {
|
||||
logger.error("Failed to load preference " + key + " from backend: " + t.getMessage());
|
||||
logger.debug("Call Stack:", t);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuffer sb = new StringBuffer("ObservablePreference{");
|
||||
sb.append("key='").append(key).append('\'');
|
||||
sb.append(", defaultValue=").append(defaultValue);
|
||||
sb.append(", value=").append(backingProperty.getValue());
|
||||
sb.append(", innerType=").append(innerType);
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public PropertySheet.Item asPropertyItem() {
|
||||
return new PropertySheet.Item() {
|
||||
@Override
|
||||
public Class<?> getType() {
|
||||
return get().getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return backingStore.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getValue() {
|
||||
return get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
set((T) value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ObservableValue<? extends Object>> getObservableValue() {
|
||||
return Optional.of(property());
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
/*
|
||||
* Copyright 2019-2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.preferences;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import javafx.beans.property.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.prefs.Preferences;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A factory to create and manage preferences exposed as {@link Property} and backed by {@link Preferences}
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class ObservablePreferenceFactory extends ReloadableItemStore<ObservablePreference<?>> {
|
||||
private static final Logger logger = Logger.create(ObservablePreferenceFactory.class);
|
||||
|
||||
public ObservablePreferenceFactory(String backingStoreKey) {
|
||||
super(backingStoreKey);
|
||||
}
|
||||
|
||||
public ObservablePreference<Boolean> booleanPreference(String key, Boolean defaultValue) {
|
||||
var p = new ObservablePreference<>(Boolean.class, key, defaultValue, backingStore) {
|
||||
@Override
|
||||
protected Property<Boolean> makeProperty(Boolean value) {
|
||||
return new SimpleBooleanProperty(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean loadFromBackend() {
|
||||
return getBackingStore().getBoolean(getKey(), getDefaultValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(Boolean value) {
|
||||
getBackingStore().putBoolean(getKey(), value);
|
||||
}
|
||||
};
|
||||
storedItems.put(p.getKey(), p);
|
||||
return p;
|
||||
}
|
||||
|
||||
public ObservablePreference<ObfuscatedString> obfuscatedStringPreference(String key, String defaultValue, ObfuscatedString.Obfuscator obfuscator) {
|
||||
var p = new ObservablePreference<>(ObfuscatedString.class, key, obfuscator.fromPlainText(defaultValue), backingStore) {
|
||||
@Override
|
||||
protected Property<ObfuscatedString> makeProperty(ObfuscatedString value) {
|
||||
return new SimpleObjectProperty<>(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ObfuscatedString loadFromBackend() {
|
||||
return obfuscator.fromObfuscatedText(getBackingStore().get(getKey(), getDefaultValue().getObfuscated()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(ObfuscatedString value) {
|
||||
getBackingStore().put(getKey(), value.getObfuscated());
|
||||
}
|
||||
};
|
||||
storedItems.put(p.getKey(), p);
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
public ObservablePreference<String> stringPreference(String key, String defaultValue) {
|
||||
var p = new ObservablePreference<>(String.class, key, defaultValue, backingStore) {
|
||||
@Override
|
||||
protected Property<String> makeProperty(String value) {
|
||||
return new SimpleStringProperty(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String loadFromBackend() {
|
||||
return getBackingStore().get(getKey(), getDefaultValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(String value) {
|
||||
getBackingStore().put(getKey(), value);
|
||||
}
|
||||
};
|
||||
storedItems.put(p.getKey(), p);
|
||||
return p;
|
||||
}
|
||||
|
||||
public ObservablePreference<Number> integerPreference(String key, Integer defaultValue) {
|
||||
var p = new ObservablePreference<>(Number.class, key, defaultValue, backingStore) {
|
||||
@Override
|
||||
protected Property<Number> makeProperty(Number value) {
|
||||
return new SimpleIntegerProperty(value.intValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer loadFromBackend() {
|
||||
return getBackingStore().getInt(getKey(), getDefaultValue().intValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(Number value) {
|
||||
getBackingStore().putInt(getKey(), value.intValue());
|
||||
}
|
||||
};
|
||||
storedItems.put(p.getKey(), p);
|
||||
return p;
|
||||
}
|
||||
|
||||
public ObservablePreference<ZonedDateTime> zoneDateTimePreference(String key, ZonedDateTime defaultValue) {
|
||||
var p = new ObservablePreference<>(ZonedDateTime.class, key, defaultValue, backingStore) {
|
||||
@Override
|
||||
protected Property<ZonedDateTime> makeProperty(ZonedDateTime value) {
|
||||
return new SimpleObjectProperty<>(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ZonedDateTime loadFromBackend() {
|
||||
return ZonedDateTime.parse(getBackingStore().get(getKey(),
|
||||
getDefaultValue().format(DateTimeFormatter.ISO_ZONED_DATE_TIME)),
|
||||
DateTimeFormatter.ISO_ZONED_DATE_TIME);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(ZonedDateTime value) {
|
||||
getBackingStore().put(getKey(), value.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
|
||||
}
|
||||
};
|
||||
storedItems.put(p.getKey(), p);
|
||||
return p;
|
||||
}
|
||||
|
||||
public ObservablePreference<LocalDateTime> localDateTimePreference(String key, LocalDateTime defaultValue) {
|
||||
var p = new ObservablePreference<>(LocalDateTime.class, key, defaultValue, backingStore) {
|
||||
@Override
|
||||
protected Property<LocalDateTime> makeProperty(LocalDateTime value) {
|
||||
return new SimpleObjectProperty<>(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LocalDateTime loadFromBackend() {
|
||||
return LocalDateTime.parse(getBackingStore().get(getKey(),
|
||||
getDefaultValue().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)),
|
||||
DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(LocalDateTime value) {
|
||||
getBackingStore().put(getKey(), value.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
||||
}
|
||||
};
|
||||
storedItems.put(p.getKey(), p);
|
||||
return p;
|
||||
}
|
||||
|
||||
public ObservablePreference<Number> longPreference(String key, Long defaultValue) {
|
||||
var p = new ObservablePreference<>(Number.class, key, defaultValue, backingStore) {
|
||||
@Override
|
||||
protected Property<Number> makeProperty(Number value) {
|
||||
return new SimpleLongProperty(value.longValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Long loadFromBackend() {
|
||||
return getBackingStore().getLong(getKey(), getDefaultValue().longValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(Number value) {
|
||||
getBackingStore().putLong(getKey(), value.longValue());
|
||||
}
|
||||
};
|
||||
storedItems.put(p.getKey(), p);
|
||||
return p;
|
||||
}
|
||||
|
||||
public ObservablePreference<Number> doublePreference(String key, Double defaultValue) {
|
||||
var p = new ObservablePreference<>(Number.class, key, defaultValue, backingStore) {
|
||||
@Override
|
||||
protected Property<Number> makeProperty(Number value) {
|
||||
return new SimpleDoubleProperty(value.doubleValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Double loadFromBackend() {
|
||||
return getBackingStore().getDouble(getKey(), getDefaultValue().doubleValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(Number value) {
|
||||
getBackingStore().putDouble(getKey(), value.doubleValue());
|
||||
}
|
||||
};
|
||||
storedItems.put(p.getKey(), p);
|
||||
return p;
|
||||
}
|
||||
|
||||
public ObservablePreference<Path> pathPreference(String key, Path defaultValue) {
|
||||
var p = new ObservablePreference<>(Path.class, key, defaultValue, backingStore) {
|
||||
@Override
|
||||
protected Property<Path> makeProperty(Path value) {
|
||||
return new SimpleObjectProperty<>(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Path loadFromBackend() {
|
||||
return Path.of(getBackingStore().get(getKey(), getDefaultValue().toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(Path value) {
|
||||
getBackingStore().put(getKey(), value.toString());
|
||||
}
|
||||
};
|
||||
storedItems.put(p.getKey(), p);
|
||||
return p;
|
||||
}
|
||||
|
||||
public ObservablePreference<URI> uriPreference(String key, URI defaultValue) {
|
||||
var p = new ObservablePreference<>(URI.class, key, defaultValue, backingStore) {
|
||||
@Override
|
||||
protected Property<URI> makeProperty(URI value) {
|
||||
return new SimpleObjectProperty<>(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected URI loadFromBackend() {
|
||||
return URI.create(getBackingStore().get(getKey(), getDefaultValue().toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(URI value) {
|
||||
getBackingStore().put(getKey(), value.toString());
|
||||
}
|
||||
};
|
||||
storedItems.put(p.getKey(), p);
|
||||
return p;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public final <E extends Enum<E>> ObservablePreference<E> enumPreference(Class<E> type, String key, E defaultValue, E... forbiddenValues) {
|
||||
var p = new ObservablePreference<>(type, key, defaultValue, backingStore) {
|
||||
@Override
|
||||
protected Property<E> makeProperty(E value) {
|
||||
return new SimpleObjectProperty<>(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected E loadFromBackend() {
|
||||
return Enum.valueOf(getInnerType(), getBackingStore().get(getKey(), getDefaultValue().name()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(E value) {
|
||||
if (forbiddenValues != null) {
|
||||
for (var val : forbiddenValues) {
|
||||
if (value.equals(val)) {
|
||||
getBackingStore().put(getKey(), getDefaultValue().name());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
getBackingStore().put(getKey(), value.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(E value) {
|
||||
if (forbiddenValues != null) {
|
||||
for (var val : forbiddenValues) {
|
||||
if (value.equals(val)) {
|
||||
super.set(getDefaultValue());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
super.set(value);
|
||||
}
|
||||
};
|
||||
storedItems.put(p.getKey(), p);
|
||||
return p;
|
||||
}
|
||||
|
||||
public <T> ObservablePreference<T> objectPreference(Class<T> type,
|
||||
String key,
|
||||
T defaultValue,
|
||||
Function<T, String> convertToString,
|
||||
Function<String, T> parseFromString) {
|
||||
var p = new ObservablePreference<>(type, key, defaultValue, backingStore) {
|
||||
@Override
|
||||
protected Property<T> makeProperty(T value) {
|
||||
return new SimpleObjectProperty<>(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected T loadFromBackend() {
|
||||
return parseFromString.apply(getBackingStore().get(getKey(), convertToString.apply(getDefaultValue())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveToBackend(T value) {
|
||||
getBackingStore().put(getKey(), convertToString.apply(value));
|
||||
}
|
||||
};
|
||||
storedItems.put(p.getKey(), p);
|
||||
return p;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <U> Optional<ObservablePreference<U>> getByName(String name, Class<U> type) {
|
||||
var p = storedItems.get(name);
|
||||
if (p == null || !type.isAssignableFrom(p.getInnerType())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of((ObservablePreference<U>) p);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
public String toString() {
|
||||
return storedItems.values().stream().map(p -> p.getKey() + "=" + p.get()).collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2019-2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.preferences;
|
||||
|
||||
import eu.binjr.common.logging.Logger;
|
||||
import eu.binjr.core.Binjr;
|
||||
import eu.binjr.core.preferences.AppEnvironment;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.MapChangeListener;
|
||||
import javafx.collections.ObservableMap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.prefs.BackingStoreException;
|
||||
import java.util.prefs.InvalidPreferencesFormatException;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* Provides methods to access and store, import and export items that are persisted into a {@link Preferences} sub tree.
|
||||
*
|
||||
* @param <T> The type of items managed by the store
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public abstract class ReloadableItemStore<T extends ReloadableItemStore.Reloadable> {
|
||||
private static final Logger logger = Logger.create(ObservablePreferenceFactory.class);
|
||||
protected final Preferences backingStore;
|
||||
protected final ObservableMap<String, T> storedItems = FXCollections.observableMap(new ConcurrentHashMap<>());
|
||||
private ObservableMap<String, T> readOnlyStoreItems = FXCollections.unmodifiableObservableMap(storedItems);
|
||||
|
||||
ReloadableItemStore(String backingStoreKey) {
|
||||
this.backingStore = getBackingPreference(backingStoreKey);
|
||||
storedItems.addListener((MapChangeListener<String, T>) c -> {
|
||||
if (c.wasAdded()) {
|
||||
logger.trace(() -> "Preference added to store: " + c.getValueAdded().toString());
|
||||
}
|
||||
if (c.wasRemoved()) {
|
||||
logger.trace(() -> "Preference removed from store: " + c.getValueAdded().toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all managed items to their default values.
|
||||
*
|
||||
* @throws BackingStoreException if an error occurs while reloading items from the backing store
|
||||
*/
|
||||
public void reset() throws BackingStoreException {
|
||||
clearSubTree(backingStore);
|
||||
storedItems.values().forEach(T::reload);
|
||||
}
|
||||
|
||||
private static Preferences getBackingPreference(String name) {
|
||||
if (Boolean.parseBoolean(System.getProperty(AppEnvironment.PORTABLE_PROPERTY))) {
|
||||
try {
|
||||
var jarLocation = Paths.get(Binjr.class.getProtectionDomain().getCodeSource().getLocation().toURI());
|
||||
var configDir = jarLocation.getParent().getParent().resolve("settings");
|
||||
if (!Files.isDirectory(configDir)) {
|
||||
Files.createDirectory(configDir);
|
||||
}
|
||||
var preferencesFile = configDir.resolve("user").toFile();
|
||||
if (!preferencesFile.exists()) {
|
||||
preferencesFile.createNewFile();
|
||||
}
|
||||
return new FilePreferences(preferencesFile, null, "").node(name);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to create file to store preferences" + e.getMessage());
|
||||
logger.debug(() -> "Stack Trace", e);
|
||||
logger.warn("Non portable preference store will be used instead");
|
||||
}
|
||||
}
|
||||
return Preferences.userRoot().node(name);
|
||||
}
|
||||
|
||||
private void clearSubTree(Preferences node) throws BackingStoreException {
|
||||
if (node.nodeExists("")) {
|
||||
node.clear();
|
||||
for (var n : node.childrenNames()) {
|
||||
clearSubTree(node.node(n));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads all managed items from the backing store.
|
||||
*/
|
||||
public void reload() {
|
||||
storedItems.values().forEach(T::reload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all managed items to a file.
|
||||
*
|
||||
* @param savePath the path to export the data to.
|
||||
* @throws IOException if an IO error occrurs.
|
||||
* @throws BackingStoreException if an error occurs while accessing the backing store.
|
||||
*/
|
||||
public void exportToFile(Path savePath) throws IOException, BackingStoreException {
|
||||
try (var os = Files.newOutputStream(savePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
|
||||
backingStore.flush();
|
||||
backingStore.exportSubtree(os);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports items into the store from a file.
|
||||
*
|
||||
* @param savePath the path to import data from.
|
||||
* @throws IOException if an IO error occrurs.
|
||||
* @throws InvalidPreferencesFormatException if an error occurs while accessing the backing store.
|
||||
*/
|
||||
public void importFromFile(Path savePath) throws IOException, InvalidPreferencesFormatException {
|
||||
try (var is = Files.newInputStream(savePath, StandardOpenOption.READ)) {
|
||||
Preferences.importPreferences(is);
|
||||
storedItems.values().forEach(T::reload);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all managed items.
|
||||
*
|
||||
* @return all managed items.
|
||||
*/
|
||||
public ObservableMap<String, T> getAll() {
|
||||
return readOnlyStoreItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a specific item by its name.
|
||||
*
|
||||
* @param name the name of the item to retrieve/
|
||||
* @return an optional of the requested item, Optional.empty if no such item exists.
|
||||
*/
|
||||
public Optional<T> getByName(String name) {
|
||||
var p = storedItems.get(name);
|
||||
if (p == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines an item whose value can be stored into a {@link ReloadableItemStore} instance.
|
||||
*/
|
||||
public interface Reloadable {
|
||||
/**
|
||||
* Reloads the value of the item from the backing store.
|
||||
*/
|
||||
void reload();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2017-2019 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.text;
|
||||
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* An implementation of {@link PrefixFormatter} for binary prefixes
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class BinaryPrefixFormatter extends PrefixFormatter {
|
||||
|
||||
public static final int BASE = 1024;
|
||||
public static final TreeMap<Double, String> SUFFIX_MAP = new TreeMap<>() {{
|
||||
put(Math.pow(BASE, -4.0), "*2⁻¹⁰⁰⁰⁰⁰⁰⁰⁰⁰");
|
||||
put(Math.pow(BASE, -3.0), "*2⁻¹⁰⁰⁰⁰⁰⁰");
|
||||
put(Math.pow(BASE, -2.0), "*2⁻¹⁰⁰⁰");
|
||||
put(Math.pow(BASE, -1.0), "*2⁻¹⁰");
|
||||
put(Math.pow(BASE, 0.0), "");
|
||||
put(Math.pow(BASE, 1.0), "ki");
|
||||
put(Math.pow(BASE, 2.0), "Mi");
|
||||
put(Math.pow(BASE, 3.0), "Gi");
|
||||
put(Math.pow(BASE, 4.0), "Ti");
|
||||
put(Math.pow(BASE, 5.0), "Pi");
|
||||
put(Math.pow(BASE, 6.0), "Ei");
|
||||
}};
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link BinaryPrefixFormatter} class
|
||||
*/
|
||||
public BinaryPrefixFormatter() {
|
||||
super(SUFFIX_MAP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link BinaryPrefixFormatter} class with the specified format pattern.
|
||||
*
|
||||
* @param pattern a non-localized pattern string
|
||||
*/
|
||||
public BinaryPrefixFormatter(String pattern) {
|
||||
super(SUFFIX_MAP, pattern);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2017-2019 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.text;
|
||||
|
||||
import java.util.NavigableMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* An implementation of {@link PrefixFormatter} for metric prefixes
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class MetricPrefixFormatter extends PrefixFormatter {
|
||||
private static final NavigableMap<Double, String> SUFFIX_MAP = new TreeMap<>() {{
|
||||
put(Math.pow(BASE, -6.0), "a");
|
||||
put(Math.pow(BASE, -5.0), "f");
|
||||
put(Math.pow(BASE, -4.0), "p");
|
||||
put(Math.pow(BASE, -3.0), "n");
|
||||
put(Math.pow(BASE, -2.0), "µ");
|
||||
put(Math.pow(BASE, -1.0), "m");
|
||||
put(Math.pow(BASE, 0.0), "");
|
||||
put(Math.pow(BASE, 1.0), "k");
|
||||
put(Math.pow(BASE, 2.0), "M");
|
||||
put(Math.pow(BASE, 3.0), "G");
|
||||
put(Math.pow(BASE, 4.0), "T");
|
||||
put(Math.pow(BASE, 5.0), "P");
|
||||
put(Math.pow(BASE, 6.0), "E");
|
||||
}};
|
||||
|
||||
public static final int BASE = 1000;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link MetricPrefixFormatter} class
|
||||
*/
|
||||
public MetricPrefixFormatter() {
|
||||
super(SUFFIX_MAP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link MetricPrefixFormatter} class with the specified format pattern.
|
||||
*
|
||||
* @param pattern a non-localized pattern string
|
||||
*/
|
||||
public MetricPrefixFormatter(String pattern) {
|
||||
super(SUFFIX_MAP, pattern);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.text;
|
||||
|
||||
import java.util.NavigableMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* An implementation of {@link PrefixFormatter} for metric prefixes
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class NoopPrefixFormatter extends PrefixFormatter {
|
||||
private static final NavigableMap<Double, String> SUFFIX_MAP = new TreeMap<>() ;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link NoopPrefixFormatter} class
|
||||
*/
|
||||
public NoopPrefixFormatter() {
|
||||
super(SUFFIX_MAP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the {@link NoopPrefixFormatter} class with the specified format pattern.
|
||||
*
|
||||
* @param pattern a non-localized pattern string
|
||||
*/
|
||||
public NoopPrefixFormatter(String pattern) {
|
||||
super(SUFFIX_MAP, pattern);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2023 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.text;
|
||||
|
||||
import eu.binjr.common.text.PrefixFormatter;
|
||||
|
||||
import java.util.NavigableMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public class PercentagePrefixFormatter extends PrefixFormatter {
|
||||
public static final int BASE = 10;
|
||||
private static final NavigableMap<Double, String> SUFFIX_MAP = new TreeMap<>() {{
|
||||
put(0.01, "");
|
||||
}};
|
||||
|
||||
|
||||
public PercentagePrefixFormatter() {
|
||||
super(SUFFIX_MAP);
|
||||
}
|
||||
|
||||
public PercentagePrefixFormatter(String pattern) {
|
||||
super(SUFFIX_MAP, pattern);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2017-2019 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.text;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.NavigableMap;
|
||||
|
||||
/**
|
||||
* This class provides the interface and base implementation for formatting double or long into string
|
||||
* with a unit prefix.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public abstract class PrefixFormatter {
|
||||
private static final String DEFAULT_PATTERN = "###,###.##";
|
||||
private final NavigableMap<Double, String> suffixMap;
|
||||
private final DecimalFormat formatter;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of {@link PrefixFormatter} with the arithmetical base and a
|
||||
* representation of the prefixes.
|
||||
*
|
||||
* @param suffixMap a map of the suffix labels and the associated divider as the key.
|
||||
*/
|
||||
protected PrefixFormatter(NavigableMap<Double, String> suffixMap) {
|
||||
this(suffixMap, DEFAULT_PATTERN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of {@link PrefixFormatter} with the arithmetical base and a
|
||||
* representation of the prefixes.
|
||||
*
|
||||
* @param suffixMap a map of the suffix labels and the associated divider as the key.
|
||||
* @param pattern a non-localized pattern string.
|
||||
*/
|
||||
protected PrefixFormatter(NavigableMap<Double, String> suffixMap, String pattern) {
|
||||
this.suffixMap = suffixMap;
|
||||
this.formatter = new DecimalFormat(pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a {@code double} as a string with a unit prefix
|
||||
*
|
||||
* @param value the value to format
|
||||
* @return the formatted string
|
||||
*/
|
||||
public String format(double value) {
|
||||
if (Double.isNaN(value)) {
|
||||
return "NaN";
|
||||
}
|
||||
if (Double.isInfinite(value)) {
|
||||
return "Infinite";
|
||||
}
|
||||
if (value == Double.MIN_VALUE) {
|
||||
return format(Double.MIN_VALUE + 1);
|
||||
}
|
||||
if (value < 0) {
|
||||
return "-" + format(-value);
|
||||
}
|
||||
var e = suffixMap.floorEntry(value);
|
||||
if (e == null) {
|
||||
return formatter.format(value);
|
||||
}
|
||||
Double divideBy = e.getKey();
|
||||
String suffix = e.getValue();
|
||||
return formatter.format(value / divideBy) + suffix;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2019 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.text;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class StringUtils {
|
||||
private final static String NON_THIN = "[^iIl1\\.,']";
|
||||
private static final Logger logger = LogManager.getLogger(StringUtils.class);
|
||||
|
||||
private static int textWidth(String str) {
|
||||
return (int) (str.length() - str.replaceAll(NON_THIN, "").length() / 2);
|
||||
}
|
||||
|
||||
public static String ellipsize(String text, int max) {
|
||||
|
||||
if (textWidth(text) <= max)
|
||||
return text;
|
||||
|
||||
// Start by chopping off at the word before max
|
||||
// This is an over-approximation due to thin-characters...
|
||||
int end = text.lastIndexOf(' ', max - 3);
|
||||
|
||||
// Just one long word. Chop it off.
|
||||
if (end == -1)
|
||||
return text.substring(0, max - 3) + "...";
|
||||
|
||||
// Step forward as long as textWidth allows.
|
||||
int newEnd = end;
|
||||
do {
|
||||
end = newEnd;
|
||||
newEnd = text.indexOf(' ', end + 1);
|
||||
|
||||
// No more spaces.
|
||||
if (newEnd == -1)
|
||||
newEnd = text.length();
|
||||
|
||||
} while (textWidth(text.substring(0, newEnd) + "...") < max);
|
||||
|
||||
return text.substring(0, end) + "...";
|
||||
}
|
||||
|
||||
public static boolean contains(String str, String searchStr, boolean caseSensitive) {
|
||||
if (str == null || searchStr == null) return false;
|
||||
if (searchStr.isEmpty()) return true;
|
||||
if (caseSensitive) {
|
||||
return str.contains(searchStr);
|
||||
}
|
||||
final int length = searchStr.length();
|
||||
for (int i = str.length() - length; i >= 0; i--) {
|
||||
if (str.regionMatches(true, i, searchStr, 0, length))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String integerToOrdinal(int i) {
|
||||
if (i < 0) {
|
||||
throw new UnsupportedOperationException("Only positive integer are supported.");
|
||||
}
|
||||
int mod100 = i % 100;
|
||||
int mod10 = i % 10;
|
||||
if (mod10 == 1 && mod100 != 11) {
|
||||
return i + "st";
|
||||
} else if (mod10 == 2 && mod100 != 12) {
|
||||
return i + "nd";
|
||||
} else if (mod10 == 3 && mod100 != 13) {
|
||||
return i + "rd";
|
||||
} else {
|
||||
return i + "th";
|
||||
}
|
||||
}
|
||||
|
||||
public static String stringToEscapeSequence(String val) {
|
||||
if (val == null){
|
||||
return null;
|
||||
}
|
||||
switch (val.trim()) {
|
||||
case "\\t" -> {
|
||||
return "\t";
|
||||
}
|
||||
case "\'" -> {
|
||||
return "'";
|
||||
}
|
||||
case "\\" -> {
|
||||
return "\"";
|
||||
}
|
||||
case "\\n" -> {
|
||||
return "\n";
|
||||
}
|
||||
case "\\r" -> {
|
||||
return "\r";
|
||||
}
|
||||
case "\\f" -> {
|
||||
return "\f";
|
||||
}
|
||||
case "\\b" -> {
|
||||
return "\b";
|
||||
}
|
||||
case "\\\\" -> {
|
||||
return "\\";
|
||||
}
|
||||
case "\\r\\n" -> {
|
||||
return "\r\n";
|
||||
}
|
||||
case "\\n\\r" -> {
|
||||
return "\n\r";
|
||||
}
|
||||
default -> {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* Copyright (c) OSGi Alliance (2004, 2007). All Rights Reserved.
|
||||
* <br>
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <br>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <br>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.version;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
public class Version implements Comparable<Version> {
|
||||
public static final String SNAPSHOT = "-SNAPSHOT";
|
||||
private final int major;
|
||||
private final int minor;
|
||||
private final int micro;
|
||||
private final String qualifier;
|
||||
private final boolean isSnapshot;
|
||||
private static final String SEPARATOR = ".";
|
||||
/**
|
||||
* The empty version "0.0.0". Equivalent to calling
|
||||
* <code>new Version(0,0,0)</code>.
|
||||
*/
|
||||
public static final Version emptyVersion = new Version(0, 0, 0);
|
||||
|
||||
/**
|
||||
* Creates a version identifier from the specified numerical components.
|
||||
* <br>
|
||||
* <br>
|
||||
* The qualifier is set to the empty string.
|
||||
*
|
||||
* @param major Major component of the version identifier.
|
||||
* @param minor Minor component of the version identifier.
|
||||
* @param micro Micro component of the version identifier.
|
||||
* @throws IllegalArgumentException If the numerical components are
|
||||
* negative.
|
||||
*/
|
||||
public Version(int major, int minor, int micro) {
|
||||
this(major, minor, micro, null);
|
||||
}
|
||||
|
||||
public Version(int major, int minor, int micro, String qualifier) {
|
||||
this(major, minor, micro, qualifier, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a version identifier from the specifed components.
|
||||
*
|
||||
* @param major Major component of the version identifier.
|
||||
* @param minor Minor component of the version identifier.
|
||||
* @param micro Micro component of the version identifier.
|
||||
* @param qualifier Qualifier component of the version identifier. If
|
||||
* <code>null</code> is specified, then the qualifier will be set
|
||||
* to the empty string.
|
||||
* @param snapshot true if the version is a snapshot.
|
||||
* @throws IllegalArgumentException If the numerical components are negative
|
||||
* or the qualifier string is invalid.
|
||||
*/
|
||||
public Version(int major, int minor, int micro, String qualifier, boolean snapshot) {
|
||||
if (qualifier == null) {
|
||||
qualifier = ""; //$NON-NLS-1$
|
||||
}
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
this.micro = micro;
|
||||
this.qualifier = qualifier;
|
||||
this.isSnapshot = snapshot;
|
||||
validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Created a version identifier from the specified string.
|
||||
* <br>
|
||||
* <br>
|
||||
* Here is the grammar for version strings.
|
||||
* <br>
|
||||
* <pre>
|
||||
* version ::= major('.'minor('.'micro('.'qualifier)?)?)?
|
||||
* major ::= digit+
|
||||
* minor ::= digit+
|
||||
* micro ::= digit+
|
||||
* qualifier ::= (alpha|digit|'_'|'-')+
|
||||
* digit ::= [0..9]
|
||||
* alpha ::= [a..zA..Z]
|
||||
* </pre>
|
||||
* <br>
|
||||
* There must be no whitespace in version.
|
||||
*
|
||||
* @param version String representation of the version identifier.
|
||||
* @throws IllegalArgumentException If <code>version</code> is improperly
|
||||
* formatted.
|
||||
*/
|
||||
public Version(String version) {
|
||||
if (version == null) {
|
||||
throw new IllegalArgumentException("Provided version string is null");
|
||||
}
|
||||
int major = 0;
|
||||
int minor = 0;
|
||||
int micro = 0;
|
||||
String qualifier = ""; //$NON-NLS-1$
|
||||
if (version.endsWith(SNAPSHOT)) {
|
||||
this.isSnapshot = true;
|
||||
version = version.substring(0, version.length() - 9);
|
||||
} else {
|
||||
isSnapshot = false;
|
||||
}
|
||||
|
||||
try {
|
||||
StringTokenizer st = new StringTokenizer(version, SEPARATOR+"-", true);
|
||||
major = Integer.parseInt(st.nextToken());
|
||||
if (st.hasMoreTokens()) {
|
||||
st.nextToken(); // consume delimiter
|
||||
minor = Integer.parseInt(st.nextToken());
|
||||
if (st.hasMoreTokens()) {
|
||||
st.nextToken(); // consume delimiter
|
||||
micro = Integer.parseInt(st.nextToken());
|
||||
if (st.hasMoreTokens()) {
|
||||
st.nextToken(); // consume delimiter
|
||||
qualifier = st.nextToken();
|
||||
if (st.hasMoreTokens()) {
|
||||
throw new IllegalArgumentException("invalid format"); //$NON-NLS-1$
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (NoSuchElementException e) {
|
||||
throw new IllegalArgumentException("invalid format"); //$NON-NLS-1$
|
||||
}
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
this.micro = micro;
|
||||
this.qualifier = qualifier;
|
||||
validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the Version constructors to validate the version components.
|
||||
*
|
||||
* @throws IllegalArgumentException If the numerical components are negative
|
||||
* or the qualifier string is invalid.
|
||||
*/
|
||||
private void validate() {
|
||||
if (major < 0) {
|
||||
throw new IllegalArgumentException("negative major"); //$NON-NLS-1$
|
||||
}
|
||||
if (minor < 0) {
|
||||
throw new IllegalArgumentException("negative minor"); //$NON-NLS-1$
|
||||
}
|
||||
if (micro < 0) {
|
||||
throw new IllegalArgumentException("negative micro"); //$NON-NLS-1$
|
||||
}
|
||||
int length = qualifier.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
if ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-".indexOf(qualifier.charAt(i)) == -1) { //$NON-NLS-1$
|
||||
throw new IllegalArgumentException("invalid qualifier"); //$NON-NLS-1$
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a version identifier from the specified string.
|
||||
* <br>
|
||||
* <br>
|
||||
* See <code>Version(String)</code> for the format of the version string.
|
||||
*
|
||||
* @param version String representation of the version identifier. Leading
|
||||
* and trailing whitespace will be ignored.
|
||||
* @return A <code>Version</code> object representing the version
|
||||
* identifier. If <code>version</code> is <code>null</code> or
|
||||
* the empty string then <code>emptyVersion</code> will be
|
||||
* returned.
|
||||
* @throws IllegalArgumentException If <code>version</code> is improperly
|
||||
* formatted.
|
||||
*/
|
||||
public static Version parseVersion(String version) {
|
||||
if (version == null) {
|
||||
return emptyVersion;
|
||||
}
|
||||
version = version.trim();
|
||||
if (version.length() == 0) {
|
||||
return emptyVersion;
|
||||
}
|
||||
return new Version(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the major component of this version identifier.
|
||||
*
|
||||
* @return The major component.
|
||||
*/
|
||||
public int getMajor() {
|
||||
return major;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minor component of this version identifier.
|
||||
*
|
||||
* @return The minor component.
|
||||
*/
|
||||
public int getMinor() {
|
||||
return minor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the micro component of this version identifier.
|
||||
*
|
||||
* @return The micro component.
|
||||
*/
|
||||
public int getMicro() {
|
||||
return micro;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the qualifier component of this version identifier.
|
||||
*
|
||||
* @return The qualifier component.
|
||||
*/
|
||||
public String getQualifier() {
|
||||
return qualifier;
|
||||
}
|
||||
|
||||
public boolean isSnapshot() {
|
||||
return isSnapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string representation of this version identifier.
|
||||
* <br>
|
||||
* <br>
|
||||
* The format of the version string will be <code>major.minor.micro</code>
|
||||
* if qualifier is the empty string or
|
||||
* <code>major.minor.micro-qualifier</code> otherwise.
|
||||
*
|
||||
* @return The string representation of this version identifier.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
String base = major + SEPARATOR + minor + SEPARATOR + micro;
|
||||
if (qualifier.length() == 0) { //$NON-NLS-1$
|
||||
return isSnapshot ? base + SNAPSHOT : base;
|
||||
} else {
|
||||
return isSnapshot ? base + "-" + qualifier + SNAPSHOT : base + "-" + qualifier;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hash code value for the object.
|
||||
*
|
||||
* @return An integer which is a hash code value for this object.
|
||||
*/
|
||||
public int hashCode() {
|
||||
return ((major << 24) + (minor << 16) + (micro << 8) + qualifier.hashCode()) * (isSnapshot ? -1 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this <code>Version</code> object to another object.
|
||||
* <br>
|
||||
* <br>
|
||||
* A version is considered to be <b>equal to </b> another version if the
|
||||
* major, minor and micro components are equal and the qualifier component
|
||||
* is equal (using <code>String.equals</code>).
|
||||
*
|
||||
* @param object The <code>Version</code> object to be compared.
|
||||
* @return <code>true</code> if <code>object</code> is a
|
||||
* <code>Version</code> and is equal to this object;
|
||||
* <code>false</code> otherwise.
|
||||
*/
|
||||
public boolean equals(Object object) {
|
||||
if (object == this) { // quicktest
|
||||
return true;
|
||||
}
|
||||
if (!(object instanceof Version)) {
|
||||
return false;
|
||||
}
|
||||
Version other = (Version) object;
|
||||
return (major == other.major) && (minor == other.minor)
|
||||
&& (micro == other.micro) && qualifier.equals(other.qualifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this <code>Version</code> object to another object.
|
||||
* <br>
|
||||
* <br>
|
||||
* A version is considered to be <b>less than </b> another version if its
|
||||
* major component is less than the other version's major component, or the
|
||||
* major components are equal and its minor component is less than the other
|
||||
* version's minor component, or the major and minor components are equal
|
||||
* and its micro component is less than the other version's micro component,
|
||||
* or the major, minor and micro components are equal and it's qualifier
|
||||
* component is less than the other version's qualifier component (using
|
||||
* <code>String.compareTo</code>).
|
||||
* <br>
|
||||
* <br>
|
||||
* A version is considered to be <b>equal to</b> another version if the
|
||||
* major, minor and micro components are equal and the qualifier component
|
||||
* is equal (using <code>String.compareTo</code>).
|
||||
*
|
||||
* @param other The <code>Version</code> object to be compared.
|
||||
* @return A negative integer, zero, or a positive integer if this object is
|
||||
* less than, equal to, or greater than the specified
|
||||
* <code>Version</code> object.
|
||||
* @throws ClassCastException If the specified object is not a
|
||||
* <code>Version</code>.
|
||||
*/
|
||||
public int compareTo(Version other) {
|
||||
if (other == this) { // quicktest
|
||||
return 0;
|
||||
}
|
||||
int result = major - other.major;
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = minor - other.minor;
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = micro - other.micro;
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = qualifier.compareTo(other.qualifier);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return (this.isSnapshot ? 0 : 1) - (other.isSnapshot ? 0 : 1);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2017-2021 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.xml;
|
||||
|
||||
import jakarta.xml.bind.JAXBContext;
|
||||
import jakarta.xml.bind.JAXBException;
|
||||
import jakarta.xml.bind.Marshaller;
|
||||
import jakarta.xml.bind.Unmarshaller;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.XMLReader;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
import javax.xml.stream.XMLInputFactory;
|
||||
import javax.xml.stream.XMLStreamException;
|
||||
import javax.xml.stream.XMLStreamReader;
|
||||
import javax.xml.transform.*;
|
||||
import javax.xml.transform.sax.SAXSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import javax.xml.transform.stream.StreamSource;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A collections of convenience methods to help with serialization and deserialization of XML to and from Java objects.
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class XmlUtils {
|
||||
|
||||
public static String getFirstAttributeValue(File file, String attribute) throws IOException, XMLStreamException {
|
||||
XMLStreamReader xmlr = XMLInputFactoryHolder.instance.createXMLStreamReader(new FileInputStream(file));
|
||||
while (xmlr.hasNext()) {
|
||||
if (xmlr.getEventType() == XMLStreamReader.START_ELEMENT) {
|
||||
for (int i = 0; i < xmlr.getAttributeCount(); i++) {
|
||||
String localName = xmlr.getAttributeName(i).getLocalPart();
|
||||
if (localName.equals(attribute)) {
|
||||
return xmlr.getAttributeValue(i);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
xmlr.next();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String processXslt(String xslString, String xmlString) throws TransformerException, IOException {
|
||||
Objects.requireNonNull(xslString, "xslString cannot be null");
|
||||
Objects.requireNonNull(xmlString, "xmlString cannot be null");
|
||||
return applyTransform(new StreamSource(new StringReader(xslString)), new StreamSource(new StringReader(xmlString)));
|
||||
}
|
||||
|
||||
public static String processXslt(InputStream inXsl, InputStream inXml) throws TransformerException, IOException {
|
||||
Objects.requireNonNull(inXsl, "inXsl cannot be null");
|
||||
Objects.requireNonNull(inXml, "inXml cannot be null");
|
||||
return applyTransform(new StreamSource(inXsl), new StreamSource(inXml));
|
||||
}
|
||||
|
||||
private static String applyTransform(StreamSource xslt, StreamSource xml) throws TransformerException, IOException {
|
||||
TransformerFactory factory = TransformerFactory.newInstance();
|
||||
Transformer transformer = factory.newTransformer(xslt);
|
||||
try (Writer outputWriter = new StringWriter()) {
|
||||
Result outputResult = new StreamResult(outputWriter);
|
||||
transformer.transform(xml, outputResult);
|
||||
return outputWriter.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T deSerialize(File file, Class<?>... classes) throws JAXBException, IOException {
|
||||
try (FileInputStream fin = new FileInputStream(file)) {
|
||||
return deSerialize(fin, classes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize the XML content of a stream into a Java object of the specified type.
|
||||
*
|
||||
* @param classes The classes of the object to unmarshall the XML as
|
||||
* @param inputStream An input stream containing the XML to deserialize
|
||||
* @param <T> The type of object to unmarshall the XML as
|
||||
* @return The deserialized object
|
||||
* @throws JAXBException if an error occurs during deserialization
|
||||
*/
|
||||
public static <T> T deSerialize(InputStream inputStream, Class<?>... classes) throws JAXBException {
|
||||
return deSerialize(new StreamSource(inputStream), classes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize the XML content of a string into a Java object of the specified type.
|
||||
*
|
||||
* @param classes The classes of the object to unmarshall the XML as
|
||||
* @param xmlString The XML to deserialize, as a string
|
||||
* @param <T> The type of object to unmarshall the XML as
|
||||
* @return The deserialized object
|
||||
* @throws JAXBException if an error occurs during deserialization
|
||||
*/
|
||||
public static <T> T deSerialize(String xmlString, Class<?>... classes) throws JAXBException {
|
||||
return deSerialize(new StreamSource(new StringReader(xmlString)), classes);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T deSerialize(StreamSource source, Class<?>... classes) throws JAXBException {
|
||||
JAXBContext jc = JAXBContext.newInstance(classes);
|
||||
Unmarshaller unmarshaller = jc.createUnmarshaller();
|
||||
return (T) unmarshaller.unmarshal(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link SAXSource} for the provided {@link InputStream} that explicitly forfeit external DTD validation
|
||||
*
|
||||
* @param in the {@link InputStream} for the {@link SAXSource}
|
||||
* @return a {@link SAXSource} for the provided {@link InputStream} that explicitly forfeit external DTD validation
|
||||
* @throws SAXException if a SAX error occurs
|
||||
* @throws ParserConfigurationException if a configuration error occurs
|
||||
*/
|
||||
public static Source toNonValidatingSAXSource(InputStream in) throws SAXException, ParserConfigurationException {
|
||||
SAXParserFactory spf = SAXParserFactory.newInstance();
|
||||
spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
|
||||
XMLReader xmlReader = spf.newSAXParser().getXMLReader();
|
||||
InputSource inputSource = new InputSource(in);
|
||||
return new SAXSource(xmlReader, inputSource);
|
||||
}
|
||||
|
||||
public static <T> void serialize(T object, Path path, Class<?>... classes) throws JAXBException, IOException {
|
||||
serialize(object, path.toFile(), classes);
|
||||
}
|
||||
|
||||
public static <T> void serialize(T object, File file, Class<?>... classes) throws JAXBException, IOException {
|
||||
try (FileOutputStream fout = new FileOutputStream(file)) {
|
||||
serialize(object, fout, classes);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> String serialize(T object, Class<?>... classes) throws IOException, JAXBException {
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
serialize(object, out, classes);
|
||||
return out.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static <T> void serialize(T object, OutputStream out, Class<?>... classes) throws JAXBException {
|
||||
if (classes == null || classes.length == 0) {
|
||||
classes = new Class<?>[]{object.getClass()};
|
||||
}
|
||||
JAXBContext jaxbContext = JAXBContext.newInstance(classes);
|
||||
Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
|
||||
jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
|
||||
jaxbMarshaller.marshal(object, out);
|
||||
}
|
||||
|
||||
private static class XMLInputFactoryHolder {
|
||||
private final static XMLInputFactory instance = XMLInputFactory.newInstance();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2017-2018 Frederic Thevenet
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.xml.adapters;
|
||||
|
||||
import jakarta.xml.bind.annotation.adapters.XmlAdapter;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
|
||||
/**
|
||||
* An {@link XmlAdapter} for {@link Color} objects
|
||||
*
|
||||
* @author Frederic Thevenet
|
||||
*/
|
||||
public class ColorXmlAdapter extends XmlAdapter<String, Color> {
|
||||
/**
|
||||
* Initializes a new instance of the {@link ColorXmlAdapter} class
|
||||
*/
|
||||
public ColorXmlAdapter() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Color unmarshal(String stringValue) {
|
||||
return stringValue != null ? Color.valueOf(stringValue) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String marshal(Color value) {
|
||||
return value != null ? value.toString() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Mikhail Sokolov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.xml.adapters;
|
||||
|
||||
|
||||
import jakarta.xml.bind.annotation.adapters.XmlAdapter;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* {@code XmlAdapter} mapping JSR-310 {@code Duration} to ISO-8601 string
|
||||
* <p>
|
||||
* String format details:
|
||||
* <ul>
|
||||
* <li>{@link Duration#parse(CharSequence)}</li>
|
||||
* <li>{@link Duration#toString()}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see XmlAdapter
|
||||
* @see Duration
|
||||
*/
|
||||
public class DurationXmlAdapter extends XmlAdapter<String, Duration> {
|
||||
@Override
|
||||
public Duration unmarshal(String stringValue) {
|
||||
return stringValue != null ? Duration.parse(stringValue) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String marshal(Duration value) {
|
||||
return value != null ? value.toString() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Mikhail Sokolov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.xml.adapters;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* {@code XmlAdapter} mapping JSR-310 {@code Instant} to ISO-8601 string
|
||||
* <p>
|
||||
* String format details: {@link DateTimeFormatter#ISO_INSTANT}
|
||||
*
|
||||
* @see Instant
|
||||
*/
|
||||
public class InstantXmlAdapter extends TemporalAccessorXmlAdapter<Instant> {
|
||||
public InstantXmlAdapter() {
|
||||
super(DateTimeFormatter.ISO_INSTANT, Instant::from);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Mikhail Sokolov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.xml.adapters;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* {@code XmlAdapter} mapping JSR-310 {@code LocalDateTime} to ISO-8601 string
|
||||
* <p>
|
||||
* String format details: {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME}
|
||||
*
|
||||
|
||||
* @see LocalDateTime
|
||||
*/
|
||||
public class LocalDateTimeXmlAdapter extends TemporalAccessorXmlAdapter<LocalDateTime> {
|
||||
public LocalDateTimeXmlAdapter() {
|
||||
super(DateTimeFormatter.ISO_LOCAL_DATE_TIME, LocalDateTime::from);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Mikhail Sokolov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.binjr.common.xml.adapters;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* {@code XmlAdapter} mapping JSR-310 {@code LocalDate} to ISO-8601 string
|
||||
* <p>
|
||||
* It uses {@link DateTimeFormatter#ISO_DATE} for parsing and serializing,
|
||||
* time-zone information ignored.
|
||||
*
|
||||
* @see LocalDate
|
||||
*/
|
||||
public class LocalDateXmlAdapter extends TemporalAccessorXmlAdapter<LocalDate> {
|
||||
public LocalDateXmlAdapter() {
|
||||
super(DateTimeFormatter.ISO_DATE, LocalDate::from);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user