diff --git a/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java b/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java index bc0215552..b10037415 100644 --- a/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java +++ b/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java @@ -130,12 +130,12 @@ public final class XPipeConnection extends BeaconConnection { @FunctionalInterface public static interface Handler { - void handle(BeaconConnection con) throws ClientException, ServerException, ConnectorException; + void handle(BeaconConnection con) throws Exception; } @FunctionalInterface public static interface Mapper { - T handle(BeaconConnection con) throws ClientException, ServerException, ConnectorException; + T handle(BeaconConnection con) throws Exception; } } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java index d1cd89aea..1db8270ab 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java @@ -1,6 +1,7 @@ package io.xpipe.beacon; import io.xpipe.beacon.exchange.StopExchange; +import io.xpipe.core.process.OsType; import lombok.experimental.UtilityClass; import java.io.BufferedReader; @@ -64,42 +65,42 @@ public class BeaconServer { } var out = new Thread( - null, - () -> { - try { - InputStreamReader isr = new InputStreamReader(proc.getInputStream()); - BufferedReader br = new BufferedReader(isr); - String line; - while ((line = br.readLine()) != null) { - if (print) { - System.out.println("[xpiped] " + line); - } - } - } catch (Exception ioe) { - ioe.printStackTrace(); + null, + () -> { + try { + InputStreamReader isr = new InputStreamReader(proc.getInputStream()); + BufferedReader br = new BufferedReader(isr); + String line; + while ((line = br.readLine()) != null) { + if (print) { + System.out.println("[xpiped] " + line); } - }, - "daemon sysout"); + } + } catch (Exception ioe) { + ioe.printStackTrace(); + } + }, + "daemon sysout"); out.setDaemon(true); out.start(); var err = new Thread( - null, - () -> { - try { - InputStreamReader isr = new InputStreamReader(proc.getErrorStream()); - BufferedReader br = new BufferedReader(isr); - String line; - while ((line = br.readLine()) != null) { - if (print) { - System.err.println("[xpiped] " + line); - } - } - } catch (Exception ioe) { - ioe.printStackTrace(); + null, + () -> { + try { + InputStreamReader isr = new InputStreamReader(proc.getErrorStream()); + BufferedReader br = new BufferedReader(isr); + String line; + while ((line = br.readLine()) != null) { + if (print) { + System.err.println("[xpiped] " + line); } - }, - "daemon syserr"); + } + } catch (Exception ioe) { + ioe.printStackTrace(); + } + }, + "daemon syserr"); err.setDaemon(true); err.start(); } @@ -110,7 +111,7 @@ public class BeaconServer { return res.isSuccess(); } - private static Optional getDaemonBasePath() { + private static Optional getDaemonBasePath(OsType type) { Path base = null; // Prepare for invalid XPIPE_HOME path value try { @@ -120,7 +121,7 @@ public class BeaconServer { } if (base == null) { - if (System.getProperty("os.name").startsWith("Windows")) { + if (type.equals(OsType.WINDOWS)) { base = Path.of(System.getenv("LOCALAPPDATA"), "X-Pipe"); } else { base = Path.of("/opt/xpipe/"); @@ -133,17 +134,20 @@ public class BeaconServer { return Optional.ofNullable(base); } - public static Optional getDaemonExecutable() { - var base = getDaemonBasePath().orElseThrow(); - var debug = BeaconConfig.launchDaemonInDebugMode(); - Path executable = null; - if (!debug) { - if (System.getProperty("os.name").startsWith("Windows")) { - executable = Path.of("app", "runtime", "bin", "xpiped.bat"); - } else { - executable = Path.of("app/bin/xpiped"); - } + public static Path getDaemonExecutableInBaseDirectory(OsType type) { + if (type.equals(OsType.WINDOWS)) { + return Path.of("app", "runtime", "bin", "xpiped.bat"); + } else { + return Path.of("app/bin/xpiped"); + } + } + public static Optional getDaemonExecutable() { + var base = getDaemonBasePath(OsType.getLocal()).orElseThrow(); + var debug = BeaconConfig.launchDaemonInDebugMode(); + Path executable; + if (!debug) { + executable = getDaemonExecutableInBaseDirectory(OsType.getLocal()); } else { String scriptName = null; if (BeaconConfig.attachDebuggerToDaemon()) { diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/NamedFunctionExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/NamedFunctionExchange.java new file mode 100644 index 000000000..e7fe60ffc --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/NamedFunctionExchange.java @@ -0,0 +1,36 @@ +package io.xpipe.beacon.exchange; + +import io.xpipe.beacon.RequestMessage; +import io.xpipe.beacon.ResponseMessage; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +public class NamedFunctionExchange implements MessageExchange { + + @Override + public String getId() { + return "namedFunction"; + } + + @Jacksonized + @Builder + @Value + public static class Request implements RequestMessage { + @NonNull + String id; + + @NonNull List arguments; + } + + @Jacksonized + @Builder + @Value + public static class Response implements ResponseMessage { + + Object returnValue; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/ProxyReadConnectionExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/ProxyReadConnectionExchange.java new file mode 100644 index 000000000..75e4a37e3 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/ProxyReadConnectionExchange.java @@ -0,0 +1,30 @@ +package io.xpipe.beacon.exchange; + +import io.xpipe.beacon.RequestMessage; +import io.xpipe.beacon.ResponseMessage; +import io.xpipe.core.source.DataSource; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +public class ProxyReadConnectionExchange implements MessageExchange { + + @Override + public String getId() { + return "proxyReadConnection"; + } + + @Jacksonized + @Builder + @Value + public static class Request implements RequestMessage { + @NonNull DataSource source; + } + + @Jacksonized + @Builder + @Value + public static class Response implements ResponseMessage { + } +} diff --git a/beacon/src/main/java/module-info.java b/beacon/src/main/java/module-info.java index 0f4997dd2..6aea13694 100644 --- a/beacon/src/main/java/module-info.java +++ b/beacon/src/main/java/module-info.java @@ -36,6 +36,7 @@ module io.xpipe.beacon { ListCollectionsExchange, ListEntriesExchange, ModeExchange, + NamedFunctionExchange, StatusExchange, StopExchange, RenameStoreExchange, @@ -43,6 +44,7 @@ module io.xpipe.beacon { StoreAddExchange, ReadDrainExchange, WritePreparationExchange, + ProxyReadConnectionExchange, WriteExecuteExchange, SelectExchange, ReadExchange, diff --git a/core/src/main/java/io/xpipe/core/impl/FileNames.java b/core/src/main/java/io/xpipe/core/impl/FileNames.java new file mode 100644 index 000000000..ed6f051a4 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/impl/FileNames.java @@ -0,0 +1,39 @@ +package io.xpipe.core.impl; + +import java.util.Arrays; +import java.util.List; + +public class FileNames { + + public static String getFileName(String file) { + var split = file.split("[\\\\/]"); + if (split.length == 0) { + return ""; + } + return split[split.length - 1]; + } + + public static String join(String... parts) { + var joined = String.join("/", parts); + return normalize(joined); + } + + public static String normalize(String file) { + var backslash = file.contains("\\"); + return backslash ? toWindows(file) : toUnix(file); + } + + private static List split(String file) { + var split = file.split("[\\\\/]"); + return Arrays.stream(split).filter(s -> !s.isEmpty()).toList(); + } + + public static String toUnix(String file) { + var joined = String.join("/", split(file)); + return file.startsWith("/") ? "/" + joined : joined; + } + + public static String toWindows(String file) { + return String.join("\\", split(file)); + } +} diff --git a/core/src/main/java/io/xpipe/core/impl/InputStreamStore.java b/core/src/main/java/io/xpipe/core/impl/InputStreamStore.java new file mode 100644 index 000000000..5f8e44fdb --- /dev/null +++ b/core/src/main/java/io/xpipe/core/impl/InputStreamStore.java @@ -0,0 +1,28 @@ +package io.xpipe.core.impl; + +import io.xpipe.core.store.StreamDataStore; + +import java.io.InputStream; + +/** + * A data store that is only represented by an InputStream. + * This can be useful for development. + */ +public class InputStreamStore implements StreamDataStore { + + private final InputStream in; + + public InputStreamStore(InputStream in) { + this.in = in; + } + + @Override + public InputStream openInput() throws Exception { + return in; + } + + @Override + public boolean canOpen() { + return true; + } +} \ No newline at end of file diff --git a/core/src/main/java/io/xpipe/core/process/OsType.java b/core/src/main/java/io/xpipe/core/process/OsType.java index 27bf75342..5322d286b 100644 --- a/core/src/main/java/io/xpipe/core/process/OsType.java +++ b/core/src/main/java/io/xpipe/core/process/OsType.java @@ -13,6 +13,8 @@ public interface OsType { String getName(); + String getTempDirectory(ShellProcessControl pc) throws Exception; + String normalizeFileName(String file); Map getProperties(ShellProcessControl pc) throws Exception; @@ -43,6 +45,11 @@ public interface OsType { return "Windows"; } + @Override + public String getTempDirectory(ShellProcessControl pc) throws Exception { + return pc.executeSimpleCommand(pc.getShellType().getPrintVariableCommand("TEMP")); + } + @Override public String normalizeFileName(String file) { return String.join("\\", file.split("[\\\\/]+")); @@ -81,6 +88,11 @@ public interface OsType { static class Linux implements OsType { + @Override + public String getTempDirectory(ShellProcessControl pc) throws Exception { + return "/tmp/"; + } + @Override public String normalizeFileName(String file) { return String.join("/", file.split("[\\\\/]+")); @@ -146,6 +158,11 @@ public interface OsType { static class Mac implements OsType { + @Override + public String getTempDirectory(ShellProcessControl pc) throws Exception { + return pc.executeSimpleCommand(pc.getShellType().getPrintVariableCommand("TEMP")); + } + @Override public String normalizeFileName(String file) { return String.join("/", file.split("[\\\\/]+")); diff --git a/core/src/main/java/io/xpipe/core/process/ShellProcessControl.java b/core/src/main/java/io/xpipe/core/process/ShellProcessControl.java index e046a7e7a..8fb1ab606 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellProcessControl.java +++ b/core/src/main/java/io/xpipe/core/process/ShellProcessControl.java @@ -21,6 +21,13 @@ public interface ShellProcessControl extends ProcessControl { return executeSimpleCommand(type.switchTo(command)); } + default void restart() throws Exception { + exitAndWait(); + start(); + } + + boolean isLocal(); + int getProcessId(); OsType getOsType(); @@ -34,7 +41,7 @@ public interface ShellProcessControl extends ProcessControl { SecretValue getElevationPassword(); default ShellProcessControl shell(@NonNull ShellType type) { - return shell(type.openCommand()); + return shell(type.openCommand()).elevation(getElevationPassword()); } default CommandProcessControl command(@NonNull ShellType type, String command) { diff --git a/core/src/main/java/io/xpipe/core/process/ShellType.java b/core/src/main/java/io/xpipe/core/process/ShellType.java index 17f993503..e7df0a628 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellType.java +++ b/core/src/main/java/io/xpipe/core/process/ShellType.java @@ -13,6 +13,8 @@ public interface ShellType { return String.join(getConcatenationOperator(), s); } + String escape(String input); + void elevate(ShellProcessControl control, String command, String displayCommand) throws Exception; default String getExitCommand() { @@ -35,6 +37,8 @@ public interface ShellType { String getSetVariableCommand(String variableName, String value); + String getPrintVariableCommand(String name); + List openCommand(); String switchTo(String cmd); diff --git a/core/src/main/java/io/xpipe/core/process/ShellTypes.java b/core/src/main/java/io/xpipe/core/process/ShellTypes.java index f347f9bb7..aa857ffe6 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellTypes.java +++ b/core/src/main/java/io/xpipe/core/process/ShellTypes.java @@ -51,6 +51,11 @@ public class ShellTypes { return "set \"" + variableName + "=" + value + "\""; } + @Override + public String getPrintVariableCommand(String name) { + return "echo %" + name + "%"; + } + @Override public String getEchoCommand(String s, boolean toErrorStream) { return toErrorStream ? "(echo " + s + ")1>&2" : "echo " + s; @@ -74,6 +79,11 @@ public class ShellTypes { return "&"; } + @Override + public String escape(String input) { + return input; + } + @Override public void elevate(ShellProcessControl control, String command, String displayCommand) throws Exception { try (CommandProcessControl c = control.command("net session >NUL 2>NUL")) { @@ -163,6 +173,11 @@ public class ShellTypes { @Value public static class PowerShell implements ShellType { + @Override + public String getPrintVariableCommand(String name) { + return "echo %" + name + "%"; + } + @Override public String getSetVariableCommand(String variableName, String value) { return "set " + variableName + "=" + value; @@ -190,6 +205,11 @@ public class ShellTypes { return true; } + @Override + public String escape(String input) { + return input; + } + @Override public void elevate(ShellProcessControl control, String command, String displayCommand) throws Exception { try (CommandProcessControl c = control.command( @@ -282,6 +302,11 @@ public class ShellTypes { @Value public static class Sh implements ShellType { + @Override + public String getPrintVariableCommand(String name) { + return "echo $" + name; + } + @Override public String getExitCommand() { return "exit 0"; @@ -292,6 +317,11 @@ public class ShellTypes { return ";"; } + @Override + public String escape(String input) { + return input.replace("$","\\$"); + } + @Override public void elevate(ShellProcessControl control, String command, String displayCommand) throws Exception { if (control.getElevationPassword() == null) { @@ -300,7 +330,7 @@ public class ShellTypes { } // For sudo to always query for a password by using the -k switch - control.executeCommand("sudo -p \"\" -k -S " + command); + control.executeCommand("sudo -p \"\" -k -S " + escape(command)); control.writeLine(control.getElevationPassword().getSecretValue()); } diff --git a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java new file mode 100644 index 000000000..ed7b58ae9 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java @@ -0,0 +1,69 @@ +package io.xpipe.core.util; + +import io.xpipe.core.impl.FileNames; +import io.xpipe.core.process.CommandProcessControl; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellProcessControl; + +import java.util.Optional; + +public class XPipeInstallation { + + public static Optional queryInstallationVersion(ShellProcessControl p) throws Exception { + var executable = getInstallationExecutable(p); + if (executable.isEmpty()) { + return Optional.empty(); + } + + try (CommandProcessControl c = p.command(executable.get() + " version").start()) { + return Optional.ofNullable(c.readOrThrow()); + } + } + + public static boolean containsCompatibleInstallation(ShellProcessControl p, String version) throws Exception { + var executable = getInstallationExecutable(p); + if (executable.isEmpty()) { + return false; + } + + try (CommandProcessControl c = p.command(executable.get() + " version").start()) { + return c.readOrThrow().equals(version); + } + } + + public static Optional getInstallationExecutable(ShellProcessControl p) throws Exception { + var installation = getDefaultInstallationBasePath(p); + var executable = FileNames.join(installation, getDaemonExecutableInInstallationDirectory(p.getOsType())); + var file = FileNames.join(installation, executable); + try (CommandProcessControl c = + p.command(p.getShellType().createFileExistsCommand(file)).start()) { + return c.startAndCheckExit() ? Optional.of(file) : Optional.empty(); + } + } + + public static String getDataBasePath(ShellProcessControl p) throws Exception { + if (p.getOsType().equals(OsType.WINDOWS)) { + var base = p.executeSimpleCommand(p.getShellType().getPrintVariableCommand("userprofile")); + return FileNames.join(base, "X-Pipe"); + } else { + return FileNames.join("~", "xpipe"); + } + } + + public static String getDefaultInstallationBasePath(ShellProcessControl p) throws Exception { + if (p.getOsType().equals(OsType.WINDOWS)) { + var base = p.executeSimpleCommand(p.getShellType().getPrintVariableCommand("LOCALAPPDATA")); + return FileNames.join(base, "X-Pipe"); + } else { + return "/opt/xpipe"; + } + } + + public static String getDaemonExecutableInInstallationDirectory(OsType type) { + if (type.equals(OsType.WINDOWS)) { + return FileNames.join("app", "runtime", "bin", "xpiped.bat"); + } else { + return FileNames.join("app/bin/xpiped"); + } + } +} diff --git a/extension/src/main/java/io/xpipe/extension/NamedFunction.java b/extension/src/main/java/io/xpipe/extension/NamedFunction.java new file mode 100644 index 000000000..776d0eadb --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/NamedFunction.java @@ -0,0 +1,79 @@ +package io.xpipe.extension; + +import io.xpipe.api.connector.XPipeConnection; +import io.xpipe.extension.event.ErrorEvent; +import lombok.Getter; +import lombok.SneakyThrows; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.ServiceLoader; + +@Getter +public class NamedFunction { + + public static final List ALL = new ArrayList<>(); + + public static void init(ModuleLayer layer) { + if (ALL.size() == 0) { + ALL.addAll(ServiceLoader.load(layer, NamedFunction.class).stream() + .map(p -> p.get()) + .toList()); + } + } + + public static NamedFunction get(String id) { + return ALL.stream() + .filter(namedFunction -> namedFunction.id.equalsIgnoreCase(id)) + .findFirst() + .orElseThrow(); + } + + public static T callLocal(String id, Object... args) { + return get(id).callLocal(args); + } + + public static T callRemote(String id, Object... args) { + XPipeConnection.execute(con -> { + con.sendRequest(null); + }); + return get(id).callLocal(args); + } + + @SneakyThrows + public static T call(Class clazz, Object... args) { + var base = args[0]; + if (base instanceof Proxyable) { + return callRemote(clazz.getDeclaredConstructor().newInstance().getId(), args); + } else { + return callLocal(clazz.getDeclaredConstructor().newInstance().getId(), args); + } + } + + private final String id; + private final Method method; + + public NamedFunction(String id, Method method) { + this.id = id; + this.method = method; + } + + public NamedFunction(String id, Class clazz) { + this.id = id; + this.method = Arrays.stream(clazz.getDeclaredMethods()) + .filter(method1 -> method1.getName().equals(id)) + .findFirst() + .orElseThrow(); + } + + public T callLocal(Object... args) { + try { + return (T) method.invoke(null, args); + } catch (Throwable ex) { + ErrorEvent.fromThrowable(ex).handle(); + return null; + } + } +} diff --git a/extension/src/main/java/io/xpipe/extension/Proxyable.java b/extension/src/main/java/io/xpipe/extension/Proxyable.java new file mode 100644 index 000000000..56fba18bf --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/Proxyable.java @@ -0,0 +1,8 @@ +package io.xpipe.extension; + +import io.xpipe.core.store.ShellStore; + +public interface Proxyable { + + ShellStore getProxy(); +} diff --git a/extension/src/main/java/io/xpipe/extension/XPipeProxy.java b/extension/src/main/java/io/xpipe/extension/XPipeProxy.java new file mode 100644 index 000000000..41080aaf3 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/XPipeProxy.java @@ -0,0 +1,85 @@ +package io.xpipe.extension; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import io.xpipe.api.connector.XPipeConnection; +import io.xpipe.beacon.exchange.ProxyReadConnectionExchange; +import io.xpipe.core.impl.InputStreamStore; +import io.xpipe.core.process.ShellProcessControl; +import io.xpipe.core.source.DataSource; +import io.xpipe.core.source.DataSourceReadConnection; +import io.xpipe.core.store.ShellStore; +import io.xpipe.core.util.JacksonMapper; +import io.xpipe.core.util.XPipeInstallation; +import io.xpipe.extension.util.XPipeDaemon; +import lombok.SneakyThrows; + +import java.io.IOException; +import java.util.Optional; +import java.util.function.Function; + +public class XPipeProxy { + + @SneakyThrows + private static DataSource downstreamTransform(DataSource input, ShellStore proxy) { + var proxyNode = JacksonMapper.newMapper().valueToTree(proxy); + var inputNode = JacksonMapper.newMapper().valueToTree(input); + var localNode = JacksonMapper.newMapper().valueToTree(ShellStore.local()); + replace(inputNode, node -> node.equals(proxyNode) ? Optional.of(localNode) : Optional.empty()); + return JacksonMapper.newMapper().treeToValue(inputNode, DataSource.class); + } + + private static JsonNode replace( + JsonNode node, Function> function) { + var value = function.apply(node); + if (value.isPresent()) { + return value.get(); + } + + if (!node.isObject()) { + return node; + } + + var replacement = JsonNodeFactory.instance.objectNode(); + var iterator = node.fields(); + while (iterator.hasNext()) { + var stringJsonNodeEntry = iterator.next(); + var resolved = function.apply(stringJsonNodeEntry.getValue()).orElse(stringJsonNodeEntry.getValue()); + replacement.set(stringJsonNodeEntry.getKey(), resolved); + } + return replacement; + } + + public static T remoteReadConnection(DataSource source, ShellStore proxy) { + var downstream = downstreamTransform(source, proxy); + return (T) XPipeConnection.execute(con -> { + con.sendRequest(ProxyReadConnectionExchange.Request.builder().source(downstream).build()); + con.receiveResponse(); + var inputSource = DataSource.createInternalDataSource( + source.determineInfo().getType(), new InputStreamStore(con.receiveBody())); + return inputSource.openReadConnection(); + }); + } + + public static ShellStore getProxy(Object base) { + return base instanceof Proxyable p ? p.getProxy() : null; + } + + public static boolean isRemote(Object base) { + return base instanceof Proxyable p && !ShellStore.isLocal(p.getProxy()); + } + + public static void checkSupport(ShellStore store) throws Exception { + var version = XPipeDaemon.getInstance().getVersion(); + try (ShellProcessControl s = store.create().start()) { + var installationVersion = XPipeInstallation.queryInstallationVersion(s); + if (installationVersion.isEmpty()) { + throw new IOException(I18n.get("noInstallationFound")); + } + + if (!version.equals(installationVersion.get())) { + throw new IOException(I18n.get("versionMismatch", version, installationVersion)); + } + } + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/ProxyChoiceComp.java b/extension/src/main/java/io/xpipe/extension/comp/ProxyChoiceComp.java new file mode 100644 index 000000000..4b9416929 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/ProxyChoiceComp.java @@ -0,0 +1,35 @@ +package io.xpipe.extension.comp; + +import io.xpipe.core.store.ShellStore; +import io.xpipe.extension.XPipeProxy; +import io.xpipe.extension.util.SimpleValidator; +import io.xpipe.extension.util.Validatable; +import io.xpipe.extension.util.Validator; +import io.xpipe.fxcomps.SimpleComp; +import javafx.beans.property.Property; +import javafx.scene.layout.Region; +import net.synedra.validatorfx.Check; + +public class ProxyChoiceComp extends SimpleComp implements Validatable { + + private final Property selected; + private final Validator validator = new SimpleValidator(); + private final Check check; + + public ProxyChoiceComp(Property selected) { + this.selected = selected; + check = Validator.exceptionWrapper(validator, selected, () -> XPipeProxy.checkSupport(selected.getValue())); + } + + @Override + protected Region createSimple() { + var choice = new ShellStoreChoiceComp<>(selected, ShellStore.class, shellStore -> true, shellStore -> true); + choice.apply(struc -> check.decorates(struc.get())); + return choice.createRegion(); + } + + @Override + public Validator getValidator() { + return validator; + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/ShellStoreChoiceComp.java b/extension/src/main/java/io/xpipe/extension/comp/ShellStoreChoiceComp.java new file mode 100644 index 000000000..54a54c5a9 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/ShellStoreChoiceComp.java @@ -0,0 +1,91 @@ +package io.xpipe.extension.comp; + +import io.xpipe.core.impl.LocalStore; +import io.xpipe.core.store.ShellStore; +import io.xpipe.extension.DataStoreProviders; +import io.xpipe.extension.I18n; +import io.xpipe.extension.event.ErrorEvent; +import io.xpipe.extension.util.CustomComboBoxBuilder; +import io.xpipe.extension.util.XPipeDaemon; +import io.xpipe.fxcomps.SimpleComp; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleStringProperty; +import javafx.scene.Node; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import lombok.AllArgsConstructor; + +import java.util.function.Predicate; +import java.util.stream.Stream; + +/* +TODO: Integrate store validation more into this comp. + */ +@AllArgsConstructor +public class ShellStoreChoiceComp extends SimpleComp { + + private final Property selected; + private final Class storeClass; + private final Predicate applicableCheck; + private final Predicate supportCheck; + + private Region createGraphic(T s) { + var provider = DataStoreProviders.byStore(s); + var imgView = + new PrettyImageComp(new SimpleStringProperty(provider.getDisplayIconFileName()), 16, 16).createRegion(); + + var name = XPipeDaemon.getInstance().getNamedStores().stream() + .filter(e -> e.equals(s)) + .findAny() + .flatMap(store -> XPipeDaemon.getInstance().getStoreName(store)) + .orElse(I18n.get("localMachine")); + + return new Label(name, imgView); + } + + @Override + @SuppressWarnings("unchecked") + protected Region createSimple() { + var comboBox = new CustomComboBoxBuilder(selected, this::createGraphic, null, n -> { + if (n != null) { + try { + n.checkComplete(); + // n.test(); + } catch (Exception ex) { + var name = XPipeDaemon.getInstance().getNamedStores().stream() + .filter(e -> e.equals(n)) + .findAny() + .flatMap(store -> XPipeDaemon.getInstance().getStoreName(store)) + .orElse(I18n.get("localMachine")); + ErrorEvent.fromMessage(I18n.get("extension.namedHostNotActive", name)) + .reportable(false) + .handle(); + return false; + } + } + + // if (n != null && !supportCheck.test(n)) { + // var name = XPipeDaemon.getInstance().getNamedStores().stream() + // .filter(e -> e.equals(n)).findAny() + // .flatMap(store -> + // XPipeDaemon.getInstance().getStoreName(store)).orElse(I18n.get("localMachine")); + // ErrorEvent.fromMessage(I18n.get("extension.namedHostFeatureUnsupported", + // name)).reportable(false).handle(); + // return false; + // } + return true; + }); + + var available = Stream.concat( + Stream.of(new LocalStore()), + XPipeDaemon.getInstance().getNamedStores().stream() + .filter(s -> storeClass.isAssignableFrom(s.getClass()) && applicableCheck.test((T) s)) + .map(s -> (ShellStore) s)) + .toList(); + available.forEach(s -> comboBox.add((T) s)); + ComboBox cb = comboBox.build(); + cb.getStyleClass().add("choice-comp"); + return cb; + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/WriteModeChoiceComp.java b/extension/src/main/java/io/xpipe/extension/comp/WriteModeChoiceComp.java index fb3bcf782..ee2714fa5 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/WriteModeChoiceComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/WriteModeChoiceComp.java @@ -5,7 +5,6 @@ import io.xpipe.extension.I18n; import io.xpipe.extension.util.SimpleValidator; import io.xpipe.extension.util.Validatable; import io.xpipe.extension.util.Validator; -import io.xpipe.extension.util.Validators; import io.xpipe.fxcomps.SimpleComp; import io.xpipe.fxcomps.util.PlatformThread; import javafx.beans.property.Property; @@ -36,7 +35,7 @@ public class WriteModeChoiceComp extends SimpleComp implements Validatable { if (available.size() == 1) { selected.setValue(available.get(0)); } - check = Validators.nonNull(validator, I18n.observable("mode"), selected); + check = Validator.nonNull(validator, I18n.observable("mode"), selected); } @Override diff --git a/extension/src/main/java/io/xpipe/extension/util/DynamicOptionsBuilder.java b/extension/src/main/java/io/xpipe/extension/util/DynamicOptionsBuilder.java index 9c786cc95..db027caf4 100644 --- a/extension/src/main/java/io/xpipe/extension/util/DynamicOptionsBuilder.java +++ b/extension/src/main/java/io/xpipe/extension/util/DynamicOptionsBuilder.java @@ -60,7 +60,7 @@ public class DynamicOptionsBuilder { public DynamicOptionsBuilder 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)); + return decorate(Validator.nonNull(v, e.name(), p)); } public DynamicOptionsBuilder addNewLine(Property prop) { diff --git a/extension/src/main/java/io/xpipe/extension/util/Validator.java b/extension/src/main/java/io/xpipe/extension/util/Validator.java index 4199a31a0..b28a88fcc 100644 --- a/extension/src/main/java/io/xpipe/extension/util/Validator.java +++ b/extension/src/main/java/io/xpipe/extension/util/Validator.java @@ -1,13 +1,36 @@ package io.xpipe.extension.util; +import io.xpipe.extension.I18n; 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 org.apache.commons.lang3.function.FailableRunnable; public interface Validator { + static Check nonNull(Validator v, ObservableValue name, ObservableValue s) { + return v.createCheck().dependsOn("val", s).withMethod(c -> { + if (c.get("val") == null) { + c.error(I18n.get("extension.mustNotBeEmpty", name.getValue())); + } + }); + } + + static Check exceptionWrapper(Validator v, ObservableValue s, FailableRunnable ex) { + return v.createCheck().dependsOn("val", s).withMethod(c -> { + if (c.get("val") == null) { + try { + ex.run(); + } catch (Exception e) { + c.error(e.getMessage()); + } + } + }); + } + public Check createCheck(); /** Add another check to the checker. Changes in the check's validationResultProperty will be reflected in the checker. diff --git a/extension/src/main/java/io/xpipe/extension/util/Validators.java b/extension/src/main/java/io/xpipe/extension/util/Validators.java index 0ac9b5963..b9a81834d 100644 --- a/extension/src/main/java/io/xpipe/extension/util/Validators.java +++ b/extension/src/main/java/io/xpipe/extension/util/Validators.java @@ -4,21 +4,11 @@ import io.xpipe.core.store.DataStore; import io.xpipe.core.impl.LocalStore; import io.xpipe.core.store.ShellStore; import io.xpipe.extension.I18n; -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 name, ObservableValue s) { - return v.createCheck().dependsOn("val", s).withMethod(c -> { - if (c.get("val") == null) { - c.error(I18n.get("extension.mustNotBeEmpty", name.getValue())); - } - }); - } - public static void nonNull(Object object, String name) { if (object == null) { throw new IllegalArgumentException(I18n.get("extension.null", name)); diff --git a/extension/src/main/java/io/xpipe/extension/util/XPipeDaemon.java b/extension/src/main/java/io/xpipe/extension/util/XPipeDaemon.java index 9c202c1cc..fefe69370 100644 --- a/extension/src/main/java/io/xpipe/extension/util/XPipeDaemon.java +++ b/extension/src/main/java/io/xpipe/extension/util/XPipeDaemon.java @@ -27,6 +27,8 @@ public interface XPipeDaemon { void withResource(String module, String file, Charsetter.FailableConsumer con); List getNamedStores(); + String getVersion(); + Image image(String file); String svgImage(String file); diff --git a/extension/src/main/java/module-info.java b/extension/src/main/java/module-info.java index 1aafebe2f..c44cd597b 100644 --- a/extension/src/main/java/module-info.java +++ b/extension/src/main/java/module-info.java @@ -40,4 +40,5 @@ open module io.xpipe.extension { uses XPipeDaemon; uses io.xpipe.extension.Cache; uses io.xpipe.extension.DataSourceActionProvider; + uses io.xpipe.extension.NamedFunction; }