This commit is contained in:
Christopher Schnick 2022-08-12 17:26:01 +02:00
parent 0f37600c96
commit b1a75f6d21
56 changed files with 673 additions and 285 deletions

2
.gitignore vendored
View file

@ -4,3 +4,5 @@ build/
dev.properties
extensions.txt
local/
.vscode
bin

View file

@ -7,6 +7,7 @@ plugins {
apply from: "$rootDir/deps/java.gradle"
apply from: "$rootDir/deps/junit.gradle"
apply from: "$rootDir/deps/jackson.gradle"
apply from: 'publish.gradle'
apply from: "$rootDir/deps/publish-base.gradle"
@ -19,18 +20,18 @@ repositories {
}
test {
enabled = false
enabled = true
}
dependencies {
// Fix warnings about missing annotations
compileOnly group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.13.0"
testCompileOnly group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.13.0"
implementation project(':core')
implementation project(':beacon')
}
configurations {
testImplementation.extendsFrom(dep)
}
def canTestWithDev = findProject(':app') != null
def home = System.getenv('XPIPE_HOME')
String daemonCommand = canTestWithDev ?
@ -52,7 +53,7 @@ test {
" -Dio.xpipe.app.logLevel=trace"
// API properties
// systemProperty 'io.xpipe.beacon.debugOutput', "true"
systemProperty 'io.xpipe.beacon.debugOutput', "true"
systemProperty 'io.xpipe.beacon.debugExecOutput', "true"
systemProperty "io.xpipe.beacon.port", "21722"
}

View file

@ -1,7 +1,9 @@
package io.xpipe.api;
import io.xpipe.api.impl.DataSourceImpl;
import io.xpipe.core.source.*;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.source.DataSourceType;
import java.io.IOException;
import java.io.InputStream;
@ -9,7 +11,6 @@ import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
/**
* Represents a reference to a data source that is managed by X-Pipe.
@ -85,36 +86,36 @@ public interface DataSource {
}
/**
* Wrapper for {@link #create(DataSourceId, String, Map, InputStream)} that creates an anonymous data source.
* Wrapper for {@link #create(DataSourceId, String, InputStream)} that creates an anonymous data source.
*/
public static DataSource createAnonymous(String type, Map<String,String> config, Path path) {
return create(null, type, config, path);
public static DataSource createAnonymous(String type, Path path) {
return create(null, type, path);
}
/**
* Wrapper for {@link #create(DataSourceId, String, Map, InputStream)}.
* Wrapper for {@link #create(DataSourceId, String, InputStream)}.
*/
public static DataSource create(DataSourceId id, String type, Map<String,String> config, Path path) {
public static DataSource create(DataSourceId id, String type, Path path) {
try (var in = Files.newInputStream(path)) {
return create(id, type, config, in);
return create(id, type, in);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Wrapper for {@link #create(DataSourceId, String, Map, InputStream)} that creates an anonymous data source.
* Wrapper for {@link #create(DataSourceId, String, InputStream)} that creates an anonymous data source.
*/
public static DataSource createAnonymous(String type, Map<String,String> config, URL url) {
return create(null, type, config, url);
public static DataSource createAnonymous(String type, URL url) {
return create(null, type, url);
}
/**
* Wrapper for {@link #create(DataSourceId, String, Map, InputStream)}.
* Wrapper for {@link #create(DataSourceId, String, InputStream)}.
*/
public static DataSource create(DataSourceId id, String type, Map<String,String> config, URL url) {
public static DataSource create(DataSourceId id, String type, URL url) {
try (var in = url.openStream()) {
return create(id, type, config, in);
return create(id, type, in);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
@ -122,10 +123,10 @@ public interface DataSource {
/**
* Wrapper for {@link #create(DataSourceId, String, Map, InputStream)} that creates an anonymous data source.
* Wrapper for {@link #create(DataSourceId, String, InputStream)} that creates an anonymous data source.
*/
public static DataSource createAnonymous(String type, Map<String,String> config, InputStream in) {
return create(null, type, config, in);
public static DataSource createAnonymous(String type, InputStream in) {
return create(null, type, in);
}
/**
@ -133,12 +134,11 @@ public interface DataSource {
*
* @param id the data source id
* @param type the data source type
* @param config additional configuration options for the specific data source type
* @param in the input stream to read
* @return a {@link DataSource} instances that can be used to access the underlying data
*/
public static DataSource create(DataSourceId id, String type, Map<String,String> config, InputStream in) {
return DataSourceImpl.create(id, type, config, in);
public static DataSource create(DataSourceId id, String type, InputStream in) {
return DataSourceImpl.create(id, type, in);
}
/**

View file

@ -1,6 +1,8 @@
package io.xpipe.api.connector;
import io.xpipe.beacon.*;
import io.xpipe.beacon.exchange.cli.DialogExchange;
import io.xpipe.core.dialog.DialogReference;
import io.xpipe.core.util.JacksonHelper;
import java.util.Optional;
@ -13,6 +15,22 @@ public final class XPipeConnection extends BeaconConnection {
return con;
}
public static void finishDialog(DialogReference reference) {
try (var con = new XPipeConnection()) {
con.constructSocket();
while (true) {
DialogExchange.Response response = con.performSimpleExchange(DialogExchange.Request.builder().dialogKey(reference.getDialogId()).build());
if (response.getElement() == null) {
break;
}
}
} catch (BeaconException e) {
throw e;
} catch (Exception e) {
throw new BeaconException(e);
}
}
public static void execute(Handler handler) {
try (var con = new XPipeConnection()) {
con.constructSocket();
@ -74,7 +92,7 @@ public final class XPipeConnection extends BeaconConnection {
}
public static Optional<BeaconClient> waitForStartup() {
for (int i = 0; i < 40; i++) {
for (int i = 0; i < 80; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {

View file

@ -10,7 +10,6 @@ import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceReference;
import java.io.InputStream;
import java.util.Map;
public abstract class DataSourceImpl implements DataSource {
@ -19,29 +18,30 @@ public abstract class DataSourceImpl implements DataSource {
var req = QueryDataSourceExchange.Request.builder().ref(ds).build();
QueryDataSourceExchange.Response res = con.performSimpleExchange(req);
var config = new DataSourceConfig(res.getProvider(), res.getConfig());
switch (res.getInfo().getType()) {
return switch (res.getInfo().getType()) {
case TABLE -> {
var data = res.getInfo().asTable();
return new DataTableImpl(res.getId(), config, data);
yield new DataTableImpl(res.getId(), config, data);
}
case STRUCTURE -> {
var info = res.getInfo().asStructure();
return new DataStructureImpl(res.getId(), config, info);
yield new DataStructureImpl(res.getId(), config, info);
}
case TEXT -> {
var info = res.getInfo().asText();
return new DataTextImpl(res.getId(), config, info);
yield new DataTextImpl(res.getId(), config, info);
}
case RAW -> {
var info = res.getInfo().asRaw();
return new DataRawImpl(res.getId(), config, info);
yield new DataRawImpl(res.getId(), config, info);
}
}
throw new AssertionError();
case COLLECTION -> throw new UnsupportedOperationException("Unimplemented case: " + res.getInfo().getType());
default -> throw new IllegalArgumentException("Unexpected value: " + res.getInfo().getType());
};
});
}
public static DataSource create(DataSourceId id, String type, Map<String,String> config, InputStream in) {
public static DataSource create(DataSourceId id, String type, InputStream in) {
var res = XPipeConnection.execute(con -> {
var req = StoreStreamExchange.Request.builder().build();
StoreStreamExchange.Response r = con.performOutputExchange(req, out -> in.transferTo(out));
@ -53,6 +53,8 @@ public abstract class DataSourceImpl implements DataSource {
var startReq = ReadExchange.Request.builder()
.provider(type)
.store(store)
.target(id)
.configureAll(false)
.build();
var startRes = XPipeConnection.execute(con -> {
ReadExchange.Response r = con.performSimpleExchange(startReq);
@ -60,13 +62,8 @@ public abstract class DataSourceImpl implements DataSource {
});
var configInstance = startRes.getConfig();
//TODO
// configInstance.getConfigInstance().getCurrentValues().putAll(config);
// var endReq = ReadExecuteExchange.Request.builder()
// .target(id).dataStore(store).config(configInstance).build();
// XPipeConnection.execute(con -> {
// con.performSimpleExchange(endReq);
// });
XPipeConnection.finishDialog(configInstance);
var ref = id != null ? DataSourceReference.id(id) : DataSourceReference.latest();
return get(ref);
}

View file

@ -5,6 +5,8 @@ import io.xpipe.api.DataTable;
import io.xpipe.api.DataTableAccumulator;
import io.xpipe.api.connector.XPipeConnection;
import io.xpipe.api.util.TypeDescriptor;
import io.xpipe.beacon.BeaconException;
import io.xpipe.beacon.exchange.ReadExchange;
import io.xpipe.beacon.exchange.StoreStreamExchange;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.DataStructureNodeAcceptor;
@ -14,6 +16,7 @@ import io.xpipe.core.data.typed.TypedDataStreamWriter;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceReference;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
@ -23,25 +26,35 @@ public class DataTableAccumulatorImpl implements DataTableAccumulator {
private final TupleType type;
private int rows;
private TupleType writtenDescriptor;
private OutputStream bodyOutput;
public DataTableAccumulatorImpl(TupleType type) {
this.type = type;
connection = XPipeConnection.open();
connection.sendRequest(StoreStreamExchange.Request.builder().build());
connection.sendBody();
bodyOutput = connection.sendBody();
}
@Override
public synchronized DataTable finish(DataSourceId id) {
connection.withOutputStream(OutputStream::close);
try {
bodyOutput.close();
} catch (IOException e) {
throw new BeaconException(e);
}
StoreStreamExchange.Response res = connection.receiveResponse();
connection.close();
// var req = ReadExecuteExchange.Request.builder()
// .target(id).dataStore(res.getStore()).build();
// XPipeConnection.execute(con -> {
// con.performSimpleExchange(req);
// });
var req = ReadExchange.Request.builder()
.target(id).store(res.getStore()).provider("xpbt").configureAll(false).build();
ReadExchange.Response response = XPipeConnection.execute(con -> {
return con.performSimpleExchange(req);
});
var configInstance = response.getConfig();
XPipeConnection.finishDialog(configInstance);
return DataSource.get(DataSourceReference.id(id)).asTable();
}

View file

@ -5,13 +5,11 @@ import io.xpipe.core.source.DataSourceId;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.util.Map;
public class DataTableTest extends DaemonControl {
@BeforeAll
public static void setupStorage() throws Exception {
DataSource.create(DataSourceId.fromString(":usernames"), "csv", Map.of(), DataTableTest.class.getResource("username.csv"));
DataSource.create(DataSourceId.fromString(":usernames"), "csv", DataTableTest.class.getResource("username.csv"));
}
@Test

View file

@ -15,8 +15,8 @@ module io.xpipe.beacon {
opens io.xpipe.beacon.exchange.data;
opens io.xpipe.beacon.exchange.cli;
requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.databind;
requires static com.fasterxml.jackson.core;
requires static com.fasterxml.jackson.databind;
requires transitive io.xpipe.core;
requires static lombok;

View file

@ -2,10 +2,8 @@ plugins {
id 'org.jreleaser' version '1.0.0'
}
if (project == rootProject) {
plugins {
id "io.codearte.nexus-staging" version "0.30.0"
}
if(project == rootProject) {
apply plugin: "io.codearte.nexus-staging"
}
version file('misc/version').text

View file

@ -24,3 +24,7 @@ archivesBaseName = 'core'
repositories {
mavenCentral()
}
dependencies{
compileOnly 'org.apache.commons:commons-exec:1.3'
}

View file

@ -86,7 +86,7 @@ public abstract class DataStructureNode implements Iterable<DataStructureNode> {
}
public boolean isNull() {
throw unsupported("null check");
return false;
}
public final ValueNode asValue() {

View file

@ -106,4 +106,10 @@ public abstract class ValueNode extends DataStructureNode {
}
public abstract byte[] getRawData();
@Override
public boolean isNull() {
return getRawData() == null;
}
}

View file

@ -10,12 +10,12 @@ import java.util.function.Supplier;
/**
* A Dialog is a sequence of questions and answers.
*
* <p>
* The dialogue API is only used for the command line interface.
* Therefore, the actual implementation is handled by the command line component.
* This API provides a way of creating server-side dialogues which makes
* it possible to create extensions that provide a commandline configuration component.
*
* <p>
* When a Dialog is completed, it can also be optionally evaluated to a value, which can be queried by calling {@link #getResult()}.
* The evaluation function can be set with {@link #evaluateTo(Supplier)}.
* Alternatively, a dialogue can also copy the evaluation function of another dialogue with {@link #evaluateTo(Dialog)}.
@ -74,9 +74,9 @@ public abstract class Dialog {
* Creates a choice dialogue.
*
* @param description the shown question description
* @param elements the available elements to choose from
* @param required signals whether a choice is required or can be left empty
* @param selected the selected element index
* @param elements the available elements to choose from
* @param required signals whether a choice is required or can be left empty
* @param selected the selected element index
*/
public static Dialog.Choice choice(String description, List<io.xpipe.core.dialog.Choice> elements, boolean required, int selected) {
Dialog.Choice c = new Dialog.Choice(description, elements, required, selected);
@ -88,10 +88,10 @@ public abstract class Dialog {
* Creates a choice dialogue from a set of objects.
*
* @param description the shown question description
* @param toString a function that maps the objects to a string
* @param required signals whether choices required or can be left empty
* @param def the element which is selected by default
* @param vals the range of possible elements
* @param toString a function that maps the objects to a string
* @param required signals whether choices required or can be left empty
* @param def the element which is selected by default
* @param vals the range of possible elements
*/
@SafeVarargs
public static <T> Dialog.Choice choice(String description, Function<T, String> toString, boolean required, T def, T... vals) {
@ -133,7 +133,7 @@ public abstract class Dialog {
return element;
}
private <T> T getConvertedValue() {
private <T> T getConvertedValue() {
return element.getConvertedValue();
}
}
@ -142,13 +142,13 @@ public abstract class Dialog {
* Creates a simple query dialogue.
*
* @param description the shown question description
* @param newLine signals whether the query should be done on a new line or not
* @param required signals whether the query can be left empty or not
* @param quiet signals whether the user should be explicitly queried for the value.
* In case the user is not queried, a value can still be set using the command line arguments
* that allow to set the specific value for a configuration query parameter
* @param value the default value
* @param converter the converter
* @param newLine signals whether the query should be done on a new line or not
* @param required signals whether the query can be left empty or not
* @param quiet signals whether the user should be explicitly queried for the value.
* In case the user is not queried, a value can still be set using the command line arguments
* that allow to set the specific value for a configuration query parameter
* @param value the default value
* @param converter the converter
*/
public static <T> Dialog.Query query(String description, boolean newLine, boolean required, boolean quiet, T value, QueryConverter<T> converter) {
var q = new <T>Dialog.Query(description, newLine, required, quiet, value, converter, false);
@ -197,7 +197,8 @@ public abstract class Dialog {
if (currentElement == null) {
DialogElement next = null;
while (current < ds.length - 1 && (next = ds[++current].start()) == null) {
};
}
;
return next;
}
@ -378,10 +379,10 @@ public abstract class Dialog {
* Creates a dialogue that will fork the control flow.
*
* @param description the shown question description
* @param elements the available elements to choose from
* @param required signals whether a choice is required or not
* @param selected the index of the element that is selected by default
* @param c the dialogue index mapping function
* @param elements the available elements to choose from
* @param required signals whether a choice is required or not
* @param selected the index of the element that is selected by default
* @param c the dialogue index mapping function
*/
public static Dialog fork(String description, List<io.xpipe.core.dialog.Choice> elements, boolean required, int selected, Function<Integer, Dialog> c) {
var choice = new ChoiceElement(description, elements, required, selected);
@ -424,7 +425,7 @@ public abstract class Dialog {
public abstract DialogElement start() throws Exception;
public Dialog evaluateTo(Dialog d) {
evaluation = () -> d.evaluation.get();
evaluation = () -> d.evaluation != null ? d.evaluation.get() : null;
return this;
}

View file

@ -32,6 +32,11 @@ public class DataSourceId {
this.entryName = entryName;
}
public static String cleanString(String input) {
var replaced = input.replaceAll(":", "");
return replaced.length() == 0 ? "-" : replaced;
}
/**
* Creates a new data source id from a collection name and an entry name.
*

View file

@ -41,12 +41,16 @@ public interface DataStore {
/**
* Creates a display string of this store.
* This can be a multiline string.
*/
default String toDisplay() {
default String toSummaryString() {
return null;
}
default String queryInformationString() throws Exception {
return null;
}
/**
* Casts this instance to the required type without checking whether a cast is possible.
*/

View file

@ -0,0 +1,27 @@
package io.xpipe.core.store;
public class DataStoreFormatter {
public static String ellipsis(String input, int length) {
var end = Math.min(input.length(), length);
if (end < input.length()) {
return input.substring(0, end) + "...";
}
return input;
}
public static String specialFormatHostName(String input) {
if (input.contains(":")) {
input = input.split(":")[0];
}
if (input.endsWith(".rds.amazonaws.com")) {
var split = input.split("\\.");
var name = split[0];
var region = split[2];
return String.format("RDS %s @ %s", name, region);
}
return null;
}
}

View file

@ -16,14 +16,14 @@ import java.nio.file.Path;
public class FileStore implements StreamDataStore, FilenameStore {
public static FileStore local(Path p) {
return new FileStore(MachineFileStore.local(), p.toString());
return new FileStore(new LocalStore(), p.toString());
}
/**
* Creates a file store for a file that is local to the callers machine.
*/
public static FileStore local(String p) {
return new FileStore(MachineFileStore.local(), p);
return new FileStore(new LocalStore(), p);
}
MachineFileStore machine;
@ -38,7 +38,7 @@ public class FileStore implements StreamDataStore, FilenameStore {
@Override
public void validate() throws Exception {
if (!machine.exists(file)) {
throw new IllegalStateException("File " + file + " could not be found on machine " + machine.toDisplay());
throw new IllegalStateException("File " + file + " could not be found on machine " + machine.toSummaryString());
}
}
@ -58,8 +58,8 @@ public class FileStore implements StreamDataStore, FilenameStore {
}
@Override
public String toDisplay() {
return file + "@" + machine.toDisplay();
public String toSummaryString() {
return file + "@" + machine.toSummaryString();
}
@Override
@ -69,6 +69,10 @@ public class FileStore implements StreamDataStore, FilenameStore {
@Override
public String getFileName() {
return file;
var split = file.split("[\\\\/]");
if (split.length == 0) {
return "";
}
return split[split.length - 1];
}
}

View file

@ -37,7 +37,7 @@ public class InMemoryStore implements StreamDataStore {
}
@Override
public String toDisplay() {
public String toSummaryString() {
return "inMemory";
}
}

View file

@ -1,33 +1,39 @@
package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.core.util.Secret;
import lombok.Value;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@JsonTypeName("local")
@Value
public class LocalStore implements ShellProcessStore {
public class LocalStore extends StandardShellStore implements MachineFileStore {
@Override
public boolean isLocal() {
return true;
}
static class LocalProcessControl extends ProcessControl {
private final InputStream input;
private final List<Secret> input;
private final ProcessBuilder builder;
private final Integer timeout;
private Process process;
LocalProcessControl(InputStream input, List<String> cmd) {
LocalProcessControl(List<Secret> input, List<String> cmd, Integer timeout) {
this.input = input;
this.timeout = timeout;
var l = new ArrayList<String>();
l.add("cmd");
l.add("/c");
@ -35,13 +41,19 @@ public class LocalStore implements ShellProcessStore {
builder = new ProcessBuilder(l);
}
private InputStream createInputStream() {
var string = input.stream().map(secret -> secret.getSecretValue()).collect(Collectors.joining("\n")) + "\r\n";
return new ByteArrayInputStream(string.getBytes(StandardCharsets.US_ASCII));
}
@Override
public void start() throws IOException {
process = builder.start();
var t = new Thread(() -> {
try {
input.transferTo(process.getOutputStream());
try (var inputStream = createInputStream()){
process.getOutputStream().flush();
inputStream.transferTo(process.getOutputStream());
process.getOutputStream().close();
} catch (IOException e) {
throw new UncheckedIOException(e);
@ -53,7 +65,11 @@ public class LocalStore implements ShellProcessStore {
@Override
public int waitFor() throws Exception {
return process.waitFor();
if (timeout != null) {
return process.waitFor(timeout, TimeUnit.SECONDS) ? 0 : -1;
} else {
return process.waitFor();
}
}
@Override
@ -68,7 +84,11 @@ public class LocalStore implements ShellProcessStore {
@Override
public Charset getCharset() {
return StandardCharsets.UTF_8;
return StandardCharsets.US_ASCII;
}
public Integer getTimeout() {
return timeout;
}
}
@ -78,8 +98,8 @@ public class LocalStore implements ShellProcessStore {
}
@Override
public String toDisplay() {
return "local";
public String toSummaryString() {
return "localhost";
}
@Override
@ -95,17 +115,17 @@ public class LocalStore implements ShellProcessStore {
}
@Override
public ProcessControl prepareCommand(InputStream input, List<String> cmd) {
return new LocalProcessControl(input, cmd);
public ProcessControl prepareCommand(List<Secret> input, List<String> cmd, Integer timeout) {
return new LocalProcessControl(input, cmd, getEffectiveTimeOut(timeout));
}
@Override
public ProcessControl preparePrivilegedCommand(InputStream input, List<String> cmd) throws Exception {
return new LocalProcessControl(input, cmd);
public ProcessControl preparePrivilegedCommand(List<Secret> input, List<String> cmd, Integer timeOut) throws Exception {
return new LocalProcessControl(input, cmd, getEffectiveTimeOut(timeOut));
}
@Override
public ShellType determineType() {
return ShellTypes.CMD;
public ShellType determineType() throws Exception {
return ShellTypes.determine(this);
}
}

View file

@ -5,10 +5,11 @@ import java.io.OutputStream;
public interface MachineFileStore extends DataStore {
static MachineFileStore local() {
return new LocalStore();
default boolean isLocal(){
return false;
}
InputStream openInput(String file) throws Exception;
OutputStream openOutput(String file) throws Exception;

View file

@ -33,7 +33,7 @@ public final class NamedStore implements DataStore {
}
@Override
public String toDisplay() {
public String toSummaryString() {
throw new UnsupportedOperationException();
}

View file

@ -14,7 +14,6 @@ public abstract class ProcessControl {
start();
var errT = discardErr();
var string = new String(getStdout().readAllBytes(), getCharset());
errT.join();
waitFor();
return string;
}
@ -34,9 +33,6 @@ public abstract class ProcessControl {
t.setDaemon(true);
t.start();
outT.join();
t.join();
var ec = waitFor();
return ec != 0 ? Optional.of(read.get()) : Optional.empty();
}

View file

@ -0,0 +1,23 @@
package io.xpipe.core.store;
public class ProcessOutputException extends Exception{
public ProcessOutputException() {
super();
}
public ProcessOutputException(String message) {
super(message);
}
public ProcessOutputException(String message, Throwable cause) {
super(message, cause);
}
public ProcessOutputException(Throwable cause) {
super(cause);
}
protected ProcessOutputException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View file

@ -1,37 +0,0 @@
package io.xpipe.core.store;
import java.io.InputStream;
import java.io.OutputStream;
public interface ShellProcessStore extends StandardShellStore {
ShellType determineType() throws Exception;
@Override
default InputStream openInput(String file) throws Exception {
var type = determineType();
var cmd = type.createFileReadCommand(file);
var p = prepareCommand(InputStream.nullInputStream(), cmd);
p.start();
return p.getStdout();
}
@Override
default OutputStream openOutput(String file) throws Exception {
return null;
// 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 = prepareCommand(InputStream.nullInputStream(), cmd);
p.start();
return p.waitFor() == 0;
}
}

View file

@ -1,35 +1,61 @@
package io.xpipe.core.store;
import io.xpipe.core.util.Secret;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
public interface ShellStore extends MachineFileStore {
public abstract class ShellStore implements DataStore {
static StandardShellStore local() {
return new LocalStore();
public Integer getTimeout() {
return null;
}
default String executeAndRead(List<String> cmd) throws Exception {
var pc = prepareCommand(InputStream.nullInputStream(), cmd);
public List<Secret> getInput() {
return List.of();
}
public boolean isLocal() {
return false;
}
public String executeAndRead(List<String> cmd, Integer timeout) throws Exception {
var pc = prepareCommand(List.of(), cmd, getEffectiveTimeOut(timeout));
pc.start();
pc.discardErr();
var string = new String(pc.getStdout().readAllBytes(), pc.getCharset());
return string;
}
default Optional<String> executeAndCheckOut(InputStream in, List<String> cmd) throws Exception {
var pc = prepareCommand(in, cmd);
public String executeAndCheckOut(List<Secret> in, List<String> cmd, Integer timeout) throws ProcessOutputException, Exception {
var pc = prepareCommand(in, cmd, getEffectiveTimeOut(timeout));
pc.start();
var outT = pc.discardErr();
AtomicReference<String> read = new AtomicReference<>();
AtomicReference<String> readError = new AtomicReference<>();
var errorThread = new Thread(() -> {
try {
readError.set(new String(pc.getStderr().readAllBytes(), pc.getCharset()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
errorThread.setDaemon(true);
errorThread.start();
var read = new ByteArrayOutputStream();
var t = new Thread(() -> {
try {
read.set(new String(pc.getStdout().readAllBytes(), pc.getCharset()));
final byte[] buf = new byte[1];
int length;
while ((length = pc.getStdout().read(buf)) > 0) {
read.write(buf, 0, length);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
@ -37,15 +63,21 @@ public interface ShellStore extends MachineFileStore {
t.setDaemon(true);
t.start();
outT.join();
t.join();
var ec = pc.waitFor();
return ec == 0 ? Optional.of(read.get()) : Optional.empty();
var readOut = read.toString(pc.getCharset());
if (ec == -1) {
throw new ProcessOutputException("Command timed out");
}
if (ec == 0) {
return readOut;
} else {
throw new ProcessOutputException("Command returned with " + ec + ": " + readError.get());
}
}
default Optional<String> executeAndCheckErr(InputStream in, List<String> cmd) throws Exception {
var pc = prepareCommand(in, cmd);
public Optional<String> executeAndCheckErr(List<Secret> in, List<String> cmd) throws Exception {
var pc = prepareCommand(in, cmd, getTimeout());
pc.start();
var outT = pc.discardOut();
@ -67,17 +99,27 @@ public interface ShellStore extends MachineFileStore {
return ec != 0 ? Optional.of(read.get()) : Optional.empty();
}
default ProcessControl prepareCommand(List<String> cmd) throws Exception {
return prepareCommand(InputStream.nullInputStream(), cmd);
public Integer getEffectiveTimeOut(Integer timeout) {
if (this.getTimeout() == null) {
return timeout;
}
if (timeout == null) {
return getTimeout();
}
return Math.min(getTimeout(), timeout);
}
ProcessControl prepareCommand(InputStream input, List<String> cmd) throws Exception;
default ProcessControl preparePrivilegedCommand(List<String> cmd) throws Exception {
return preparePrivilegedCommand(InputStream.nullInputStream(), cmd);
public ProcessControl prepareCommand(List<String> cmd, Integer timeout) throws Exception {
return prepareCommand(List.of(), cmd, timeout);
}
default ProcessControl preparePrivilegedCommand(InputStream input, List<String> cmd) throws Exception {
public abstract ProcessControl prepareCommand(List<Secret> input, List<String> cmd, Integer timeout) throws Exception;
public ProcessControl preparePrivilegedCommand(List<String> cmd, Integer timeout) throws Exception {
return preparePrivilegedCommand(List.of(), cmd, timeout);
}
public ProcessControl preparePrivilegedCommand(List<Secret> input, List<String> cmd, Integer timeout) throws Exception {
throw new UnsupportedOperationException();
}
}

View file

@ -1,7 +1,9 @@
package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.xpipe.core.util.Secret;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@ -10,28 +12,28 @@ import java.util.List;
public class ShellTypes {
public static StandardShellStore.ShellType determine(ShellStore store) throws Exception {
var o = store.executeAndCheckOut(InputStream.nullInputStream(), List.of("echo", "$0"));
if (o.isPresent() && !o.get().equals("$0")) {
var o = store.executeAndCheckOut(List.of(), List.of("echo", "$0"), null).strip();
if (!o.equals("$0")) {
return SH;
} else {
o = store.executeAndCheckOut(InputStream.nullInputStream(), List.of("(dir 2>&1 *`|echo CMD);&<# rem #>echo PowerShell"));
if (o.isPresent() && o.get().equals("PowerShell")) {
o = store.executeAndCheckOut(List.of(), List.of("(dir 2>&1 *`|echo CMD);&<# rem #>echo PowerShell"), null).trim();
if (o.equals("PowerShell")) {
return POWERSHELL;
} else {
} else
return CMD;
}
}
}
public static StandardShellStore.ShellType[] getAvailable(ShellStore store) throws Exception {
var o = store.executeAndCheckOut(InputStream.nullInputStream(), List.of("echo", "$0"));
if (o.isPresent() && !o.get().trim().equals("$0")) {
var o = store.executeAndCheckOut(List.of(), List.of("echo", "$0"), null);
if (!o.trim().equals("$0")) {
return getLinuxShells();
} else {
return getWindowsShells();
}
}
@JsonProperty("powershell")
public static final StandardShellStore.ShellType POWERSHELL = new StandardShellStore.ShellType() {
@Override
@ -70,8 +72,14 @@ public class ShellTypes {
public String getDisplayName() {
return "PowerShell";
}
@Override
public List<String> getOperatingSystemNameCommand() {
return List.of("systeminfo", "|", "findstr", "/B", "/C:\"OS Name\"");
}
};
@JsonProperty("cmd")
public static final StandardShellStore.ShellType CMD = new StandardShellStore.ShellType() {
@Override
@ -83,9 +91,9 @@ public class ShellTypes {
}
@Override
public ProcessControl prepareElevatedCommand(ShellStore st, InputStream in, List<String> cmd, String pw) throws Exception {
public ProcessControl prepareElevatedCommand(ShellStore st, List<Secret> in, List<String> cmd, Integer timeout, String pw) throws Exception {
var l = List.of("net", "session", ";", "if", "%errorLevel%", "!=", "0");
return st.prepareCommand(InputStream.nullInputStream(), l);
return st.prepareCommand(List.of(), l, timeout);
}
@Override
@ -117,8 +125,14 @@ public class ShellTypes {
public String getDisplayName() {
return "cmd.exe";
}
@Override
public List<String> getOperatingSystemNameCommand() {
return List.of("Get-ComputerInfo");
}
};
@JsonProperty("sh")
public static final StandardShellStore.ShellType SH = new StandardShellStore.ShellType() {
@Override
@ -127,12 +141,12 @@ public class ShellTypes {
}
@Override
public ProcessControl prepareElevatedCommand(ShellStore st, InputStream in, List<String> cmd, String pw) throws Exception {
public ProcessControl prepareElevatedCommand(ShellStore st, List<Secret> in, List<String> cmd, Integer timeout, String pw) throws Exception {
var l = new ArrayList<>(cmd);
l.add(0, "sudo");
l.add(1, "-S");
var pws = new ByteArrayInputStream(pw.getBytes(getCharset()));
return st.prepareCommand(pws, l);
return st.prepareCommand(List.of(Secret.createForSecretValue(pw)), l, timeout);
}
@Override
@ -164,6 +178,11 @@ public class ShellTypes {
public String getDisplayName() {
return "/bin/sh";
}
@Override
public List<String> getOperatingSystemNameCommand() {
return List.of("uname", "-o");
}
};
public static StandardShellStore.ShellType getDefault() {

View file

@ -1,17 +1,21 @@
package io.xpipe.core.store;
import io.xpipe.core.util.Secret;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.List;
public interface StandardShellStore extends ShellStore {
public abstract class StandardShellStore extends ShellStore implements MachineFileStore {
static interface ShellType {
public static interface ShellType {
List<String> switchTo(List<String> cmd);
default ProcessControl prepareElevatedCommand(ShellStore st, InputStream in, List<String> cmd, String pw) throws Exception {
return st.prepareCommand(in, cmd);
default ProcessControl prepareElevatedCommand(ShellStore st, List<Secret> in, List<String> cmd, Integer timeout, String pw) throws Exception {
return st.prepareCommand(in, cmd, timeout);
}
List<String> createFileReadCommand(String file);
@ -25,7 +29,42 @@ public interface StandardShellStore extends ShellStore {
String getName();
String getDisplayName();
List<String> getOperatingSystemNameCommand();
}
ShellType determineType() throws Exception;
public abstract ShellType determineType() throws Exception;
public final String querySystemName() throws Exception {
var result = executeAndCheckOut(List.of(), determineType().getOperatingSystemNameCommand(), getTimeout());
return result.strip();
}
@Override
public InputStream openInput(String file) throws Exception {
var type = determineType();
var cmd = type.createFileReadCommand(file);
var p = prepareCommand(List.of(), cmd, null);
p.start();
return p.getStdout();
}
@Override
public OutputStream openOutput(String file) throws Exception {
return null;
// var type = determineType();
// var cmd = type.createFileWriteCommand(file);
// var p = prepare(cmd).redirectErrorStream(true);
// var proc = p.start();
// return proc.getOutputStream();
}
@Override
public boolean exists(String file) throws Exception {
var type = determineType();
var cmd = type.createFileExistsCommand(file);
var p = prepareCommand(List.of(), cmd, null);
p.start();
return p.waitFor() == 0;
}
}

View file

@ -20,8 +20,8 @@ module io.xpipe.core {
opens io.xpipe.core.data.typed;
opens io.xpipe.core.dialog;
requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.databind;
requires static com.fasterxml.jackson.core;
requires static com.fasterxml.jackson.databind;
requires java.net.http;
requires static lombok;

1
deps

@ -1 +0,0 @@
Subproject commit 49a1ad06bc6872f72c1d20ea864d24f3df59b7c5

View file

@ -25,17 +25,13 @@ version = file('../misc/version').text
group = 'io.xpipe'
archivesBaseName = 'extension'
repositories {
mavenCentral()
}
dependencies {
compileOnly 'net.synedra:validatorfx:0.3.1'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.8.2'
compileOnly 'com.jfoenix:jfoenix:9.0.10'
implementation project(':core')
implementation project(':fxcomps')
//implementation 'io.xpipe:fxcomps:0.2'
// implementation project(':fxcomps')
implementation 'io.xpipe:fxcomps:0.2.1'
implementation 'org.controlsfx:controlsfx:11.1.1'
}

View file

@ -15,7 +15,7 @@ import java.util.Map;
public interface DataSourceProvider<T extends DataSource<?>> {
static enum GeneralType {
static enum Category {
FILE,
DATABASE;
}
@ -25,13 +25,13 @@ public interface DataSourceProvider<T extends DataSource<?>> {
getSourceClass();
}
default GeneralType getGeneralType() {
default Category getGeneralType() {
if (getFileProvider() != null) {
return GeneralType.FILE;
return Category.FILE;
}
if (getDatabaseProvider() != null) {
return GeneralType.DATABASE;
return Category.DATABASE;
}
throw new ExtensionException("Provider has no general type");
@ -56,7 +56,7 @@ public interface DataSourceProvider<T extends DataSource<?>> {
return getId() + "." + key;
}
default Region createConfigGui(Property<T> source) {
default Region createConfigGui(Property<T> source, Property<T> appliedSource) {
return null;
}

View file

@ -1,10 +1,7 @@
package io.xpipe.extension;
import io.xpipe.core.dialog.Dialog;
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.*;
import javafx.beans.property.Property;
import java.net.URI;
@ -35,9 +32,14 @@ public interface DataStoreProvider {
throw new ExtensionException("Gui Dialog is not implemented by provider " + getId());
}
default void init() throws Exception {
default boolean init() throws Exception {
return true;
}
String queryInformationString(DataStore store, int length) throws Exception;
public String toSummaryString(DataStore store, int length);
default boolean isHidden() {
return false;
}
@ -81,7 +83,7 @@ public interface DataStoreProvider {
}
default String display(DataStore store) {
return store.toDisplay();
return store.toSummaryString();
}
List<String> getPossibleNames();

View file

@ -1,6 +1,7 @@
package io.xpipe.extension;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.store.DataStore;
import io.xpipe.extension.event.ErrorEvent;
import java.net.URI;
@ -53,6 +54,11 @@ public class DataStoreProviders {
return ALL.stream().map(d -> d.dialogForString(s)).filter(Objects::nonNull).findAny();
}
public static <T extends DataStoreProvider> T byStore(DataStore store) {
return byStoreClass(store.getClass());
}
@SuppressWarnings("unchecked")
public static <T extends DataStoreProvider> T byStoreClass(Class<?> c) {
if (ALL == null) {

View file

@ -1,28 +1,57 @@
package io.xpipe.extension;
import io.xpipe.core.source.DataSource;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.Region;
import java.util.function.Supplier;
import lombok.AllArgsConstructor;
import lombok.Value;
public interface SupportedApplicationProvider {
enum Category {
PROGRAMMING_LANGUAGE,
APPLICATION
APPLICATION,
OTHER
}
Region createRetrieveInstructions(ObservableValue<String> id);
enum AccessType {
ACTIVE,
PASSIVE
}
@Value
@AllArgsConstructor
public static class InstructionsDisplay {
Region region;
Runnable onFinish;
public InstructionsDisplay(Region region) {
this.region = region;
onFinish = null;
}
}
default InstructionsDisplay createRetrievalInstructions(DataSource<?> source, ObservableValue<String> id) {
return null;
}
default InstructionsDisplay createUpdateInstructions(DataSource<?> source, ObservableValue<String> id) {
return null;
}
String getId();
Supplier<String> getName();
ObservableValue<String> getName();
Category getCategory();
AccessType getAccessType();
String getSetupGuideURL();
default String getGraphicIcon() {
return null;
}
}

View file

@ -10,7 +10,7 @@ public interface UniformDataSourceProvider<T extends DataSource<?>> extends Data
@Override
default Dialog configDialog(T source, boolean all) {
return Dialog.empty();
return Dialog.empty().evaluateTo(() -> source);
}
@Override
@ -19,7 +19,7 @@ public interface UniformDataSourceProvider<T extends DataSource<?>> extends Data
try {
return (T) getSourceClass().getDeclaredConstructors()[0].newInstance(input);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new AssertionError(e);
throw new ExtensionException(e);
}
}
}

View file

@ -1,8 +1,11 @@
package io.xpipe.extension;
import io.xpipe.core.store.ShellStore;
import javafx.beans.value.ObservableValue;
import net.synedra.validatorfx.Check;
import java.util.function.Predicate;
public class Validators {
public static Check nonNull(Validator v, ObservableValue<String> name, ObservableValue<?> s) {
@ -12,4 +15,16 @@ public class Validators {
}
});
}
public static void nonNull(Object object, String name) {
if (object == null) {
throw new IllegalArgumentException(I18n.get("extension.null", name));
}
}
public static void hostFeature(ShellStore host, Predicate<ShellStore> predicate, String name) {
if (!predicate.test(host)) {
throw new IllegalArgumentException(I18n.get("extension.hostFeatureUnsupported", name));
}
}
}

View file

@ -4,42 +4,33 @@ import io.xpipe.fxcomps.Comp;
import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.SimpleCompStructure;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.layout.HBox;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
import java.util.HashMap;
import java.util.Map;
public class CharChoiceComp extends Comp<CompStructure<HBox>> {
private final Property<Character> value;
private final Property<Character> charChoiceValue;
private final BidiMap<Character, ObservableValue<String>> range;
private final Map<Character, ObservableValue<String>> range;
private final ObservableValue<String> customName;
public CharChoiceComp(Property<Character> value, BidiMap<Character, ObservableValue<String>> range, ObservableValue<String> customName) {
public CharChoiceComp(Property<Character> value, Map<Character, ObservableValue<String>> range, ObservableValue<String> customName) {
this.value = value;
this.range = range;
this.customName = customName;
this.charChoiceValue = new SimpleObjectProperty<>(range.containsKey(value.getValue()) ? value.getValue() : null);
value.addListener((c, o, n) -> {
if (!range.containsKey(n)) {
charChoiceValue.setValue(null);
} else {
charChoiceValue.setValue(n);
}
});
}
@Override
public CompStructure<HBox> createBase() {
var charChoice = new CharComp(value);
var rangeCopy = new DualLinkedHashBidiMap<>(range);
var rangeCopy = new HashMap<>(range);
if (customName != null) {
rangeCopy.put(null, customName);
}
var choice = new ChoiceComp<Character>(charChoiceValue, rangeCopy);
var choice = new ChoiceComp<Character>(value, rangeCopy);
var charChoiceR = charChoice.createRegion();
var choiceR = choice.createRegion();
var box = new HBox(charChoiceR, choiceR);

View file

@ -7,10 +7,11 @@ import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ComboBox;
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.List;
public class CharsetChoiceComp extends ReplacementComp<CompStructure<ComboBox<Charset>>> {
@ -23,9 +24,10 @@ public class CharsetChoiceComp extends ReplacementComp<CompStructure<ComboBox<Ch
@Override
protected Comp<CompStructure<ComboBox<Charset>>> createComp() {
var map = new LinkedHashMap<Charset, ObservableValue<String>>();
for (var e : Charset.availableCharsets().entrySet()) {
map.put(e.getValue(), new SimpleStringProperty(e.getKey()));
for (var e : List.of(StandardCharsets.UTF_8, StandardCharsets.UTF_16,
StandardCharsets.UTF_16BE, StandardCharsets.ISO_8859_1, Charset.forName("Windows-1251"), Charset.forName("Windows-1252"), StandardCharsets.US_ASCII)) {
map.put(e, new SimpleStringProperty(e.displayName()));
}
return new ChoiceComp<>(charset, new DualLinkedHashBidiMap<>(map));
return new ChoiceComp<>(charset, map);
}
}

View file

@ -5,42 +5,65 @@ import io.xpipe.fxcomps.Comp;
import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.SimpleCompStructure;
import io.xpipe.fxcomps.util.PlatformThread;
import io.xpipe.fxcomps.util.SimpleChangeListener;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.scene.control.ComboBox;
import javafx.util.StringConverter;
import org.apache.commons.collections4.BidiMap;
import lombok.Value;
import java.util.Map;
@Value
public class ChoiceComp<T> extends Comp<CompStructure<ComboBox<T>>> {
private final Property<T> value;
private final BidiMap<T, ObservableValue<String>> range;
Property<T> value;
ObservableValue<Map<T, ObservableValue<String>>> range;
public ChoiceComp(Property<T> value, BidiMap<T, ObservableValue<String>> range) {
public ChoiceComp(Property<T> value, Map<T, ObservableValue<String>> range) {
this.value = value;
this.range = new SimpleObjectProperty<>(range);
}
public ChoiceComp(Property<T> value, ObservableValue<Map<T, ObservableValue<String>>> range) {
this.value = value;
this.range = range;
}
@Override
public CompStructure<ComboBox<T>> createBase() {
var list = FXCollections.observableArrayList(range.keySet());
var cb = new ComboBox<>(list);
cb.setConverter(new StringConverter<>() {
var cb = new ComboBox<T>();
cb.setConverter(new StringConverter<T>() {
@Override
public String toString(T object) {
if (object == null) {
return I18n.get("extension.none");
}
return range.get(object).getValue();
var found = range.getValue().get(object);
if (found == null) {
return "";
}
return found.getValue();
}
@Override
public T fromString(String string) {
throw new UnsupportedOperationException();
}
});
SimpleChangeListener.apply(PlatformThread.sync(range), c -> {
var list = FXCollections.observableArrayList(c.keySet());
if (!list.contains(null)) {
list.add(null);
}
cb.setItems(list);
});
PlatformThread.connect(value, cb.valueProperty());
cb.getStyleClass().add("choice-comp");
return new SimpleCompStructure<>(cb);

View file

@ -11,7 +11,6 @@ import javafx.beans.value.ObservableValue;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import net.synedra.validatorfx.Check;
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
import java.nio.charset.Charset;
import java.util.ArrayList;
@ -24,6 +23,7 @@ public class DynamicOptionsBuilder<T> {
private final List<DynamicOptionsComp.Entry> entries = new ArrayList<>();
private final List<Property<?>> props = new ArrayList<>();
private final List<Property<?>> lazyProperties = new ArrayList<>();
private final ObservableValue<String> title;
private final boolean wrap;
@ -43,7 +43,15 @@ public class DynamicOptionsBuilder<T> {
this.title = title;
}
public DynamicOptionsBuilder<T> makeLazy() {
var p = props.get(props.size() - 1);
props.remove(p);
lazyProperties.add(p);
return this;
}
public DynamicOptionsBuilder<T> decorate(Check c) {
entries.get(entries.size() - 1).comp().apply(s -> c.decorates(s.get()));
return this;
}
@ -59,28 +67,42 @@ public class DynamicOptionsBuilder<T> {
for (var e : NewLine.values()) {
map.put(e, I18n.observable("extension." + e.getId()));
}
var comp = new ChoiceComp<>(prop, new DualLinkedHashBidiMap<>(map));
var comp = new ChoiceComp<>(prop,map);
entries.add(new DynamicOptionsComp.Entry(I18n.observable("extension.newLine"), comp));
props.add(prop);
return this;
}
public DynamicOptionsBuilder<T> addCharacter(Property<Character> prop, ObservableValue<String> name, Map<Character, ObservableValue<String>> names) {
var comp = new CharChoiceComp(prop, new DualLinkedHashBidiMap<>(names), null);
var comp = new CharChoiceComp(prop, names, null);
entries.add(new DynamicOptionsComp.Entry(name, comp));
props.add(prop);
return this;
}
public DynamicOptionsBuilder<T> addCharacter(Property<Character> prop, ObservableValue<String> name, Map<Character, ObservableValue<String>> names, ObservableValue<String> customName) {
var comp = new CharChoiceComp(prop, new DualLinkedHashBidiMap<>(names), customName);
var comp = new CharChoiceComp(prop, names, customName);
entries.add(new DynamicOptionsComp.Entry(name, comp));
props.add(prop);
return this;
}
public <V> DynamicOptionsBuilder<T> addToggle(Property<V> prop, ObservableValue<String> name, Map<V, ObservableValue<String>> names) {
var comp = new ToggleGroupComp<>(prop, new DualLinkedHashBidiMap<>(names));
var comp = new ToggleGroupComp<>(prop, names);
entries.add(new DynamicOptionsComp.Entry(name, comp));
props.add(prop);
return this;
}
public <V> DynamicOptionsBuilder<T> addChoice(Property<V> prop, ObservableValue<String> name, Map<V, ObservableValue<String>> names) {
var comp = new ChoiceComp<>(prop, names);
entries.add(new DynamicOptionsComp.Entry(name, comp));
props.add(prop);
return this;
}
public <V> DynamicOptionsBuilder<T> addChoice(Property<V> prop, ObservableValue<String> name, ObservableValue<Map<V, ObservableValue<String>>> names) {
var comp = new ChoiceComp<>(prop, names);
entries.add(new DynamicOptionsComp.Entry(name, comp));
props.add(prop);
return this;
@ -107,6 +129,14 @@ public class DynamicOptionsBuilder<T> {
return this;
}
public DynamicOptionsBuilder<T> addLazyString(String nameKey, Property<String> prop, Property<String> lazy) {
var comp = new TextFieldComp(prop, lazy);
entries.add(new DynamicOptionsComp.Entry(I18n.observable(nameKey), comp));
props.add(prop);
lazyProperties.add(lazy);
return this;
}
public DynamicOptionsBuilder<T> addString(ObservableValue<String> name, Property<String> prop) {
var comp = new TextFieldComp(prop);
entries.add(new DynamicOptionsComp.Entry(name, comp));
@ -120,6 +150,10 @@ public class DynamicOptionsBuilder<T> {
return this;
}
public DynamicOptionsBuilder<T> addSecret(String nameKey, Property<Secret> prop) {
return addSecret(I18n.observable(nameKey), prop);
}
public DynamicOptionsBuilder<T> addSecret(ObservableValue<String> name, Property<Secret> prop) {
var comp = new SecretFieldComp(prop);
entries.add(new DynamicOptionsComp.Entry(name, comp));
@ -134,20 +168,61 @@ public class DynamicOptionsBuilder<T> {
return this;
}
public <V extends T> Comp<?> buildComp(Supplier<V> creator, Property<T> toSet) {
public DynamicOptionsBuilder<T> addInteger(String nameKey, Property<Integer> prop) {
var comp = new IntFieldComp(prop);
entries.add(new DynamicOptionsComp.Entry(I18n.observable(nameKey), comp));
props.add(prop);
return this;
}
@SafeVarargs
public final <V extends T> DynamicOptionsBuilder<T> bind(Supplier<V> creator, Property<T>... toSet) {
props.forEach(prop -> {
prop.addListener((c,o,n) -> {
toSet.setValue(creator.get());
prop.addListener((c, o, n) -> {
for (Property<T> p : toSet) {
p.setValue(creator.get());
}
});
});
toSet.setValue(creator.get());
for (Property<T> p : toSet) {
p.setValue(creator.get());
}
return this;
}
public <V extends T> DynamicOptionsBuilder<T> bindLazy(Supplier<V> creator, Property<T> toLazySet) {
lazyProperties.forEach(prop -> {
prop.addListener((c,o,n) -> {
toLazySet.setValue(creator.get());
});
});
toLazySet.setValue(creator.get());
return this;
}
public <V extends T> Comp<?> buildComp() {
if (title != null) {
entries.add(0, new DynamicOptionsComp.Entry(null, Comp.of(() -> new Label(title.getValue())).styleClass("title")));
}
return new DynamicOptionsComp(entries, wrap);
}
public <V extends T> Region build(Supplier<V> creator, Property<T> toSet) {
return buildComp(creator, toSet).createRegion();
public <V extends T> Comp<?> buildBindingComp(Supplier<Property<? extends V>> creator, Property<T> toSet) {
props.forEach(prop -> {
prop.addListener((c,o,n) -> {
toSet.unbind();
toSet.bind(creator.get());
});
});
toSet.bind(creator.get());
if (title != null) {
entries.add(0, new DynamicOptionsComp.Entry(null, Comp.of(() -> new Label(title.getValue())).styleClass("title")));
}
return new DynamicOptionsComp(entries, wrap);
}
public <V extends T> Region build() {
return buildComp().createRegion();
}
}

View file

@ -22,7 +22,7 @@ public class SecretFieldComp extends Comp<CompStructure<TextField>> {
var text = new PasswordField();
text.setText(value.getValue() != null ? value.getValue().getSecretValue() : null);
text.textProperty().addListener((c, o, n) -> {
value.setValue(n.length() > 0 ? Secret.createForSecretValue(n) : null);
value.setValue(n != null && n.length() > 0 ? Secret.createForSecretValue(n) : null);
});
value.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> {

View file

@ -25,7 +25,10 @@ public class TabPaneComp extends Comp<CompStructure<JFXTabPane>> {
for (var e : entries) {
Tab tab = new Tab();
var ll = new Label(null, new FontIcon(e.graphic()));
var ll = new Label(null);
if (e.graphic != null) {
ll.setGraphic(new FontIcon(e.graphic()));
}
ll.textProperty().bind(e.name());
ll.getStyleClass().add("name");
ll.setAlignment(Pos.CENTER);

View file

@ -5,14 +5,25 @@ import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.SimpleCompStructure;
import io.xpipe.fxcomps.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.event.EventHandler;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public class TextFieldComp extends Comp<CompStructure<TextField>> {
private final Property<String> value;
private final Property<String> lazyValue;
public TextFieldComp(Property<String> value) {
this.value = value;
this.lazyValue = value;
}
public TextFieldComp(Property<String> value, Property<String> lazyValue) {
this.value = value;
this.lazyValue = lazyValue;
}
@Override
@ -26,6 +37,15 @@ public class TextFieldComp extends Comp<CompStructure<TextField>> {
text.setText(n);
});
});
text.setOnKeyPressed(new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent ke) {
if (ke.getCode().equals(KeyCode.ENTER)) {
lazyValue.setValue(value.getValue());
}
ke.consume();
}
});
return new SimpleCompStructure<>(text);
}
}

View file

@ -9,22 +9,19 @@ import javafx.beans.value.ObservableValue;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.HBox;
import org.apache.commons.collections4.BidiMap;
import java.util.Map;
public class ToggleGroupComp<T> extends Comp<CompStructure<HBox>> {
private final Property<T> value;
private final BidiMap<T, ObservableValue<String>> range;
private final Map<T, ObservableValue<String>> range;
public ToggleGroupComp(Property<T> value, BidiMap<T, ObservableValue<String>> range) {
public ToggleGroupComp(Property<T> value, Map<T, ObservableValue<String>> range) {
this.value = value;
this.range = range;
}
public BidiMap<T, ObservableValue<String>> getRange() {
return range;
}
@Override
public CompStructure<HBox> createBase() {
var box = new HBox();

View file

@ -33,6 +33,11 @@ public class ErrorEvent {
.description(msg);
}
public static ErrorEventBuilder fromMessage(String msg) {
return builder()
.description(msg);
}
public void handle() {
EventHandler.get().handle(this);
}

View file

@ -2,9 +2,45 @@ package io.xpipe.extension.prefs;
import io.xpipe.extension.I18n;
import io.xpipe.extension.Translatable;
import lombok.SneakyThrows;
import java.util.Arrays;
import java.util.List;
public interface PrefsChoiceValue extends Translatable {
@SuppressWarnings("unchecked")
@SneakyThrows
static <T> List<T> getAll(Class<T> type) {
if (Enum.class.isAssignableFrom(type)) {
return Arrays.asList(type.getEnumConstants());
}
try {
type.getDeclaredField("ALL");
} catch (NoSuchFieldException e) {
return null;
}
try {
return (List<T>) type.getDeclaredField("ALL").get(null);
} catch (IllegalAccessException | NoSuchFieldException e) {
return List.of(type.getEnumConstants());
}
}
static <T extends PrefsChoiceValue> List<T> getSupported(Class<T> type) {
try {
return (List<T>) type.getDeclaredField("SUPPORTED").get(null);
} catch (IllegalAccessException | NoSuchFieldException e) {
return getAll(type).stream().filter(t -> t.isSupported()).toList();
}
}
default boolean isSupported() {
return true;
}
@Override
default String toTranslatedString() {
return I18n.get(getId());

View file

@ -13,7 +13,6 @@ module io.xpipe.extension {
requires javafx.graphics;
requires transitive javafx.controls;
requires io.xpipe.fxcomps;
requires static org.apache.commons.collections4;
requires static lombok;
requires static com.dlsc.preferencesfx;
requires static com.dlsc.formsfx;

View file

@ -5,3 +5,8 @@ lf=LF (Linux)
none=None
nullPointer=Null Pointer: $MSG$
mustNotBeEmpty=$NAME$ must not be empty
null=$VALUE$ must be not null
hostFeatureUnsupported=Host does not support the feature $FEATURE$
namedHostFeatureUnsupported=$HOST$ does not support this feature
namedHostNotActive=$HOST$ is not active
noInformationAvailable=No information available

0
gradlew vendored Executable file → Normal file
View file

View file

@ -5,8 +5,8 @@ def canonicalVersion = file('misc/canonical_version').text
jreleaser {
environment {
properties.put('rawChangelog', file("misc/changelogs/${canonicalVersion}.txt").exists() ?
file("misc/changelogs/${canonicalVersion}.txt").text.replace('\r\n', '\n') : "")
properties.put('rawChangelog', file("misc/changelogs/${version}.txt").exists() ?
file("misc/changelogs/${version}.txt").text.replace('\r\n', '\n') : "")
}
project {

View file

@ -1 +0,0 @@
0.1

View file

@ -2,10 +2,7 @@
🚀 {{projectName}} {{projectVersion}} has been released: {{releaseNotesUrl}}
If you have already installed {{projectName}} and have updates enabled,
this update will be automatically installed when launching it the next time.
You can get the standalone version here: {{releaseNotesUrl}}
The documentation and maven repositories should be automatically updated within the next couple of hours.
Changes in {{projectVersion}}:
{{{rawChangelog}}}

View file

@ -5,8 +5,7 @@
Note that as this is not a final release, there might still be some small issues with it.
Please report them if you stumble upon one.
Also note that pre-releases are not downloaded by the automatic updater
until they are ready for a full release or you have opted in to receiving pre-releases.
The documentation and maven repositories should be automatically updated within the next couple of hours.
Changes in {{projectVersion}}:
{{{rawChangelog}}}

View file

@ -1 +1 @@
0.1-SNAPSHOT
0.0.1-SNAPSHOT

View file

@ -4,3 +4,12 @@ include 'api'
include 'core'
include 'beacon'
include 'extension'
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.30.0"
}
}