mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-10-01 09:40:35 +13:00
Add validators, rework machine stores
This commit is contained in:
parent
f0f1417980
commit
4eb0c80d60
23 changed files with 608 additions and 33 deletions
|
@ -114,9 +114,9 @@ public class BeaconServer {
|
||||||
// Prepare for invalid XPIPE_HOME path value
|
// Prepare for invalid XPIPE_HOME path value
|
||||||
try {
|
try {
|
||||||
if (System.getProperty("os.name").startsWith("Windows")) {
|
if (System.getProperty("os.name").startsWith("Windows")) {
|
||||||
file = Path.of(env, "app", "xpipe.exe");
|
file = Path.of(env, "app", "xpiped.exe");
|
||||||
} else {
|
} else {
|
||||||
file = Path.of(env, "app", "bin", "xpipe");
|
file = Path.of(env, "app", "bin", "xpiped");
|
||||||
}
|
}
|
||||||
return Files.exists(file) ? Optional.of(file) : Optional.empty();
|
return Files.exists(file) ? Optional.of(file) : Optional.empty();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
@ -132,9 +132,9 @@ public class BeaconServer {
|
||||||
|
|
||||||
Path file;
|
Path file;
|
||||||
if (System.getProperty("os.name").startsWith("Windows")) {
|
if (System.getProperty("os.name").startsWith("Windows")) {
|
||||||
file = Path.of(System.getenv("LOCALAPPDATA"), "X-Pipe", "app", "xpipe.exe");
|
file = Path.of(System.getenv("LOCALAPPDATA"), "X-Pipe", "app", "xpiped.exe");
|
||||||
} else {
|
} else {
|
||||||
file = Path.of("/opt/xpipe/app/bin/xpipe");
|
file = Path.of("/opt/xpipe/app/bin/xpiped");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Files.exists(file)) {
|
if (Files.exists(file)) {
|
||||||
|
|
|
@ -16,6 +16,10 @@ import java.util.Optional;
|
||||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
||||||
public interface DataStore {
|
public interface DataStore {
|
||||||
|
|
||||||
|
default boolean isComplete() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
default void validate() throws Exception {
|
default void validate() throws Exception {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,22 +13,29 @@ import java.nio.file.Path;
|
||||||
public class FileStore implements StreamDataStore, FilenameStore {
|
public class FileStore implements StreamDataStore, FilenameStore {
|
||||||
|
|
||||||
public static FileStore local(Path p) {
|
public static FileStore local(Path p) {
|
||||||
return new FileStore(MachineStore.local(), p.toString());
|
return new FileStore(MachineFileStore.local(), p.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FileStore local(String p) {
|
public static FileStore local(String p) {
|
||||||
return new FileStore(MachineStore.local(), p);
|
return new FileStore(MachineFileStore.local(), p);
|
||||||
}
|
}
|
||||||
|
|
||||||
MachineStore machine;
|
MachineFileStore machine;
|
||||||
String file;
|
String file;
|
||||||
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public FileStore(MachineStore machine, String file) {
|
public FileStore(MachineFileStore machine, String file) {
|
||||||
this.machine = machine;
|
this.machine = machine;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate() throws Exception {
|
||||||
|
if (!machine.exists(file)) {
|
||||||
|
throw new IllegalStateException("File " + file + " could not be found on machine " + machine.toDisplay());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream openInput() throws Exception {
|
public InputStream openInput() throws Exception {
|
||||||
return machine.openInput(file);
|
return machine.openInput(file);
|
||||||
|
@ -40,7 +47,7 @@ public class FileStore implements StreamDataStore, FilenameStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canOpen() {
|
public boolean canOpen() throws Exception {
|
||||||
return machine.exists(file);
|
return machine.exists(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +63,6 @@ public class FileStore implements StreamDataStore, FilenameStore {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFileName() {
|
public String getFileName() {
|
||||||
return Path.of(file).getFileName().toString();
|
return file;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
package io.xpipe.core.store;
|
package io.xpipe.core.store;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||||
|
import lombok.Value;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@JsonTypeName("local")
|
@JsonTypeName("local")
|
||||||
public class LocalMachineStore implements MachineStore {
|
@Value
|
||||||
|
public class LocalStore implements ShellStore {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean exists(String file) {
|
public boolean exists(String file) {
|
||||||
|
@ -31,4 +35,19 @@ public class LocalMachineStore implements MachineStore {
|
||||||
var p = Path.of(file);
|
var p = Path.of(file);
|
||||||
return Files.newOutputStream(p);
|
return Files.newOutputStream(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String executeAndRead(List<String> cmd) throws Exception {
|
||||||
|
var p = prepare(cmd).redirectErrorStream(true);
|
||||||
|
var proc = p.start();
|
||||||
|
var b = proc.getInputStream().readAllBytes();
|
||||||
|
proc.waitFor();
|
||||||
|
//TODO
|
||||||
|
return new String(b, StandardCharsets.UTF_16LE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> createCommand(List<String> cmd) {
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -3,15 +3,15 @@ package io.xpipe.core.store;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
public interface MachineStore extends DataStore {
|
public interface MachineFileStore extends DataStore {
|
||||||
|
|
||||||
static MachineStore local() {
|
static MachineFileStore local() {
|
||||||
return new LocalMachineStore();
|
return new LocalStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
InputStream openInput(String file) throws Exception;
|
InputStream openInput(String file) throws Exception;
|
||||||
|
|
||||||
OutputStream openOutput(String file) throws Exception;
|
OutputStream openOutput(String file) throws Exception;
|
||||||
|
|
||||||
public boolean exists(String file);
|
public boolean exists(String file) throws Exception;
|
||||||
}
|
}
|
19
core/src/main/java/io/xpipe/core/store/ShellStore.java
Normal file
19
core/src/main/java/io/xpipe/core/store/ShellStore.java
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package io.xpipe.core.store;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ShellStore extends MachineFileStore {
|
||||||
|
|
||||||
|
static ShellStore local() {
|
||||||
|
return new LocalStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
default ProcessBuilder prepare(List<String> cmd) throws Exception {
|
||||||
|
var toExec = createCommand(cmd);
|
||||||
|
return new ProcessBuilder(toExec);
|
||||||
|
}
|
||||||
|
|
||||||
|
String executeAndRead(List<String> cmd) throws Exception;
|
||||||
|
|
||||||
|
List<String> createCommand(List<String> cmd);
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package io.xpipe.core.store;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface StandardShellStore extends ShellStore {
|
||||||
|
|
||||||
|
static interface ShellType {
|
||||||
|
|
||||||
|
List<String> createFileReadCommand(String file);
|
||||||
|
|
||||||
|
List<String> createFileWriteCommand(String file);
|
||||||
|
|
||||||
|
List<String> createFileExistsCommand(String file);
|
||||||
|
|
||||||
|
Charset getCharset();
|
||||||
|
|
||||||
|
String getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
default String executeAndRead(List<String> cmd) throws Exception {
|
||||||
|
var type = determineType();
|
||||||
|
var p = prepare(cmd).redirectErrorStream(true);
|
||||||
|
var proc = p.start();
|
||||||
|
var s = new String(proc.getInputStream().readAllBytes(), type.getCharset());
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> createCommand(List<String> cmd);
|
||||||
|
|
||||||
|
ShellType determineType();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default InputStream openInput(String file) throws Exception {
|
||||||
|
var type = determineType();
|
||||||
|
var cmd = type.createFileReadCommand(file);
|
||||||
|
var p = prepare(cmd).redirectErrorStream(true);
|
||||||
|
var proc = p.start();
|
||||||
|
return proc.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default OutputStream openOutput(String file) throws Exception {
|
||||||
|
var type = determineType();
|
||||||
|
var cmd = type.createFileWriteCommand(file);
|
||||||
|
var p = prepare(cmd).redirectErrorStream(true);
|
||||||
|
var proc = p.start();
|
||||||
|
return proc.getOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default boolean exists(String file) throws Exception {
|
||||||
|
var type = determineType();
|
||||||
|
var cmd = type.createFileExistsCommand(file);
|
||||||
|
var p = prepare(cmd).redirectErrorStream(true);
|
||||||
|
var proc = p.start();
|
||||||
|
return proc.waitFor() == 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,7 +37,7 @@ public interface StreamDataStore extends DataStore {
|
||||||
throw new UnsupportedOperationException("Can't open store output");
|
throw new UnsupportedOperationException("Can't open store output");
|
||||||
}
|
}
|
||||||
|
|
||||||
default boolean canOpen() {
|
default boolean canOpen() throws Exception {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package io.xpipe.core.store;
|
package io.xpipe.core.store;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
@ -10,11 +10,15 @@ import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
@JsonTypeName("string")
|
@JsonTypeName("string")
|
||||||
@AllArgsConstructor
|
|
||||||
public class StringStore implements StreamDataStore {
|
public class StringStore implements StreamDataStore {
|
||||||
|
|
||||||
byte[] value;
|
byte[] value;
|
||||||
|
|
||||||
|
@JsonCreator
|
||||||
|
public StringStore(byte[] value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
public StringStore(String s) {
|
public StringStore(String s) {
|
||||||
value = s.getBytes(StandardCharsets.UTF_8);
|
value = s.getBytes(StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ public class CoreJacksonModule extends SimpleModule {
|
||||||
new NamedType(LocalDirectoryDataStore.class),
|
new NamedType(LocalDirectoryDataStore.class),
|
||||||
new NamedType(CollectionEntryDataStore.class),
|
new NamedType(CollectionEntryDataStore.class),
|
||||||
new NamedType(StringStore.class),
|
new NamedType(StringStore.class),
|
||||||
new NamedType(LocalMachineStore.class),
|
new NamedType(LocalStore.class),
|
||||||
new NamedType(NamedStore.class),
|
new NamedType(NamedStore.class),
|
||||||
|
|
||||||
new NamedType(ValueType.class),
|
new NamedType(ValueType.class),
|
||||||
|
|
|
@ -30,10 +30,12 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
compileOnly 'net.synedra:validatorfx:0.3.1'
|
||||||
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.8.2'
|
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.8.2'
|
||||||
compileOnly 'com.jfoenix:jfoenix:9.0.10'
|
compileOnly 'com.jfoenix:jfoenix:9.0.10'
|
||||||
implementation project(':core')
|
implementation project(':core')
|
||||||
|
|
||||||
implementation 'io.xpipe:fxcomps:0.2'
|
implementation project(':fxcomps')
|
||||||
|
//implementation 'io.xpipe:fxcomps:0.2'
|
||||||
implementation 'org.controlsfx:controlsfx:11.1.1'
|
implementation 'org.controlsfx:controlsfx:11.1.1'
|
||||||
}
|
}
|
||||||
|
|
103
extension/src/main/java/io/xpipe/extension/ChainedValidator.java
Normal file
103
extension/src/main/java/io/xpipe/extension/ChainedValidator.java
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
package io.xpipe.extension;
|
||||||
|
|
||||||
|
import javafx.beans.Observable;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.StringBinding;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||||
|
import net.synedra.validatorfx.Check;
|
||||||
|
import net.synedra.validatorfx.ValidationMessage;
|
||||||
|
import net.synedra.validatorfx.ValidationResult;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class ChainedValidator implements io.xpipe.extension.Validator {
|
||||||
|
|
||||||
|
private final List<Validator> validators;
|
||||||
|
private final ReadOnlyObjectWrapper<ValidationResult> validationResultProperty = new ReadOnlyObjectWrapper<>(new ValidationResult());
|
||||||
|
private final ReadOnlyBooleanWrapper containsErrorsProperty = new ReadOnlyBooleanWrapper();
|
||||||
|
|
||||||
|
public ChainedValidator(List<Validator> validators) {
|
||||||
|
this.validators = validators;
|
||||||
|
validators.forEach(v -> {
|
||||||
|
v.containsErrorsProperty().addListener((c,o,n) -> {
|
||||||
|
containsErrorsProperty.set(containsErrors());
|
||||||
|
});
|
||||||
|
|
||||||
|
v.validationResultProperty().addListener((c,o,n) -> {
|
||||||
|
validationResultProperty.set(getValidationResult());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Check createCheck() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(Check check) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(Check check) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationResult getValidationResult() {
|
||||||
|
var list = new ArrayList<ValidationMessage>();
|
||||||
|
for (var val : validators) {
|
||||||
|
list.addAll(val.getValidationResult().getMessages());
|
||||||
|
}
|
||||||
|
|
||||||
|
var r = new ValidationResult();
|
||||||
|
r.addAll(list);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReadOnlyObjectProperty<ValidationResult> validationResultProperty() {
|
||||||
|
return validationResultProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReadOnlyBooleanProperty containsErrorsProperty() {
|
||||||
|
return containsErrorsProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsErrors() {
|
||||||
|
return validators.stream().anyMatch(Validator::containsErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validate() {
|
||||||
|
for (var val : validators) {
|
||||||
|
if (!val.validate()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StringBinding createStringBinding() {
|
||||||
|
return createStringBinding("- ", "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StringBinding createStringBinding(String prefix, String separator) {
|
||||||
|
var list = new ArrayList<Observable>(validators.stream().map(Validator::createStringBinding).toList());
|
||||||
|
Observable[] observables = list.toArray(Observable[]::new);
|
||||||
|
return Bindings.createStringBinding(() -> {
|
||||||
|
return validators.stream().map(v -> v.createStringBinding(prefix, separator).get()).collect(Collectors.joining("\n"));
|
||||||
|
}, observables);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,10 @@ package io.xpipe.extension;
|
||||||
|
|
||||||
import io.xpipe.core.dialog.Dialog;
|
import io.xpipe.core.dialog.Dialog;
|
||||||
import io.xpipe.core.store.DataStore;
|
import io.xpipe.core.store.DataStore;
|
||||||
|
import io.xpipe.core.store.MachineFileStore;
|
||||||
|
import io.xpipe.core.store.ShellStore;
|
||||||
import io.xpipe.core.store.StreamDataStore;
|
import io.xpipe.core.store.StreamDataStore;
|
||||||
import javafx.beans.property.Property;
|
import javafx.beans.property.Property;
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -13,19 +14,25 @@ public interface DataStoreProvider {
|
||||||
|
|
||||||
enum Category {
|
enum Category {
|
||||||
STREAM,
|
STREAM,
|
||||||
|
MACHINE,
|
||||||
DATABASE;
|
DATABASE;
|
||||||
}
|
}
|
||||||
|
|
||||||
default Category getCategory() {
|
default Category getCategory() {
|
||||||
if (StreamDataStore.class.isAssignableFrom(getStoreClasses().get(0))) {
|
var c = getStoreClasses().get(0);
|
||||||
|
if (StreamDataStore.class.isAssignableFrom(c)) {
|
||||||
return Category.STREAM;
|
return Category.STREAM;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (MachineFileStore.class.isAssignableFrom(c) || ShellStore.class.isAssignableFrom(c)) {
|
||||||
|
return Category.MACHINE;
|
||||||
|
}
|
||||||
|
|
||||||
throw new ExtensionException("Provider " + getId() + " has no set category");
|
throw new ExtensionException("Provider " + getId() + " has no set category");
|
||||||
}
|
}
|
||||||
|
|
||||||
default Region createConfigGui(Property<DataStore> store) {
|
default GuiDialog guiDialog(Property<DataStore> store) {
|
||||||
return null;
|
throw new ExtensionException("Gui Dialog is not implemented by provider " + getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
default void init() throws Exception {
|
default void init() throws Exception {
|
||||||
|
@ -65,7 +72,9 @@ public interface DataStoreProvider {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog defaultDialog();
|
default Dialog defaultDialog() {
|
||||||
|
throw new ExtensionException("CLI Dialog not implemented by provider");
|
||||||
|
}
|
||||||
|
|
||||||
default String display(DataStore store) {
|
default String display(DataStore store) {
|
||||||
return store.toDisplay();
|
return store.toDisplay();
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
package io.xpipe.extension;
|
||||||
|
|
||||||
|
import javafx.beans.Observable;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.StringBinding;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import net.synedra.validatorfx.Check;
|
||||||
|
import net.synedra.validatorfx.ValidationResult;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class ExclusiveValidator<T> implements io.xpipe.extension.Validator {
|
||||||
|
|
||||||
|
private final Map<T, ? extends Validator> validators;
|
||||||
|
private final ObservableValue<T> obs;
|
||||||
|
|
||||||
|
public ExclusiveValidator(Map<T, ? extends Validator> validators, ObservableValue<T> obs) {
|
||||||
|
this.validators = validators;
|
||||||
|
this.obs = obs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Validator get() {
|
||||||
|
return validators.get(obs.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Check createCheck() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(Check check) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(Check check) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationResult getValidationResult() {
|
||||||
|
return get().getValidationResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReadOnlyObjectProperty<ValidationResult> validationResultProperty() {
|
||||||
|
return get().validationResultProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReadOnlyBooleanProperty containsErrorsProperty() {
|
||||||
|
return get().containsErrorsProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsErrors() {
|
||||||
|
return get().containsErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validate() {
|
||||||
|
return get().validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StringBinding createStringBinding() {
|
||||||
|
return createStringBinding("- ", "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StringBinding createStringBinding(String prefix, String separator) {
|
||||||
|
var list = new ArrayList<Observable>(validators.values().stream().map(Validator::createStringBinding).toList());
|
||||||
|
list.add(obs);
|
||||||
|
Observable[] observables = list.toArray(Observable[]::new);
|
||||||
|
return Bindings.createStringBinding(() -> {
|
||||||
|
return get().createStringBinding(prefix, separator).get();
|
||||||
|
}, observables);
|
||||||
|
}
|
||||||
|
}
|
18
extension/src/main/java/io/xpipe/extension/GuiDialog.java
Normal file
18
extension/src/main/java/io/xpipe/extension/GuiDialog.java
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package io.xpipe.extension;
|
||||||
|
|
||||||
|
import io.xpipe.fxcomps.Comp;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Value;
|
||||||
|
|
||||||
|
@Value
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class GuiDialog {
|
||||||
|
|
||||||
|
Comp<?> comp;
|
||||||
|
Validator validator;
|
||||||
|
|
||||||
|
public GuiDialog(Comp<?> comp) {
|
||||||
|
this.comp = comp;
|
||||||
|
this.validator = new SimpleValidator();
|
||||||
|
}
|
||||||
|
}
|
17
extension/src/main/java/io/xpipe/extension/Properties.java
Normal file
17
extension/src/main/java/io/xpipe/extension/Properties.java
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package io.xpipe.extension;
|
||||||
|
|
||||||
|
import javafx.beans.property.Property;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Properties {
|
||||||
|
|
||||||
|
public static <T, V> void bindExclusive(Property<V> selected, Map<V, ? extends Property<T>> map, Property<T> toBind) {
|
||||||
|
selected.addListener((c,o,n) -> {
|
||||||
|
toBind.unbind();
|
||||||
|
toBind.bind(map.get(n));
|
||||||
|
});
|
||||||
|
|
||||||
|
toBind.bind(map.get(selected.getValue()));
|
||||||
|
}
|
||||||
|
}
|
118
extension/src/main/java/io/xpipe/extension/SimpleValidator.java
Normal file
118
extension/src/main/java/io/xpipe/extension/SimpleValidator.java
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package io.xpipe.extension;
|
||||||
|
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.StringBinding;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import net.synedra.validatorfx.Check;
|
||||||
|
import net.synedra.validatorfx.Severity;
|
||||||
|
import net.synedra.validatorfx.ValidationMessage;
|
||||||
|
import net.synedra.validatorfx.ValidationResult;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class SimpleValidator implements Validator {
|
||||||
|
|
||||||
|
private final Map<Check, ChangeListener<ValidationResult>> checks = new LinkedHashMap<>();
|
||||||
|
private final ReadOnlyObjectWrapper<ValidationResult> validationResultProperty = new ReadOnlyObjectWrapper<>(new ValidationResult());
|
||||||
|
private final ReadOnlyBooleanWrapper containsErrorsProperty = new ReadOnlyBooleanWrapper();
|
||||||
|
|
||||||
|
/** Create a check that lives within this checker's domain.
|
||||||
|
* @return A check object whose dependsOn, decorates, etc. methods can be called
|
||||||
|
*/
|
||||||
|
public Check createCheck() {
|
||||||
|
Check check = new Check();
|
||||||
|
add(check);
|
||||||
|
return check;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add another check to the checker. Changes in the check's validationResultProperty will be reflected in the checker.
|
||||||
|
* @param check The check to add.
|
||||||
|
*/
|
||||||
|
public void add(Check check) {
|
||||||
|
ChangeListener<ValidationResult> listener = (obs, oldv, newv) -> refreshProperties();
|
||||||
|
checks.put(check, listener);
|
||||||
|
check.validationResultProperty().addListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes a check from this validator.
|
||||||
|
* @param check The check to remove from this validator.
|
||||||
|
*/
|
||||||
|
public void remove(Check check) {
|
||||||
|
ChangeListener<ValidationResult> listener = checks.remove(check);
|
||||||
|
if (listener != null) {
|
||||||
|
check.validationResultProperty().removeListener(listener);
|
||||||
|
}
|
||||||
|
refreshProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retrieves current validation result
|
||||||
|
* @return validation result
|
||||||
|
*/
|
||||||
|
public ValidationResult getValidationResult() {
|
||||||
|
return validationResultProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Can be used to track validation result changes
|
||||||
|
* @return The Validation result property.
|
||||||
|
*/
|
||||||
|
public ReadOnlyObjectProperty<ValidationResult> validationResultProperty() {
|
||||||
|
return validationResultProperty.getReadOnlyProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A read-only boolean property indicating whether any of the checks of this validator emitted an error. */
|
||||||
|
public ReadOnlyBooleanProperty containsErrorsProperty() {
|
||||||
|
return containsErrorsProperty.getReadOnlyProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean containsErrors() {
|
||||||
|
return containsErrorsProperty().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run all checks (decorating nodes if appropriate)
|
||||||
|
* @return true if no errors were found, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean validate() {
|
||||||
|
for (Check check : checks.keySet()) {
|
||||||
|
check.recheck();
|
||||||
|
}
|
||||||
|
return ! containsErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshProperties() {
|
||||||
|
ValidationResult nextResult = new ValidationResult();
|
||||||
|
for (Check check : checks.keySet()) {
|
||||||
|
nextResult.addAll(check.getValidationResult().getMessages());
|
||||||
|
}
|
||||||
|
validationResultProperty.set(nextResult);
|
||||||
|
boolean hasErrors = false;
|
||||||
|
for (ValidationMessage msg : nextResult.getMessages()) {
|
||||||
|
hasErrors = hasErrors || msg.getSeverity() == Severity.ERROR;
|
||||||
|
}
|
||||||
|
containsErrorsProperty.set(hasErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a string property that depends on the validation result.
|
||||||
|
* Each error message will be displayed on a separate line prefixed with a bullet.
|
||||||
|
*/
|
||||||
|
public StringBinding createStringBinding() {
|
||||||
|
return createStringBinding("- ", "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StringBinding createStringBinding(String prefix, String separator) {
|
||||||
|
return Bindings.createStringBinding( () -> {
|
||||||
|
StringBuilder str = new StringBuilder();
|
||||||
|
for (ValidationMessage msg : validationResultProperty.get().getMessages()) {
|
||||||
|
if (str.length() > 0) {
|
||||||
|
str.append(separator);
|
||||||
|
}
|
||||||
|
str.append(prefix).append(msg.getText());
|
||||||
|
}
|
||||||
|
return str.toString();
|
||||||
|
}, validationResultProperty);
|
||||||
|
}
|
||||||
|
}
|
56
extension/src/main/java/io/xpipe/extension/Validator.java
Normal file
56
extension/src/main/java/io/xpipe/extension/Validator.java
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package io.xpipe.extension;
|
||||||
|
|
||||||
|
import javafx.beans.binding.StringBinding;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
|
import net.synedra.validatorfx.Check;
|
||||||
|
import net.synedra.validatorfx.ValidationResult;
|
||||||
|
|
||||||
|
public interface Validator {
|
||||||
|
|
||||||
|
public Check createCheck();
|
||||||
|
|
||||||
|
/** Add another check to the checker. Changes in the check's validationResultProperty will be reflected in the checker.
|
||||||
|
* @param check The check to add.
|
||||||
|
*/
|
||||||
|
public void add(Check check);
|
||||||
|
|
||||||
|
/** Removes a check from this validator.
|
||||||
|
* @param check The check to remove from this validator.
|
||||||
|
*/
|
||||||
|
public void remove(Check check);
|
||||||
|
|
||||||
|
/** Retrieves current validation result
|
||||||
|
* @return validation result
|
||||||
|
*/
|
||||||
|
public ValidationResult getValidationResult();
|
||||||
|
|
||||||
|
/** Can be used to track validation result changes
|
||||||
|
* @return The Validation result property.
|
||||||
|
*/
|
||||||
|
public ReadOnlyObjectProperty<ValidationResult> validationResultProperty();
|
||||||
|
|
||||||
|
/** A read-only boolean property indicating whether any of the checks of this validator emitted an error. */
|
||||||
|
public ReadOnlyBooleanProperty containsErrorsProperty();
|
||||||
|
|
||||||
|
public boolean containsErrors();
|
||||||
|
|
||||||
|
/** Run all checks (decorating nodes if appropriate)
|
||||||
|
* @return true if no errors were found, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean validate();
|
||||||
|
|
||||||
|
/** Create a string property that depends on the validation result.
|
||||||
|
* Each error message will be displayed on a separate line prefixed with a bullet.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public StringBinding createStringBinding();
|
||||||
|
|
||||||
|
/** Create a string property that depends on the validation result.
|
||||||
|
* @param prefix The string to prefix each validation message with
|
||||||
|
* @param separator The string to separate consecutive validation messages with
|
||||||
|
* @param severities The severities to consider; If none is given, only Severity.ERROR will be considered
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public StringBinding createStringBinding(String prefix, String separator);
|
||||||
|
}
|
15
extension/src/main/java/io/xpipe/extension/Validators.java
Normal file
15
extension/src/main/java/io/xpipe/extension/Validators.java
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package io.xpipe.extension;
|
||||||
|
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import net.synedra.validatorfx.Check;
|
||||||
|
|
||||||
|
public class Validators {
|
||||||
|
|
||||||
|
public static Check nonNull(Validator v, ObservableValue<String> name, ObservableValue<?> s) {
|
||||||
|
return v.createCheck().dependsOn("val", s).withMethod(c -> {
|
||||||
|
if (c.get("val") == null ) {
|
||||||
|
c.error(I18n.get("extension.mustNotBeEmpty", name.getValue()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,11 +3,14 @@ package io.xpipe.extension.comp;
|
||||||
import io.xpipe.core.charsetter.NewLine;
|
import io.xpipe.core.charsetter.NewLine;
|
||||||
import io.xpipe.core.util.Secret;
|
import io.xpipe.core.util.Secret;
|
||||||
import io.xpipe.extension.I18n;
|
import io.xpipe.extension.I18n;
|
||||||
|
import io.xpipe.extension.Validator;
|
||||||
|
import io.xpipe.extension.Validators;
|
||||||
import io.xpipe.fxcomps.Comp;
|
import io.xpipe.fxcomps.Comp;
|
||||||
import javafx.beans.property.Property;
|
import javafx.beans.property.Property;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
|
import net.synedra.validatorfx.Check;
|
||||||
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
|
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
|
||||||
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
@ -15,7 +18,7 @@ import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public class DynamicOptionsBuilder<T> {
|
public class DynamicOptionsBuilder<T> {
|
||||||
|
|
||||||
|
@ -40,6 +43,17 @@ public class DynamicOptionsBuilder<T> {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DynamicOptionsBuilder<T> decorate(Check c) {
|
||||||
|
entries.get(entries.size() - 1).comp().apply(s -> c.decorates(s.get()));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DynamicOptionsBuilder<T> nonNull(Validator v) {
|
||||||
|
var e = entries.get(entries.size() - 1);
|
||||||
|
var p = props.get(props.size() - 1);
|
||||||
|
return decorate(Validators.nonNull(v, e.name(), p));
|
||||||
|
}
|
||||||
|
|
||||||
public DynamicOptionsBuilder<T> addNewLine(Property<NewLine> prop) {
|
public DynamicOptionsBuilder<T> addNewLine(Property<NewLine> prop) {
|
||||||
var map = new LinkedHashMap<NewLine, ObservableValue<String>>();
|
var map = new LinkedHashMap<NewLine, ObservableValue<String>>();
|
||||||
for (var e : NewLine.values()) {
|
for (var e : NewLine.values()) {
|
||||||
|
@ -79,6 +93,13 @@ public class DynamicOptionsBuilder<T> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DynamicOptionsBuilder<T> addString(String nameKey, Property<String> prop) {
|
||||||
|
var comp = new TextFieldComp(prop);
|
||||||
|
entries.add(new DynamicOptionsComp.Entry(I18n.observable(nameKey), comp));
|
||||||
|
props.add(prop);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public DynamicOptionsBuilder<T> addString(ObservableValue<String> name, Property<String> prop) {
|
public DynamicOptionsBuilder<T> addString(ObservableValue<String> name, Property<String> prop) {
|
||||||
var comp = new TextFieldComp(prop);
|
var comp = new TextFieldComp(prop);
|
||||||
entries.add(new DynamicOptionsComp.Entry(name, comp));
|
entries.add(new DynamicOptionsComp.Entry(name, comp));
|
||||||
|
@ -86,8 +107,8 @@ public class DynamicOptionsBuilder<T> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DynamicOptionsBuilder<T> addComp(Comp<?> comp) {
|
public DynamicOptionsBuilder<T> addComp(ObservableValue<String> name, Comp<?> comp) {
|
||||||
entries.add(new DynamicOptionsComp.Entry(null, comp));
|
entries.add(new DynamicOptionsComp.Entry(name, comp));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,19 +126,20 @@ public class DynamicOptionsBuilder<T> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public <V extends T> Comp<?> buildComp(Function<T, V> creator, Property<T> toSet) {
|
public <V extends T> Comp<?> buildComp(Supplier<V> creator, Property<T> toSet) {
|
||||||
props.forEach(prop -> {
|
props.forEach(prop -> {
|
||||||
prop.addListener((c,o,n) -> {
|
prop.addListener((c,o,n) -> {
|
||||||
toSet.setValue(creator.apply(toSet.getValue()));
|
toSet.setValue(creator.get());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
toSet.setValue(creator.get());
|
||||||
if (title != null) {
|
if (title != null) {
|
||||||
entries.add(0, new DynamicOptionsComp.Entry(null, Comp.of(() -> new Label(title.getValue())).styleClass("title")));
|
entries.add(0, new DynamicOptionsComp.Entry(null, Comp.of(() -> new Label(title.getValue())).styleClass("title")));
|
||||||
}
|
}
|
||||||
return new DynamicOptionsComp(entries, wrap);
|
return new DynamicOptionsComp(entries, wrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
public <V extends T> Region build(Function<T, V> creator, Property<T> toSet) {
|
public <V extends T> Region build(Supplier<V> creator, Property<T> toSet) {
|
||||||
return buildComp(creator, toSet).createRegion();
|
return buildComp(creator, toSet).createRegion();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,18 @@ import com.jfoenix.controls.JFXTabPane;
|
||||||
import io.xpipe.fxcomps.Comp;
|
import io.xpipe.fxcomps.Comp;
|
||||||
import io.xpipe.fxcomps.CompStructure;
|
import io.xpipe.fxcomps.CompStructure;
|
||||||
import io.xpipe.fxcomps.SimpleCompStructure;
|
import io.xpipe.fxcomps.SimpleCompStructure;
|
||||||
|
import io.xpipe.fxcomps.util.PlatformThread;
|
||||||
|
import javafx.beans.property.Property;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
|
import lombok.Getter;
|
||||||
import org.kordamp.ikonli.javafx.FontIcon;
|
import org.kordamp.ikonli.javafx.FontIcon;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
public class TabPaneComp extends Comp<CompStructure<JFXTabPane>> {
|
public class TabPaneComp extends Comp<CompStructure<JFXTabPane>> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -32,12 +36,24 @@ public class TabPaneComp extends Comp<CompStructure<JFXTabPane>> {
|
||||||
content.prefWidthProperty().bind(tabPane.widthProperty());
|
content.prefWidthProperty().bind(tabPane.widthProperty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tabPane.getSelectionModel().select(entries.indexOf(selected.getValue()));
|
||||||
|
tabPane.getSelectionModel().selectedIndexProperty().addListener((c,o,n) -> {
|
||||||
|
selected.setValue(entries.get(n.intValue()));
|
||||||
|
});
|
||||||
|
selected.addListener((c,o,n) -> {
|
||||||
|
PlatformThread.runLaterIfNeeded(() -> {
|
||||||
|
tabPane.getSelectionModel().select(entries.indexOf(n));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return new SimpleCompStructure<>(tabPane);
|
return new SimpleCompStructure<>(tabPane);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final Property<Entry> selected;
|
||||||
private final List<Entry> entries;
|
private final List<Entry> entries;
|
||||||
|
|
||||||
public TabPaneComp(List<Entry> entries) {
|
public TabPaneComp(Property<Entry> selected, List<Entry> entries) {
|
||||||
|
this.selected = selected;
|
||||||
this.entries = entries;
|
this.entries = entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ module io.xpipe.extension {
|
||||||
requires static org.controlsfx.controls;
|
requires static org.controlsfx.controls;
|
||||||
requires java.desktop;
|
requires java.desktop;
|
||||||
requires org.fxmisc.richtext;
|
requires org.fxmisc.richtext;
|
||||||
|
requires static net.synedra.validatorfx;
|
||||||
requires org.fxmisc.flowless;
|
requires org.fxmisc.flowless;
|
||||||
requires org.fxmisc.undofx;
|
requires org.fxmisc.undofx;
|
||||||
requires org.fxmisc.wellbehavedfx;
|
requires org.fxmisc.wellbehavedfx;
|
||||||
|
|
|
@ -3,4 +3,5 @@ newLine=Newline
|
||||||
crlf=CRLF (Windows)
|
crlf=CRLF (Windows)
|
||||||
lf=LF (Linux)
|
lf=LF (Linux)
|
||||||
none=None
|
none=None
|
||||||
nullPointer=Null Pointer: $MSG$
|
nullPointer=Null Pointer: $MSG$
|
||||||
|
mustNotBeEmpty=$NAME$ must not be empty
|
Loading…
Reference in a new issue